diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..1efdb220a1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,20 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+
+**How To Reproduce**
+
+**Payload from client (handle_request)**
+
+**Response from backend**
+
+**Expected behavior**
+
+**Additional context**
diff --git a/.gitmodules b/.gitmodules
index 455695422f..8cc7f3bc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "openslides-meta"]
- path = global/meta
+ path = meta
url = https://github.com/OpenSlides/openslides-meta.git
diff --git a/Dockerfile b/Dockerfile
index d77d6ca825..340b0c33ad 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,7 +18,8 @@ ENV PYTHONPATH /app
COPY --chown=appuser:appuser scripts scripts
COPY --chown=appuser:appuser entrypoint.sh ./
COPY --chown=appuser:appuser openslides_backend openslides_backend
-COPY --chown=appuser:appuser global global
+COPY --chown=appuser:appuser meta meta
+COPY --chown=appuser:appuser data data
ARG VERSION=dev
RUN echo "$VERSION" > openslides_backend/version.txt
diff --git a/Makefile b/Makefile
index 658f265ab5..cb8ad0fe09 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
# Development and testing inside docker container or without docker (only unit and integration tests)
-paths = openslides_backend/ tests/ cli/ global/meta/dev/src/
+paths = openslides_backend/ tests/ cli/ meta/dev/src/
all: pyupgrade black autoflake isort flake8 mypy
@@ -40,7 +40,7 @@ test-unit-integration:
check-all: validate-models-yml check-models check-initial-data-json check-example-data-json check-permissions
validate-models-yml:
- make -C global/meta/dev validate-models
+ make -C meta/dev validate-models
generate-models:
python cli/generate_models.py $(MODELS_PATH)
@@ -57,10 +57,10 @@ check-permissions:
python cli/generate_permissions.py --check
check-initial-data-json:
- python cli/check_json.py global/data/initial-data.json
+ python cli/check_json.py data/initial-data.json
check-example-data-json:
- python cli/check_json.py global/data/example-data.json
+ python cli/check_json.py data/example-data.json
run-debug:
OPENSLIDES_DEVELOPMENT=1 python -m openslides_backend
diff --git a/README.md b/README.md
index a09268a280..a99c478612 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ To generate a new models.py file run (inside the docker container)
$ make generate-models
-The original models.yml is now included in this repository at global/meta/. If you do not want to generate from the current backend, you can provide either a local path or an URL via the variable `MODELS_PATH`. This way, you can generate only partial changes if multiple changes to the models file were merged into the main:
+The original models.yml is now included in this repository at meta/. If you do not want to generate from the current backend, you can provide either a local path or a URL via the variable `MODELS_PATH`. This way, you can generate only partial changes if multiple changes to the models file were merged into the main:
$ MODEL_PATH="local path or GitHub link" make generate-models
diff --git a/cli/generate_models.py b/cli/generate_models.py
index 641ea97373..dbd2109725 100644
--- a/cli/generate_models.py
+++ b/cli/generate_models.py
@@ -20,7 +20,7 @@
)
from openslides_backend.shared.patterns import KEYSEPARATOR, Collection
-SOURCE = "./global/meta/models.yml"
+SOURCE = "./meta/models.yml"
DESTINATION = os.path.abspath(
os.path.join(
diff --git a/cli/generate_permissions.py b/cli/generate_permissions.py
index 0740edcc20..74f40221f1 100644
--- a/cli/generate_permissions.py
+++ b/cli/generate_permissions.py
@@ -10,7 +10,7 @@
from cli.util.util import assert_equal, open_output, open_yml_file, parse_arguments
from openslides_backend.permissions.get_permission_parts import get_permission_parts
-SOURCE = "./global/meta/permission.yml"
+SOURCE = "./meta/permission.yml"
DESTINATION = os.path.abspath(
os.path.join(
diff --git a/global/data/example-data.json b/data/example-data.json
similarity index 99%
rename from global/data/example-data.json
rename to data/example-data.json
index d842581ee9..8480f4cf2b 100644
--- a/global/data/example-data.json
+++ b/data/example-data.json
@@ -1,5 +1,5 @@
{
- "_migration_index": 63,
+ "_migration_index": 64,
"gender":{
"1":{
"id": 1,
@@ -72,7 +72,8 @@
"gender": "gender",
"pronoun": "pronoun",
"is_active": "is_active",
- "is_physical_person": "is_person"
+ "is_physical_person": "is_person",
+ "member_number": "member_number"
}
}
},
@@ -1556,7 +1557,7 @@
"recommendation_label": "Acceptance",
"css_class": "green",
"set_number": true,
- "merge_amendment_into_final": "undefined",
+ "merge_amendment_into_final": "do_merge",
"previous_state_ids": [
1
],
@@ -1578,7 +1579,7 @@
"recommendation_label": "Rejection",
"css_class": "red",
"set_number": true,
- "merge_amendment_into_final": "undefined",
+ "merge_amendment_into_final": "do_not_merge",
"previous_state_ids": [
1
],
diff --git a/global/data/initial-data.json b/data/initial-data.json
similarity index 96%
rename from global/data/initial-data.json
rename to data/initial-data.json
index ba6880235f..cc59fff307 100644
--- a/global/data/initial-data.json
+++ b/data/initial-data.json
@@ -1,5 +1,5 @@
{
- "_migration_index": 63,
+ "_migration_index": 64,
"gender":{
"1":{
"id": 1,
@@ -58,7 +58,8 @@
"gender": "gender",
"pronoun": "pronoun",
"is_active": "is_active",
- "is_physical_person": "is_person"
+ "is_physical_person": "is_person",
+ "member_number": "member_number"
}
}
},
diff --git a/dev/Dockerfile.dev b/dev/Dockerfile.dev
index 47a45a06fa..2000f9e75e 100644
--- a/dev/Dockerfile.dev
+++ b/dev/Dockerfile.dev
@@ -13,8 +13,9 @@ COPY dev/cleanup.sh .
# Copy files which are mounted to make the full stack work
COPY scripts scripts
-COPY global global
COPY cli cli
+COPY data data
+COPY meta meta
COPY Makefile .
COPY setup.cfg .
diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml
index 572106bd6f..f0150c79f4 100644
--- a/dev/docker-compose.dev.yml
+++ b/dev/docker-compose.dev.yml
@@ -15,7 +15,9 @@ services:
- ../openslides_backend:/app/openslides_backend
- ../tests:/app/tests
- ../cli:/app/cli
- - ../global:/app/global
+ - ../data:/app/data
+ - ../meta:/app/meta
+ - ../requirements:/app/requirements
- ../scripts:/app/scripts
environment:
- DATASTORE_READER_HOST=datastore-reader
diff --git a/docs/Presenters-Overview.md b/docs/Presenters-Overview.md
index 5ffc1db58a..58ef16d284 100644
--- a/docs/Presenters-Overview.md
+++ b/docs/Presenters-Overview.md
@@ -6,6 +6,7 @@ Available presenters:
- [get_forwarding_meetings](presenters/get_forwarding_meetings.md)
- [get_meetings](presenters/get_meetings.md)
- [get_users](presenters/get_users.md)
+- [get_user_editable](presenters/get_user_editable.md)
- [get_user_related_models](presenters/get_user_related_models.md)
- [get_user_scope](presenters/get_user_scope.md)
- [search_deleted_models](presenters/search_deleted_models.md)
diff --git a/docs/actions/assignment.update.md b/docs/actions/assignment.update.md
index c4edf9d2e8..ae2078fa40 100644
--- a/docs/actions/assignment.update.md
+++ b/docs/actions/assignment.update.md
@@ -20,5 +20,7 @@
## Action
Updates an assignment.
+If phase is newly set to `voting`, the candidates of the assignment are put in the assignments `list_of_speakers` if they are not already.
+
## Permissions
The user needs `assignment.can_manage`.
diff --git a/docs/actions/group.update.md b/docs/actions/group.update.md
index ac4b815345..73f53e953b 100644
--- a/docs/actions/group.update.md
+++ b/docs/actions/group.update.md
@@ -12,7 +12,7 @@
```
## Action
-Updates the group. Permissions are restricted to the following enum: https://github.com/OpenSlides/openslides-backend/blob/fae36a0b055bbaa463da4768343080c285fe8178/global/meta/models.yml#L1621-L1656
+Updates the group. Permissions are restricted to the group.permissions enum inside https://github.com/OpenSlides/openslides-meta/blob/main/models.yml.
If the group is the meetings anonymous group, the name may not be changed and the permissions have to be in the following whitelist:
- agenda_item.can_see,
@@ -27,6 +27,7 @@ If the group is the meetings anonymous group, the name may not be changed and th
- meeting.can_see_livestream,
- motion.can_see,
- motion.can_see_internal,
+- poll.can_see_progress,
- projector.can_see,
- user.can_see,
- user.can_see_sensitive_data
diff --git a/docs/actions/meeting.create.md b/docs/actions/meeting.create.md
index 6386c9a33d..6dda7ffbd0 100644
--- a/docs/actions/meeting.create.md
+++ b/docs/actions/meeting.create.md
@@ -24,10 +24,10 @@
It has to be checked whether the `organization.limit_of_meetings` is unlimited (=0) or lower than the amount of active meetings in `organization.active_meeting_ids`.
When creating a meeting the following objects have to be created, too:
-- Groups: `Default`, `Admin`, `Delegates`, `Staff`, `Committees`. The first one is set as `meeting/default_group_id`, the second one as `meeting/admin_group_id`. The permissions can be found in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/global/data/initial-data.json)).
+- Groups: `Default`, `Admin`, `Delegates`, `Staff`, `Committees`. The first one is set as `meeting/default_group_id`, the second one as `meeting/admin_group_id`. The permissions can be found in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json)).
- Projector: One projector named `"Default projector"` must be created and set as `meeting/reference_projector_id`.
- All default projectors (`meeting/default_projector_*_ids`, see `models.yml`) must be set to the one projector
-- Motion workflow and states: Create one workflow `"simple workflow"` which is set as `meeting/motions_default_workflow_id` and `meeting/motions_default_amendment_workflow_id`. Create four states (analog as in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/global/data/initial-data.json)).
+- Motion workflow and states: Create one workflow `"simple workflow"` which is set as `meeting/motions_default_workflow_id` and `meeting/motions_default_amendment_workflow_id`. Create four states (analog as in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json)).
- Two countdowns are created and set as `meeting/list_of_speakers_countdown` (name: "List of speakers countdown") and `meeting/voting_countdown` (name: "Voting countdown").
If `user_ids` are given, it must be checked that it is a subset of `committee/user_ids`. Each user is added to the meeting by being added to the default group.
diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md
index 6bffd3dc45..dabf7bf7c5 100644
--- a/docs/actions/meeting.update.md
+++ b/docs/actions/meeting.update.md
@@ -78,6 +78,7 @@
motions_enable_reason_on_projector: boolean;
motions_enable_sidebox_on_projector: boolean;
motions_enable_recommendation_on_projector: boolean;
+ motions_create_enable_additional_submitter_text:boolean;
motions_show_referring_motions: boolean;
motions_show_sequential_number: boolean;
motions_recommendations_by: string;
diff --git a/docs/actions/motion.create.md b/docs/actions/motion.create.md
index 703c2feaae..a016ab7dd8 100644
--- a/docs/actions/motion.create.md
+++ b/docs/actions/motion.create.md
@@ -56,7 +56,8 @@ This is the logic for other fields depending on the motion type:
There are some fields that need special attention:
- `workflow_id`: If it is given, the motion's state is set to the workflow's first state. The workflow must be from the same meeting. If the field is not given, one of the three default (`meeting/motions_default_workflow_id` or `meeting/motions_default_amendment_workflow_id`) workflows is used depending on the type of the motion to create.
-- `submitter_ids`: These are **user ids** and not ids of the `submitter` model. If nothing is given (`[]`), the request user's id is used. For each id in the list a `motion_submitter` model is created. The weight must be set to the order of the given list.
+- `additional_submitter` a text field where text-based submitter information may be entered. Cannot be set unless `meeting/motions_create_enable_additional_submitter_text` is `true`. Requires permissions `Motion.CAN_CREATE` and `Motion.CAN_MANAGE_METADATA`.
+- `submitter_ids`: These are **user ids** and not ids of the `motion_submitter` model. If nothing is given (`[]`) and the field `additional_submitter` isn't filled, the request user's id is used. For each id in the list a `motion_submitter` model is created. The weight must be set to the order of the given list. Requires permissions `Motion.CAN_CREATE`, `Motion.CAN_MANAGE_METADATA` and `User.CAN_SEE`.
- `agenda_*`: See [Agenda](https://github.com/OpenSlides/OpenSlides/wiki/Agenda#additional-fields-during-creation-of-agenda-content-objects).
Other things to do when creating motions:
diff --git a/docs/actions/motion.import.md b/docs/actions/motion.import.md
deleted file mode 100644
index d93d50c8ee..0000000000
--- a/docs/actions/motion.import.md
+++ /dev/null
@@ -1,21 +0,0 @@
-## Payload
-```js
-{
-// required
- id: Id; // action worker id
- import: boolean;
-}
-```
-
-
-## Action
-If `import` is `True`, use the rows from the given action worker and check that the import type
-matches and whether it should still be created (row state `new`) or update (row state `done`).
-On row state `new`, the username must not exist yet. On row state `done`,
-the record with the matching `id` should still have the same username. On error, don't import anything,
-but create data as in json_upload. Do the actual import with bulk actions.
-
-If `import` is `False` or the import was successful, remove the action worker.
-
-## Permission
-The request user needs permission `motion.can_manage`, but only allow importing data if there are no errors in preview.
\ No newline at end of file
diff --git a/docs/actions/motion.json_upload.md b/docs/actions/motion.json_upload.md
deleted file mode 100644
index eae90a8ac2..0000000000
--- a/docs/actions/motion.json_upload.md
+++ /dev/null
@@ -1,54 +0,0 @@
-## Payload
-
-Because the data fields are all converted from CSV import file, **they are all of type `string`**.
-The types noted below are the internal types after conversion in the backend. See [here](preface_special_imports.md#internal-types) for the representation of the types.
-```js
-{
- // required for new motions
- data: {
- // required in create
- title: string, // info: done, error
- text: string, // info: done, error
- // all optional, but see rules below
- number: string, // unique when set, info: done, generated or error
- reason: string, // required for create if the meeting has "motions_reason_required", info: done or error
- submitters_verbose: string[],
- submitters_username: string[], // info: done, generated, warning, error if len(submitters_verbose) > len(submitters_username)
- supporters_verbose: string[],
- supporters_username: string[], // info: done, warning, error if len(supporters_verbose) > len(supporters_username)
- category_name: string, // info: done or warning, partial reference to: motion_category
- category_prefix: string,
- tags: string[], // info: done or warning, reference to: tag
- block: string, // info: done or warning, reference to: motion_block
- motion_amendment: boolean, // info: done or warning, if True, warning, that motion amendments cannot be imported
- }[];
- meeting_id: Id, // id of the current meeting.
-}
-```
-## Return value and object fields
-
-Besides the usual headers as seen in payload (name and type), there are these differences:
-
-- `submitters`, `supporter_users`, `category_name/prefix`, `tags` and `block`: Objects that show if the model has been found (`done`) or not (`warning`).
-- `text`: will be surrounded in html `
` tags if the string isn't encased in html tags already.
-- `number`: will be object with error, if the field is set and another row in the payload has the same number. If the `number` field is left empty and the motion is going to be created in a `motion_state` where `set_number` is true, a new `number` will be generated and the object is going to have the info `generated`.
-
-The row state can be one of "new", "done" or "error". In case of an error, no import should be possible.
-
-See [common description](preface_special_imports.md#general-format-of-the-result-send-to-the-client-for-preview).
-
-Other than the validity check for the username-fields, `submitters_verbose` and `supporters_verbose` are NOT otherwise used or taken note of in the import. They are merely accepted in order to check that someone didn't accidentally edit the wrong column in a file that has both verbose and non-verbose columns.
-They are not included in the return value.
-
-
-## Action
-The data will create or update motions.
-
-### Motion matching
-
-Motions can be updated via their `number`.
-If a motion has a `number`, it will be matched with and updated with the data of any import date that has the same `number`.
-Therefore motions that don't have a number can not be overwritten.
-
-## Permission
-Permission `motion.can_manage`
\ No newline at end of file
diff --git a/docs/actions/option.update.md b/docs/actions/option.update.md
index 8a197c2010..81d99e4bdf 100644
--- a/docs/actions/option.update.md
+++ b/docs/actions/option.update.md
@@ -20,4 +20,4 @@ If the poll's state is *created* and at least one vote value is given (`Y`, `N`
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/organization.update.md b/docs/actions/organization.update.md
index c49aec75a4..0e739f1e5f 100644
--- a/docs/actions/organization.update.md
+++ b/docs/actions/organization.update.md
@@ -37,6 +37,96 @@
## Action
Updates the organization.
It has to be checked that the theme_id has to be one of the theme_ids.
+This is an example of the saml_attr_mapping, where you can see the mappable fields.
+```js
+{
+ "email": "email",
+ "title": "title",
+ "gender": "gender",
+ "pronoun": "pronoun",
+ "saml_id": "username",
+ "is_active": "is_active",
+ "last_name": "lastName",
+ "first_name": "firstName",
+ "member_number": "member_number",
+ "meeting_mappers": [
+ {
+ "name": "A mapper",
+ "mappings": {
+ "groups": [
+ {
+ "default": "not_a_group",
+ "attribute": "idp_group_attribute"
+ },
+ {
+ "default": "not_a_group",
+ "attribute": "group_2"
+ }
+ ],
+ "number": {
+ "attribute": "participant_number"
+ },
+ "comment": {
+ "default": "Vote weight, groups and structure levels set via SSO.",
+ "attribute": "idp_commentary"
+ },
+ "present": {
+ "default": "True",
+ "attribute": "presence"
+ },
+ "vote_weight": {
+ "default": "1.000000",
+ "attribute": "vw"
+ },
+ "structure_levels": [
+ {
+ "default": "structure1",
+ "attribute": "structure"
+ }
+ ]
+ },
+ "conditions": [
+ {
+ "attribute": "member_number",
+ "condition": "LV_.*"
+ },
+ {
+ "attribute": "email",
+ "condition": "[\\w\\.]+@([\\w-]+\\.)+[\\w]{2,4}"
+ }
+ ],
+ "external_id": "Bundestag"
+ },
+ {
+ "name": "Bundestag visitors",
+ "conditions": [
+ {
+ "attribute": "member_number",
+ "condition": "^(?!11600).*"
+ }
+ ],
+ "external_id": "Bundestag"
+ },
+ {
+ "name": "A second mapper",
+ "mappings": {
+ "number": {
+ "attribute": "participant_number"
+ },
+ "comment": {
+ "default": "This mapper adds everyone to the Landtag default group.",
+ },
+ "present": {
+ "default": "True",
+ "attribute": "presence"
+ },
+ },
+ "external_id": "Landtag"
+ }
+ ],
+ "is_physical_person": "is_person"
+}
+```
## Permissions
- Users with OML of `can_manage_organization` can modify group A
diff --git a/docs/actions/poll.anonymize.md b/docs/actions/poll.anonymize.md
index ac0fbeb14f..442ffb07e5 100644
--- a/docs/actions/poll.anonymize.md
+++ b/docs/actions/poll.anonymize.md
@@ -12,4 +12,4 @@ Only for non-analog polls in the state *finished* or *published*. Sets all `vote
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.create.md b/docs/actions/poll.create.md
index 543194f794..0c6db1fad9 100644
--- a/docs/actions/poll.create.md
+++ b/docs/actions/poll.create.md
@@ -1,55 +1,57 @@
## Payload
Helper Interface for options to create:
-```
+```js
Interface Option {
// Exactly one of text, content_object_id or poll_candidate_user_ids must be given
- text: string; // topic-poll
- content_object_id: Fqid; // must be one of user or motion.
- poll_candidate_user_ids: [user_ids]; // sorted list of user ids for candidate list election
+ text: string, // topic-poll
+ content_object_id: Fqid, // must be one of user or motion.
+ poll_candidate_user_ids: [user_ids], // sorted list of user ids for candidate list election
// Only for type==analog, optional votes can be given
- Y?: decimal(6); // Y, YN, YNA mode
- N?: decimal(6); // N, YN, YNA mode
- A?: decimal(6); // YNA mode
-}}
+ Y?: decimal(6), // Y, YN, YNA mode
+ N?: decimal(6), // N, YN, YNA mode
+ A?: decimal(6) // YNA mode
+}
```
Payload:
-```
+```js
{
// Required
- title: string;
- type: string;
- pollmethod: string;
+ title: string,
+ type: string,
+ pollmethod: string,
- meeting_id: Id;
- options: Option[]; // must have at least one entry.
+ meeting_id: Id,
+ options: Option[], // must have at least one entry.
// Optional
- content_object_id: Fqid;
- description: string;
- min_votes_amount: number;
- max_votes_amount: number;
- allow_multiple_votes_per_candidate: boolean;
- global_yes: boolean;
- global_no: boolean;
- global_abstain: boolean;
- onehundred_percent_base: string;
+ content_object_id: Fqid,
+ description: string,
+ min_votes_amount: number,
+ max_votes_amount: number,
+ max_votes_per_option: number,
+ allow_multiple_votes_per_candidate: boolean,
+ global_yes: boolean,
+ global_no: boolean,
+ global_abstain: boolean,
+ onehundred_percent_base: string,
+ backend: string,
// Only for non analog types
- entitled_group_ids: Id[];
+ entitled_group_ids: Id[],
// Only for type==analog
- publish_immediately: boolean;
+ publish_immediately: boolean,
// Only for type==analog, optional votes can be given
- votesvalid?: decimal(6);
- votesinvalid?: decimal(6);
- votescast?: decimal(6);
- amount_global_yes?: decimal(6);
- amount_global_no?: decimal(6);
- amount_global_abstain?: decimal(6);
+ votesvalid?: decimal(6),
+ votesinvalid?: decimal(6),
+ votescast?: decimal(6),
+ amount_global_yes?: decimal(6),
+ amount_global_no?: decimal(6),
+ amount_global_abstain?: decimal(6)
}
```
@@ -64,8 +66,10 @@ If the `content_object_id` points to a `motion` and the `motion_state` of the mo
The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
+The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount`.
+
## Permissions
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.delete.md b/docs/actions/poll.delete.md
index f6ef35e38d..cfea2d9a28 100644
--- a/docs/actions/poll.delete.md
+++ b/docs/actions/poll.delete.md
@@ -10,4 +10,4 @@ Deletes the given poll and all linked options with all votes, too.
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.publish.md b/docs/actions/poll.publish.md
index 38df11f899..a5a0c8af5e 100644
--- a/docs/actions/poll.publish.md
+++ b/docs/actions/poll.publish.md
@@ -10,4 +10,4 @@ Sets the state to *published*. Only allowed for polls in the *finished* state.
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.reset.md b/docs/actions/poll.reset.md
index 33081bc0eb..0d8368b0bc 100644
--- a/docs/actions/poll.reset.md
+++ b/docs/actions/poll.reset.md
@@ -12,4 +12,4 @@ If `type != "pseudoanonymous"`, `is_pseudoanonymized` may be reset to `false` (i
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.start.md b/docs/actions/poll.start.md
index d50b49991c..1278779135 100644
--- a/docs/actions/poll.start.md
+++ b/docs/actions/poll.start.md
@@ -12,4 +12,4 @@ If `meeting/poll_couple_countdown` is true and the poll is an electronic poll, t
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.stop.md b/docs/actions/poll.stop.md
index a44d7abd42..d271a3e2bb 100644
--- a/docs/actions/poll.stop.md
+++ b/docs/actions/poll.stop.md
@@ -16,4 +16,4 @@ Some fields have to be calculated upon stopping a poll:
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.update.md b/docs/actions/poll.update.md
index c880c442da..bad10079dc 100644
--- a/docs/actions/poll.update.md
+++ b/docs/actions/poll.update.md
@@ -1,34 +1,36 @@
## Payload
-```
+```js
{
// Required
- id: Id;
+ id: Id,
// Optional, only if state == created
- pollmethod: string;
- min_votes_amount: number;
- max_votes_amount: number;
- allow_multiple_votes_per_candidate: boolean;
- global_yes: boolean;
- global_no: boolean;
- global_abstain: boolean;
+ pollmethod: string,
+ min_votes_amount: number,
+ max_votes_amount: number,
+ max_votes_per_option: number,
+ allow_multiple_votes_per_candidate: boolean,
+ global_yes: boolean,
+ global_no: boolean,
+ global_abstain: boolean,
+ backend: string,
// Optional, only if state == created, only for non analog types
- entitled_group_ids: Id[];
+ entitled_group_ids: Id[],
// Optional, every state
- title: string;
- description: string;
- onehundred_percent_base: string;
+ title: string,
+ description: string,
+ onehundred_percent_base: string,
// type==analog, every state
- votesvalid?: number;
- votesinvalid?: number;
- votescast?: number;
- publish_immediately: boolean;
+ votesvalid?: number,
+ votesinvalid?: number,
+ votescast?: number,
+ publish_immediately: boolean,
// action called internally
- entitled_users_at_stop: json;
+ entitled_users_at_stop: json
}
```
@@ -39,8 +41,10 @@ For electronic polls some fields can only be updated, if the state is *created*.
The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
+The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount` after the model had been updated.
+
## Permissions
The request user needs:
- `motion.can_manage_polls` if the poll's content object is a motion
- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` else
+- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/preface_special_imports.md b/docs/actions/preface_special_imports.md
index 7dd79b63fd..12ea76a17b 100644
--- a/docs/actions/preface_special_imports.md
+++ b/docs/actions/preface_special_imports.md
@@ -115,7 +115,7 @@ The internal types will be created by the backend service from the CSV-strings
- **boolean** for `True` use one of "True", "true", "T", "t", "Yes", "yes", "Y", "y" or "1", for `False` one of "False", "false", "F", "f", "No", "no", "N", "n" or "0"
- **integer** Use something like "1234" without fraction
- **number** means a `,` (comma) or `.` (point) separated value like `3123,45` or `3123.45`, but not `3.123,45` or `3,123.45`
-- **decimal** A floating point number with exactly 6 digits, e.g. `1.500000`
+- **decimal** A decimal number with exactly 6 digits after the decimal seperator dot, e.g. `1.500000`
- **date** Use a string in Isoformat "YYYY-MM-DD", for example "2023-04-26"
## Import_Preview to store the data to import in database
@@ -154,4 +154,4 @@ The internal types will be created by the backend service from the CSV-strings
}[[]]; // nested lists for actions and data row per action
}
-``
\ No newline at end of file
+``
diff --git a/docs/actions/user.merge_together.md b/docs/actions/user.merge_together.md
index 1219ffa4d1..6d3f0ee9c0 100644
--- a/docs/actions/user.merge_together.md
+++ b/docs/actions/user.merge_together.md
@@ -57,7 +57,7 @@ An error is thrown if:
### Functionality
The primary user is updated with the information from the secondary users using the following rules:
- `organization_management_level` is set to highest oml among the users.
-- `can_change_own_password` is set to true if it is true on any selected user.
+- `can_change_own_password` is set to true if it is true on any selected user, unless the primary user has a `saml_id`, in which case it is ignored.
- relation-lists are set to the union of their content among all selected users, except the `is_present_in_meeting_ids`- and `meeting_user_ids`-relation, which are handled separately
- login data (`saml_id`, `username`, `password`) remains untouched
- If any user has a`member_number` it is used
diff --git a/docs/actions/user.save_saml_account.md b/docs/actions/user.save_saml_account.md
index 523507efce..c852ecc51e 100644
--- a/docs/actions/user.save_saml_account.md
+++ b/docs/actions/user.save_saml_account.md
@@ -1,6 +1,6 @@
## Payload
-```
+```js
{
saml_id: string, // required
title: string,
@@ -11,6 +11,8 @@
pronoun: string,
is_active: boolean,
is_physical_person: boolean,
+ member_number: string,
+ // Additional meeting related data can be given. See below explanation on meeting mappers.
}
```
@@ -31,7 +33,61 @@ Extras to do on creation:
As you can see there is no password for local login and the user can't change it.
-- Add user to the meeting by adding him to the group given in the organization-wide field-mapping as `"meeting": { "external_id": "xyz", "external_group_id": "delegates"}` if a `meeting`-entry is given. If it fails for any reason, a log entry is written, but no exception thrown. Add the user always to the group, if it fails try to add him to the default group.
+### Meeting Mappers
+- The saml attribute mapping can have a list of 'meeting_mappers' that can be used to assign users meeting related data. (See example below. A full example can be found in the [organization.update.md](organization.update.md))
+ - A mapper can be given a 'name' for debugging purposes.
+ - The 'external_id' maps to the meeting and is required (logged as warning if meeting does not exist). Multiple mappers can map to the same meeting.
+ - If 'allow_update' is set to false, the mapper is only used if the user does not already exist. If it is not given it defaults to true.
+ - Mappers are only used if every condition in the list of 'conditions' resolves to true. For this the value for 'attribute' in the payload data has to be a string and match the string or regex given in 'condition'. If no condition is given this defaults to true.
+ - The actual mappings are objects or lists of objects of attribute-default pairs (exception: number, which only has the option of an attribute).
+ - The attribute refers to the payloads data.
+ - A default value can be given in case the payloads attribute does not exist or contains no data. (Logged as debug)
+ - Groups and structure levels are given as a list of attribute-default pairs.
+- On conflict of multiple mappers mappings on a same meetings field the last given mappers data for that field is used. Exception to this are groups and structure levels. Their data is combined.
+- Values for groups and structure levels can additionally be given in comma separated lists composed as a single string.
+- Values for groups are interpreted as their external ID and structure levels as their name within that meeting.
+- If no group exists for a meeting and no default is given, the meetings default group is used. (Logged as warning)
+- If a structure level does not exist, it is created.
+- Vote weights need to be given as 6 digit decimal strings.
+
+```js
+"meeting_mappers": [{
+ "name": "Mapper-Name",
+ "external_id": "M2025",
+ "allow_update": "false",
+ "conditions": [{
+ "attribute": "membernumber",
+ "condition": "1426\d{4,6}$"
+ }, {
+ "attribute": "function",
+ "condition": "board"
+ }],
+ "mappings": {
+ "groups": [{
+ "attribute": "membership",
+ "default": "admin, standard"
+ }],
+ "structure_levels": [{
+ "attribute": "ovname",
+ "default": "struct1, struct2"
+ }],
+ "number": {"attribute": "p_number"},
+ "comment": {
+ "attribute": "idp_comment",
+ "default": "Group set via SSO"
+ },
+ "vote_weight": {
+ "attribute": "vote",
+ "default":"1.000000"
+ },
+ "present": {
+ "attribute": "present_key",
+ "default":"True"
+ }
+ }
+}]
+```
+If you are using Keycloak as your SAML-server, make sure to fill the attributes of all users. Then you also need to configure for each attribute in 'Clients' a mapping for your Openslides services 'Client Scopes'. Choose 'User Attribute' and assign the 'User Attribute' as in the step before and the 'SAML Attribut Name' as defined in Openslides 'meeting_mappers'.
## Return Value
diff --git a/docs/actions/user.set_password.md b/docs/actions/user.set_password.md
index af0a4e856d..cc64952402 100644
--- a/docs/actions/user.set_password.md
+++ b/docs/actions/user.set_password.md
@@ -9,7 +9,6 @@
set_as_default: boolean; // default false, if not given
}
```
-
## Action
Sets the password of the user given by `id` to `password`. If `set_as_default` is true, the `default_password` is also updated.
diff --git a/docs/actions/user.update_self.md b/docs/actions/user.update_self.md
index 61d879898c..a50bf77c20 100644
--- a/docs/actions/user.update_self.md
+++ b/docs/actions/user.update_self.md
@@ -8,6 +8,7 @@
pronoun: string;
meeting_id: ID;
vote_delegated_to_id: Id;
+ vote_delegations_from_ids: Id[];
}
```
diff --git a/docs/migration_route.md b/docs/migration_route.md
index 26bd427f6d..8a80883d2a 100644
--- a/docs/migration_route.md
+++ b/docs/migration_route.md
@@ -23,7 +23,7 @@ enum MigrationState {
"success": true,
"status"?: MigrationState,
"output"?: str,
- "exception"?: str,
+ "exception"?: str
}
```
`output` always contains the full output of the migration command up to this point. `exception` contains the thrown exception, if any, which can only be the case if the command is finished (meaning `status != "migration_running"`). After issuing a migration command, it is waited a short period of time for the thread to finish, so the status can be all of these things for any command (e.g. after calling `migrate`, the returned status can be either `MIGRATION_RUNNING` if the migrations did not finish directly or `FINALIZATION_REQUIRED` if the migration is already done).
@@ -41,7 +41,7 @@ The `stats` return value is the following:
"positions": int,
"events": int,
"partially_migrated_positions": int,
- "fully_migrated_positions": int,
+ "fully_migrated_positions": int
}
}
```
diff --git a/docs/presenters/check_database.md b/docs/presenters/check_database.md
index 57518ec938..cf1574bf81 100644
--- a/docs/presenters/check_database.md
+++ b/docs/presenters/check_database.md
@@ -1,23 +1,23 @@
# Payload
-```
+```js
{
// optional
- meeting_id: integer;
+ meeting_id: integer
}
```
# Returns
If okay:
-```
+```js
{
- ok: boolean,
+ ok: boolean
}
```
else:
-```
+```js
{
ok: boolean,
- errors: string,
+ errors: string
}
```
diff --git a/docs/presenters/check_database_all.md b/docs/presenters/check_database_all.md
index 393c25ae7e..e2881c5bac 100644
--- a/docs/presenters/check_database_all.md
+++ b/docs/presenters/check_database_all.md
@@ -1,21 +1,21 @@
# Payload
-```
+```js
{
}
```
# Returns
If okay:
-```
+```js
{
- ok: boolean,
+ ok: boolean
}
```
else:
-```
+```js
{
ok: boolean,
- errors: string,
+ errors: string
}
```
diff --git a/docs/presenters/export_meeting.md b/docs/presenters/export_meeting.md
index 02d73abc1b..b94aaab652 100644
--- a/docs/presenters/export_meeting.md
+++ b/docs/presenters/export_meeting.md
@@ -1,8 +1,8 @@
# Payload
-```
+```js
{
- meeting_id: Id
+ meeting_id: Id // required
}
```
diff --git a/docs/presenters/get_active_users_amount.md b/docs/presenters/get_active_users_amount.md
index 018973a645..5e6ce7a141 100644
--- a/docs/presenters/get_active_users_amount.md
+++ b/docs/presenters/get_active_users_amount.md
@@ -6,7 +6,7 @@ Nothing
```js
{
- active_users_amount: Number;
+ active_users_amount: Number
}
```
diff --git a/docs/presenters/get_forwarding_committees.md b/docs/presenters/get_forwarding_committees.md
index df01a69435..afc410ec47 100644
--- a/docs/presenters/get_forwarding_committees.md
+++ b/docs/presenters/get_forwarding_committees.md
@@ -1,14 +1,14 @@
# Payload
-```
+```js
{
- meeting_id: Id
+ meeting_id: Id // required
}
```
# Returns
-```
+```js
string[]
```
diff --git a/docs/presenters/get_forwarding_meetings.md b/docs/presenters/get_forwarding_meetings.md
index b76a5c454a..d971356012 100644
--- a/docs/presenters/get_forwarding_meetings.md
+++ b/docs/presenters/get_forwarding_meetings.md
@@ -1,19 +1,19 @@
# Payload
-```
+```js
{
- meeting_id: Id
+ meeting_id: Id // required
}
```
# Returns
-```
+```js
[
{
- id: Id
- name: string
- default_meeting_id: Id
+ id: Id,
+ name: string,
+ default_meeting_id: Id,
meeting: [{id: Id, name: string, start_time:timestamp|null, end_time:timestamp|null}, ...]
},
...
diff --git a/docs/presenters/get_mediafile_context.md b/docs/presenters/get_mediafile_context.md
index e67fb81923..cd91e4352d 100644
--- a/docs/presenters/get_mediafile_context.md
+++ b/docs/presenters/get_mediafile_context.md
@@ -2,7 +2,7 @@
```js
{
- mediafile_ids: Id[];
+ mediafile_ids: Id[] // required
}
```
@@ -15,16 +15,16 @@
published: boolean,
meetings_of_interest: {
[meeting_id: Id]: {
- name: string;
- holds_attachments: boolean;
- holds_logos: boolean;
- holds_fonts: boolean;
- holds_current_projections: boolean;
- holds_history_projections: boolean;
- holds_preview_projections: boolean;
+ name: string,
+ holds_attachments: boolean,
+ holds_logos: boolean,
+ holds_fonts: boolean,
+ holds_current_projections: boolean,
+ holds_history_projections: boolean,
+ holds_preview_projections: boolean
}
},
- children_amount: int,
+ children_amount: int
}
}
```
diff --git a/docs/presenters/get_meetings.md b/docs/presenters/get_meetings.md
deleted file mode 100644
index 00567e7c34..0000000000
--- a/docs/presenters/get_meetings.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Payload
-
-```
-{
- with_deleted: boolean, # default: False
- with_archived: boolean, # default: False
-}
-```
-
-# Returns
-
-```
-[
- {
- id: Id,
- name: string,
- deleted: boolean,
- is_active_in_organization: int,
- },
- ...
-]
-```
-
-# Logic
-
-The request user needs OML `can_manage_users` or higher or CML `can_manage`.
-
-This presenter creates a filtered list of meetings for various situations. With CML permission the list shows only meetings of committees, where the user has the needed permission.
diff --git a/docs/presenters/get_user_editable.md b/docs/presenters/get_user_editable.md
new file mode 100644
index 0000000000..480e5bafb2
--- /dev/null
+++ b/docs/presenters/get_user_editable.md
@@ -0,0 +1,31 @@
+## Payload
+
+```js
+{
+ user_ids: Id[], // required
+ fields: string[] // required
+}
+```
+
+## Returns
+
+```js
+{
+ user_id: Id: {
+ field: str: (
+ editable: boolean, // true if user can be updated or deleted,
+ message?: string // error message if an exception was caught
+ ),
+ ...
+ },
+ ...
+}
+```
+
+## Logic
+
+It iterates over the given `user_ids` and calculates whether a user can be updated depending on the given payload fields, permissions in shared committees and meetings, OML and the user-scope. The user scope is defined [here](https://github.com/OpenSlides/OpenSlides/wiki/Users#user-scopes). The payload field permissions are described [here](https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.update.md) and [here](https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.create.md).
+
+## Permissions
+
+There are no special permissions necessary.
\ No newline at end of file
diff --git a/docs/presenters/get_user_related_models.md b/docs/presenters/get_user_related_models.md
index db71df47f8..b5e057be54 100644
--- a/docs/presenters/get_user_related_models.md
+++ b/docs/presenters/get_user_related_models.md
@@ -1,27 +1,27 @@
## Payload
-```js
+```
{
- user_ids: Id[];
+ user_ids: Id[] // required
}
```
## Returns
-```js
+```
{
[user_id: Id]: {
organization_management_level: OML-String,
- committees: [{ id: Id; name: String; cml: CML-String; }],
+ committees: [{ id: Id, name: String, cml: CML-String }],
meetings: [{
- id: Id;
- name: String;
- is_active_in_organization_id: Id;
- is_locked: boolean;
- motion_submitter_ids: Id[];
- assignment_candidate_ids: Id[];
- speaker_ids: Id[];
- locked_out: boolean;
+ id: Id,
+ name: String,
+ is_active_in_organization_id: Id,
+ is_locked: boolean,
+ motion_submitter_ids: Id[],
+ assignment_candidate_ids: Id[],
+ speaker_ids: Id[],
+ locked_out: boolean
}]
}
}
diff --git a/docs/presenters/get_user_scope.md b/docs/presenters/get_user_scope.md
index 928d8fc250..0119f79b2e 100644
--- a/docs/presenters/get_user_scope.md
+++ b/docs/presenters/get_user_scope.md
@@ -1,21 +1,22 @@
## Payload
-```js
+```
{
- user_ids: Id[];
+ user_ids: Id[] // required
}
```
## Returns
-```js
+```
{
user_id: Id: {
collection: String, # one of "meeting", "committee" or "organization"
id: Id,
user_oml: String, # one of "superadmin", "can_manage_organization", "can_manage_users", ""
- committee_ids: int[] // Ids of all committees the user is part of
- }
+ committee_ids: Id[] // Ids of all committees the user is part of
+ },
+ ...
}
```
diff --git a/docs/presenters/get_users.md b/docs/presenters/get_users.md
index b20707f5f4..65b147a663 100644
--- a/docs/presenters/get_users.md
+++ b/docs/presenters/get_users.md
@@ -1,24 +1,25 @@
# Payload
-```
+```js
{
+ // optional
start_index: number,
entries: number,
sort_criteria: string[], // can contain ["username", "first_name", "last_name"],
reverse: boolean,
- filter?: string,
+ filter?: string
}
```
# Returns
-```
+```js
[
{
id: Id,
username: string,
first_name: string,
- last_name: string,
+ last_name: string
},
...
]
@@ -28,4 +29,4 @@
The request user needs OML `can_manage_users` or higher. Otherwise an error is returned.
-Returns all users, that have `filer` in `username`, `first_name`, `last_name`. If filter is `null`, all users are returned. The users are sorted by `sort_criteria`. If it is not given, the default is `["username", "first_name", "last_name"]`. If `reverse` is true, the order is reversed. Lastly, the users are paginated beginning at `start_index` with at max `entries` number of users.
+Returns all users, that have `filter` in `username`, `first_name`, `last_name`. If filter is `null`, all users are returned. The users are sorted by `sort_criteria`. If it is not given, the default is `["username", "first_name", "last_name"]`. If `reverse` is true, the order is reversed. Lastly, the users can be paginated beginning at `start_index` with at max `entries` number of users.
diff --git a/docs/presenters/search_deleted_models.md b/docs/presenters/search_deleted_models.md
deleted file mode 100644
index 3abb15b104..0000000000
--- a/docs/presenters/search_deleted_models.md
+++ /dev/null
@@ -1,36 +0,0 @@
-## Payload
-
-```js
-{
- collection: string,
- filter_string: string,
- meeting_id: Id,
-}
-```
-
-## Presenter
-
-Searches all deleted models of the given collection in the given meeting for the given filter string. The fields which
-are searched differ from collection to collection:
-```
-{
- "assignment": ["title"],
- "motion": ["number", "title"],
- "user": [
- "username",
- "first_name",
- "last_name",
- "title",
- "pronoun",
- "structure_level",
- "number",
- "email",
- ],
-}
-```
-These 3 are also the only allowed collections. The result list is returned as a mapping from the
-model's id to the searched fields of the model (see above).
-
-## Permissions
-
-TODO
diff --git a/docs/presenters/search_for_id_by_external_id.md b/docs/presenters/search_for_id_by_external_id.md
index ebbd42a818..b90145c899 100644
--- a/docs/presenters/search_for_id_by_external_id.md
+++ b/docs/presenters/search_for_id_by_external_id.md
@@ -2,22 +2,22 @@
```js
{
// required
- collection: string;
- external_id: string;
- context_id: Id;
+ collection: string,
+ external_id: string,
+ context_id: Id
}
```
## Returns
```js
{
- id: Id;
+ id: Id
}
```
in the case one id is found.
```js
{
- id: null;
- error: string;
+ id: null,
+ error: string
}
```
else.
diff --git a/docs/presenters/search_users.md b/docs/presenters/search_users.md
index 416cafead2..a220075874 100644
--- a/docs/presenters/search_users.md
+++ b/docs/presenters/search_users.md
@@ -2,6 +2,7 @@
```js
{
+ // required
permission_type: "meeting" | "committee" | "organization"
permission_id: number, // Id of permission scope object
search: {
@@ -10,7 +11,7 @@
"first_name": string,
"last_name": string,
"email": string,
- "member_number": string,
+ "member_number": string
}[]
}
```
@@ -24,7 +25,7 @@
"first_name": string,
"last_name": string,
"email": string,
- "member_number": string,
+ "member_number": string
}[][]
```
A double array: The outer array has the same length as the request's `search` array and contains
diff --git a/global/meta b/global/meta
deleted file mode 160000
index f95f007fcb..0000000000
--- a/global/meta
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit f95f007fcb0761a581063b973b492e216dbf4c07
diff --git a/meta b/meta
new file mode 160000
index 0000000000..ab9fa3bd04
--- /dev/null
+++ b/meta
@@ -0,0 +1 @@
+Subproject commit ab9fa3bd042f21160e4912eaa02b31f1879f202e
diff --git a/openslides_backend/action/actions/assignment/create_update_delete.py b/openslides_backend/action/actions/assignment/create_update_delete.py
index 4ee9582ad3..b268799b5d 100644
--- a/openslides_backend/action/actions/assignment/create_update_delete.py
+++ b/openslides_backend/action/actions/assignment/create_update_delete.py
@@ -1,5 +1,9 @@
+from typing import Any
+
from ....models.models import Assignment
from ....permissions.permissions import Permissions
+from ....services.datastore.commands import GetManyRequest
+from ....shared.patterns import fqid_from_collection_and_id
from ....shared.schema import id_list_schema
from ...action_set import ActionSet
from ...generics.update import UpdateAction
@@ -17,6 +21,7 @@
CreateActionWithListOfSpeakersMixin,
)
from ..meeting_mediafile.attachment_mixin import AttachmentMixin
+from ..speaker.create import SpeakerCreateAction
class AssignmentCreate(
@@ -30,7 +35,58 @@ class AssignmentCreate(
class AssignmentUpdate(AttachmentMixin, UpdateAction):
- pass
+ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
+ if instance.get("phase") == "voting":
+ assignment = self.datastore.get(
+ fqid_from_collection_and_id("assignment", instance["id"]),
+ ["meeting_id", "candidate_ids", "list_of_speakers_id", "phase"],
+ )
+ if (
+ assignment.get("phase") != "voting"
+ and (candidate_ids := assignment.get("candidate_ids"))
+ and self.datastore.get(
+ fqid_from_collection_and_id("meeting", assignment["meeting_id"]),
+ ["assignment_poll_add_candidates_to_list_of_speakers"],
+ ).get("assignment_poll_add_candidates_to_list_of_speakers")
+ ):
+ speaker_ids = self.datastore.get(
+ fqid_from_collection_and_id(
+ "list_of_speakers", assignment["list_of_speakers_id"]
+ ),
+ ["speaker_ids"],
+ ).get("speaker_ids", [])
+ pre_existing_speakers_meeting_user_ids = [
+ pre_existing_speaker["meeting_user_id"]
+ for pre_existing_speaker in self.datastore.get_many(
+ [
+ GetManyRequest(
+ "speaker",
+ speaker_ids,
+ ["meeting_user_id"],
+ )
+ ]
+ )["speaker"].values()
+ ]
+ payloads = [
+ {
+ "list_of_speakers_id": assignment["list_of_speakers_id"],
+ "meeting_user_id": candidate["meeting_user_id"],
+ }
+ for candidate in self.datastore.get_many(
+ [
+ GetManyRequest(
+ "assignment_candidate",
+ candidate_ids,
+ ["meeting_user_id"],
+ )
+ ]
+ )["assignment_candidate"].values()
+ if candidate["meeting_user_id"]
+ not in pre_existing_speakers_meeting_user_ids
+ ]
+ for payload in payloads:
+ self.execute_other_action(SpeakerCreateAction, [payload])
+ return super().update_instance(instance)
@register_action_set("assignment")
diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py
index 27528d0020..d267bce632 100644
--- a/openslides_backend/action/actions/meeting/update.py
+++ b/openslides_backend/action/actions/meeting/update.py
@@ -4,6 +4,8 @@
CheckUniqueInContextMixin,
)
+from ....i18n.translator import Translator
+from ....i18n.translator import translate as _
from ....models.models import Meeting
from ....permissions.management_levels import (
CommitteeManagementLevel,
@@ -101,6 +103,7 @@
"motions_enable_text_on_projector",
"motions_enable_reason_on_projector",
"motions_enable_sidebox_on_projector",
+ "motions_create_enable_additional_submitter_text",
"motions_hide_metadata_background",
"motions_enable_recommendation_on_projector",
"motions_show_referring_motions",
@@ -214,9 +217,15 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
set_as_template = instance.pop("set_as_template", None)
db_meeting = self.datastore.get(
fqid_from_collection_and_id("meeting", instance["id"]),
- ["template_for_organization_id", "locked_from_inside", "admin_group_id"],
+ [
+ "template_for_organization_id",
+ "locked_from_inside",
+ "admin_group_id",
+ "language",
+ ],
lock_result=False,
)
+ Translator.set_translation_language(db_meeting["language"])
lock_meeting = (
instance.get("locked_from_inside")
if instance.get("locked_from_inside") is not None
@@ -311,7 +320,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
if instance.get("enable_anonymous") and not anonymous_group_id:
group_result = self.execute_other_action(
GroupCreate,
- [{"name": "Public", "weight": 0, "meeting_id": instance["id"]}],
+ [{"name": _("Public"), "weight": 0, "meeting_id": instance["id"]}],
)
instance["anonymous_group_id"] = anonymous_group_id = cast(
list[dict[str, Any]], group_result
diff --git a/openslides_backend/action/actions/meeting_user/create.py b/openslides_backend/action/actions/meeting_user/create.py
index 929cfc4083..5b08281db6 100644
--- a/openslides_backend/action/actions/meeting_user/create.py
+++ b/openslides_backend/action/actions/meeting_user/create.py
@@ -54,24 +54,22 @@ def get_history_information(self) -> HistoryInformation | None:
information = {}
for instance in self.instances:
instance_information = []
- if "group_ids" in instance:
- if len(instance["group_ids"]) == 1:
- instance_information.extend(
- [
- "Participant added to group {} in meeting {}",
- fqid_from_collection_and_id(
- "group", instance["group_ids"][0]
- ),
- ]
+ fqids_per_collection = {
+ collection_name: [
+ fqid_from_collection_and_id(
+ collection_name,
+ _id,
)
- else:
- instance_information.append(
- "Participant added to multiple groups in meeting {}",
- )
- else:
- instance_information.append(
- "Participant added to meeting {}",
- )
+ for _id in ids
+ ]
+ for collection_name in ["group", "structure_level"]
+ if (ids := instance.get(f"{collection_name}_ids"))
+ }
+ instance_information.append(
+ self.compose_history_string(list(fqids_per_collection.items()))
+ )
+ for collection_name, fqids in fqids_per_collection.items():
+ instance_information.extend(fqids)
instance_information.append(
fqid_from_collection_and_id("meeting", instance["meeting_id"]),
)
@@ -79,3 +77,29 @@ def get_history_information(self) -> HistoryInformation | None:
instance_information
)
return information
+
+ def compose_history_string(
+ self, fqids_per_collection: list[tuple[str, list[str]]]
+ ) -> str:
+ """
+ Composes a string of the shape:
+ Participant added to groups {}, {} and structure levels {} in meeting {}.
+ """
+ middle_sentence_parts = [
+ " ".join(
+ [ # prefix and to collection name if it's not the first in list
+ ("and " if collection_name != fqids_per_collection[0][0] else "")
+ + collection_name.replace("_", " ") # replace for human readablity
+ + ("s" if len(fqids) != 1 else ""), # plural s
+ ", ".join(["{}" for _ in range(len(fqids))]),
+ ]
+ )
+ for collection_name, fqids in fqids_per_collection
+ ]
+ return " ".join(
+ [
+ "Participant added to",
+ *middle_sentence_parts,
+ ("in " if fqids_per_collection else "") + "meeting {}.",
+ ]
+ )
diff --git a/openslides_backend/action/actions/meeting_user/delete.py b/openslides_backend/action/actions/meeting_user/delete.py
index c4ac6fb585..da82a4a221 100644
--- a/openslides_backend/action/actions/meeting_user/delete.py
+++ b/openslides_backend/action/actions/meeting_user/delete.py
@@ -34,9 +34,11 @@ def get_history_information(self) -> HistoryInformation | None:
def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
meeting_user = self.datastore.get(
- fqid_from_collection_and_id("meeting_user", instance["id"]), ["speaker_ids"]
+ fqid_from_collection_and_id("meeting_user", instance["id"]),
+ ["speaker_ids", "user_id", "meeting_id"],
)
speaker_ids = meeting_user.get("speaker_ids", [])
self.conditionally_delete_speakers(speaker_ids)
+ self.remove_presence(meeting_user["user_id"], meeting_user["meeting_id"])
return super().update_instance(instance)
diff --git a/openslides_backend/action/actions/motion/__init__.py b/openslides_backend/action/actions/motion/__init__.py
index f4b731bfe1..9f504ec2a3 100644
--- a/openslides_backend/action/actions/motion/__init__.py
+++ b/openslides_backend/action/actions/motion/__init__.py
@@ -3,8 +3,6 @@
create_forwarded,
delete,
follow_recommendation,
- import_,
- json_upload,
reset_recommendation,
reset_state,
set_recommendation,
diff --git a/openslides_backend/action/actions/motion/create.py b/openslides_backend/action/actions/motion/create.py
index 2a7128b83c..9522ed717f 100644
--- a/openslides_backend/action/actions/motion/create.py
+++ b/openslides_backend/action/actions/motion/create.py
@@ -1,4 +1,5 @@
-from typing import Any
+from collections import defaultdict
+from typing import Any, cast
from ....models.models import Motion
from ....permissions.base_classes import Permission
@@ -146,14 +147,30 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
if not has_perm(self.datastore, self.user_id, perm, instance["meeting_id"]):
raise MissingPermission(perm)
- # whitelist the fields depending on the user's permissions
- whitelist = []
- forbidden_fields = set()
- perm = Permissions.Mediafile.CAN_SEE
- if has_perm(self.datastore, self.user_id, perm, instance["meeting_id"]):
- whitelist.append("attachment_mediafile_ids")
- elif "attachment_mediafile_ids" in instance:
- forbidden_fields.add("attachment_mediafile_ids")
+ # Whitelist the fields depending on the user's permissions. Each field can require multiple conjunctive permissions.
+ can_manage_whitelist = set()
+ forbidden_fields = defaultdict(set)
+ permission_to_fields: dict[Permission, list[str]] = {
+ Permissions.AgendaItem.CAN_MANAGE: list(agenda_creation_properties.keys()),
+ Permissions.Mediafile.CAN_SEE: ["attachment_mediafile_ids"],
+ Permissions.Motion.CAN_MANAGE_METADATA: [
+ "additional_submitter",
+ "submitter_ids",
+ ],
+ Permissions.User.CAN_SEE: ["submitter_ids"],
+ }
+ for perm, fields in permission_to_fields.items():
+ has_permission = has_perm(
+ self.datastore, self.user_id, perm, instance["meeting_id"]
+ )
+ for field in fields:
+ if has_permission:
+ if field not in forbidden_fields:
+ can_manage_whitelist.add(field)
+ else:
+ if field in instance:
+ forbidden_fields[field].add(perm)
+ can_manage_whitelist.discard(field)
perm = Permissions.Motion.CAN_MANAGE
if (
@@ -162,24 +179,26 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
)
== []
):
- whitelist += [
- "title",
- "text",
- "reason",
- "lead_motion_id",
- "amendment_paragraphs",
- "category_id",
- "workflow_id",
- "id",
- "meeting_id",
- ]
+ can_manage_whitelist.update(
+ [
+ "title",
+ "text",
+ "reason",
+ "lead_motion_id",
+ "amendment_paragraphs",
+ "category_id",
+ "workflow_id",
+ "id",
+ "meeting_id",
+ ]
+ )
if instance.get("lead_motion_id"):
- whitelist.remove("category_id")
+ can_manage_whitelist.discard("category_id")
for field in instance:
- if field not in whitelist:
- forbidden_fields.add(field)
+ if field not in can_manage_whitelist:
+ forbidden_fields[field].add(perm)
if forbidden_fields:
msg = f"You are not allowed to perform action {self.name}. "
- msg += f"Forbidden fields: {', '.join(forbidden_fields)}"
+ msg += f"Forbidden fields: {', '.join(field + ' with possibly needed permission(s): ' + ', '.join(perm for perm in sorted(cast(list[str], perms))) for field, perms in forbidden_fields.items())}"
raise PermissionDenied(msg)
diff --git a/openslides_backend/action/actions/motion/create_base.py b/openslides_backend/action/actions/motion/create_base.py
index c930f78efb..d676c119fa 100644
--- a/openslides_backend/action/actions/motion/create_base.py
+++ b/openslides_backend/action/actions/motion/create_base.py
@@ -50,8 +50,8 @@ def set_state_from_workflow(
)
def create_submitters(self, instance: dict[str, Any]) -> None:
- submitter_ids = instance.pop("submitter_ids", None)
- if not submitter_ids:
+ submitter_ids = instance.pop("submitter_ids", [])
+ if not submitter_ids and not instance.get("additional_submitter"):
submitter_ids = [self.user_id]
self.apply_instance(instance)
weight = 1
diff --git a/openslides_backend/action/actions/motion/import_.py b/openslides_backend/action/actions/motion/import_.py
deleted file mode 100644
index 3e17e5d6c6..0000000000
--- a/openslides_backend/action/actions/motion/import_.py
+++ /dev/null
@@ -1,635 +0,0 @@
-from typing import Any, cast
-
-from openslides_backend.action.mixins.import_mixins import (
- BaseImportAction,
- ImportRow,
- ImportState,
- Lookup,
- ResultType,
-)
-from openslides_backend.action.util.register import register_action
-from openslides_backend.permissions.permissions import Permissions
-from openslides_backend.shared.exceptions import ActionException
-from openslides_backend.shared.filters import And, Filter, FilterOperator, Or
-
-from ....models.models import ImportPreview
-from ....shared.patterns import fqid_from_collection_and_id
-from ....shared.schema import required_id_schema
-from ...util.default_schema import DefaultSchema
-from ..meeting_user.create import MeetingUserCreate
-from ..motion_submitter.create import MotionSubmitterCreateAction
-from ..motion_submitter.delete import MotionSubmitterDeleteAction
-from ..motion_submitter.sort import MotionSubmitterSort
-from .create import MotionCreate
-from .payload_validation_mixin import (
- MotionActionErrorData,
- MotionCreatePayloadValidationMixin,
- MotionErrorType,
- MotionUpdatePayloadValidationMixin,
-)
-from .update import MotionUpdate
-
-
-@register_action("motion.import")
-class MotionImport(
- BaseImportAction,
- MotionCreatePayloadValidationMixin,
- MotionUpdatePayloadValidationMixin,
-):
- """
- Action to import a result from the import_preview.
- """
-
- model = ImportPreview()
- schema = DefaultSchema(ImportPreview()).get_default_schema(
- additional_required_fields={
- "id": required_id_schema,
- "import": {"type": "boolean"},
- }
- )
- permission = Permissions.Motion.CAN_MANAGE
- skip_archived_meeting_check = True
- import_name = "motion"
- number_lookup: Lookup
- username_lookup: dict[str, list[dict[str, Any]]]
- category_lookup: dict[str, list[dict[str, Any]]]
- tags_lookup: dict[str, list[dict[str, Any]]]
- block_lookup: dict[str, list[dict[str, Any]]]
- _user_ids_to_meeting_user: dict[int, Any]
- _submitter_ids_to_user_id: dict[int, int]
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- if not instance["import"]:
- return {}
-
- instance = super().update_instance(instance)
- meeting_id = self.get_meeting_id(instance)
- self.setup_lookups(meeting_id)
-
- self.rows = [self.validate_entry(row) for row in self.result["rows"]]
-
- if self.import_state != ImportState.ERROR:
- create_action_payload: list[dict[str, Any]] = []
- update_action_payload: list[dict[str, Any]] = []
- submitter_create_action_payload: list[dict[str, Any]] = []
- submitter_delete_action_payload: list[dict[str, Any]] = []
-
- motion_to_submitter_user_ids: dict[int, list[int]] = {}
- old_submitters: dict[int, dict[int, int]] = (
- {}
- ) # {motion_id: {user_id:submitter_id}}
- for row in self.rows:
- payload: dict[str, Any] = row["data"].copy()
- used_list = ["text", "reason", "title", "number"]
- for field in used_list:
- if field in payload:
- if type(dvalue := payload[field]) is dict:
- payload[field] = dvalue["value"]
- self.remove_fields_from_data(
- payload,
- ["submitters_verbose", "supporters_verbose", "motion_amendment"],
- )
- if (category := payload.pop("category_name", None)) and category[
- "info"
- ] == ImportState.DONE:
- payload["category_id"] = (
- category["id"] if category.get("id") else None
- )
- if (block := payload.pop("block", None)) and block[
- "info"
- ] == ImportState.DONE:
- payload["block_id"] = block["id"] if block.get("id") else None
- payload["tag_ids"] = self.get_ids_from_object_list(
- payload.pop("tags", [])
- )
- meeting_users_to_create = [
- {"user_id": submitter["id"], "meeting_id": meeting_id}
- for submitter in payload["submitters_username"]
- if submitter["info"] == ImportState.GENERATED
- and submitter["id"] not in self._user_ids_to_meeting_user.keys()
- ]
- if len(meeting_users_to_create):
- meeting_users = cast(
- list[dict[str, int]],
- self.execute_other_action(
- MeetingUserCreate, meeting_users_to_create
- ),
- )
- for i in range(len(meeting_users)):
- self._user_ids_to_meeting_user[
- meeting_users_to_create[i]["user_id"]
- ] = meeting_users[i]
- submitters = self.get_ids_from_object_list(
- payload.pop("submitters_username")
- )
- supporters = [
- self._user_ids_to_meeting_user[supporter_id]["id"]
- for supporter_id in self.get_ids_from_object_list(
- payload.pop("supporters_username", [])
- )
- ]
- payload["supporter_meeting_user_ids"] = supporters
- payload.pop("category_prefix", None)
- errors: list[MotionActionErrorData] = []
- if row["state"] == ImportState.NEW:
- payload.update({"submitter_ids": submitters})
- create_action_payload.append(payload)
- errors = self.get_create_payload_integrity_error_message(
- payload, meeting_id
- )
- else:
- id_ = payload["id"]
- motion_to_submitter_user_ids[id_] = submitters
- motion = {
- k: v
- for k, v in (
- [
- motion
- for motion in self.number_lookup.name_to_ids.get(
- payload["number"], []
- )
- if motion.get("id")
- ][0]
- ).items()
- }
- for field in ["category_id", "block_id"]:
- if payload.get(field) is None:
- if not motion.get(field):
- payload.pop(field, None)
- if len(submitters):
- motion_submitter_ids: list[int] = (
- motion.get("submitter_ids", []) or []
- )
- matched_submitters = {
- self._submitter_ids_to_user_id[submitter_id]: submitter_id
- for submitter_id in motion_submitter_ids
- if self._submitter_ids_to_user_id.get(submitter_id)
- in submitters
- }
- submitter_create_action_payload.extend(
- [
- {
- "meeting_user_id": self._user_ids_to_meeting_user[
- user_id
- ]["id"],
- "motion_id": id_,
- }
- for user_id in submitters
- if user_id not in matched_submitters.keys()
- ]
- )
- submitter_delete_action_payload.extend(
- [
- {"id": submitter_id}
- for submitter_id in motion_submitter_ids
- if submitter_id not in matched_submitters.values()
- ]
- )
- old_submitters[id_] = matched_submitters
-
- payload.pop("meeting_id", None)
- update_action_payload.append(payload)
- errors = self.get_update_payload_integrity_error_message(
- payload, meeting_id
- )
- for err in errors:
- if err["type"] != MotionErrorType.REASON:
- raise ActionException("Error: " + err["message"])
- if not (
- row["data"].get("reason")
- and isinstance(row["data"]["reason"], dict)
- ):
- row["data"]["reason"] = {
- "value": row["data"].get("reason", ""),
- "info": ImportState.ERROR,
- }
- else:
- row["data"]["reason"]["info"] = ImportState.ERROR
- row["data"]["reason"].pop("id", 0)
- row["messages"].append("Error: " + err["message"])
- row["state"] = ImportState.ERROR
- self.import_state = ImportState.ERROR
- if self.import_state != ImportState.ERROR:
- created_submitters: list[dict[str, int]] = []
- if create_action_payload:
- self.execute_other_action(MotionCreate, create_action_payload)
- if update_action_payload:
- self.execute_other_action(MotionUpdate, update_action_payload)
- if len(submitter_create_action_payload):
- created_submitters = cast(
- list[dict[str, int]],
- self.execute_other_action(
- MotionSubmitterCreateAction, submitter_create_action_payload
- ),
- )
- if len(submitter_delete_action_payload):
- self.execute_other_action(
- MotionSubmitterDeleteAction, submitter_delete_action_payload
- )
- new_submitters: dict[int, dict[int, int]] = (
- {}
- ) # {motion_id: {meeting_user_id:submitter_id}}
- for i in range(len(created_submitters)):
- motion_id = submitter_create_action_payload[i]["motion_id"]
- new_submitters[motion_id] = {
- **new_submitters.get(motion_id, {}),
- submitter_create_action_payload[i][
- "meeting_user_id"
- ]: created_submitters[i]["id"],
- }
- sort_payload: list[dict[str, Any]] = []
- for motion_id in motion_to_submitter_user_ids:
- sorted_motion_submitter_ids: list[int] = []
- for submitter_user_id in motion_to_submitter_user_ids[motion_id]:
- meeting_user_id = cast(
- int, self._user_ids_to_meeting_user[submitter_user_id]["id"]
- )
- if (
- submitter_user_id
- in old_submitters.get(motion_id, {}).keys()
- ):
- sorted_motion_submitter_ids.append(
- old_submitters[motion_id][submitter_user_id]
- )
- elif (
- meeting_user_id in new_submitters.get(motion_id, {}).keys()
- ):
- sorted_motion_submitter_ids.append(
- new_submitters[motion_id][meeting_user_id]
- )
- else:
- raise Exception(
- f"Submitter sorting failed due to submitter for user/{submitter_user_id} not being found"
- )
- if len(sorted_motion_submitter_ids):
- sort_payload.append(
- {
- "motion_id": motion_id,
- "motion_submitter_ids": sorted_motion_submitter_ids,
- }
- )
- for payload in sort_payload:
- self.execute_other_action(MotionSubmitterSort, [payload])
-
- return {}
-
- def get_ids_from_object_list(self, object_list: list[dict[str, Any]]) -> list[int]:
- return [
- obj["id"]
- for obj in object_list
- if obj.get("info") != ImportState.WARNING
- and obj.get("info") != ImportState.ERROR
- ]
-
- def remove_fields_from_data(
- self, data: dict[str, Any], fieldnames: list[str]
- ) -> None:
- for fieldname in fieldnames:
- data.pop(fieldname, None)
-
- def validate_entry(self, row: ImportRow) -> ImportRow:
- entry = row["data"]
-
- if ("id" in entry) != ("id" in entry.get("number", {})):
- raise ActionException(
- f"Invalid JsonUpload data: A data row with state '{ImportState.DONE}' must have an 'id'"
- )
-
- number = self.get_value_from_union_str_object(entry.get("number"))
- if number:
- check_result = self.number_lookup.check_duplicate(number)
- id_ = cast(int, self.number_lookup.get_field_by_name(number, "id"))
-
- if check_result == ResultType.FOUND_ID and id_ != 0:
- if row["state"] != ImportState.DONE:
- row["messages"].append(
- f"Error: Row state expected to be '{ImportState.DONE}', but it is '{row['state']}'."
- )
- row["state"] = ImportState.ERROR
- entry["number"]["info"] = ImportState.ERROR
- elif entry["id"] != id_:
- row["state"] = ImportState.ERROR
- entry["number"]["info"] = ImportState.ERROR
- row["messages"].append(
- f"Error: Number '{number}' found in different id ({id_} instead of {entry['id']})"
- )
- elif check_result == ResultType.FOUND_MORE_IDS:
- row["state"] = ImportState.ERROR
- entry["number"]["info"] = ImportState.ERROR
- row["messages"].append(
- f"Error: Number '{number}' is duplicated in import."
- )
- elif check_result == ResultType.NOT_FOUND_ANYMORE:
- row["messages"].append(
- f"Error: Motion {entry['number']['id']} not found anymore for updating motion '{number}'."
- )
- row["state"] = ImportState.ERROR
-
- category_name = self.get_value_from_union_str_object(entry.get("category_name"))
- if category_name and entry["category_name"].get("info") == ImportState.DONE:
- category_prefix = entry.get("category_prefix") or None
- if "id" not in entry["category_name"]:
- raise ActionException(
- f"Invalid JsonUpload data: A category_name entry with state '{ImportState.DONE}' must have an 'id'"
- )
- categories = self.category_lookup.get(category_name, [])
- categories = [
- category
- for category in categories
- if category.get("prefix") == category_prefix
- ]
- if len(categories) > 0:
- if not any(
- [
- category.get("id") == entry["category_name"].get("id")
- for category in categories
- ]
- ):
- row["messages"].append(
- "Error: Category search didn't deliver the same result as in the preview"
- )
- entry["category_name"] = {
- "value": category_name,
- "info": ImportState.ERROR,
- }
- row["state"] = ImportState.ERROR
- else:
- entry["category_name"] = {
- "value": category_name,
- "info": ImportState.ERROR,
- }
- row["state"] = ImportState.ERROR
- row["messages"].append("Error: Category could not be found anymore")
-
- block = self.get_value_from_union_str_object(entry.get("block"))
- if block and entry["block"].get("info") == ImportState.DONE:
- if "id" not in entry["block"]:
- raise ActionException(
- f"Invalid JsonUpload data: A block entry with state '{ImportState.DONE}' must have an 'id'"
- )
- found_blocks = self.block_lookup.get(block, [])
- if len(found_blocks) > 0:
- if not any(
- [block.get("id") == entry["block"]["id"] for block in found_blocks]
- ):
- entry["block"] = {"value": block, "info": ImportState.ERROR}
- row["messages"].append(
- "Error: Motion block search didn't deliver the same result as in the preview"
- )
- row["state"] = ImportState.ERROR
- else:
- entry["block"] = {
- "value": block,
- "info": ImportState.ERROR,
- }
- row["messages"].append("Error: Couldn't find motion block anymore")
- row["state"] = ImportState.ERROR
-
- if isinstance(entry.get("tags"), list):
- different: list[str] = []
- not_found: list[str] = []
- for tag_entry in entry.get("tags", []):
- tag = self.get_value_from_union_str_object(tag_entry)
- if tag and tag_entry.get("info") == ImportState.DONE:
- if "id" not in tag_entry:
- raise ActionException(
- f"Invalid JsonUpload data: A tag entry with state '{ImportState.DONE}' must have an 'id'"
- )
- found_tags = self.tags_lookup.get(tag, [])
- if len(found_tags) > 0:
- if not any(
- [tag.get("id") == tag_entry["id"] for tag in found_tags]
- ):
- tag_entry["info"] = ImportState.ERROR
- tag_entry.pop("id")
- different.append(tag)
- else:
- tag_entry["info"] = ImportState.ERROR
- tag_entry.pop("id")
- not_found.append(tag)
- if len(different):
- row["messages"].append(
- "Error: Tag search didn't deliver the same result as in the preview: "
- + ", ".join(different)
- )
- row["state"] = ImportState.ERROR
- if len(not_found):
- row["messages"].append(
- "Error: Couldn't find tag anymore: " + ", ".join(not_found)
- )
- row["state"] = ImportState.ERROR
-
- for fieldname in ["submitter", "supporter"]:
- if isinstance(entry.get(f"{fieldname}s_username"), list):
- different = []
- not_found = []
- for user_entry in entry.get(f"{fieldname}s_username", []):
- user = self.get_value_from_union_str_object(user_entry)
- if user and (
- user_entry.get("info") == ImportState.DONE
- or user_entry.get("info") == ImportState.GENERATED
- ):
- if "id" not in user_entry:
- raise ActionException(
- f"Invalid JsonUpload data: A {fieldname} entry with state '{ImportState.DONE}' or '{ImportState.GENERATED}' must have an 'id'"
- )
- found_users = self.username_lookup.get(user, [])
- if len(found_users) == 1:
- if found_users[0].get("id") != user_entry["id"]:
- user_entry["info"] = ImportState.ERROR
- user_entry.pop("id")
- different.append(user)
- elif len(found_users) > 1:
- raise ActionException(
- f"Database corrupt: Found multiple users with the username {user}."
- )
- else:
- user_entry["info"] = ImportState.ERROR
- user_entry.pop("id")
- not_found.append(user)
- if len(different):
- row["messages"].append(
- f"Error: {fieldname[0].capitalize() + fieldname[1:]} search didn't deliver the same result as in the preview: "
- + ", ".join(different)
- )
- row["state"] = ImportState.ERROR
- if len(not_found):
- row["messages"].append(
- f"Error: Couldn't find {fieldname} anymore: "
- + ", ".join(not_found)
- )
- row["state"] = ImportState.ERROR
-
- row["messages"] = list(set(row["messages"]))
-
- if row["state"] == ImportState.ERROR and self.import_state == ImportState.DONE:
- self.import_state = ImportState.ERROR
- return {
- "state": row["state"],
- "data": row["data"],
- "messages": row.get("messages", []),
- }
-
- def setup_lookups(self, meeting_id: int) -> None:
- rows = self.result["rows"]
- self.number_lookup = Lookup(
- self.datastore,
- "motion",
- [
- (entry["number"]["value"], entry)
- for row in rows
- if "number" in (entry := row["data"])
- and entry["number"].get("info") != ImportState.WARNING
- ],
- field="number",
- mapped_fields=["submitter_ids", "category_id", "block_id"],
- global_and_filter=FilterOperator("meeting_id", "=", meeting_id),
- )
- self.block_lookup = self.get_lookup_dict(
- "motion_block",
- [
- entry["block"]["value"]
- for row in rows
- if "block" in (entry := row["data"])
- and entry["block"].get("info") != ImportState.WARNING
- ],
- "title",
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
- self.category_lookup = self.get_lookup_dict(
- "motion_category",
- [
- entry["category_name"]["value"]
- for row in rows
- if "category_name" in (entry := row["data"])
- and entry["category_name"].get("info") != ImportState.WARNING
- ],
- "name",
- ["prefix"],
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
-
- self.username_lookup = self.get_lookup_dict(
- "user",
- list(
- {
- user["value"]
- for row in rows
- if (
- users := [
- *row["data"].get("submitters_username", []),
- *row["data"].get("supporters_username", []),
- ]
- )
- for user in users
- if user and user.get("info") != ImportState.WARNING
- }
- ),
- "username",
- ["meeting_ids", "meeting_user_ids"],
- )
- self.username_lookup = {
- username: [
- date
- for date in self.username_lookup[username]
- if (
- date.get("meeting_ids")
- and (meeting_id in date["meeting_ids"])
- or date.get("id") == self.user_id
- )
- ]
- for username in self.username_lookup
- }
- all_user_ids = list(
- [
- submitter["id"]
- for submitters in self.username_lookup.values()
- for submitter in submitters
- ]
- )
- all_meeting_users: dict[int, dict[str, Any]] = {}
- if len(all_user_ids):
- all_meeting_users = self.datastore.filter(
- "meeting_user",
- And(
- FilterOperator("meeting_id", "=", meeting_id),
- Or(
- *[
- FilterOperator("user_id", "=", user_id)
- for user_id in all_user_ids
- ]
- ),
- ),
- [
- "id",
- "user_id",
- "motion_submitter_ids",
- "supported_motion_ids",
- ],
- lock_result=False,
- )
- self._user_ids_to_meeting_user = {
- all_meeting_users[meeting_user_id]["user_id"]: all_meeting_users[
- meeting_user_id
- ]
- for meeting_user_id in all_meeting_users
- if all_meeting_users[meeting_user_id].get("user_id")
- }
- self._submitter_ids_to_user_id = {
- submitter_id: all_meeting_users[meeting_user_id]["user_id"]
- for meeting_user_id in all_meeting_users
- for submitter_id in (
- all_meeting_users[meeting_user_id].get("motion_submitter_ids", []) or []
- )
- if all_meeting_users[meeting_user_id].get("user_id")
- }
- self.tags_lookup = self.get_lookup_dict(
- "tag",
- [
- tag["value"]
- for row in rows
- if "tags" in (entry := row["data"])
- for tag in entry["tags"]
- if tag.get("info") != ImportState.WARNING
- ],
- "name",
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
-
- def get_lookup_dict(
- self,
- collection: str,
- entries: list[str],
- fieldname: str = "name",
- mapped_fields: list[str] = [],
- and_filters: list[Filter] = [],
- ) -> dict[str, list[dict[str, Any]]]:
- lookup: dict[str, list[dict[str, Any]]] = {}
- if len(entries):
- data = self.datastore.filter(
- collection,
- And(
- *and_filters,
- Or([FilterOperator(fieldname, "=", name) for name in set(entries)]),
- ),
- [*mapped_fields, "id", fieldname],
- lock_result=False,
- )
- for date_id in data:
- date = data[date_id]
- lookup[date[fieldname]] = [
- *lookup.get(date[fieldname], []),
- date,
- ]
- return lookup
-
- def get_meeting_id(self, instance: dict[str, Any]) -> int:
- store_id = instance["id"]
- worker = self.datastore.get(
- fqid_from_collection_and_id("import_preview", store_id),
- ["name", "result"],
- lock_result=False,
- )
- if worker.get("name") == self.import_name:
- return next(iter(worker.get("result", {})["rows"]))["data"]["meeting_id"]
- raise ActionException("Import data cannot be found.")
diff --git a/openslides_backend/action/actions/motion/json_upload.py b/openslides_backend/action/actions/motion/json_upload.py
deleted file mode 100644
index fcb5741a41..0000000000
--- a/openslides_backend/action/actions/motion/json_upload.py
+++ /dev/null
@@ -1,599 +0,0 @@
-from collections import defaultdict
-from collections.abc import Iterable
-from re import search, sub
-from typing import Any, cast
-
-from openslides_backend.shared.filters import And, Filter, FilterOperator, Or
-
-from ....models.models import Motion
-from ....permissions.permissions import Permissions
-from ....shared.exceptions import ActionException
-from ....shared.schema import required_id_schema
-from ...mixins.import_mixins import (
- BaseJsonUploadAction,
- ImportState,
- Lookup,
- ResultType,
-)
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from .payload_validation_mixin import (
- MotionActionErrorData,
- MotionCreatePayloadValidationMixin,
- MotionErrorType,
- MotionUpdatePayloadValidationMixin,
-)
-
-LIST_TYPE = {
- "anyOf": [
- {
- "type": "array",
- "items": {"type": "string"},
- },
- {"type": "string"},
- ]
-}
-
-
-@register_action("motion.json_upload")
-class MotionJsonUpload(
- BaseJsonUploadAction,
- MotionCreatePayloadValidationMixin,
- MotionUpdatePayloadValidationMixin,
-):
- """
- Action to allow to upload a json. It is used as first step of an import.
- """
-
- model = Motion()
- schema = DefaultSchema(Motion()).get_default_schema(
- additional_required_fields={
- "data": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- **model.get_properties(
- "title",
- "text",
- "number",
- "reason",
- ),
- **{
- "submitters_verbose": LIST_TYPE,
- "submitters_username": LIST_TYPE,
- "supporters_verbose": LIST_TYPE,
- "supporters_username": LIST_TYPE,
- "category_name": {"type": "string"},
- "category_prefix": {"type": "string"},
- "tags": LIST_TYPE,
- "block": {"type": "string"},
- "motion_amendment": {"type": "boolean"},
- },
- },
- "required": [],
- "additionalProperties": False,
- },
- "minItems": 1,
- "uniqueItems": False,
- },
- "meeting_id": required_id_schema,
- }
- )
-
- headers = [
- {"property": "title", "type": "string", "is_object": True},
- {"property": "text", "type": "string", "is_object": True},
- {"property": "number", "type": "string", "is_object": True},
- {"property": "reason", "type": "string", "is_object": True},
- {
- "property": "submitters_verbose",
- "type": "string",
- "is_list": True,
- "is_hidden": True,
- },
- {
- "property": "submitters_username",
- "type": "string",
- "is_object": True,
- "is_list": True,
- },
- {
- "property": "supporters_verbose",
- "type": "string",
- "is_list": True,
- "is_hidden": True,
- },
- {
- "property": "supporters_username",
- "type": "string",
- "is_object": True,
- "is_list": True,
- },
- {"property": "category_name", "type": "string", "is_object": True},
- {"property": "category_prefix", "type": "string"},
- {"property": "tags", "type": "string", "is_object": True, "is_list": True},
- {"property": "block", "type": "string", "is_object": True},
- {
- "property": "motion_amendment",
- "type": "boolean",
- "is_object": True,
- "is_hidden": True,
- },
- ]
- permission = Permissions.Motion.CAN_MANAGE
- row_state: ImportState
- number_lookup: Lookup
- username_lookup: dict[str, list[dict[str, Any]]] = {}
- category_lookup: dict[str, list[dict[str, Any]]] = {}
- tags_lookup: dict[str, list[dict[str, Any]]] = {}
- block_lookup: dict[str, list[dict[str, Any]]] = {}
- _first_state_id: int | None = None
- _operator_username: str | None = None
- _previous_numbers: list[str]
- import_name = "motion"
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- # transform instance into a correct create/update payload
- # try to find a pre-existing motion with the same number
- # if there is one, validate for a motion.update, otherwise for a motion.create
- # using get_update_payload_integrity_error_message and get_create_payload_integrity_error_message
-
- data = instance.pop("data")
- data = self.add_payload_index_to_action_data(data)
- self.setup_lookups(data, instance["meeting_id"])
-
- # enrich data with meeting_id
- for entry in data:
- entry["meeting_id"] = instance["meeting_id"]
-
- self._previous_numbers = []
- self.rows = [self.validate_entry(entry) for entry in data]
-
- # generate statistics
- self.generate_statistics()
- return {}
-
- def validate_entry(self, entry: dict[str, Any]) -> dict[str, Any]:
- messages: list[str] = []
- id_: int | None = None
- meeting_id: int = entry["meeting_id"]
- set_entry_id = False
-
- if (is_amendment := entry.get("motion_amendment")) is not None:
- entry["motion_amendment"] = {
- "value": is_amendment,
- "info": ImportState.DONE,
- }
- if is_amendment:
- entry["motion_amendment"]["info"] = ImportState.WARNING
- messages.append("Amendments cannot be correctly imported")
-
- if category_name := entry.get("category_name"):
- category_prefix = entry.get("category_prefix")
- categories = self.category_lookup.get(category_name, [])
- categories = [
- category
- for category in categories
- if category.get("prefix") == category_prefix
- ]
- if len(categories) == 1 and categories[0].get("id") != 0:
- entry["category_name"] = {
- "value": category_name,
- "info": ImportState.DONE,
- "id": categories[0].get("id"),
- }
- else:
- entry["category_name"] = {
- "value": category_name,
- "info": ImportState.WARNING,
- }
- messages.append("Category could not be found")
- elif category_prefix := entry.get("category_prefix"):
- entry["category_name"] = {"value": "", "info": ImportState.WARNING}
- messages.append("Category could not be found")
-
- if number := entry.get("number"):
- check_result = self.number_lookup.check_duplicate(number)
- id_ = cast(int, self.number_lookup.get_field_by_name(number, "id"))
- if check_result == ResultType.FOUND_ID and id_ != 0:
- self.row_state = ImportState.DONE
- set_entry_id = True
- entry["number"] = {
- "value": number,
- "info": ImportState.DONE,
- "id": id_,
- }
- elif check_result == ResultType.NOT_FOUND or id_ == 0:
- self.row_state = ImportState.NEW
- entry["number"] = {
- "value": number,
- "info": ImportState.DONE,
- }
- elif check_result == ResultType.FOUND_MORE_IDS:
- self.row_state = ImportState.ERROR
- entry["number"] = {
- "value": number,
- "info": ImportState.ERROR,
- }
- messages.append("Error: Found multiple motions with the same number")
- else:
- category_id: int | None = None
- if entry.get("category_name"):
- category_id = entry["category_name"].get("id")
- self.row_state = ImportState.NEW
- value: dict[str, Any] = {}
- self.set_number(
- value,
- meeting_id,
- self._get_first_workflow_state_id(meeting_id),
- None,
- category_id,
- other_forbidden_numbers=self._previous_numbers,
- )
- if number := value.get("number"):
- entry["number"] = {"value": number, "info": ImportState.GENERATED}
- self._previous_numbers.append(number)
-
- has_submitter_error: bool = False
- for fieldname in ["submitter", "supporter"]:
- if users := entry.get(f"{fieldname}s_username"):
- verbose = entry.get(f"{fieldname}s_verbose", [])
- verbose_user_mismatch = len(verbose) > len(users)
- username_set: set[str] = set()
- entry_list: list[dict[str, Any]] = []
- duplicates: set[str] = set()
- not_found: set[str] = set()
- for user in users:
- if verbose_user_mismatch:
- entry_list.append({"value": user, "info": ImportState.ERROR})
- elif user in username_set:
- entry_list.append({"value": user, "info": ImportState.WARNING})
- duplicates.add(user)
- else:
- username_set.add(user)
- found_users = self.username_lookup.get(user, [])
- if len(found_users) == 1 and found_users[0].get("id") != 0:
- user_id = cast(int, found_users[0].get("id"))
- entry_list.append(
- {
- "value": user,
- "info": ImportState.DONE,
- "id": user_id,
- }
- )
- elif len(found_users) <= 1:
- entry_list.append(
- {
- "value": user,
- "info": ImportState.WARNING,
- }
- )
- not_found.add(user)
- else:
- raise ActionException(
- f"Database corrupt: Found multiple users with the username {user}"
- )
- entry[f"{fieldname}s_username"] = entry_list
- if verbose_user_mismatch:
- self.row_state = ImportState.ERROR
- messages.append(
- f"Error: Verbose field is set and has more entries than the username field for {fieldname}s"
- )
- if fieldname == "submitter":
- has_submitter_error = True
- if len(duplicates):
- messages.append(
- f"At least one {fieldname} has been referenced multiple times: "
- + ", ".join(duplicates)
- )
- if len(not_found):
- messages.append(
- f"Could not find at least one {fieldname}: "
- + ", ".join(not_found)
- )
-
- if not has_submitter_error:
- if (
- len(cast(list[dict[str, Any]], entry.get("submitters_username", [])))
- == 0
- ):
- entry["submitters_username"] = [self._get_self_username_object()]
- elif (
- len(
- [
- entry
- for entry in (
- cast(
- list[dict[str, Any]],
- entry.get("submitters_username", []),
- )
- )
- if entry.get("info") and (entry["info"] != ImportState.WARNING)
- ]
- )
- == 0
- ):
- entry["submitters_username"].append(self._get_self_username_object())
-
- if tags := entry.get("tags"):
- entry_list = []
- duplicates = set()
- not_found = set()
- multiple: set[str] = set()
- tags_set: set[str] = set()
- for tag in tags:
- if tag in tags_set:
- entry_list.append({"value": tag, "info": ImportState.WARNING})
- duplicates.add(tag)
- else:
- tags_set.add(tag)
- found_tags = self.tags_lookup.get(tag, [])
- if len(found_tags) == 1 and found_tags[0].get("id") != 0:
- tag_id = cast(int, found_tags[0].get("id"))
- entry_list.append(
- {
- "value": tag,
- "info": ImportState.DONE,
- "id": tag_id,
- }
- )
- elif len(found_tags) <= 1:
- entry_list.append(
- {
- "value": tag,
- "info": ImportState.WARNING,
- }
- )
- not_found.add(tag)
- else:
- entry_list.append(
- {
- "value": tag,
- "info": ImportState.WARNING,
- }
- )
- multiple.add(tag)
- entry["tags"] = entry_list
- if len(duplicates):
- messages.append(
- "At least one tag has been referenced multiple times: "
- + ", ".join(duplicates)
- )
- if len(not_found):
- messages.append(
- "Could not find at least one tag: " + ", ".join(not_found)
- )
- if len(multiple):
- messages.append(
- "Found multiple tags with the same name: " + ", ".join(multiple)
- )
-
- if (block := entry.get("block")) and isinstance(block, str):
- found_blocks = self.block_lookup.get(block, [])
- if len(found_blocks) == 1 and found_blocks[0].get("id") != 0:
- block_id = cast(int, found_blocks[0].get("id"))
- entry["block"] = {
- "value": block,
- "info": ImportState.DONE,
- "id": block_id,
- }
- elif len(found_blocks) <= 1:
- entry["block"] = {
- "value": block,
- "info": ImportState.WARNING,
- }
- messages.append("Could not find motion block")
- else:
- entry["block"] = {
- "value": block,
- "info": ImportState.WARNING,
- }
- messages.append("Found multiple motion blocks with the same name")
-
- if id_ and set_entry_id:
- entry["id"] = id_
-
- if (text := entry.get("text")) and not search(
- r"^<\w+[^>]*>[\w\W]*?<\/\w>$", text
- ):
- entry["text"] = (
- ""
- + sub(r"\n", "
", sub(r"\n([ \t]*\n)+", "
", text))
- + "
"
- )
-
- for field in ["title", "text", "reason"]:
- if (date := entry.get(field)) and isinstance(date, str):
- if date == "":
- del entry[field]
- else:
- entry[field] = {"value": date, "info": ImportState.DONE}
-
- # check via mixin
- payload = {
- **{
- k: v.get("value")
- for k, v in entry.items()
- if k in ["title", "text", "number", "reason"]
- },
- **{
- k: self._get_field_ids(entry, v)
- for k, v in {
- "submitter_ids": "submitters_username",
- "supporter_meeting_user_ids": "supporters_username",
- "tag_ids": "tags",
- }.items()
- },
- **{
- k: self._get_field_id(entry, v)
- for k, v in {
- "category_id": "category_name",
- "block_id": "block",
- }.items()
- if entry.get(v)
- },
- }
-
- errors: list[MotionActionErrorData] = []
- if id_:
- payload = {"id": id_, **payload}
- errors = self.get_update_payload_integrity_error_message(
- payload, meeting_id
- )
- else:
- payload = {"meeting_id": meeting_id, **payload}
- errors = self.get_create_payload_integrity_error_message(
- payload, meeting_id
- )
-
- for err in errors:
- entry = self._add_error_to_entry(entry, err)
- messages.append("Error: " + err["message"])
-
- return {"state": self.row_state, "messages": messages, "data": entry}
-
- def setup_lookups(self, data: Iterable[dict[str, Any]], meeting_id: int) -> None:
- self.number_lookup = Lookup(
- self.datastore,
- "motion",
- [(number, entry) for entry in data if (number := entry.get("number"))],
- field="number",
- mapped_fields=[],
- global_and_filter=FilterOperator("meeting_id", "=", meeting_id),
- )
- self.block_lookup = self.get_lookup_dict(
- "motion_block",
- [title for entry in data if (title := entry.get("block"))],
- "title",
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
- self.category_lookup = self.get_lookup_dict(
- "motion_category",
- [name for entry in data if (name := entry.get("category_name"))],
- "name",
- ["prefix"],
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
- self.username_lookup = self.get_lookup_dict(
- "user",
- list(
- {
- username
- for entry in data
- if (
- usernames := [
- *entry.get("submitters_username", []),
- *entry.get("supporters_username", []),
- ]
- )
- for username in usernames
- if username
- }
- ),
- "username",
- ["meeting_ids"],
- )
- self.username_lookup = {
- username: [
- date
- for date in self.username_lookup[username]
- if date.get("meeting_ids") and (meeting_id in date["meeting_ids"])
- ]
- for username in self.username_lookup
- }
- self.tags_lookup = self.get_lookup_dict(
- "tag",
- [
- name
- for entry in data
- if (names := entry.get("tags"))
- for name in names
- if name
- ],
- "name",
- and_filters=[FilterOperator("meeting_id", "=", meeting_id)],
- )
-
- def get_lookup_dict(
- self,
- collection: str,
- entries: list[str],
- fieldname: str = "name",
- mapped_fields: list[str] = [],
- and_filters: list[Filter] = [],
- ) -> dict[str, list[dict[str, Any]]]:
- lookup: dict[str, list[dict[str, Any]]] = defaultdict(list)
- if len(entries):
- data = self.datastore.filter(
- collection,
- And(
- *and_filters,
- Or([FilterOperator(fieldname, "=", name) for name in set(entries)]),
- ),
- [*mapped_fields, "id", fieldname],
- lock_result=False,
- )
- for date in data.values():
- lookup[date[fieldname]].append(date)
- return lookup
-
- def _get_self_username_object(self) -> dict[str, Any]:
- if not self._operator_username:
- user = self.datastore.get("user/" + str(self.user_id), ["username"])
- if not (user and user.get("username")):
- raise ActionException("Couldn't find operator's username")
- self._operator_username = cast(str, user["username"])
- return {
- "value": self._operator_username,
- "info": ImportState.GENERATED,
- "id": self.user_id,
- }
-
- def _get_first_workflow_state_id(self, meeting_id: int) -> int:
- if not self._first_state_id:
- default_workflows = self.datastore.filter(
- "motion_workflow",
- FilterOperator("default_workflow_meeting_id", "=", meeting_id),
- mapped_fields=["first_state_id"],
- ).values()
- if len(default_workflows) != 1:
- raise ActionException("Couldn't determine default workflow")
- self._first_state_id = cast(
- int, list(default_workflows)[0].get("first_state_id")
- )
- return self._first_state_id
-
- def _get_field_ids(self, entry: dict[str, Any], fieldname: str) -> list[int]:
- value = entry.get(fieldname, [])
- if not isinstance(value, list):
- value = [entry[fieldname]]
- return [val["id"] for val in value if val.get("id")]
-
- def _get_field_id(self, entry: dict[str, Any], fieldname: str) -> int:
- return entry[fieldname].get("id")
-
- def _add_error_to_entry(
- self, entry: dict[str, Any], err: MotionActionErrorData
- ) -> dict[str, Any]:
- fieldname = ""
- match err["type"]:
- case MotionErrorType.UNIQUE_NUMBER:
- fieldname = "number"
- case MotionErrorType.TEXT:
- fieldname = "text"
- case MotionErrorType.REASON:
- fieldname = "reason"
- case MotionErrorType.TITLE:
- fieldname = "title"
- case _:
- raise ActionException("Error: " + err["message"])
- if not (entry.get(fieldname) and isinstance(entry[fieldname], dict)):
- entry[fieldname] = {
- "value": entry.get(fieldname, ""),
- "info": ImportState.ERROR,
- }
- else:
- entry[fieldname]["info"] = ImportState.ERROR
- self.row_state = ImportState.ERROR
- return entry
diff --git a/openslides_backend/action/actions/motion/payload_validation_mixin.py b/openslides_backend/action/actions/motion/payload_validation_mixin.py
index 3a7f621aa4..5f3551ded2 100644
--- a/openslides_backend/action/actions/motion/payload_validation_mixin.py
+++ b/openslides_backend/action/actions/motion/payload_validation_mixin.py
@@ -11,6 +11,7 @@
class MotionErrorType(str, Enum):
+ ADDITIONAL_SUBMITTER = "addtional_submitter"
UNIQUE_NUMBER = "number_unique"
RECO_EXTENSION = "recommendation_extension"
STATE_EXTENSION = "state_extension"
@@ -140,6 +141,16 @@ def _create_conduct_before_checks(
errors.append(
{"type": MotionErrorType.REASON, "message": "Reason is required"}
)
+ if "additional_submitter" in instance and not self.datastore.get(
+ fqid_from_collection_and_id("meeting", meeting_id),
+ ["motions_create_enable_additional_submitter_text"],
+ ).get("motions_create_enable_additional_submitter_text"):
+ errors.append(
+ {
+ "type": MotionErrorType.ADDITIONAL_SUBMITTER,
+ "message": "This meeting doesn't allow additional_submitter to be set in creation",
+ }
+ )
return errors
def _create_conduct_after_checks(
diff --git a/openslides_backend/action/actions/organization/update.py b/openslides_backend/action/actions/organization/update.py
index c82a1c90fc..5f007b8f95 100644
--- a/openslides_backend/action/actions/organization/update.py
+++ b/openslides_backend/action/actions/organization/update.py
@@ -60,13 +60,73 @@ class OrganizationUpdate(
field: {**optional_str_schema, "max_length": 256}
for field in allowed_user_fields
}
- saml_props["meeting"] = {
- "type": ["object", "null"],
- "properties": {
- field: {**optional_str_schema, "max_length": 256}
- for field in ("external_id", "external_group_id")
+ saml_props["meeting_mappers"] = {
+ "type": ["array", "null"],
+ "items": {
+ "type": "object",
+ "properties": {
+ **{
+ field: {**optional_str_schema, "max_length": 256}
+ for field in ("external_id", "name", "allow_update")
+ },
+ "conditions": {
+ "type": ["array", "null"],
+ "items": {
+ "type": ["object", "null"],
+ "properties": {
+ **{
+ field: {**optional_str_schema, "max_length": 256}
+ for field in ("attribute", "condition")
+ },
+ },
+ },
+ },
+ "mappings": {
+ "type": ["object", "null"],
+ "properties": {
+ **{
+ mapping_field: {
+ "type": ["object", "null"],
+ "properties": {
+ field: {**optional_str_schema, "max_length": 256}
+ for field in ("attribute", "default")
+ },
+ "additionalProperties": False,
+ }
+ for mapping_field in [
+ "number",
+ "comment",
+ "vote_weight",
+ "present",
+ ]
+ },
+ **{
+ mapping_field: {
+ "type": ["array", "null"],
+ "items": {
+ "type": ["object", "null"],
+ "properties": {
+ field: {
+ **optional_str_schema,
+ "max_length": 256,
+ }
+ for field in ("attribute", "default")
+ },
+ "additionalProperties": False,
+ },
+ }
+ for mapping_field in [
+ "groups",
+ "structure_levels",
+ ]
+ },
+ },
+ "additionalProperties": False,
+ },
+ },
+ "required": ["external_id"],
+ "additionalProperties": False,
},
- "additionalProperties": False,
}
schema = DefaultSchema(Organization()).get_update_schema(
optional_properties=group_A_fields + group_B_fields,
diff --git a/openslides_backend/action/actions/poll/create.py b/openslides_backend/action/actions/poll/create.py
index 7e7eeb4b40..34f52443bb 100644
--- a/openslides_backend/action/actions/poll/create.py
+++ b/openslides_backend/action/actions/poll/create.py
@@ -13,7 +13,7 @@
from ...util.register import register_action
from ..option.create import OptionCreateAction
from .base import base_check_onehundred_percent_base
-from .mixins import PollHistoryMixin, PollPermissionMixin
+from .mixins import PollHistoryMixin, PollPermissionMixin, PollValidationMixin
options_schema = {
"description": "A option inside a poll create schema",
@@ -32,6 +32,7 @@
@register_action("poll.create")
class PollCreateAction(
+ PollValidationMixin,
SequentialNumbersMixin,
CreateAction,
PollPermissionMixin,
diff --git a/openslides_backend/action/actions/poll/functions.py b/openslides_backend/action/actions/poll/functions.py
index ebd9666f9f..d4b9bbcc24 100644
--- a/openslides_backend/action/actions/poll/functions.py
+++ b/openslides_backend/action/actions/poll/functions.py
@@ -1,8 +1,8 @@
from ....permissions.permission_helper import has_perm
from ....permissions.permissions import Permission, Permissions
from ....services.datastore.interface import DatastoreService
-from ....shared.exceptions import MissingPermission
-from ....shared.patterns import KEYSEPARATOR
+from ....shared.exceptions import ActionException, MissingPermission
+from ....shared.patterns import KEYSEPARATOR, collection_from_fqid
def check_poll_or_option_perms(
@@ -15,7 +15,11 @@ def check_poll_or_option_perms(
perm: Permission = Permissions.Motion.CAN_MANAGE_POLLS
elif content_object_id.startswith("assignment" + KEYSEPARATOR):
perm = Permissions.Assignment.CAN_MANAGE
- else:
+ elif content_object_id.startswith("topic" + KEYSEPARATOR):
perm = Permissions.Poll.CAN_MANAGE
+ else:
+ raise ActionException(
+ f"'{collection_from_fqid(content_object_id)}' is not a valid poll collection."
+ )
if not has_perm(datastore, user_id, perm, meeting_id):
raise MissingPermission(perm)
diff --git a/openslides_backend/action/actions/poll/mixins.py b/openslides_backend/action/actions/poll/mixins.py
index 557b72c203..90d3d1e54f 100644
--- a/openslides_backend/action/actions/poll/mixins.py
+++ b/openslides_backend/action/actions/poll/mixins.py
@@ -5,7 +5,7 @@
from openslides_backend.shared.typing import HistoryInformation
from ....services.datastore.commands import GetManyRequest
-from ....shared.exceptions import VoteServiceException
+from ....shared.exceptions import ActionException, VoteServiceException
from ....shared.interfaces.write_request import WriteRequest
from ....shared.patterns import (
collection_from_fqid,
@@ -20,6 +20,35 @@
from .functions import check_poll_or_option_perms
+class PollValidationMixin(Action):
+ def validate_instance(self, instance: dict[str, Any]) -> None:
+ super().validate_instance(instance)
+
+ if poll_id := instance.get("id"):
+ poll = self.datastore.get(
+ fqid_from_collection_and_id("poll", poll_id),
+ ["max_votes_amount", "min_votes_amount", "max_votes_per_option"],
+ )
+ max_votes_amount = instance.get(
+ "max_votes_amount", poll["max_votes_amount"] if poll_id else 1
+ )
+ min_votes_amount = instance.get(
+ "min_votes_amount", poll["min_votes_amount"] if poll_id else 1
+ )
+ max_votes_per_option = instance.get(
+ "max_votes_per_option", poll["max_votes_per_option"] if poll_id else 1
+ )
+
+ if max_votes_amount < max_votes_per_option:
+ raise ActionException(
+ "The maximum votes per option cannot be higher than the maximum amount of votes in total."
+ )
+ if max_votes_amount < min_votes_amount:
+ raise ActionException(
+ "The minimum amount of votes cannot be higher than the maximum amount of votes."
+ )
+
+
class PollPermissionMixin(Action):
def check_permissions(self, instance: dict[str, Any]) -> None:
if "meeting_id" in instance:
@@ -33,6 +62,8 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
)
content_object_id = poll.get("content_object_id", "")
meeting_id = poll["meeting_id"]
+ if not content_object_id:
+ raise ActionException("No 'content_object_id' was given")
check_poll_or_option_perms(
content_object_id, self.datastore, self.user_id, meeting_id
)
diff --git a/openslides_backend/action/actions/poll/update.py b/openslides_backend/action/actions/poll/update.py
index 00573b2ade..3c45977259 100644
--- a/openslides_backend/action/actions/poll/update.py
+++ b/openslides_backend/action/actions/poll/update.py
@@ -11,11 +11,12 @@
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from .base import base_check_onehundred_percent_base
-from .mixins import PollHistoryMixin, PollPermissionMixin
+from .mixins import PollHistoryMixin, PollPermissionMixin, PollValidationMixin
@register_action("poll.update")
class PollUpdateAction(
+ PollValidationMixin,
ExtendHistoryMixin,
UpdateAction,
PollPermissionMixin,
diff --git a/openslides_backend/action/actions/speaker/create.py b/openslides_backend/action/actions/speaker/create.py
index cf9aaed91f..e0eff2684a 100644
--- a/openslides_backend/action/actions/speaker/create.py
+++ b/openslides_backend/action/actions/speaker/create.py
@@ -294,7 +294,7 @@ def validate_fields(self, instance: dict[str, Any]) -> dict[str, Any]:
user = self.datastore.get(user_fqid, ["is_present_in_meeting_ids"])
if meeting_id not in user.get("is_present_in_meeting_ids", ()):
raise ActionException(
- "Only present users can be on the lists of speakers."
+ "Only present users can be on the list of speakers."
)
if not meeting.get("list_of_speakers_allow_multiple_speakers"):
diff --git a/openslides_backend/action/actions/speaker/update.py b/openslides_backend/action/actions/speaker/update.py
index f3a29acd59..758b17c3ea 100644
--- a/openslides_backend/action/actions/speaker/update.py
+++ b/openslides_backend/action/actions/speaker/update.py
@@ -136,7 +136,12 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
)
result = self.datastore.get_many(requests)
meeting = result["meeting"][speaker["meeting_id"]]
- user_id = result.get("meeting_user", {}).get(meeting_user_id, {}).get("user_id")
+ if meeting_user_id:
+ user_id = (
+ result.get("meeting_user", {}).get(meeting_user_id, {}).get("user_id")
+ )
+ else:
+ user_id = None
self.check_point_of_order_fields(
instance, meeting, user_id, speaker.get("point_of_order")
)
diff --git a/openslides_backend/action/actions/user/conditional_speaker_cascade_mixin.py b/openslides_backend/action/actions/user/conditional_speaker_cascade_mixin.py
index 35ef5f1bb3..77fede8615 100644
--- a/openslides_backend/action/actions/user/conditional_speaker_cascade_mixin.py
+++ b/openslides_backend/action/actions/user/conditional_speaker_cascade_mixin.py
@@ -6,6 +6,7 @@
from ....shared.patterns import fqid_from_collection_and_id
from ...action import Action
from ..speaker.delete import SpeakerDeleteAction
+from .set_present import UserSetPresentAction
class ConditionalSpeakerCascadeMixinHelper(Action):
@@ -41,6 +42,18 @@ def conditionally_delete_speakers(self, speaker_ids: list[int]) -> None:
[{"id": speaker["id"]} for speaker in speakers_to_delete],
)
+ def remove_presence(self, user_id: int, meeting_id: int) -> None:
+ self.execute_other_action(
+ UserSetPresentAction,
+ [
+ {
+ "id": user_id,
+ "meeting_id": meeting_id,
+ "present": False,
+ }
+ ],
+ )
+
class ConditionalSpeakerCascadeMixin(ConditionalSpeakerCascadeMixinHelper):
"""
@@ -65,6 +78,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
for speaker_id in val.get("speaker_ids", [])
]
self.conditionally_delete_speakers(speaker_ids)
+ self.remove_presence(instance["id"], removed_meeting_id)
return super().update_instance(instance)
diff --git a/openslides_backend/action/actions/user/create.py b/openslides_backend/action/actions/user/create.py
index 55a3cb586e..a25d6ea194 100644
--- a/openslides_backend/action/actions/user/create.py
+++ b/openslides_backend/action/actions/user/create.py
@@ -2,6 +2,9 @@
from typing import Any
from openslides_backend.permissions.permissions import Permissions
+from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
+ CreateUpdatePermissionsMixin,
+)
from ....models.models import User
from ....shared.exceptions import ActionException
@@ -15,13 +18,13 @@
from ...util.register import register_action
from ...util.typing import ActionResultElement
from ..meeting_user.mixin import CheckLockOutPermissionMixin
-from .create_update_permissions_mixin import CreateUpdatePermissionsMixin
from .password_mixins import SetPasswordMixin
from .user_mixins import LimitOfUserMixin, UserMixin, UsernameMixin, check_gender_exists
@register_action("user.create")
class UserCreate(
+ UserMixin,
EmailCheckMixin,
CreateAction,
CreateUpdatePermissionsMixin,
diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py
index 1e96e701d1..a03ea98147 100644
--- a/openslides_backend/action/actions/user/merge_together.py
+++ b/openslides_backend/action/actions/user/merge_together.py
@@ -94,9 +94,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"email",
"default_vote_weight",
],
- "highest": [
- "can_change_own_password",
- ],
"error": [
"is_demo_user",
],
@@ -117,6 +114,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"organization_management_level",
"saml_id", # error if set on secondary users, otherwise ignore the field
"member_number",
+ "can_change_own_password", # ignore on secondary users if primary has a saml_id, else highest
],
},
)
@@ -393,14 +391,14 @@ def call_other_actions(
[{"id": id_} for id_ in to_delete],
)
- if main_user_payload.get("default_vote_weight") == "0.000000":
- main_user_payload["default_vote_weight"] = "0.000001"
- self.execute_other_action(UserUpdate, [main_user_payload])
- if len(to_delete := update_operations["user"]["delete"]):
- self.execute_other_action(
- UserDelete,
- [{"id": id_} for id_ in to_delete],
- )
+ if main_user_payload.get("default_vote_weight") == "0.000000":
+ main_user_payload["default_vote_weight"] = "0.000001"
+ self.execute_other_action(UserUpdate, [main_user_payload])
+ if len(to_delete := update_operations["user"]["delete"]):
+ self.execute_other_action(
+ UserDelete,
+ [{"id": id_} for id_ in to_delete],
+ )
def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> None:
all_models = [into, *other_models]
@@ -589,6 +587,19 @@ def handle_special_field(
collection, into_, ranked_others, into_["id"], field
)
return None
+ case "can_change_own_password":
+ if into_.get("saml_id"):
+ return None
+ if len(
+ comp_data := [
+ date
+ for model in [into_, *ranked_others]
+ if (date := model.get("can_change_own_password"))
+ is not None
+ ]
+ ):
+ return any(comp_data)
+ return None
return super().handle_special_field(
collection, field, into_, ranked_others, update_operations
)
diff --git a/openslides_backend/action/actions/user/participant_common.py b/openslides_backend/action/actions/user/participant_common.py
index 2069cc94c0..54f11e63e4 100644
--- a/openslides_backend/action/actions/user/participant_common.py
+++ b/openslides_backend/action/actions/user/participant_common.py
@@ -11,14 +11,14 @@
)
from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.exceptions import MissingPermission
+from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
+ CreateUpdatePermissionsFailingFields,
+ PermissionVarStore,
+)
from openslides_backend.shared.patterns import fqid_from_collection_and_id
from ....shared.filters import And, FilterOperator, Or
from ..meeting_user.mixin import CheckLockOutPermissionMixin
-from ..user.create_update_permissions_mixin import (
- CreateUpdatePermissionsFailingFields,
- PermissionVarStore,
-)
class ParticipantCommon(BaseImportJsonUploadAction, CheckLockOutPermissionMixin):
@@ -51,6 +51,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
)
self.permission_check = CreateUpdatePermissionsFailingFields(
+ self.user_id,
permstore,
self.services,
self.datastore,
diff --git a/openslides_backend/action/actions/user/save_saml_account.py b/openslides_backend/action/actions/user/save_saml_account.py
index 1989cde148..cbe358db46 100644
--- a/openslides_backend/action/actions/user/save_saml_account.py
+++ b/openslides_backend/action/actions/user/save_saml_account.py
@@ -1,13 +1,17 @@
-from collections.abc import Iterable
+import re
+from collections import defaultdict
+from collections.abc import Generator, Iterable
from typing import Any, cast
import fastjsonschema
+from openslides_backend.shared.patterns import DECIMAL_PATTERN
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
+from ....models.fields import TRUE_VALUES
from ....models.models import User
from ....shared.exceptions import ActionException
-from ....shared.filters import And, FilterOperator
+from ....shared.filters import And, FilterOperator, Or
from ....shared.interfaces.event import Event
from ....shared.schema import schema_version
from ....shared.typing import Schema
@@ -18,6 +22,7 @@
from ...util.register import register_action
from ...util.typing import ActionData, ActionResultElement
from ..gender.create import GenderCreate
+from ..structure_level.create import StructureLevelCreateAction
from .create import UserCreate
from .update import UserUpdate
from .user_mixins import UsernameMixin
@@ -32,6 +37,16 @@
"pronoun",
"is_active",
"is_physical_person",
+ "member_number",
+]
+
+allowed_meeting_user_fields = [
+ "groups",
+ "structure_levels",
+ "number",
+ "comment",
+ "vote_weight",
+ "present",
]
@@ -63,7 +78,9 @@ def validate_instance(self, instance: dict[str, Any]) -> None:
raise ActionException(
"SingleSignOn is not enabled in OpenSlides configuration"
)
- self.saml_attr_mapping = organization.get("saml_attr_mapping", {})
+ self.saml_attr_mapping: dict[str, Any] = organization.get(
+ "saml_attr_mapping", dict()
+ )
if not self.saml_attr_mapping or not isinstance(self.saml_attr_mapping, dict):
raise ActionException(
"SingleSignOn field attributes are not configured in OpenSlides"
@@ -111,7 +128,10 @@ def validate_instance(self, instance: dict[str, Any]) -> None:
def validate_fields(self, instance_old: dict[str, Any]) -> dict[str, Any]:
"""
- Transforms the payload fields into model fields, removes the possible array-wrapped format
+ Transforms the payload fields into model fields, removes the possible array-wrapped format.
+ Mapper data is comprised on a per meeting basis. On conflicts the last statement is used.
+ Groups and structure levels are combined, however.
+ Meeting related data will be transformed via the idp attributes to the actual model data.
"""
instance: dict[str, Any] = dict()
for model_field, payload_field in self.saml_attr_mapping.items():
@@ -120,14 +140,14 @@ def validate_fields(self, instance_old: dict[str, Any]) -> dict[str, Any]:
and payload_field in instance_old
and model_field in allowed_user_fields
):
- value = (
+ payload_value = (
tx[0]
if isinstance((tx := instance_old[payload_field]), list) and len(tx)
else tx
)
- if value not in (None, []):
- instance[model_field] = value
-
+ if payload_value not in (None, []):
+ instance[model_field] = payload_value
+ self.apply_meeting_mapping(instance, instance_old)
return super().validate_fields(instance)
def prepare_action_data(self, action_data: ActionData) -> ActionData:
@@ -138,49 +158,51 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
pass
def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- meeting_id, group_id = self.check_for_group_add()
users = self.datastore.filter(
"user",
FilterOperator("saml_id", "=", instance["saml_id"]),
- ["id", "gender_id", *allowed_user_fields],
+ [
+ "id",
+ "meeting_user_ids",
+ "is_present_in_meeting_ids",
+ "gender_id",
+ *allowed_user_fields,
+ ],
)
-
- if gender := instance.get("gender"):
- if gender == "":
- instance["gender_id"] = None
+ if gender := instance.pop("gender", None):
+ gender_dict = self.datastore.filter(
+ "gender",
+ FilterOperator("name", "=", gender),
+ ["id"],
+ )
+ gender_id = None
+ if gender_dict:
+ gender_id = next(iter(gender_dict.keys()))
else:
- gender_dict = self.datastore.filter(
- "gender",
- FilterOperator("name", "=", gender),
- ["id"],
+ action_result = self.execute_other_action(
+ GenderCreate, [{"name": gender}]
)
- if gender_dict:
- gender_id = next(iter(gender_dict.keys()))
- else:
- action_result = self.execute_other_action(
- GenderCreate, [{"name": gender}]
- )
- gender_id = action_result[0].get("id", 0) # type: ignore
+ if action_result and action_result[0]:
+ gender_id = action_result[0].get("id", 0)
+ if gender_id:
instance["gender_id"] = gender_id
- del instance["gender"]
+ else:
+ self.logger.warning(
+ f"save_saml_account could neither find nor create {gender}. Not handling gender."
+ )
+ # Empty string: remove gender_id
elif gender == "":
instance["gender_id"] = None
- del instance["gender"]
+ meeting_users = instance.pop("meeting_user_data", None)
+ user_id = None
if len(users) == 1:
self.user = next(iter(users.values()))
- instance["id"] = (user_id := cast(int, self.user["id"]))
- if meeting_id and group_id:
- meeting_user = get_meeting_user(
- self.datastore, meeting_id, user_id, ["id", "group_ids"]
+ instance["id"] = (user_id := self.user["id"])
+ if meeting_users:
+ meeting_users = self.apply_meeting_user_data(
+ instance, meeting_users, user_id, True
)
- if meeting_user:
- old_group_ids = meeting_user["group_ids"]
- if group_id not in old_group_ids:
- instance["meeting_id"] = meeting_id
- instance["group_ids"] = old_group_ids + [group_id]
- else:
- instance["meeting_id"] = meeting_id
- instance["group_ids"] = [group_id]
+ self.update_meeting_users_from_db(meeting_users, user_id)
instance = {
k: v for k, v in instance.items() if k == "id" or v != self.user.get(k)
}
@@ -188,19 +210,24 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
self.execute_other_action(UserUpdate, [instance])
elif len(users) == 0:
instance = self.set_defaults(instance)
- if group_id:
- instance["meeting_id"] = meeting_id
- instance["group_ids"] = [group_id]
- self.execute_other_action(UserCreate, [instance])
+ response = self.execute_other_action(UserCreate, [instance])
+ if response and response[0]:
+ user_id = response[0].get("id")
+ if meeting_users and user_id:
+ meeting_users = self.apply_meeting_user_data(
+ instance, meeting_users, user_id, False
+ )
else:
ActionException(
f"More than one existing user found in database with saml_id {instance['saml_id']}"
)
+ if meeting_users:
+ self.execute_other_action(UserUpdate, [mu for mu in meeting_users.values()])
return instance
def create_events(self, instance: dict[str, Any]) -> Iterable[Event]:
"""
- delegated to execute_other_actions
+ delegated to execute_other_action
"""
return []
@@ -218,46 +245,327 @@ def set_defaults(self, instance: dict[str, Any]) -> dict[str, Any]:
instance["username"] = self.generate_usernames([instance.get("saml_id", "")])[0]
return instance
- def check_for_group_add(self) -> tuple[int, int] | tuple[None, None]:
- NoneResult = (None, None)
- if not (
- meeting_info := cast(dict, self.saml_attr_mapping.get("meeting"))
- ) or not (external_id := meeting_info.get("external_id")):
- return NoneResult
-
- meetings = self.datastore.filter(
- collection="meeting",
- filter=FilterOperator("external_id", "=", external_id),
- mapped_fields=["id", "default_group_id"],
+ def validate_meeting_mapper(
+ self, instance: dict[str, Any], meeting_mapper: dict[str, Any]
+ ) -> bool:
+ """
+ Validates the meeting mapper to be complete. Returns False if not.
+ Returns True if the mapper matches its criteria on instances values or no conditions were given.
+ Instances values can not be None or empty string.
+ """
+ if not meeting_mapper.get("external_id"):
+ return False
+ if not (mapper_conditions := meeting_mapper.get("conditions")):
+ return True
+ return all(
+ (
+ (instance_value := instance.get(mapper_condition.get("attribute")))
+ and regex_condition.search(instance_value)
+ )
+ for mapper_condition in mapper_conditions
+ if (regex_condition := re.compile(mapper_condition.get("condition")))
)
- if len(meetings) == 1:
- meeting = next(iter(meetings.values()))
- group_id = meeting["default_group_id"]
- else:
+
+ def apply_meeting_mapping(
+ self, instance: dict[str, Any], instance_old: dict[str, Any]
+ ) -> None:
+ if meeting_mappers := cast(
+ list[dict[str, Any]],
+ self.saml_attr_mapping.get("meeting_mappers", []),
+ ):
+ meeting_user_data: dict[str, Any] = defaultdict(dict)
+ for meeting_mapper in meeting_mappers:
+ if self.validate_meeting_mapper(instance_old, meeting_mapper):
+ meeting_external_id = meeting_mapper["external_id"]
+ mapping_results = meeting_user_data[meeting_external_id]
+ allow_update: str | bool
+ if isinstance(
+ allow_update := meeting_mapper.get("allow_update", True),
+ str,
+ ):
+ allow_update = allow_update.lower() in TRUE_VALUES
+ mapping_results["for_create"] = {
+ key: value
+ for key, value in self.get_field_data(
+ instance_old,
+ mapping_results.get("for_create", dict()),
+ meeting_mapper,
+ )
+ }
+ if allow_update:
+ mapping_results["for_update"] = {
+ key: value
+ for key, value in self.get_field_data(
+ instance_old,
+ mapping_results.get("for_update", dict()),
+ meeting_mapper,
+ )
+ }
+ if meeting_user_data:
+ instance["meeting_user_data"] = meeting_user_data
+ else:
+ self.logger.warning(
+ "save_saml_account found no matching meeting mappers."
+ )
+
+ def apply_meeting_user_data(
+ self,
+ instance: dict[str, Any],
+ meeting_user_data: dict[int, dict[str, Any]],
+ user_id: int,
+ is_update: bool,
+ ) -> dict[int, dict[str, Any]] | None:
+ if not (
+ external_meeting_ids := sorted(
+ [ext_id for ext_id in meeting_user_data.keys()]
+ )
+ ):
+ return None
+ meetings = {
+ meeting_id: meeting
+ for meeting_id, meeting in sorted(
+ self.datastore.filter(
+ "meeting",
+ Or(
+ FilterOperator("external_id", "=", external_meeting_id)
+ for external_meeting_id in external_meeting_ids
+ ),
+ ["id", "default_group_id", "external_id"],
+ ).items()
+ )
+ }
+ missing_meetings = [
+ external_meeting_id
+ for external_meeting_id in external_meeting_ids
+ if external_meeting_id
+ not in {meeting.get("external_id") for meeting in meetings.values()}
+ ]
+ if missing_meetings:
self.logger.warning(
- f"save_saml_account found {len(meetings)} meetings with external_id '{external_id}'"
+ f"save_saml_account found no meetings for {len(missing_meetings)} meetings with external_ids {missing_meetings}"
)
- return NoneResult
- if external_group_id := meeting_info.get("external_group_id"):
+ # declare and half way through initialize mu data
+ result: dict[int, dict[str, Any]] = dict()
+ for (
+ meeting_id,
+ meeting,
+ ) in meetings.items():
+ if not (
+ instance_meeting_user_data := meeting_user_data.get(
+ meeting["external_id"]
+ )
+ ):
+ continue
+ if is_update:
+ instance_meeting_user = instance_meeting_user_data.get("for_update")
+ else:
+ instance_meeting_user = instance_meeting_user_data.get("for_create")
+ if instance_meeting_user is not None:
+ instance_meeting_user["id"] = user_id
+ instance_meeting_user["meeting_id"] = meeting_id
+ for saml_meeting_user_field in ["groups", "structure_levels"]:
+ names = sorted(
+ instance_meeting_user.pop(saml_meeting_user_field, [])
+ )
+ if saml_meeting_user_field == "groups":
+ ids = self.get_group_ids(names, meeting)
+ elif saml_meeting_user_field == "structure_levels":
+ ids = self.get_structure_level_ids(names, meeting)
+ if ids:
+ instance_meeting_user[
+ f"{saml_meeting_user_field.rstrip('s')}_ids"
+ ] = ids
+ if instance_meeting_user.pop("present", ""):
+ present_in_meeting_ids = instance.get(
+ "is_present_in_meeting_ids", []
+ )
+ if meeting_id not in present_in_meeting_ids:
+ present_in_meeting_ids.append(meeting_id)
+ instance["is_present_in_meeting_ids"] = present_in_meeting_ids
+ result[meeting_id] = instance_meeting_user
+ return result
+
+ def update_meeting_users_from_db(
+ self, meeting_users: dict[int, dict[str, Any]], user_id: int
+ ) -> None:
+ """updates meeting users with groups and structure level relations from database"""
+ for meeting_id, meeting_user in meeting_users.items():
+ if meeting_user_db := get_meeting_user(
+ self.datastore,
+ meeting_id,
+ user_id,
+ ["id", "group_ids", "structure_level_ids"],
+ ):
+ for field_name in ["group_ids", "structure_level_ids"]:
+ if old_ids := meeting_user_db.get(field_name):
+ ids = meeting_user.get(field_name, [])
+ for _id in ids:
+ if _id not in old_ids:
+ meeting_user[field_name] = old_ids + [_id]
+
+ def get_field_data(
+ self,
+ instance: dict[str, Any],
+ meeting_user: dict[str, Any],
+ meeting_mapper: dict[str, dict[str, Any]],
+ ) -> Generator[tuple[str, Any]]:
+ """
+ returns the field data for the given idp mapping field. Groups the groups and structure levels for each meeting.
+ Uses mappers for generating default values.
+ """
+ missing_attributes = []
+ for saml_meeting_user_field in allowed_meeting_user_fields:
+ result: set[str] | str | bool = ""
+ meeting_mapping = meeting_mapper.get("mappings", dict())
+ result = meeting_user.get(saml_meeting_user_field, "")
+ if saml_meeting_user_field in ["groups", "structure_levels"]:
+ attr_default_list = meeting_mapping.get(saml_meeting_user_field, [])
+ else:
+ attr_default_list = [
+ meeting_mapping.get(saml_meeting_user_field, dict())
+ ]
+ for attr_default in attr_default_list:
+ idp_attribute = attr_default.get("attribute", "")
+ if saml_meeting_user_field == "number":
+ # Number cannot have a default.
+ if value := instance.get(idp_attribute):
+ result = value
+ else:
+ missing_attributes.append(idp_attribute)
+ elif not (value := instance.get(idp_attribute)):
+ missing_attributes.append(idp_attribute)
+ value = attr_default.get("default")
+ if value:
+ if saml_meeting_user_field in ["groups", "structure_levels"]:
+ # Need to append to group and structure_level for same meeting.
+ if not result:
+ result = set()
+ cast(set, result).update(value.split(", "))
+ elif saml_meeting_user_field == "comment":
+ # Want comments from all matching mappers.
+ if result:
+ result = cast(str, result) + " " + value
+ else:
+ result = value
+ elif saml_meeting_user_field == "present":
+ # Result is int or bool. int will later be interpreted as bool.
+ result = (
+ value
+ if not isinstance(value, str)
+ else (
+ False
+ if value.casefold() == "false".casefold()
+ else True
+ )
+ )
+ elif saml_meeting_user_field == "vote_weight":
+ # Result must be string and have 6 digits after dot.
+ if isinstance(value, int):
+ value = f"{value:f}"
+ elif isinstance(value, str) and re.compile(
+ r"^(\d\.|[1-9]\d*\.?)\d{0,6}$"
+ ).match(value):
+ if re.compile(r"^\d*$").match(value):
+ value += ".000000"
+ else:
+ while not DECIMAL_PATTERN.match(value):
+ value += "0"
+ else:
+ mapper_name = meeting_mapper.get("name", "unnamed")
+ saml_id = instance.get(self.saml_attr_mapping["saml_id"])
+ self.logger.debug(
+ f"Meeting mapper: {mapper_name} The data '{value}' send for vote_weight of user '{saml_id}' must be invalid, eg. float or badly formatted string."
+ )
+ continue
+ result = value
+ else:
+ result = value
+ if result:
+ yield saml_meeting_user_field, result
+ if fields := ",".join(missing_attributes):
+ mapper_name = meeting_mapper.get("name", "unnamed")
+ self.logger.debug(
+ f"Meeting mapper: {mapper_name} could not find value in idp data for fields: {fields}. Using default if available."
+ )
+
+ def get_group_ids(self, group_names: list[str], meeting: dict) -> list[int]:
+ """
+ Gets the group ids from given group names in that meeting.
+ If none of the groups exists in the meeting, the meetings default group is returned.
+ """
+ if group_names:
groups = self.datastore.filter(
- collection="group",
- filter=And(
- [
- FilterOperator("external_id", "=", external_group_id),
- FilterOperator("meeting_id", "=", meeting.get("id")),
- ]
+ "group",
+ And(
+ FilterOperator("meeting_id", "=", meeting["id"]),
+ Or(
+ FilterOperator("external_id", "=", group_name)
+ for group_name in group_names
+ ),
),
- mapped_fields=["id"],
+ ["meeting_user_ids"],
)
- if len(groups) == 1:
- group_id = next(iter(groups.keys()))
- else:
- self.logger.warning(
- f"save_saml_account found no group in meeting '{external_id}' for '{external_group_id}', but use default_group of meeting"
- )
- if not group_id:
+ if len(groups) > 0:
+ return sorted(groups)
+ if default_group_id := meeting["default_group_id"]:
+ external_meeting_id = meeting["external_id"]
self.logger.warning(
- f"save_saml_account found no group in meeting '{external_id}' for '{external_group_id}'"
+ f"save_saml_account found no group in meeting '{external_meeting_id}' for {group_names}, but used default_group of meeting"
)
- return NoneResult
- return meeting.get("id"), group_id
+ return [default_group_id]
+ else:
+ assert False
+
+ def get_structure_level_ids(
+ self, structure_level_names: list[str], meeting: dict[str, Any]
+ ) -> list[int]:
+ """
+ Gets the structure level ids from given structure level names in that meeting.
+ For this also creates new structure levels not already existing in the meeting.
+ """
+ if structure_level_names:
+ meeting_id = meeting["id"]
+ found_structure_levels = self.datastore.filter(
+ "structure_level",
+ And(
+ FilterOperator("meeting_id", "=", meeting_id),
+ Or(
+ FilterOperator("name", "=", structure_level_name)
+ for structure_level_name in structure_level_names
+ if structure_level_name
+ ),
+ ),
+ ["meeting_user_ids"],
+ )
+ found_structure_level_ids = list(found_structure_levels.keys())
+ if len(found_structure_levels) == len(structure_level_names):
+ return found_structure_level_ids
+ else:
+ found_structure_level_names = [
+ structure_level.get("name")
+ for structure_level in found_structure_levels.values()
+ ]
+ to_be_created_structure_levels = [
+ sl_name
+ for sl_name in structure_level_names
+ if sl_name and sl_name not in found_structure_level_names
+ ]
+ # meeting_user_ids are only known during UserUpdate. Hence we cannot do batch create for all meeting users
+ if structure_levels_result := (
+ self.execute_other_action(
+ StructureLevelCreateAction,
+ [
+ {"name": structure_level_name, "meeting_id": meeting_id}
+ for structure_level_name in to_be_created_structure_levels
+ ],
+ )
+ ):
+ return sorted(
+ [
+ structure_level["id"]
+ for structure_level in structure_levels_result
+ if structure_level
+ ]
+ + found_structure_level_ids
+ )
+ return []
diff --git a/openslides_backend/action/actions/user/update.py b/openslides_backend/action/actions/user/update.py
index a5df918baa..3357487449 100644
--- a/openslides_backend/action/actions/user/update.py
+++ b/openslides_backend/action/actions/user/update.py
@@ -2,6 +2,9 @@
from typing import Any
from openslides_backend.permissions.permissions import Permissions
+from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
+ CreateUpdatePermissionsMixin,
+)
from ....action.action import original_instances
from ....action.util.typing import ActionData
@@ -17,7 +20,6 @@
from ...util.register import register_action
from ..meeting_user.mixin import CheckLockOutPermissionMixin
from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin
-from .create_update_permissions_mixin import CreateUpdatePermissionsMixin
from .user_mixins import (
AdminIntegrityCheckMixin,
LimitOfUserMixin,
@@ -29,6 +31,7 @@
@register_action("user.update")
class UserUpdate(
+ UserMixin,
EmailCheckMixin,
CreateUpdatePermissionsMixin,
UpdateAction,
@@ -137,8 +140,11 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
raise PermissionException(
"A superadmin is not allowed to set himself inactive."
)
- if instance.get("is_active") and not user.get("is_active"):
- self.check_limit_of_user(1)
+ if is_active := instance.get("is_active"):
+ if not user.get("is_active"):
+ self.check_limit_of_user(1)
+ elif is_active is False and user.get("is_active"):
+ self.auth.clear_sessions_by_user_id(instance["id"])
check_gender_exists(self.datastore, instance)
return instance
diff --git a/openslides_backend/action/actions/user/update_self.py b/openslides_backend/action/actions/user/update_self.py
index 287851d6d4..94ce727246 100644
--- a/openslides_backend/action/actions/user/update_self.py
+++ b/openslides_backend/action/actions/user/update_self.py
@@ -3,8 +3,9 @@
from ....models.models import MeetingUser, User
from ....permissions.permission_helper import has_perm
from ....permissions.permissions import Permissions
-from ....shared.exceptions import MissingPermission
+from ....shared.exceptions import ActionException, MissingPermission
from ...generics.update import UpdateAction
+from ...mixins.meeting_user_helper import get_meeting_user
from ...mixins.send_email_mixin import EmailCheckMixin
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
@@ -21,7 +22,9 @@ class UserUpdateSelf(EmailCheckMixin, UpdateAction, UserMixin, UpdateHistoryMixi
schema = DefaultSchema(User()).get_default_schema(
optional_properties=["username", "pronoun", "gender_id", "email"],
additional_optional_fields={
- **MeetingUser().get_properties("meeting_id", "vote_delegated_to_id")
+ **MeetingUser().get_properties(
+ "meeting_id", "vote_delegated_to_id", "vote_delegations_from_ids"
+ )
},
)
check_email_field = "email"
@@ -35,11 +38,33 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
check_gender_exists(self.datastore, instance)
return instance
+ def meeting_user_set_data(self, instance: dict[str, Any]) -> None:
+ if (deleg := set(instance.get("vote_delegations_from_ids", []))) and len(
+ deleg.difference(
+ (
+ get_meeting_user(
+ self.datastore,
+ instance["meeting_id"],
+ instance["id"],
+ ["vote_delegations_from_ids"],
+ )
+ or {}
+ ).get("vote_delegations_from_ids", [])
+ )
+ ):
+ raise ActionException(
+ "Can't add delegations from other people with user.update_self."
+ )
+ super().meeting_user_set_data(instance)
+
def check_permissions(self, instance: dict[str, Any]) -> None:
self.assert_not_anonymous()
if (
(meeting_id := instance.get("meeting_id"))
- and "vote_delegated_to_id" in instance
+ and (
+ "vote_delegated_to_id" in instance
+ or "vote_delegations_from_ids" in instance
+ )
and not has_perm(
self.datastore,
self.user_id,
diff --git a/openslides_backend/action/actions/user/user_mixins.py b/openslides_backend/action/actions/user/user_mixins.py
index f2b3c836d5..807e00fb91 100644
--- a/openslides_backend/action/actions/user/user_mixins.py
+++ b/openslides_backend/action/actions/user/user_mixins.py
@@ -91,6 +91,10 @@ class UserMixin(CheckForArchivedMeetingMixin):
"locked_out": {"type": "boolean"},
}
+ def check_permissions(self, instance: dict[str, Any]) -> None:
+ self.assert_not_anonymous()
+ super().check_permissions(instance)
+
def validate_instance(self, instance: dict[str, Any]) -> None:
super().validate_instance(instance)
if "meeting_id" not in instance and any(
@@ -142,10 +146,8 @@ def strip_field(self, field: str, instance: dict[str, Any]) -> None:
def check_meeting_and_users(
self, instance: dict[str, Any], user_fqid: FullQualifiedId
) -> None:
- if instance.get("meeting_id") is not None:
- self.datastore.apply_changed_model(
- user_fqid, {"meeting_id": instance.get("meeting_id")}
- )
+ if (meeting_id := instance.get("meeting_id")) is not None:
+ self.datastore.apply_changed_model(user_fqid, {"meeting_id": meeting_id})
def meeting_user_set_data(self, instance: dict[str, Any]) -> None:
meeting_user_data = {}
diff --git a/openslides_backend/action/mixins/import_mixins.py b/openslides_backend/action/mixins/import_mixins.py
index 87df99e143..dfe4e68b7b 100644
--- a/openslides_backend/action/mixins/import_mixins.py
+++ b/openslides_backend/action/mixins/import_mixins.py
@@ -317,7 +317,7 @@ def flatten_copied_object_fields(
) -> list[ImportRow]:
"""The self.rows will be deepcopied, flattened and returned, without
changes on the self.rows.
- This is necessary for using the data in the executution of actions.
+ This is necessary for using the data in the execution of actions.
The requests response should be given with the unchanged self.rows.
Parameter:
hook_method:
diff --git a/openslides_backend/action/mixins/singular_action_mixin.py b/openslides_backend/action/mixins/singular_action_mixin.py
index 33827c2a21..a02e6d4b54 100644
--- a/openslides_backend/action/mixins/singular_action_mixin.py
+++ b/openslides_backend/action/mixins/singular_action_mixin.py
@@ -20,7 +20,7 @@
class SingularActionMixin(Action):
"""
- Mixin to ensure that the action data contains only on object.
+ Mixin to ensure that the action data contains only one object.
"""
is_singular = True
diff --git a/openslides_backend/http/application.py b/openslides_backend/http/application.py
index d55d647ab6..2b5e51ca51 100644
--- a/openslides_backend/http/application.py
+++ b/openslides_backend/http/application.py
@@ -57,7 +57,6 @@ def create_initial_data(self) -> None:
Path(__file__).parent
/ ".."
/ ".."
- / "global"
/ "data"
/ f"{file_prefix}-data.json"
)
diff --git a/openslides_backend/i18n/messages/it.po b/openslides_backend/i18n/messages/it.po
index 7ab2ba8234..a83401c2ce 100644
--- a/openslides_backend/i18n/messages/it.po
+++ b/openslides_backend/i18n/messages/it.po
@@ -1,11 +1,12 @@
#
# Translators:
# Katharina , 2022
-# Alexandra Damm , 2022
-#
+# Alexandra Damm , 2024
+# Albano Battistella , 2024
+#
msgid ""
msgstr ""
-"Last-Translator: Alexandra Damm , 2022\n"
+"Last-Translator: Albano Battistella , 2024\n"
"Language-Team: Italian (https://app.transifex.com/openslides/teams/14270/it/)\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -20,7 +21,7 @@ msgid "\"0\" means an unlimited number of active meetings"
msgstr "\"0\" indica un numero illimitato di riunioni attive"
msgid "%num% emails were send sucessfully."
-msgstr ""
+msgstr "%n um% Le e-mail sono state inviate con successo."
msgid "-Ä"
msgstr "-Ä"
@@ -70,19 +71,21 @@ msgstr "Serve una password"
msgid "A server error occured. Please contact your system administrator."
msgstr "Errore del server. Prego contattare l'amministratore del server."
+msgid "A time is required and must be in min:secs format."
+msgstr "E' richiesto un tempo e deve essere indicato nel formato min:sec "
+
msgid "A title is required"
msgstr "Serve un titolo"
msgid "A topic needs a title"
-msgstr ""
-
-msgid "A total time is required and must be greater than 0."
-msgstr ""
+msgstr "L'argomento necessita di un titolo"
msgid ""
"A user with the username '%username%' and the first name '%first_name%' was "
"created."
msgstr ""
+"È stato creato un account con il nome utente ' 1%u sername%' e il nome di "
+"battesimo ' 2%f irst_name%'."
msgid "About me"
msgstr "Su di me"
@@ -91,7 +94,7 @@ msgid "Abstain"
msgstr "Astensione"
msgid "Accent color"
-msgstr ""
+msgstr "Colore dell'accento"
msgid "Accept"
msgstr "Accettare"
@@ -102,14 +105,23 @@ msgstr "Dati d'accesso (pdf)"
msgid "Access groups"
msgstr "Gruppi d'accesso"
+msgid ""
+"Access only possible for participants of this meeting. All other accounts "
+"(including organization and committee admins) may not open the closed "
+"meeting. It is locked from the inside."
+msgstr ""
+"Accesso possibile solo per i partecipanti a questa riunione. Tutti gli altri account "
+"(inclusi gli amministratori dell'organizzazione e del comitato) non possono aprire la riunione chiusa "
+"in quanto è bloccata dall'interno."
+
msgid "Access-data"
msgstr "Dati d'accesso"
msgid "Account"
-msgstr ""
+msgstr "Account"
msgid "Account admin"
-msgstr "Amministratore del conto"
+msgstr "Account amministratore"
msgid "Account successfully assigned"
msgstr "Account assegnato con successo"
@@ -118,16 +130,16 @@ msgid "Accounts"
msgstr "Accounts"
msgid "Accounts created"
-msgstr ""
+msgstr "Creato Account"
msgid "Accounts updated"
-msgstr ""
+msgstr "Account aggiornato"
msgid "Accounts with errors"
-msgstr ""
+msgstr "Account con errori"
msgid "Accounts with warnings: affected cells will be skipped"
-msgstr ""
+msgstr "Account con avvertimenti: le celle interessate saranno saltate"
msgid "Activate"
msgstr "Attivare"
@@ -135,17 +147,17 @@ msgstr "Attivare"
msgid "Activate amendments"
msgstr "Attivare mozioni di modifica"
-msgid "Activate design"
-msgstr ""
+msgid "Activate closed meeting"
+msgstr "Attiva riunione chiusa"
-msgid "Activate statute amendments"
-msgstr "Attivare modifiche di statuto"
+msgid "Activate design"
+msgstr "Attivare design"
msgid "Activate the selection field 'motion editor'"
-msgstr ""
+msgstr "Attivare il campo di selezione \"motion edition\""
-msgid "Activate the selection field 'spokesman'"
-msgstr ""
+msgid "Activate the selection field 'spokesperson'"
+msgstr "Attivare il campo di selezione \"spokesperson\"/portavoce"
msgid "Activate vote delegations"
msgstr "Attivare le deleghe di voto"
@@ -157,24 +169,37 @@ msgid ""
"Activates the automatic logging of the date and time when this state was "
"first reached. A set time stamp cannot be removed."
msgstr ""
+"Attiva la registrazione automatica della data e dell'ora in cui questo stato"
+" è stato raggiunto per la prima volta. Un timbro temporale impostato non può"
+" essere rimosso."
msgid ""
"Activates the automatic setting of a number for motions that reach this "
"state. The scheme for numbering can be customized under > [Settings] > "
"[Motions]."
msgstr ""
+"Attiva l'impostazione automatica di un numero per le mozioni che raggiungono"
+" questo stato. Lo schema di numerazione può essere personalizzato in > "
+"[Impostazioni] > [Movimenti]."
msgid ""
"Activates the extension field for the selected state, which can be filled with free text as desired.\n"
"\n"
"Example: When activated, the state \"in progress\" can be expanded to e.g. \"in progress by the motion committee\"."
msgstr ""
+"Attiva il campo di estensione per lo stato selezionato, che può essere "
+"riempito con testo libero a piacere. Esempio: Se attivato, lo stato \"in "
+"corso\" può essere esteso ad esempio a \"in corso da parte della commissione"
+" mozioni\"."
msgid ""
"Activates the extension field of the recommendation in this state, which can"
" be filled with free text or extended with references to other motions or "
"committees as desired."
msgstr ""
+"Attiva il campo di estensione della raccomandazione in questo stato, che può"
+" essere riempito con testo libero o esteso con riferimenti ad altre mozioni "
+"o commissioni, come desiderato."
msgid "Active"
msgstr "Attivo"
@@ -201,13 +226,13 @@ msgid "Add new custom translation"
msgstr "Aggiungere nuova traduzione utenti"
msgid "Add new entry"
-msgstr ""
+msgstr "Aggiungere una nuova voce"
msgid "Add option"
msgstr "Aggiungi opzioni"
msgid "Add timer"
-msgstr ""
+msgstr "Aggiungere il timer"
msgid "Add to agenda"
msgstr "Aggiungere all´ordine del giorno"
@@ -218,6 +243,9 @@ msgstr "Aggiungi alle riunioni"
msgid "Add to queue"
msgstr "Aggiungere alla coda"
+msgid "Add up"
+msgstr "Aggiungere"
+
msgid "Add yourself to the current list of speakers to join the conference"
msgstr "Aggiungiti alla lista dei relatori per parteicpare alla conferenza"
@@ -225,7 +253,10 @@ msgid "Add/remove groups ..."
msgstr "Aggiungere / cancellare gruppi ..."
msgid "Add/remove structure levels ..."
-msgstr ""
+msgstr "Aggiungere/rimuovere livelli di struttura"
+
+msgid "Add/subtract"
+msgstr "Aggiungere/sottrarre"
msgid ""
"Additional columns after the required ones may be present and will not "
@@ -235,7 +266,7 @@ msgstr ""
"all´importazione."
msgid "Administration roles"
-msgstr "Ruoli di amministrazione"
+msgstr "Ruoli dell'amministrazione"
msgid "Administration roles (at organization level)"
msgstr "Ruoli di amministrazione (a livello organizzativo)"
@@ -248,11 +279,15 @@ msgstr "Dopo controllo cliccare su \"Importare\" (in alto a destra). "
msgid "After verifying the preview click on \"import\" please (see top right)."
msgstr ""
+"Dopo aver verificato l'anteprima, fare clic su \"Importa\" (vedere in alto a"
+" destra)."
msgid ""
"Afterwards you may be unable to regain your status in this meeting on your "
"own. Are you sure you want to do this?"
msgstr ""
+"In seguito potreste non essere in grado di riguadagnare il vostro status in "
+"questa riunione da soli. È sicuro di volerlo fare?"
msgid "Agenda"
msgstr "Ordine del Giorno"
@@ -263,10 +298,13 @@ msgstr ""
"attendere ..."
msgid "Agenda visibility"
-msgstr "Visibilità nell'Ordine del Giorno"
+msgstr "Visibilità nell'Ordine del giorno"
-msgid "Alignment"
-msgstr ""
+msgid "Align"
+msgstr "Allinea"
+
+msgid "All"
+msgstr "Tutto"
msgid "All casted ballots"
msgstr "Tutte le schede di votazione consegnate"
@@ -278,16 +316,16 @@ msgid "All lists of speakers will be cleared."
msgstr "Tutte le liste di relatori veranno cancellati."
msgid "All meetings"
-msgstr ""
+msgstr "Tutte le riunioni"
msgid "All other fields are optional and may be empty."
msgstr "Tutti gli altri campi sono facoltativi e possono essere vuoti."
msgid "All present entitled users"
-msgstr ""
+msgstr "Tutti gli utenti presenti autorizzati"
msgid "All structure levels"
-msgstr ""
+msgstr "Tutti i livelli di struttura"
msgid "All topics will be deleted and won't be accessible afterwards."
msgstr "Tutti i temi saranno cancellati e non più accessibili."
@@ -311,7 +349,7 @@ msgid "Allow forwarding of motions"
msgstr "Consentire l'inoltro di mozioni"
msgid "Allow one participant multiple times on the same list"
-msgstr ""
+msgstr "Ammettere un partecipante molte volte nella stessa lista"
msgid ""
"Allow only current speakers and list of speakers managers to enter the live "
@@ -375,9 +413,10 @@ msgstr "Numero di voti"
msgid "An email with a password reset link has been sent."
msgstr ""
+"E' stata inviata un'e-mail con un link per la reimpostazione della password."
msgid "An error occurred while voting."
-msgstr ""
+msgstr "Si è verificato un errore nella votazione"
msgid "An unknown error occurred."
msgstr "Si è verificato un errore sconosciuto."
@@ -400,6 +439,9 @@ msgstr "Immagine particle d'applauso in URL"
msgid "Applause visualization"
msgstr "Visualizzazione dell'applauso"
+msgid "Application update in progress."
+msgstr "Aggiornamento dell'applicazione in corso."
+
msgid "Apply"
msgstr "Applicare"
@@ -413,16 +455,24 @@ msgid "Archived"
msgstr "Archiviato"
msgid "Archived meetings"
-msgstr ""
+msgstr "Riunioni archiviate"
msgid ""
"Are you sure you want to activate this color set? This will change the "
"colors in all meetings."
msgstr ""
+"Siete sicuri di voler attivare questo set di colori? Questo cambierà i "
+"colori in tutte le riunioni."
msgid "Are you sure you want to activate this meeting?"
msgstr "È sicuro di voler attivare questa riunione?"
+msgid ""
+"Are you sure you want to add the following time onto every structure level?"
+msgstr ""
+"Siete sicuri di voler aggiungere il seguente tempo a ogni livello della "
+"struttura?"
+
msgid "Are you sure you want to anonymize all votes? This cannot be undone."
msgstr ""
"Sei sicuro di voler rendere anonimi tutti i voti? Non può essere "
@@ -440,12 +490,14 @@ msgstr "Sicuro di voler cancellare tutti i relatori da tutte le liste?"
msgid ""
"Are you sure you want to delete all next speakers from this list of "
"speakers?"
-msgstr ""
+msgstr "Sicuro di voler eliminare tutti i prossimi oratori da questo elenco?"
msgid ""
"Are you sure you want to delete all previous speakers from this list of "
"speakers?"
msgstr ""
+"È sicuro di voler eliminare tutti gli oratori precedenti da questo elenco di"
+" oratori?"
msgid "Are you sure you want to delete all selected elections?"
msgstr "Sei sicuro di voler cancellare tutte le elezioni selezionate?"
@@ -454,7 +506,7 @@ msgid "Are you sure you want to delete all selected files and folders?"
msgstr "Sicuro di voler cancellare tutti i file e registri selezionati?"
msgid "Are you sure you want to delete all selected meetings?"
-msgstr ""
+msgstr "Siete sicuri di voler eliminare tutte le riunioni selezionate?"
msgid "Are you sure you want to delete all selected motions?"
msgstr "Sicuro di voler cancellare tutte le mozioni selezionate?"
@@ -467,7 +519,7 @@ msgid ""
msgstr "Sicuro di voler cancellare tutti i relatori da questo elenco?"
msgid "Are you sure you want to delete the editorial final version?"
-msgstr ""
+msgstr "Sei sicuro di voler cancellare la versione editoriale finale?"
msgid "Are you sure you want to delete these accounts?"
msgstr "Sicuro di voler eliminare questi account?"
@@ -515,19 +567,16 @@ msgid "Are you sure you want to delete this motion block?"
msgstr "Sicuro di voler cancellare questa sezione di mozione? "
msgid "Are you sure you want to delete this motion? "
-msgstr ""
+msgstr "Sicuro di voler cancellare questa mozione? "
msgid "Are you sure you want to delete this projector?"
msgstr "Sicuro di voler cancellare questo proiettore?"
msgid "Are you sure you want to delete this state?"
-msgstr ""
-
-msgid "Are you sure you want to delete this statute paragraph?"
-msgstr "Sicuro di voler cancellare questa paragrafo dello statuto?"
+msgstr "Siete sicuri di voler cancellare questo stato?"
msgid "Are you sure you want to delete this structure level?"
-msgstr ""
+msgstr "Siete sicuri di voler eliminare questo livello di struttura?"
msgid "Are you sure you want to delete this tag?"
msgstr "Sicuro di voler cancellare questa parole chiave?"
@@ -551,6 +600,8 @@ msgid ""
"Are you sure you want to end this contribution which still has interposed "
"question(s)?"
msgstr ""
+"Sei sicuro di voler terminare questo contributo, che ha ancora delle domande"
+" da porre?"
msgid ""
"Are you sure you want to generate new passwords for all selected "
@@ -560,7 +611,7 @@ msgstr ""
"selezionati?"
msgid "Are you sure you want to irrevocably remove your point of order?"
-msgstr ""
+msgstr "È sicuro di voler eliminare irrevocabilmente la mozione d'ordine?"
msgid "Are you sure you want to number all agenda items?"
msgstr "Sicuro di voler numerare tutti i punti dell´ordine del giorno?"
@@ -577,7 +628,7 @@ msgstr ""
"giorno?"
msgid "Are you sure you want to remove these participants?"
-msgstr ""
+msgstr "Siete sicuri di voler rimuovere questi partecipanti?"
msgid "Are you sure you want to remove this entry from the agenda?"
msgstr "Sicuro di voler cancellare questo contributo dall´ordine del giorno?"
@@ -587,7 +638,7 @@ msgstr ""
"Sicuro di voler cancellare questa mozione dalla sezione delle mozioni?"
msgid "Are you sure you want to remove this participant?"
-msgstr ""
+msgstr "Sei sicuro di voler rimuovere questo partecipante?"
msgid ""
"Are you sure you want to remove this speaker from the list of speakers?"
@@ -603,21 +654,36 @@ msgstr ""
msgid "Are you sure you want to reset all options to default settings?"
msgstr ""
+"Siete sicuri di voler ripristinare tutte le opzioni alle impostazioni "
+"predefinite?"
msgid ""
"Are you sure you want to reset all options to default settings? All changes "
"of this settings group will be lost!"
msgstr ""
+"Siete sicuri di voler ripristinare tutte le opzioni alle impostazioni "
+"predefinite? Tutte le modifiche apportate a questo gruppo di impostazioni "
+"andranno perse!"
msgid "Are you sure you want to reset all passwords to the default ones?"
msgstr "Sicuro di voler resettare tutti i password a quelli iniziali?"
+msgid ""
+"Are you sure you want to reset the time to the last set value? It will be "
+"reset to:"
+msgstr ""
+"Siete sicuri di voler ripristinare l'ora all'ultimo valore impostato? Verrà "
+"reimpostato su:"
+
msgid "Are you sure you want to reset this vote?"
msgstr "Sicuro di voler resettare questa votazione?"
msgid "Are you sure you want to send an invitation email to the user?"
msgstr "Sicuro di voler inviare un invito a questo utente?"
+msgid "Are you sure you want to send an invitation email?"
+msgstr "Siete sicuri di voler inviare un'e-mail di invito?"
+
msgid "Are you sure you want to send emails to all selected participants?"
msgstr "Sicuro di voler inviare e-mails a tutti i partecipanti selezionati?"
@@ -661,13 +727,16 @@ msgstr ""
"\"Aggiungi alle riunioni\" nella vista dei dettagli dell'account."
msgid "Attention: First enter the wifi data in [Settings > General]"
-msgstr ""
+msgstr "Attenzione: Inserire prima i dati wifi in [Impostazioni > Generali]."
+
+msgid "Attention: Not selected accounts will be merged and then deleted."
+msgstr "Attenzione: gli account non selezionati verranno uniti e poi eliminati."
msgid "Attention: This action cannot be undone!"
-msgstr ""
+msgstr "Attenzione: Questa azione non può essere annullata!"
msgid "Attribute mapping (JSON)"
-msgstr ""
+msgstr "Mappatura degli attributi (JSON)"
msgid "Automatically open the microphone for new conference speakers"
msgstr ""
@@ -682,8 +751,11 @@ msgstr ""
msgid "Autopilot"
msgstr "Autopilota"
+msgid "Autopilot widgets"
+msgstr "Widget del pilota automatico"
+
msgid "Autoupdate unhealthy"
-msgstr ""
+msgstr "Aggiornamento automatico dannoso"
msgid "Available sizes are 10, 11 and 12"
msgstr "Le misure disponibili sono 10, 11 e 12"
@@ -707,13 +779,13 @@ msgid "Ballot"
msgstr "Votazione"
msgid "Ballot anonymized"
-msgstr ""
+msgstr "Voto anonimo"
msgid "Ballot created"
-msgstr ""
+msgstr "Creazione della votazione"
msgid "Ballot deleted"
-msgstr ""
+msgstr "Votazione eliminata"
msgid "Ballot opened"
msgstr "Votazione aperta"
@@ -722,26 +794,29 @@ msgid "Ballot papers"
msgstr "Scheda elettorale"
msgid "Ballot published"
-msgstr ""
+msgstr "Votazione pubblicata"
msgid "Ballot reset"
-msgstr ""
+msgstr "Votazione resettata"
msgid "Ballot started"
-msgstr ""
+msgstr "Votazione iniziata"
msgid "Ballot stopped"
-msgstr ""
+msgstr "Votazione interrotta"
msgid "Ballot stopped/published"
-msgstr ""
+msgstr "Votazione interrotta/pubblicata"
msgid "Ballot updated"
-msgstr ""
+msgstr "Votazione aggiornata"
msgid "Ballots"
msgstr "Votazioni"
+msgid "Ballots cast"
+msgstr "Voti espressi"
+
msgid "Base folder"
msgstr "Cartella base"
@@ -751,18 +826,21 @@ msgstr "Inizio relazione"
msgid "Blank between prefix and number, e.g. 'A 001'."
msgstr "Spazio tra prefisso e numero, p. e. ´A 001´."
-msgid "Blocks"
+msgid "Blockquote"
msgstr ""
msgid "Bold"
-msgstr ""
+msgstr "Grassetto"
-msgid "CSV export options"
-msgstr "Opzioni di esportazione CSV"
+msgid "Bullet list"
+msgstr ""
msgid "CSV import"
msgstr "Importazione CSV"
+msgid "CSV options"
+msgstr "Opzioni CSV"
+
msgid "Calendar"
msgstr "Calendario"
@@ -776,20 +854,25 @@ msgid "Called with"
msgstr "E' chiamato anche"
msgid "Can activate and deactivate logos and fonts under > [Files]."
-msgstr ""
+msgstr "È possibile attivare e disattivare loghi e font in > [File]."
msgid ""
"Can add or delete speakers to or from the list of speakers, mark, sort, "
"start/stop and open/close the list of speakers."
msgstr ""
+"È possibile aggiungere o eliminare oratori dall'elenco degli oratori, "
+"contrassegnare, ordinare, avviare/arrestare e aprire/chiudere l'elenco degli"
+" oratori."
msgid ""
"Can add their name to the list of candidates in the [Search for candidates] "
"phase."
msgstr ""
+"Può aggiungere il proprio nome all'elenco dei candidati nella fase [Ricerca "
+"di candidati]."
msgid "Can change the presence status of other participants."
-msgstr ""
+msgstr "Può modificare lo stato di presenza di altri partecipanti."
msgid "Can create amendments"
msgstr "Può creare mozioni di modifica"
@@ -798,6 +881,8 @@ msgid ""
"Can create amendments and modify them later, depending on the workflow, but "
"cannot delete them."
msgstr ""
+"Può creare modifiche e modificarle in seguito, a seconda del flusso di "
+"lavoro, ma non può cancellarle."
msgid "Can create motions"
msgstr "Può creare mozioni"
@@ -806,53 +891,73 @@ msgid ""
"Can create motions and modify them later, depending on the workflow, but "
"cannot delete them."
msgstr ""
+"Può creare mozioni e modificarle in seguito, a seconda del flusso di lavoro,"
+" ma non può eliminarle."
msgid "Can create, change, delete tags for the agenda and for motions."
msgstr ""
+"Può creare, modificare e cancellare tag per l'ordine del giorno e per le "
+"mozioni."
msgid "Can create, change, start/stop and delete polls."
-msgstr ""
+msgstr "Può creare, modificare, avviare/arrestare ed eliminare i sondaggi."
msgid "Can create, configure, control and delete projectors."
-msgstr ""
+msgstr "Può creare, configurare, controllare ed eliminare i proiettori."
msgid ""
"Can create, modify and delete elections and candidate lists, as well as "
"start/stop and reset ballots. "
msgstr ""
+"Può creare, modificare e cancellare elezioni e liste di candidati, nonché "
+"avviare/arrestare e reimpostare le votazioni."
msgid ""
"Can create, modify and delete motions and votings, amendments and change "
"recommendations, and edit the metadata of a motion. Including the management"
" of categories, motion blocks, tags, workflows and comment fields."
msgstr ""
+"Può creare, modificare e cancellare mozioni e votazioni, emendamenti e "
+"raccomandazioni di modifica, nonché modificare i metadati di una mozione. "
+"Comprende la gestione di categorie, blocchi di mozioni, tag, flussi di "
+"lavoro e campi di commento."
msgid ""
"Can create, modify and delete topics, add motions and elections to the "
"agenda, sort, number and tag agenda items."
msgstr ""
+"Può creare, modificare e cancellare argomenti, aggiungere mozioni ed "
+"elezioni all'ordine del giorno, ordinare, numerare ed etichettare i punti "
+"all'ordine del giorno."
msgid ""
"Can create, modify, delete chat groups and define permissions.\n"
"\n"
"Note: The chat menu item becomes visible to all participants, except admins, as soon as a chat has been created."
msgstr ""
+"Può creare, modificare, eliminare gruppi di chat e definire le "
+"autorizzazioni. Nota: La voce di menu chat diventa visibile a tutti i "
+"partecipanti, tranne gli amministratori, non appena viene creata una chat."
msgid ""
"Can create, modify, delete participant datasets and administrate group "
"permissions."
msgstr ""
+"Can create, modify, delete participant datasets and administrate group "
+"permissions."
msgid "Can create, modify, start/stop and delete votings."
-msgstr ""
+msgstr "Può creare, modificare, avviare/arrestare ed eliminare le votazioni."
msgid "Can edit all moderation notes."
-msgstr ""
+msgstr "Può modificare tutte le note di moderazione."
msgid ""
"Can edit and assign the following motion metadata: Submitter, state, "
"recommendation, category, motion blocks and tags."
msgstr ""
+"Può modificare e assegnare i seguenti metadati del movimento: Presentatore, "
+"stato, raccomandazione, categoria, blocchi di mozioni e tag."
msgid "Can forward motions"
msgstr "Può inoltrare mozioni"
@@ -868,6 +973,11 @@ msgid ""
"2. target meeting must be created.\n"
"3. forwarding must be activated in the workflow in the state."
msgstr ""
+"Può inoltrare le mozioni ad altre riunioni all'interno dell'istanza "
+"OpenSlides. Altri requisiti: 1. la gerarchia di inoltro deve essere "
+"impostata a livello organizzativo nel comitato. 2. deve essere creata una "
+"riunione di destinazione. 3. l'inoltro deve essere attivato nel flusso di "
+"lavoro nello stato."
msgid "Can manage agenda"
msgstr "Può gestire l'ordine del giorno"
@@ -885,7 +995,7 @@ msgid "Can manage logos and fonts"
msgstr "Può gestire i logo e caratteri"
msgid "Can manage moderation notes"
-msgstr ""
+msgstr "Può gestire le note di moderazione"
msgid "Can manage motion metadata"
msgstr "Può gestire mozioni metadata"
@@ -917,6 +1027,10 @@ msgstr "Può gestire la chat"
msgid "Can manage the projector"
msgstr "Può gestire il proiettore"
+msgid "Can modify existing participants, but cannot create or delete them."
+msgstr ""
+"Può modificare i partecipanti esistenti, ma non può crearli o eliminarli."
+
msgid "Can nominate another participant"
msgstr "Può nominare altri partecipati per l'elezione"
@@ -928,9 +1042,11 @@ msgid ""
"\n"
"Requires group permission: [Can see participants]"
msgstr ""
+"Può nominare altri partecipanti come candidati. Richiede l'autorizzazione "
+"del gruppo: [Può vedere i partecipanti]."
msgid "Can not import because of errors"
-msgstr ""
+msgstr "Impossibile importare a causa di errori"
msgid "Can put oneself on the list of speakers"
msgstr "Può mettersi sulla lista dei relatori"
@@ -945,19 +1061,24 @@ msgid "Can see agenda"
msgstr "Può vedere l'ordine del giorno"
msgid "Can see all internal topics, schedules and comments."
-msgstr ""
+msgstr "Può vedere tutti gli argomenti interni, gli orari e i commenti."
msgid "Can see all lists of speakers"
-msgstr ""
+msgstr "Può vedere tutti gli elenchi dei relatori"
msgid "Can see all moderation notes in each list of speakers."
-msgstr ""
+msgstr "Può vedere tutte le note di moderazione in ogni elenco di oratori."
msgid "Can see elections"
msgstr "Può vedere le elezioni"
-msgid "Can see files"
+msgid "Can see email, username and SSO identification of all participants."
msgstr ""
+"Può vedere l'e-mail, il nome utente e l'identificazione SSO di tutti i "
+"partecipanti."
+
+msgid "Can see files"
+msgstr "Può vedere i files"
msgid "Can see history"
msgstr "Può vedere lo storico"
@@ -970,7 +1091,7 @@ msgid "Can see list of speakers"
msgstr "Può vedere la lista dei relatori"
msgid "Can see moderation notes"
-msgstr ""
+msgstr "Può vedere le note di moderazione"
msgid "Can see motions"
msgstr "Può vedere le mozioni"
@@ -983,47 +1104,70 @@ msgid ""
"\n"
"Tip: Cross-check desired visibility of motions with test delegate account. "
msgstr ""
+"Può vedere i movimenti nello stato interno che sono limitati nel flusso di "
+"lavoro sotto Restrizioni con la stessa descrizione: Verificare la visibilità "
+" desiderata dei movimenti con l'account del delegato di prova. "
msgid "Can see participants"
msgstr "Può vedere i partecipanti"
+msgid "Can see sensitive data"
+msgstr "Può vedere i dati sensibili"
+
msgid "Can see the Agenda menu item and all public topics in the agenda."
msgstr ""
+"Può vedere la voce di menu Agenda e tutti gli argomenti pubblici "
+"dell'agenda."
msgid ""
"Can see the Autopilot menu item with all content for which appropriate "
"permissions are set."
msgstr ""
+"Può vedere la voce di menu Autopilot con tutti i contenuti per i quali sono "
+"state impostate le autorizzazioni appropriate."
msgid ""
"Can see the Files menu item and all shared folders and files.\n"
"\n"
"Note: Sharing of folders and files may be restricted by group assignment."
msgstr ""
+"Può vedere la voce di menu File e tutte le cartelle e i file condivisi. "
+"Nota: la condivisione di cartelle e file può essere limitata "
+"dall'assegnazione di un gruppo."
msgid ""
"Can see the History menu item with the history of processing timestamps for motions, elections and participants. \n"
"\n"
"Note: For privacy reasons, it is recommended to limit the rights to view the History significantly."
msgstr ""
+"Può visualizzare la voce di menu Cronologia con la cronologia dei tempi di "
+"elaborazione delle mozioni, delle elezioni e dei partecipanti. Nota: per "
+"motivi di privacy, si consiglia di limitare notevolmente i diritti di "
+"visualizzazione della Cronologia."
msgid "Can see the Home menu item."
-msgstr ""
+msgstr "Può vedere la voce di menù Home"
msgid ""
"Can see the Motions menu item and all motions unless they are limited by "
"access restrictions in the workflow."
msgstr ""
+"Può vedere la voce di menu Mozioni e tutte le mozioni, a meno che non siano "
+"limitate da restrizioni di accesso nel flusso di lavoro."
msgid ""
"Can see the Projector menu item and all projectors (in the Autopilot as well"
" as in the Projector menu item)"
msgstr ""
+"Può vedere la voce di menu Proiettore e tutti i proiettori (sia "
+"nell'Autopilota che nella voce di menu Proiettore)"
msgid ""
"Can see the Settings menu item and edit all settings as well as the start "
"page of the meeting."
msgstr ""
+"Può vedere la voce di menu Impostazioni e modificare tutte le impostazioni, "
+"nonché la pagina iniziale della riunione."
msgid "Can see the autopilot"
msgstr "Può vedere l'autopilota"
@@ -1038,18 +1182,27 @@ msgid ""
"Can see the livestream if there is a livestream URL entered in > [Settings] "
"> [Livestream]."
msgstr ""
+"È possibile vedere il livestream se è stato inserito un URL livestream in > "
+"[Impostazioni] > [Livestream]."
msgid ""
"Can see the menu item Elections, including the list of candidates and results. \n"
"\n"
"Note: The right to vote is defined directly in the ballot."
msgstr ""
+"Può vedere la voce di menu Elezioni, compreso l'elenco dei candidati e i "
+"risultati. Nota: Il diritto di voto è definito direttamente nella scheda "
+"elettorale."
msgid ""
"Can see the menu item Participants and therefore the following data from all participants: \n"
"Personal data: Name, pronoun, gender. \n"
"Meeting specific information: Structure level, Group, Participant number, About me, Presence status."
msgstr ""
+"Può vedere la voce di menu Partecipanti e quindi i seguenti dati di tutti i "
+"partecipanti: Dati personali: Nome, pronome, sesso. Informazioni specifiche "
+"della riunione: Livello della struttura, Gruppo, Numero del partecipante, "
+"Informazioni su di me, Stato della presenza."
msgid "Can see the projector"
msgstr "Può vedere il proiettore"
@@ -1061,11 +1214,19 @@ msgid ""
"Can support motions. The support function must be enabled in > [Settings] > "
"[Motions] as well as for the corresponding state in > [Workflow]."
msgstr ""
+"Può supportare le mozioni. La funzione di supporto deve essere abilitata in "
+"> [Impostazioni] > [Movimenti] così come per lo stato corrispondente in > "
+"[Flusso di lavoro]."
+
+msgid "Can update participants"
+msgstr "Può eleggere partecipanti"
msgid ""
"Can upload, modify and delete files, administrate folders and change access "
"restrictions."
msgstr ""
+"Può caricare, modificare e cancellare file, amministrare cartelle e "
+"modificare le restrizioni di accesso."
msgid "Cancel"
msgstr "Annulla"
@@ -1077,13 +1238,13 @@ msgid "Cancel editing without saving"
msgstr "Annulla lavorazione senza salvare"
msgid "Candidate"
-msgstr ""
+msgstr "Candidato"
msgid "Candidate added"
-msgstr ""
+msgstr "Aggiunto candidato"
msgid "Candidate removed"
-msgstr ""
+msgstr "Candidato rimosso"
msgid "Candidates"
msgstr "Candidati"
@@ -1092,13 +1253,10 @@ msgid "Cannot do that in demo mode!"
msgstr "Questa opzione non disponibile nel modus demo!"
msgid "Cannot forward motions"
-msgstr "Non può inoltrare mozioni"
-
-msgid "Cannot navigate to the selected history element."
-msgstr "Non si può aprire il documento storico selezionato."
+msgstr "Impossibile inoltrare mozioni"
msgid "Cannot receive motions"
-msgstr "Non può ricevere mozioni"
+msgstr "Impossibile ricevere mozioni"
msgid "Categories"
msgstr "Categorie"
@@ -1119,7 +1277,7 @@ msgid "Center"
msgstr "Centro"
msgid "Change color set"
-msgstr ""
+msgstr "Cambia il set di colori"
msgid "Change paragraph"
msgstr "Cambiare paragrafo"
@@ -1167,39 +1325,49 @@ msgid "Check in or check out participants based on their participant numbers:"
msgstr "Check in o check out dei partecipanti secondo la propria numerazione"
msgid "Checkmate! You lost!"
-msgstr ""
+msgstr "Scacco matto! Hai perso!"
msgid "Checkmate! You won!"
-msgstr ""
+msgstr "Scacco matto! Hai vinto!"
msgid "Chess"
-msgstr ""
+msgstr "Scacchi"
msgid "Choice"
msgstr "Scelta"
msgid "Choose 0 to disable Intervention."
+msgstr "Scegliere 0 per disabilitare Intervento."
+
+msgid ""
+"Choose 0 to disable speaking times widget for structure level countdowns."
msgstr ""
+"Scegliere 0 per disattivare il widget dei tempi di parola per i conti alla "
+"rovescia dei livelli della struttura."
msgid "Choose 0 to disable the supporting system."
msgstr "Per disattivare il sistema di sostegno premere \"0\"."
-msgid ""
-"Choose a number greater than 0 to activate speaking times widget for "
-"structure level countdowns."
-msgstr ""
-
msgid "Chyron"
-msgstr "Fascetta"
+msgstr "Sottopancia"
-msgid "Chyron background color"
-msgstr "Colore sfondo fascetta"
+msgid "Chyron agenda item, background color"
+msgstr "Voce dell'agenda del sottopancia, colore dello sfondo"
-msgid "Chyron font color"
-msgstr "Colore carattere fascetta"
+msgid "Chyron agenda item, font color"
+msgstr "Voce dell'agenda del sottopancia, colore del carattere"
msgid "Chyron speaker name"
-msgstr ""
+msgstr "Sottopancia con nome del relatore"
+
+msgid "Chyron speaker, background color"
+msgstr "Sottopancia con nome del relatore, colore dello sfondo"
+
+msgid "Chyron speaker, font color"
+msgstr "Sottopancia con nome del relatore, colore della scritta"
+
+msgid "Classic"
+msgstr "Classico"
msgid "Clear"
msgstr "Cancella"
@@ -1211,7 +1379,10 @@ msgid "Clear all list of speakers"
msgstr "Cancellare tutte le liste dei relatori"
msgid "Clear current projection"
-msgstr ""
+msgstr "Cancella la proiezione corrente"
+
+msgid "Clear formatting"
+msgstr "Cancella formattazione"
msgid "Clear list"
msgstr "Cancellare la lista"
@@ -1220,7 +1391,7 @@ msgid "Clear motion block"
msgstr "Cancellare sezione di mozioni"
msgid "Clear recommendation"
-msgstr ""
+msgstr "Cancella la raccomandazione"
msgid "Clear tags"
msgstr "Cancellare parole chiave"
@@ -1229,19 +1400,19 @@ msgid "Click here to vote!"
msgstr "Premere qui per votare!"
msgid "Close"
-msgstr ""
+msgstr "Chiudi"
msgid "Close edit mode"
-msgstr ""
+msgstr "Chiudere la modalità di modifica"
msgid "Close list of speakers"
msgstr "Chiudere lista dei relatori"
-msgid "Closed items"
-msgstr "Mozioni deliberate"
+msgid "Closed"
+msgstr "Chiusa"
-msgid "Code"
-msgstr ""
+msgid "Closed items"
+msgstr "Articoli chiusi"
msgid "Collapse all"
msgstr "Chiudere tutto"
@@ -1250,7 +1421,7 @@ msgid "Color"
msgstr "Colore"
msgid "Color set"
-msgstr ""
+msgstr "Set di colori"
msgid "Column separator"
msgstr "Divisore colonne"
@@ -1268,10 +1439,10 @@ msgid "Comment fields"
msgstr "Campo per il commento"
msgid "Comment section"
-msgstr ""
+msgstr "Sezione commenti"
msgid "Comment sections"
-msgstr ""
+msgstr "Sezioni commenti"
msgid "Comment updated"
msgstr "Commento aggiornato"
@@ -1292,7 +1463,7 @@ msgid "Committee"
msgstr "Comitato"
msgid "Committee Management Level changed"
-msgstr ""
+msgstr "Livello comitato di Gestione cambiato"
msgid "Committee admin"
msgstr "Amministratore del Comitato"
@@ -1310,16 +1481,16 @@ msgid "Committees and meetings"
msgstr "Comitati e riunioni"
msgid "Committees created"
-msgstr ""
+msgstr "Creati comitati"
msgid "Committees updated"
-msgstr ""
+msgstr "Comitati aggiornati"
msgid "Committees with errors"
-msgstr ""
+msgstr "Comitati con errori"
msgid "Committees with warnings: affected cells will be skipped"
-msgstr ""
+msgstr "Comitati con avvertenze: le celle interessate saranno saltate"
msgid "Conference room"
msgstr "Stanza delle conferenze."
@@ -1328,13 +1499,13 @@ msgid "Confirm new password"
msgstr "Confermare la nuova password"
msgid "Confirmation of the nomination list"
-msgstr ""
+msgstr "Conferma della lista delle nomine"
msgid "Congratuations! Your browser is supported by OpenSlides."
msgstr "Congratulazionii! Il suo browser viene supportato da OpenSlides."
msgid "Connect 4"
-msgstr ""
+msgstr "4 vince"
msgid "Connect all users to live conference automatically."
msgstr ""
@@ -1356,16 +1527,16 @@ msgid "Contribution"
msgstr "Contributo"
msgid "Contributions"
-msgstr ""
+msgstr "Contributi"
msgid "Copy report to clipboard"
-msgstr ""
+msgstr "Copiare il rapporto negli appunti"
msgid "Count completed requests to speak"
msgstr "Contare richieste di parola concluse"
msgid "Count logged-in users"
-msgstr ""
+msgstr "Conteggio degli utenti connessi"
msgid "Countdown"
msgstr "Countdown"
@@ -1386,10 +1557,10 @@ msgid "Create"
msgstr "Creare"
msgid "Create editorial final version"
-msgstr ""
+msgstr "Creare la versione editoriale finale"
msgid "Create subitem"
-msgstr ""
+msgstr "Creare una sottovoce"
msgid "Create user"
msgstr "Creare utene"
@@ -1403,6 +1574,9 @@ msgstr "Creazione"
msgid "Creation date"
msgstr "Data di creazione"
+msgid "Current agenda item"
+msgstr "Punto all'ordine del giorno attuale"
+
msgid "Current date"
msgstr "Data attuale"
@@ -1410,16 +1584,19 @@ msgid "Current list of speakers"
msgstr "Lista dei relatori attuale"
msgid "Current list of speakers (as slide)"
-msgstr ""
+msgstr "Lista degli oratori attuale (come slide)"
msgid "Current slide"
-msgstr ""
+msgstr "Slide corrente"
msgid "Current speaker"
-msgstr ""
+msgstr "Oratore corrente"
msgid "Current speaker chyron"
-msgstr ""
+msgstr "Sottopancia dell'attuale relatore/relatrice"
+
+msgid "Current window"
+msgstr "Finestra corrente"
msgid "Currently no livestream available."
msgstr "Attualmente livestream non disponibile."
@@ -1436,11 +1613,14 @@ msgstr "Numero personalizzato di schede elettorali"
msgid "Custom translations"
msgstr "Traduzioni personalizzate"
+msgid "Customize autopilot"
+msgstr "Personalizzare il pilota automatico"
+
msgid "Dark mode"
msgstr "Modalità scura"
msgid "Dashboard"
-msgstr ""
+msgstr "Dashboard"
msgid "Datastore is corrupt! See the console for errors."
msgstr "Il datastore è corrotto! Vedere la console per gli errori."
@@ -1454,11 +1634,8 @@ msgstr "Decisione"
msgid "Default"
msgstr "Default"
-msgid "Default 100 % base of a voting result"
-msgstr "Preimpostato base di 100% per risultato di votazione"
-
-msgid "Default 100 % base of an election result"
-msgstr "Preimpostato base di 100% del risultato d'elezione"
+msgid "Default 100 % base"
+msgstr "Predefinito 100 % base"
msgid "Default election method"
msgstr "Preimpostato metodo d'elezione "
@@ -1475,12 +1652,18 @@ msgstr "Gruppi preimpostati con diritto di voto"
msgid "Default line numbering"
msgstr "Numerazione di linea predefinita"
-msgid "Default speaking time for structure levels in seconds"
+msgid ""
+"Default speaking time contingent for parliamentary groups (structure levels)"
+" in seconds"
msgstr ""
+"Contingente di tempo di parola predefinito per i gruppi parlamentari "
+"(livelli di struttura) in secondi"
msgid ""
"Default text version for change recommendations and projection of motions"
msgstr ""
+"Versione di testo predefinita per le raccomandazioni di modifica e la "
+"proiezione di mozioni"
msgid "Default visibility for new agenda items (except topics)"
msgstr ""
@@ -1501,9 +1684,15 @@ msgid ""
"- If no option is selected, the motions in the selected state are visible to all; The prerequisite for this is group permission: [Can see motions].\n"
"- Selecting one or more options restricts access to those groups for which the selected authorization option is defined under > [Participants] > [Groups]."
msgstr ""
+"Definisce per lo stato selezionato quali gruppi hanno accesso:- Se non viene"
+" selezionata alcuna opzione, le mozioni nello stato selezionato sono "
+"visibili a tutti; il prerequisito è l'autorizzazione del gruppo: [puoi "
+"vedere mozioni]. Selezionando una o più opzioni, l'accesso è limitato ai "
+"gruppi per i quali l'opzione di autorizzazione selezionata è definita in > "
+"[Partecipanti] > [Gruppi]."
msgid "Defines the colour for the state button."
-msgstr ""
+msgstr "Definisce il colore del pulsante di stato."
msgid ""
"Defines the maximum deflection. Entering zero will use the amount of present"
@@ -1523,7 +1712,7 @@ msgstr "Definisce il tempo nel quale si sommano applausi."
msgid ""
"Defines the wording of the recommendation that belongs to this state.\n"
-"Example: State = Accepted / Recommendation = Acceptance. \n"
+"Example: State = Accepted / Recommendation = Acceptance.\n"
"\n"
"To activate the recommendation system, a recommender (for example, a motion committee) must be defined under > [Settings] > [Motions] > [Name of recommender].\n"
"Example recommender: motion committee\n"
@@ -1531,21 +1720,31 @@ msgid ""
"Additional information:\n"
"In combination with motion blocks, the recommendation of multiple motions can be followed simultaneously."
msgstr ""
+"Definisce la formulazione della raccomandazione che appartiene a questo stato.\n"
+"Esempio: Stato = Accettato / Raccomandazione = Accettazione.\n"
+"\n"
+"Per attivare il sistema di raccomandazione, è necessario definire un raccomandante (ad esempio, un comitato di mozione) in > [Impostazioni] > [Mozioni] > [Nome del raccomandante].\n"
+"Esempio di raccomandante: comitato di mozione\n"
+"\n"
+"Informazioni aggiuntive:\n"
+"In combinazione con blocchi di mozione, è possibile seguire contemporaneamente la raccomandazione di più mozioni."
msgid "Defines which states can be selected next in the workflow."
msgstr ""
+"Definisce quali stati possono essere selezionati successivamente nel flusso "
+"di lavoro."
msgid "Delegation of vote"
-msgstr "Trasferimento diritto di voto"
+msgstr "Delega di voto"
msgid "Delete"
msgstr "Cancellare"
msgid "Delete color set"
-msgstr ""
+msgstr "Cancella il set di colori"
msgid "Delete editorial final version"
-msgstr ""
+msgstr "Cancella la versione editoriale finale"
msgid "Delete projector"
msgstr "Cancellare proiettore"
@@ -1553,10 +1752,10 @@ msgstr "Cancellare proiettore"
msgid "Deleted user"
msgstr "Utente cancellato"
-msgid ""
-"Deleting this motion will likely impact it's amendments negatively and they "
-"could become unusable."
+msgid "Deleting this motion will also delete the amendments."
msgstr ""
+"L'eliminazione di questa mozione comporta l'eliminazione anche degli "
+"emendamenti."
msgid "Deletion"
msgstr "Cancellazione"
@@ -1582,20 +1781,21 @@ msgstr "Non ha ricevuto un'e-mail"
msgid "Diff version"
msgstr "Versione di modifica"
+msgid "Disable connection closing on inactivity"
+msgstr "Disabilita la chiusura della connessione in caso di inattività "
+
msgid "Disabled (no percents)"
msgstr "Disabilitati (senza percentuali)"
msgid "Disallow new point of order when list of speakers is closed"
msgstr ""
+"Respinge una nuova mozione d'ordine quando l'elenco degli oratori è chiuso"
msgid "Display type"
msgstr "Formato display"
msgid "Distribute overhang time"
-msgstr ""
-
-msgid "Div"
-msgstr ""
+msgstr "Riportare il tempo di recupero"
msgid "Divergent:"
msgstr "Divergente"
@@ -1603,11 +1803,14 @@ msgstr "Divergente"
msgid "Do not forget to save your changes!"
msgstr "Non dimenticare di salvare modifiche!"
+msgid "Do not show recommendations publicly"
+msgstr "Non mostrare pubblicamente le raccomandazioni"
+
msgid "Do you accept?"
-msgstr ""
+msgstr "Accetti?"
msgid "Do you really want to delete this color set?"
-msgstr ""
+msgstr "Volete davvero eliminare questo set di colori?"
msgid "Do you really want to discard all your changes?"
msgstr "Volete davvero scartare tutte le vostre modifiche?"
@@ -1643,6 +1846,9 @@ msgstr "Scaricare il file CSV di esempio"
msgid "Download folder"
msgstr "Cartella Download"
+msgid "Download the file"
+msgstr "Scarica il file"
+
msgid "Drop files into this area OR click here to select files"
msgstr "Spostare files in questa sezione O premere qui per selezionare files"
@@ -1650,7 +1856,7 @@ msgid "Duplicate"
msgstr "Duplicare"
msgid "Duplicate from"
-msgstr "Duplicato da"
+msgstr "Copiato da"
msgid "Duplicates"
msgstr ""
@@ -1659,7 +1865,7 @@ msgid "Duration"
msgstr "Durata"
msgid "Duration in minutes"
-msgstr ""
+msgstr "Durata in minuti"
msgid "Duration of all requests to speak"
msgstr "Durata di tutte le richieste di parola"
@@ -1684,8 +1890,11 @@ msgstr ""
msgid "Edit"
msgstr "Modificare"
+msgid "Edit HTML content"
+msgstr "Modifica il contenuto HTML"
+
msgid "Edit account"
-msgstr ""
+msgstr "Modifica account"
msgid "Edit comment field"
msgstr "Modificare campo commento"
@@ -1700,31 +1909,28 @@ msgid "Edit details for"
msgstr "Modificare dettagli per "
msgid "Edit editorial final version"
-msgstr ""
+msgstr "Modifica la versione editoriale finale"
msgid "Edit group"
-msgstr ""
+msgstr "Modifica gruppo"
msgid "Edit meeting"
msgstr "Modifica riunione"
msgid "Edit moderation note"
-msgstr ""
+msgstr "Modifica nota di moderazione"
msgid "Edit point of order ..."
-msgstr ""
+msgstr "Elaborare la mozione d'ordine"
msgid "Edit projector"
msgstr "Modificare proiettore"
msgid "Edit queue"
-msgstr ""
+msgstr "Modifica la coda d'attesa"
msgid "Edit state"
-msgstr ""
-
-msgid "Edit statute paragraph"
-msgstr "Modificare paragrafo statuto"
+msgstr "Modifica lo stato"
msgid "Edit tag"
msgstr "Modificare etichetta"
@@ -1738,14 +1944,11 @@ msgstr "Modificare per inserire voti"
msgid "Edit topic"
msgstr "Modifica argomento"
-msgid "Edit total time"
-msgstr ""
-
msgid "Edit workflow"
-msgstr ""
+msgstr "Modifica flusso di lavoro"
msgid "Editorial final version"
-msgstr ""
+msgstr "Versione editoriale finale"
msgid "Election"
msgstr "Elezione"
@@ -1756,14 +1959,17 @@ msgstr "Documenti d´elezione"
msgid "Elections"
msgstr "Elezioni"
+msgid "Elections (PDF settings)"
+msgstr "Elezioni (impostazioni PDF)"
+
msgid "Element"
-msgstr ""
+msgstr "Elemento"
msgid "Email"
msgstr "E-mail"
msgid "Email address"
-msgstr "Indirizzo Email"
+msgstr "Indirizzo e-mail"
msgid "Email body"
msgstr "corpo della e-mail"
@@ -1781,7 +1987,7 @@ msgid "Empty text field"
msgstr "Campo di testo vuoto"
msgid "Enable SSO via SAML"
-msgstr ""
+msgstr "Abilita SSO via SAML"
msgid "Enable chat globally"
msgstr "Abilitare la chat a livello globale"
@@ -1793,7 +1999,7 @@ msgid "Enable forspeech / counter speech"
msgstr "abilita intercessione / contro-parola"
msgid "Enable interposed questions"
-msgstr ""
+msgstr "Attivare domande intermedie"
msgid "Enable numbering for agenda items"
msgstr "Abilitare la numerazione dei punti dell'ordine del giorno"
@@ -1805,13 +2011,13 @@ msgid "Enable point of order"
msgstr "Abilitazione del punto d'ordine"
msgid "Enable point of orders for other participants"
-msgstr ""
+msgstr "Abilitare la mozione d'ordine per gli altri partecipanti"
msgid "Enable specifications and ranking for possible motions"
-msgstr ""
+msgstr "Consentire specifiche e classifiche per le possibili mozioni"
msgid "Enable star icon usage by speakers"
-msgstr ""
+msgstr "Abilita l'uso dell'icona a stella da parte degli oratori"
msgid "Enable virtual applause"
msgstr "Attivare applauso virtuale"
@@ -1829,23 +2035,35 @@ msgid ""
"Enables for the selected state the possibility for submitters to change the "
"state of the motion. Other administrative functions are excluded."
msgstr ""
+"Abilita, per lo stato selezionato, la possibilità per i proponenti di "
+"modificare lo stato della mozione. Sono escluse altre funzioni "
+"amministrative."
msgid "Enables the ability to create votings for motions in this state."
msgstr ""
+"Abilita la possibilità di creare votazioni per le mozioni in questo stato."
msgid ""
"Enables the editing of the motion text and reason by submitters in the "
"selected state after the motion has been created."
msgstr ""
+"Consente la modifica del testo della mozione e del motivo da parte dei "
+"proponenti nello stato selezionato dopo la creazione della mozione."
msgid ""
-"Enables the forwarding of motions to other meetings within the OpenSlides instance in the selected state. \n"
+"Enables the forwarding of motions to other meetings within the OpenSlides instance in the selected state.\n"
"\n"
"Prerequisites:\n"
-"1. forwarding hierarchy must be set at the organizational level in the committee. \n"
+"1. forwarding hierarchy must be set at the organizational level in the committee.\n"
"2. target meeting must be created.\n"
"3. user must have group permission for forwarding."
msgstr ""
+"Abilita l'inoltro di mozioni ad altre riunioni all'interno dell'istanza di OpenSlides nello stato selezionato.\n"
+"\n"
+"Prerequisiti:\n"
+"1. la gerarchia di inoltro deve essere impostata a livello organizzativo nel comitato.\n"
+"2. la riunione di destinazione deve essere creata.\n"
+"3. l'utente deve avere l'autorizzazione di gruppo per l'inoltro."
msgid ""
"Enables the support function for motions in the selected state. The support "
@@ -1853,12 +2071,20 @@ msgid ""
"corresponding group permission in > [Participants] > [Groups] > [Motions] > "
"[Can support motions]."
msgstr ""
+"Abilita la funzione di supporto per le mozioni nello stato selezionato. La "
+"funzione di supporto deve essere attivata in > [Impostazioni] > [Movimenti] "
+"come anche l'autorizzazione di gruppo corrispondente in > [Partecipanti] > "
+"[Gruppi] > [Mozioni] > [Può supportare mozioni]."
msgid ""
"Enables the visibility of amendments directly in the corresponding main motion. The text of amendments is embedded within the text of the motion.\n"
"\n"
"Note: Does not affect the visibility of change recommendations."
msgstr ""
+"Consente la visibilità degli emendamenti direttamente nella mozione "
+"principale corrispondente. Il testo degli emendamenti è incorporato nel "
+"testo della mozione. Nota: non influisce sulla visibilità delle "
+"raccomandazioni di modifica."
msgid "Encoding of the file"
msgstr "Codificare il file"
@@ -1900,7 +2126,7 @@ msgid "Enter your email to send the password reset link"
msgstr "Inserire l´e-mail per ricevere il link per resettare la password"
msgid "Entitled present users"
-msgstr ""
+msgstr "Utenti presenti aventi diritto"
msgid "Entitled to vote"
msgstr "Avente diritto di voto"
@@ -1938,12 +2164,13 @@ msgstr ""
"manager solo per la lista degli oratori)"
msgid ""
-"Existing accounts can be reused by entering email (with given name and "
-"surname) OR by entering the username in the csv file."
+"Existing accounts can be reused or updated by using: - Membership "
+"number (recommended)
- Username
- Email address AND first name AND "
+"last name"
msgstr ""
-
-msgid "Exit"
-msgstr "Terminare"
+"Gli account esistenti possono essere riutilizzati o aggiornati tramite: "
+"Numero di iscrizione (consigliato) Nome utente, Indirizzo e-mail E nome E "
+"cognome"
msgid "Exit conference room"
msgstr "Esci dalla stanza della conferenza"
@@ -1982,16 +2209,19 @@ msgid "Extension"
msgstr "Estensione"
msgid "External ID"
-msgstr ""
+msgstr "ID esterno"
+
+msgid "Fallback"
+msgstr "Fallback"
msgid "Favorites"
msgstr "Preferiti"
msgid "File"
-msgstr ""
+msgstr "File"
msgid "File is being used"
-msgstr ""
+msgstr "Il file è in uso"
msgid "Filename"
msgstr "Nome del file"
@@ -2002,6 +2232,9 @@ msgstr "Files"
msgid "Filter"
msgstr "Filtro"
+msgid "Filtered single votes"
+msgstr "Voti singoli filtrati"
+
msgid "Final version"
msgstr "Versione finale"
@@ -2024,23 +2257,34 @@ msgid "Following users are currently editing this motion:"
msgstr "I seguenti utenti stanno revisionando al momento la mozione:"
msgid "Font bold"
-msgstr ""
+msgstr "Carattere in grassetto"
msgid "Font bold italic"
-msgstr ""
+msgstr "Carattere grassetto corsivo"
msgid "Font italic"
-msgstr ""
+msgstr "Carattere corsivo"
msgid "Font monospace"
-msgstr ""
+msgstr "Caratteri a spaziatura singola"
msgid "Font regular"
-msgstr ""
+msgstr "Font regolare"
msgid "Font size in pt"
msgstr "Dimensione del carattere in pt"
+msgid ""
+"For activation:
\n"
+" 1. Assign group permission (define the group that can support motions)
\n"
+" 2. Adjust workflow (define state in which motions can be supported)
\n"
+" 3. Enter minimum number (see next field)"
+msgstr ""
+"Per l'attivazione: 1. Assegnare l'autorizzazione al gruppo (definire il "
+"gruppo che può supportare le mozioni) 2. Adattare il flusso di lavoro "
+"(definire lo stato in cui i movimenti possono essere supportati) 3. Inserire"
+" il numero minimo (vedere il campo successivo)"
+
msgid ""
"For large instances this may block the server to the point of unusability"
msgstr ""
@@ -2053,6 +2297,9 @@ msgstr "Colore in primo piano"
msgid "Forgot Password?"
msgstr "Dimenticato password?"
+msgid "Formalities"
+msgstr "Formalità "
+
msgid "Format"
msgstr "Format"
@@ -2063,16 +2310,16 @@ msgid "Forward"
msgstr "Inoltrare"
msgid "Forward motions"
-msgstr "Inoltrare mozioni"
+msgstr "Inoltra mozioni"
msgid "Forward motions to"
msgstr "Inoltrare mozioni a"
msgid "Forwarded motion deleted"
-msgstr ""
+msgstr "Mozione presentata e cancellata"
msgid "Forwarded to {}"
-msgstr ""
+msgstr "Inoltrato a {}"
msgid "Forwarding"
msgstr "Inoltro"
@@ -2093,7 +2340,7 @@ msgid "Fullscreen"
msgstr "Schermo intero"
msgid "Game draw!"
-msgstr ""
+msgstr "Pareggio di partita!"
msgid "Gender"
msgstr "Genere"
@@ -2126,16 +2373,16 @@ msgid "Given name"
msgstr "Nome assegnato"
msgid "Global headbar color"
-msgstr ""
+msgstr "Colore della intestazione"
msgid "Go to line"
msgstr "Vai alla riga"
msgid "Got an email"
-msgstr "Ricevi una e-mail"
+msgstr "Ha ricevuto un'e-mail"
msgid "Group"
-msgstr ""
+msgstr "Gruppo"
msgid "Group name"
msgstr "Nome gruppo"
@@ -2152,10 +2399,10 @@ msgid "Groups"
msgstr "Gruppi"
msgid "Groups changed in meeting {}"
-msgstr ""
+msgstr "Gruppi modificati in riunione {}"
msgid "Groups changed in multiple meetings"
-msgstr ""
+msgstr "Gruppi modificati in più incontri"
msgid "Groups with read permissions"
msgstr "Gruppi con diritto di leggere"
@@ -2167,37 +2414,55 @@ msgid "Guest"
msgstr "Ospiti"
msgid "Has SSO identification"
-msgstr ""
+msgstr "Ha SSO identificazione"
+
+msgid "Has a membership number"
+msgstr "Ha un numero di iscrizione"
msgid "Has amendments"
msgstr "Ha richieste di modifica mozione"
msgid "Has an email address"
-msgstr "Ha un indirizzo e-mail"
+msgstr "Ha un indirizzo email"
msgid "Has changed vote weight"
-msgstr "Ha cambiato peso di voto"
+msgstr "Ha cambiato il peso del voto"
+
+msgid "Has email"
+msgstr "Ha email"
msgid "Has forwardings"
msgstr "Dispone di inoltri"
+msgid "Has identical motions"
+msgstr "Ha mozioni identiche"
+
msgid "Has logged in"
-msgstr ""
+msgstr "Ha effettuato l'accesso"
msgid "Has no SSO identification"
-msgstr ""
+msgstr "Non ha identificazione SSO"
msgid "Has no email address"
-msgstr "Non ha un indirizzo e-mail"
+msgstr "Non ha indirizzo email"
+
+msgid "Has no identical motions"
+msgstr "Non ha mozioni identiche"
+
+msgid "Has no membership number"
+msgstr "Non ha numero di iscrizione"
msgid "Has no speakers"
msgstr "Nessuna richiesta di parola"
msgid "Has not logged in yet"
-msgstr ""
+msgstr "Non ha ancora effettuato l'accesso"
+
+msgid "Has not spoken"
+msgstr "Non ha parlato"
msgid "Has not voted"
-msgstr ""
+msgstr "Non ha votato"
msgid "Has notes"
msgstr "Ha annotazioni"
@@ -2205,41 +2470,29 @@ msgstr "Ha annotazioni"
msgid "Has speakers"
msgstr "Presenti richieste di parola"
+msgid "Has spoken"
+msgstr "Ha parlato"
+
msgid "Has unchanged vote weight"
-msgstr "Peso di voto invariato"
+msgstr "Ha un peso di voto invariato"
msgid "Has voted"
-msgstr ""
+msgstr "Ha votato"
msgid "Header"
msgstr "Intestazione"
-msgid "Header 1"
-msgstr ""
-
-msgid "Header 2"
-msgstr ""
-
-msgid "Header 3"
-msgstr ""
-
-msgid "Header 4"
-msgstr ""
-
-msgid "Header 5"
-msgstr ""
-
-msgid "Header 6"
-msgstr ""
-
msgid "Header background color"
-msgstr "Colore sfondo altezza testa"
+msgstr "Colore intestazione"
msgid "Header font color"
-msgstr "Coloro scrittura altezza testa"
+msgstr "Colore scrittura intestazione"
-msgid "Headers"
-msgstr ""
+msgid "Heading"
+msgstr "Intestazione"
+
+msgid "Headings"
+msgstr "Intestazioni"
msgid "Headline color"
msgstr "Colore titolo"
@@ -2253,12 +2506,18 @@ msgstr "Testo di aiuto per i dati di accesso e PDF di benvenuto"
msgid "Hidden item"
msgstr "Voce nascosta"
+msgid "Hide"
+msgstr "Nascondi"
+
msgid "Hide main menu"
-msgstr ""
+msgstr "Nascondi il menù principale"
msgid "Hide more text"
msgstr "Dimostrare meno"
+msgid "Hide note on number of multiple contributions"
+msgstr "Nascondi nota sul numero di contributi multipli"
+
msgid "Hide password"
msgstr "Nascondere password"
@@ -2284,12 +2543,20 @@ msgid ""
"IMPORTANT: The sender address (noreply@openslides.com) is defined in the OpenSlides server settings and cannot be changed here.\n"
" To receive replies you have to enter a reply address in the next field. Please test the email dispatch in case of changes!"
msgstr ""
+"IMPORTANTE: L'indirizzo del mittente (noreply@openslides.com) è definito "
+"nelle impostazioni del server OpenSlides e non può essere modificato qui. "
+"Per ricevere le risposte è necessario inserire un indirizzo di risposta nel "
+"campo successivo. Si prega di testare l'invio di e-mail in caso di "
+"modifiche!"
+
+msgid "Identical motions"
+msgstr "Mozioni identiche"
msgid "Identical with"
-msgstr ""
+msgstr "Identico a "
msgid "Identifier"
-msgstr ""
+msgstr "Identificatore"
msgid "If deactivated it is displayed below the title"
msgstr "Se è disattivata, viene visualizzata sotto il titolo."
@@ -2303,6 +2570,10 @@ msgstr ""
msgid "If the value is set to 0 the time counts up as stopwatch."
msgstr ""
+"Se il valore è impostato su 0, il tempo viene conteggiato come cronometro."
+
+msgid "Image description"
+msgstr "Descrizione dell'immagine"
msgid "Import"
msgstr "Importa"
@@ -2325,14 +2596,11 @@ msgstr "Importare mozioni"
msgid "Import participants"
msgstr "Importare partecipanti"
-msgid "Import statute"
-msgstr "Importare statuto"
-
msgid "Import successful"
-msgstr ""
+msgstr "Importazione riuscita"
msgid "Import successful with some warnings"
-msgstr ""
+msgstr "Importazione riuscita con alcuni avvertimenti"
msgid "Import topics"
msgstr "Importare temi"
@@ -2344,7 +2612,7 @@ msgid "In motion list, motion detail and PDF."
msgstr "Nella lista di mozioni, dettaglio mozione e PDF."
msgid "In progress, please wait ..."
-msgstr ""
+msgstr "In corso, attendere prego ..."
msgid "In the election process"
msgstr "Nel processo d'elezione"
@@ -2362,7 +2630,7 @@ msgid "Initial password"
msgstr "Password iniziale"
msgid "Inline"
-msgstr ""
+msgstr "Formati dei caratteri"
msgid "Insert after"
msgstr "Inserire dopo"
@@ -2376,29 +2644,38 @@ msgstr "Inserire dietro"
msgid "Insert topics here"
msgstr "Inserire temi qui"
+msgid "Insert/Edit Link"
+msgstr "Inserisci/Modifica link"
+
+msgid "Insert/edit image"
+msgstr "Inserisci/modifica immagine"
+
+msgid "Insert/edit link"
+msgstr "Inserisci/modifica link"
+
msgid "Insertion"
msgstr "Inserzione"
msgid "Insufficient material! It's a draw!"
-msgstr ""
+msgstr "Materiale insufficiente! E' un pareggio!"
msgid "Internal"
msgstr "Interno"
msgid "Internal item"
-msgstr "Contributo interno"
+msgstr "Voce interna"
msgid "Internal login"
-msgstr ""
+msgstr "Login interno"
msgid "Interposed question"
-msgstr ""
+msgstr "Domanda interposta"
msgid "Intervention"
-msgstr ""
+msgstr "Intervento"
msgid "Intervention speaking time in seconds"
-msgstr ""
+msgstr "Tempo di parola dell'intervento in secondi"
msgid "Invalid line number"
msgstr "Numero di riga invalido"
@@ -2410,13 +2687,13 @@ msgid "Invite to conference room"
msgstr "Invitare nell'aula della conferenza"
msgid "Is a committee"
-msgstr "E' un comitato"
+msgstr ""
msgid "Is a natural person"
msgstr "E' una persona naturale"
msgid "Is a template"
-msgstr ""
+msgstr "E' un modello"
msgid "Is active"
msgstr "E' attivo"
@@ -2427,6 +2704,9 @@ msgid ""
"Note:\n"
"Optional combination of requests to speak with presence status is possible. ( > [Settings] > [List of speakers] > [General] )"
msgstr ""
+"Può aggiungersi all'elenco degli oratori. Nota: È possibile combinare le "
+"richieste di intervento con lo stato di presenza. ( > [Impostazioni] > "
+"[Elenco degli interlocutori] > [Generale] )"
msgid "Is already projected"
msgstr "E' già proiettato"
@@ -2435,10 +2715,10 @@ msgid "Is amendment"
msgstr "E' una richiesta di modifica mozione"
msgid "Is archived"
-msgstr ""
+msgstr "E' archiviato"
msgid "Is being projected"
-msgstr ""
+msgstr "Viene proiettato"
msgid "Is candidate"
msgstr "E' un candidato / una candidata"
@@ -2450,10 +2730,10 @@ msgid "Is favorite"
msgstr "E' il preferito"
msgid "Is in active meetings"
-msgstr ""
+msgstr "È in riunioni attive"
msgid "Is in archived meetings"
-msgstr ""
+msgstr "È nelle riunioni archiviate"
msgid "Is manager"
msgstr "E' amministratore/amministratrice"
@@ -2468,22 +2748,22 @@ msgid "Is not a committee"
msgstr "Non è un comitato"
msgid "Is not a template"
-msgstr ""
+msgstr "Non è un modello"
msgid "Is not active"
msgstr "Non è attivo"
msgid "Is not archived"
-msgstr ""
+msgstr "Non è archiviato"
msgid "Is not favorite"
msgstr "Non è il preferito"
msgid "Is not in active meetings"
-msgstr ""
+msgstr "Non è in riunioni attive"
msgid "Is not in archived meetings"
-msgstr ""
+msgstr "Non è presente nelle riunioni archiviate"
msgid "Is not present"
msgstr "Non è presente"
@@ -2504,25 +2784,25 @@ msgstr ""
"degli oratori o per i sondaggi."
msgid "It's a draw!"
-msgstr ""
+msgstr "E' un pareggio!"
msgid "It's your opponent's turn"
-msgstr ""
+msgstr "È il turno dell'avversario"
msgid "It's your turn!"
-msgstr ""
+msgstr "E' il tuo turno"
msgid "Italic"
-msgstr ""
+msgstr "Corsivo"
msgid "Item"
-msgstr ""
+msgstr "Voce inserita"
msgid "Item number"
msgstr "Numero dell'ordine del giorno"
msgid "Items"
-msgstr ""
+msgstr "Voci inserite"
msgid "Jitsi domain"
msgstr "Dominio Jitsi"
@@ -2533,6 +2813,9 @@ msgstr "Nome dell'aula Jitsi"
msgid "Jitsi room password"
msgstr "Password dell'aula Jitsi"
+msgid "Justify"
+msgstr "Giustificare"
+
msgid "Keep each item in a single line."
msgstr "Utilizzare per ogni contributo una riga"
@@ -2540,13 +2823,13 @@ msgid "Label color"
msgstr "Colore etichetta "
msgid "Language"
-msgstr ""
+msgstr "Lingua"
msgid "Last email sent"
msgstr "Ulitma e-mail inviata"
msgid "Last login"
-msgstr ""
+msgstr "Ultimo login"
msgid "Last modified"
msgstr "Ultima modifica"
@@ -2558,10 +2841,10 @@ msgid "Leave"
msgstr "Lasciare"
msgid "Leave blank to automatically generate the password."
-msgstr ""
+msgstr "Lasciare vuoto per generare automaticamente la password."
msgid "Leave blank to automatically generate the username."
-msgstr ""
+msgstr "Lasciare vuoto per generare automaticamente la password."
msgid "Left"
msgstr "Sinistra"
@@ -2597,7 +2880,7 @@ msgid "Line spacing"
msgstr "Interlinea"
msgid "List of amendments: "
-msgstr ""
+msgstr "Elenco delle modifiche"
msgid "List of electronic votes"
msgstr "Votazioni elettroniche"
@@ -2611,6 +2894,9 @@ msgstr "Lista dei partecipanti (PDF)"
msgid "List of speakers"
msgstr "Liste dei relatori"
+msgid "List of speakers as overlay"
+msgstr "Elenco dei relatori in sovrimpressione"
+
msgid "List of speakers is initially closed"
msgstr "Lista dei relatori è inizialmente chiusa"
@@ -2618,7 +2904,7 @@ msgid "List view"
msgstr "Vista liste"
msgid "Lists of speakers"
-msgstr ""
+msgstr "Elenco degli oratori"
msgid "Live conference"
msgstr "Conferenza dal vivo."
@@ -2629,6 +2915,9 @@ msgstr "Livestream"
msgid "Livestream URL"
msgstr "Livestream URL"
+msgid "Livestream poster image"
+msgstr "Immagine del poster in live streaming"
+
msgid "Livestream poster image url"
msgstr "Livestream immagine poster URL"
@@ -2636,7 +2925,7 @@ msgid "Loading data. Please wait ..."
msgstr "Dati vengono caricati. Attendere prego ..."
msgid "Logged-in users"
-msgstr ""
+msgstr "Utenti registrati"
msgid "Login"
msgstr "Login"
@@ -2648,7 +2937,7 @@ msgid "Login as guest"
msgstr "Login come ospite"
msgid "Login button text"
-msgstr ""
+msgstr "Testo del pulsante di accesso"
msgid "Logout"
msgstr "Logout"
@@ -2657,10 +2946,10 @@ msgid "Lowest applause amount"
msgstr "Valore minimo dell'applauso"
msgid "Main motion and line number"
-msgstr ""
+msgstr "Mozione principale e numero di riga"
msgid "Mandates switched sucessfully!"
-msgstr ""
+msgstr "Autorizzazioni scambiate con successo!"
msgid "Mark as personal favorite"
msgstr "Evidenziare come preferito personale"
@@ -2672,7 +2961,7 @@ msgid "Maximum amount of votes per option"
msgstr "Numero massimo di voti per opzione"
msgid "Maximum number of columns on motion block slide"
-msgstr ""
+msgstr "Numero massimo di colonne sulla slide del blocco di mozione"
msgid "Media access is denied"
msgstr "Accesso media negato"
@@ -2687,13 +2976,16 @@ msgid "Meeting administrator"
msgstr "Amministratore della riunione"
msgid "Meeting date"
-msgstr ""
+msgstr "Data della riunione"
msgid "Meeting information"
msgstr "Informazioni della riunione"
+msgid "Meeting is closed"
+msgstr "La riunione è chiusa"
+
msgid "Meeting not found"
-msgstr ""
+msgstr "Riunione non trovata"
msgid "Meeting specific information"
msgstr "Informazioni specifiche sulla riunione"
@@ -2709,14 +3001,26 @@ msgstr ""
" da tutti gli amministratori delle commissioni."
msgid "Meeting title"
-msgstr ""
+msgstr "Nome della riunione"
msgid "Meetings"
msgstr "Riunioni"
+msgid "Meetings affected:"
+msgstr "Riunioni interessate:"
+
msgid "Meetings selected"
msgstr "Riunioni selezionate"
+msgid "Membership number"
+msgstr "Numero d'iscrizione"
+
+msgid "Merge"
+msgstr "Unire"
+
+msgid "Merge accounts"
+msgstr "Unire account"
+
msgid "Message"
msgstr "Messaggio"
@@ -2727,10 +3031,10 @@ msgid "Meta information"
msgstr "Informazione meta"
msgid "Metadata of Identity Provider (IdP)"
-msgstr ""
+msgstr "Metadati del fornitore di identità (IdP)"
msgid "Metadata of Service Provider (SP)"
-msgstr ""
+msgstr "Metadati del fornitore di servizi (SP)"
msgid "Min votes cannot be greater than max votes."
msgstr ""
@@ -2746,16 +3050,19 @@ msgid "Minimum amount of votes"
msgstr "Numero minimo di voti"
msgid "Minimum number of digits for motion identifier"
-msgstr ""
+msgstr "Numero minimo di cifre per l'identificativo della mozione"
msgid "Moderation note"
-msgstr ""
+msgstr "Nota di moderazione"
+
+msgid "Modern"
+msgstr "Moderno"
msgid "Modify design"
-msgstr ""
+msgstr "Modificare il design"
msgid "Module"
-msgstr ""
+msgstr "Modulo"
msgid "More"
msgstr "Più"
@@ -2791,16 +3098,16 @@ msgid "Motion created"
msgstr "Mozione creata"
msgid "Motion created (forwarded)"
-msgstr ""
+msgstr "Mozione creata (inoltrata)"
msgid "Motion deleted"
msgstr "Mozione cancellata"
msgid "Motion editor"
-msgstr ""
+msgstr "Redattore della mozione"
msgid "Motion editors"
-msgstr ""
+msgstr "Redattori della mozione"
msgid "Motion forwarded to"
msgstr "Mozione inoltrata a"
@@ -2809,7 +3116,7 @@ msgid "Motion forwarding"
msgstr "Inoltro di mozione"
msgid "Motion identifier"
-msgstr ""
+msgstr "Identificativo di mozione"
msgid "Motion preamble"
msgstr "Preambolo mozione"
@@ -2823,6 +3130,9 @@ msgstr "Voti mozione"
msgid "Motions"
msgstr "Mozioni"
+msgid "Motions (PDF settings)"
+msgstr "Mozioni (impostazioni PDF)"
+
msgid "Motions are in process. Please wait ..."
msgstr "Mozioni in elaborazione. Attendere prego...."
@@ -2851,7 +3161,7 @@ msgid "Multiselect"
msgstr "Selezione multipla"
msgid "Must be unique"
-msgstr ""
+msgstr "Deve essere unico"
msgid "My account"
msgstr "Il mio account"
@@ -2868,15 +3178,30 @@ msgstr "Nome"
msgid "Name of recommender"
msgstr "Nome del raccomandatore"
-msgid "Name of recommender for statute amendments"
-msgstr "Nome raccomandatore per mozioni di modifica statuto"
-
msgid "Name of the new category"
msgstr "Nome della nuova categoria"
msgid "Natural person"
msgstr "Persona naturale"
+msgid "Navigate to account page from "
+msgstr "Vai alla pagina dell'account da "
+
+msgid "Navigate to committee detail view from "
+msgstr "Passare alla visualizzazione dettagliata del comitato da "
+
+msgid "Navigate to meeting "
+msgstr "Vai alla riunione "
+
+msgid "Navigate to motion"
+msgstr "Vai alla mozione "
+
+msgid "Navigate to participant page from "
+msgstr "Vai alla pagina dei partecipanti da "
+
+msgid "Navigate to the folder"
+msgstr "Vai alla cartella"
+
msgid "Negative votes are not allowed."
msgstr "Non sono ammessi voti negativi"
@@ -2899,7 +3224,7 @@ msgid "New change recommendation"
msgstr "Nuova raccomandazione di modifica"
msgid "New chat group"
-msgstr ""
+msgstr "Nuovo gruppo chat"
msgid "New comment field"
msgstr "Nuovo campo di commento"
@@ -2908,7 +3233,7 @@ msgid "New committee"
msgstr "Nuovo comitato"
msgid "New design"
-msgstr ""
+msgstr "Nuovo design"
msgid "New directory"
msgstr "Nuovo elenco"
@@ -2917,16 +3242,16 @@ msgid "New election"
msgstr "Nuova elezione"
msgid "New file"
-msgstr ""
+msgstr "Nuovo file"
msgid "New file name"
msgstr "Nuovo nome file"
msgid "New folder"
-msgstr ""
+msgstr "Nuova cartella"
msgid "New group"
-msgstr ""
+msgstr "Nuovo gruppo"
msgid "New meeting"
msgstr "Nuova mozione"
@@ -2947,14 +3272,11 @@ msgid "New password"
msgstr "Nuova password"
msgid "New projector"
-msgstr ""
+msgstr "Nuovo proiettore"
msgid "New state"
msgstr "Nuovo stato"
-msgid "New statute paragraph"
-msgstr "Nuovo paragrafo statuto"
-
msgid "New tag"
msgstr "Nuova etichetta"
@@ -2964,6 +3286,9 @@ msgstr "Nuovo tema"
msgid "New vote"
msgstr "Nuovo voto"
+msgid "New window"
+msgstr "Nuova finestra"
+
msgid "New workflow"
msgstr "Nuova lavorazione"
@@ -2982,9 +3307,6 @@ msgstr "Nessun ruolo di amministratore"
msgid "No category"
msgstr "Nessuna categoria"
-msgid "No category set"
-msgstr "Nessuna categoria impostata"
-
msgid "No change recommendations yet"
msgstr "Finora nessuna raccomandazione di modifica"
@@ -2998,16 +3320,16 @@ msgid "No comment"
msgstr "Nessun commento"
msgid "No committee admin"
-msgstr "Nessun amministratore di comitato"
+msgstr "Nessun amminstratore del comitato"
msgid "No data"
msgstr "Nessun dato"
msgid "No delegation of vote"
-msgstr "Nessuna delega del diritto di voto"
+msgstr "Nessuna delega di voto"
msgid "No emails were send."
-msgstr ""
+msgstr "Nessuna email inviata"
msgid "No encryption"
msgstr "Nessun criptaggio"
@@ -3033,12 +3355,6 @@ msgstr "Nessuna riunione disponibile"
msgid "No meetings have been selected."
msgstr "Non è stata selezionata alcuna riunione"
-msgid "No motion block set"
-msgstr "Nessuna sezione di mozione assegnato"
-
-msgid "No motion editors"
-msgstr ""
-
msgid "No one has voted for this poll"
msgstr "Nessuno ha votato per questo sondaggio"
@@ -3051,41 +3367,32 @@ msgstr "No per candidato"
msgid "No personal note"
msgstr "Nessuna annotazione personale"
-msgid "No recommendation"
-msgstr "Nessuna raccomandazione"
-
msgid "No results found"
-msgstr ""
+msgstr "Nessun risultato trovato"
msgid "No results yet."
msgstr "Finora nessun risultato"
-msgid "No spokesperson"
-msgstr ""
-
-msgid "No statute paragraphs"
-msgstr "Nessun paragrafo di statuto"
-
msgid "No structure level"
-msgstr ""
-
-msgid "No tags"
-msgstr "No etichette"
+msgstr "Nessun livello di struttura"
msgid "No verbose name is defined"
msgstr "Non è definito un nome dettagliato"
msgid "No."
-msgstr "N."
+msgstr "No."
msgid "Nomination list"
-msgstr ""
+msgstr "Elenco delle candidature"
msgid "None"
msgstr "Nessuno"
msgid "None of the selected motions can be forwarded."
-msgstr ""
+msgstr "Nessuna delle mozioni selezionate può essere inoltrata."
+
+msgid "Normal (http/2)"
+msgstr "Normale (http/2)"
msgid "Not found"
msgstr "Non trovato"
@@ -3135,7 +3442,7 @@ msgid "Number of last speakers to be shown on the projector"
msgstr "Numero degli ultimi oratori da visualizzare sul proiettore"
msgid "Number of motions"
-msgstr ""
+msgstr "Numero di mozioni"
msgid ""
"Number of next speakers automatically connecting to the live conference"
@@ -3144,7 +3451,7 @@ msgstr ""
"alla conferenza in diretta"
msgid "Number of participants"
-msgstr ""
+msgstr "Numero di partecipanti"
msgid "Number of persons to be elected"
msgstr "Numero delle persone da eleggere"
@@ -3158,12 +3465,18 @@ msgstr "Numero dei prossimi relatori da dimostrare con il proiettore"
msgid "Number set"
msgstr "Numero assegnato"
+msgid "Numbered list"
+msgstr "Lista numerata"
+
msgid "Numbered per category"
msgstr "Numerare per categoria"
msgid "Numbering"
msgstr "Numerazione"
+msgid "Numbering and sorting"
+msgstr "Numerazione e classificazione"
+
msgid "Numbering prefix for agenda items"
msgstr "Numerazione prefisso per contributi dell'ordine del giorno"
@@ -3173,20 +3486,35 @@ msgstr "Sistema di numerazione per contributi dell'ordine del giorno"
msgid "OK"
msgstr "Ok"
+msgid "OR"
+msgstr "O"
+
+msgid "Off"
+msgstr "spento"
+
msgid "Offline mode"
msgstr "Modus offline"
msgid "Ok"
msgstr "Ok"
+msgid "Old account of"
+msgstr "Vecchio account di"
+
msgid "Old password"
msgstr "Vecchia password"
+msgid "On"
+msgstr "Acceso"
+
msgid "One email was send sucessfully."
-msgstr ""
+msgstr "Una email è stata inviata con successo"
msgid "Only for internal notes."
-msgstr "Solo per annotazioni interni."
+msgstr "Solo per annotazioni interne."
+
+msgid "Only groups and participant number are switched."
+msgstr "mozione originale cancellata"
msgid "Only main agenda items"
msgstr "Solo contributi principali dell'ordine del giorno"
@@ -3196,7 +3524,7 @@ msgstr ""
"Solo partecipanti presenti possono essere aggiunti alla lista dei relatori"
msgid "Only time"
-msgstr ""
+msgstr "Solo tempo"
msgid "Only traffic light"
msgstr "Solo semaforo"
@@ -3205,13 +3533,16 @@ msgid "Open Jitsi in new tab"
msgstr "Aprire Jitsi in un nuovo tab"
msgid "Open a meeting to play \"Connect 4\""
-msgstr ""
+msgstr "Aprire una riunione per giocare a \"Connect 4\""
msgid "Open a meeting to play chess"
-msgstr ""
+msgstr "Apri una riunione per giocare a scacchi"
msgid "Open items"
-msgstr "Aprire contributi"
+msgstr "Apri elementi"
+
+msgid "Open link in ..."
+msgstr "Apri il link in ..."
msgid "Open list of speakers"
msgstr "Aprire la lista dei relatori"
@@ -3234,11 +3565,23 @@ msgstr "Data d'accesso OpenSlide "
msgid "OpenSlides help (FAQ)"
msgstr "OpenSlides help (FAQ)"
+msgid ""
+"OpenSlides offers various speaking list customizations for use in "
+"parliament. These include the configuration of speaking time quotas for "
+"parliamentary groups (e.g. fractions, coalitions) as well as extended types "
+"of speeches such as short interventions and (parliamentary) interposed "
+"questions."
+msgstr ""
+"OpenSlides offre diverse personalizzazioni delle liste di intervento per "
+"l'uso in parlamento. Queste includono la configurazione di quote di tempo di"
+" parola per i gruppi parlamentari (ad esempio, frazioni, coalizioni) e tipi "
+"di discorsi estesi, come brevi interventi e interrogazioni (parlamentari)."
+
msgid "OpenSlides recommends"
msgstr "OpenSlides raccomanda"
msgid "Option"
-msgstr ""
+msgstr "Opzione"
msgid "Options"
msgstr "Opzioni"
@@ -3247,25 +3590,25 @@ msgid "Organization"
msgstr "Organizzazione"
msgid "Organization Management Level changed"
-msgstr ""
+msgstr "Livello di Gestione dell'Organizzazione modificato"
msgid "Organization admin"
msgstr "Amministratore dell'organizzazione"
msgid "Organization language"
-msgstr ""
+msgstr "Lingua dell'organizzazione"
msgid "Organization specific information"
msgstr "Informazioni specifiche sull'organizzazione"
msgid "Organizations"
-msgstr ""
+msgstr "Organizzazioni"
msgid "Origin"
msgstr "Origine"
msgid "Origin motion deleted"
-msgstr ""
+msgstr "Mozione originale cancellata"
msgid "Original"
msgstr "Originale"
@@ -3274,26 +3617,17 @@ msgid "Original version"
msgstr "Versione originale"
msgid "Out of sync"
-msgstr ""
+msgstr "Non sincronizzato"
msgid "Outside"
msgstr "Fuori"
-msgid "Overlay"
-msgstr "Copertura"
-
msgid "PDF"
msgstr "PDF"
msgid "PDF ballot paper logo"
msgstr "PDF logo scheda elettorale "
-msgid "PDF export"
-msgstr "PDF export"
-
-msgid "PDF export options"
-msgstr "Opzioni di esportazione PDF"
-
msgid "PDF footer logo (left)"
msgstr "PDF logo piè di pagina (sinistra)"
@@ -3334,7 +3668,7 @@ msgid "Page numbers"
msgstr "Numeri pagine"
msgid "Paragraph"
-msgstr ""
+msgstr "Paragrafo"
msgid "Paragraph-based, Diff-enabled"
msgstr "Basato sul paragrafo con visualizzazione modifiche"
@@ -3345,51 +3679,57 @@ msgstr "Caricare in parallelo"
msgid "Parent agenda item"
msgstr "Contributo madre nell'ordine del giorno"
+msgid "Parliament options"
+msgstr "Opzione parlamento"
+
msgid "Participant"
msgstr "Partecipante"
msgid "Participant added to group {} in meeting {}"
-msgstr ""
+msgstr "Partecipante aggiunto al gruppo {} nella riunione {}"
msgid "Participant added to multiple groups in meeting {}"
-msgstr ""
+msgstr "Partecipante aggiunto a più gruppi nella riunione {}"
msgid "Participant added to multiple groups in multiple meetings"
-msgstr ""
+msgstr "Partecipante aggiunto a più gruppi in più riunioni"
msgid "Participant created"
-msgstr ""
+msgstr "Creato partecipante"
msgid "Participant created in meeting {}"
-msgstr ""
+msgstr "Partecipante creato nella riunione {}"
msgid "Participant data updated in meeting {}"
-msgstr ""
+msgstr "Dati dei partecipanti aggiornati nella riunione {}"
msgid "Participant data updated in multiple meetings"
-msgstr ""
+msgstr "Dati dei partecipanti aggiornati in più incontri"
msgid "Participant deleted"
-msgstr ""
+msgstr "Partecipante cancellato"
msgid "Participant deleted in meeting {}"
-msgstr ""
+msgstr "Partecipante cancellato dalla riunione {}"
msgid "Participant number"
msgstr "Numero partecipante"
msgid "Participant removed from group {} in meeting {}"
-msgstr ""
+msgstr "Partecipante rimosso dal gruppo {} nella riunione {}"
msgid "Participant removed from multiple groups in meeting {}"
-msgstr ""
+msgstr "Partecipante rimosso da più gruppi nella riunione {}"
msgid "Participant removed from multiple groups in multiple meetings"
-msgstr ""
+msgstr "Partecipante rimosso da più gruppi in più riunioni"
msgid "Participants"
msgstr "Partecipanti"
+msgid "Participants (PDF settings)"
+msgstr "Partecipanti (impostazioni PDF)"
+
msgid ""
"Participants and administrators are copied completely and cannot be edited "
"here."
@@ -3398,19 +3738,19 @@ msgstr ""
" essere modificati qui."
msgid "Participants created"
-msgstr ""
+msgstr "Partecipanti creati"
msgid "Participants skipped"
-msgstr ""
+msgstr "Partecipanti saltati"
msgid "Participants updated"
-msgstr ""
+msgstr "Partecipanti aggiornati"
msgid "Participants with errors"
-msgstr ""
+msgstr "Partecipanti con errori"
msgid "Participants with warnings: affected cells will be skipped"
-msgstr ""
+msgstr "Partecipanti con avvertenze: le celle interessate verranno saltate"
msgid "Particles"
msgstr "Particelle"
@@ -3419,7 +3759,7 @@ msgid "Password"
msgstr "Password"
msgid "Password changed"
-msgstr ""
+msgstr "La password è cambiata"
msgid "Password changed successfully!"
msgstr "Password cambiata con successo!"
@@ -3431,13 +3771,13 @@ msgid "Paste/write your topics in this textbox."
msgstr "Copiare / scrivere i propri temi nel campo di testo."
msgid "Pause speech"
-msgstr ""
+msgstr "Pausa di discorso"
msgid "Permissions"
msgstr "Permessi"
msgid "Personal data changed"
-msgstr ""
+msgstr "I dati personali sono cambiati"
msgid "Personal information"
msgstr "Informazioni personali"
@@ -3452,7 +3792,7 @@ msgid "Phase"
msgstr "Fase"
msgid "Playing against"
-msgstr ""
+msgstr "Giocare contro"
msgid "Please allow OpenSlides to access your microphone and/or camera"
msgstr ""
@@ -3474,11 +3814,14 @@ msgstr "Prego, da inserire la nuova password"
msgid "Please join the conference room now!"
msgstr "Prego entra nell'aula conferenza ora!"
+msgid "Please select a primary account."
+msgstr "Seleziona un account principale."
+
msgid "Please select a vote weight greater than or equal to 0.000001"
-msgstr ""
+msgstr "Selezionare un peso di voto maggiore o uguale a 0,000001."
msgid "Please select a vote weight greater than zero."
-msgstr ""
+msgstr "Selezionare un peso di voto maggiore di 0."
msgid "Please select the directory:"
msgstr "Prego, da selezionare l'elenco: "
@@ -3502,13 +3845,10 @@ msgid "Polls"
msgstr "Sondaggio"
msgid "Possible options"
-msgstr ""
+msgstr "Opzioni possibili"
msgid "Possible points of order"
-msgstr ""
-
-msgid "Pre"
-msgstr ""
+msgstr "Possibili mozioni d'ordine"
msgid "Preamble text for PDF document (all elections)"
msgstr "Testo d'introduzione per documenti PDF (tutte le elezioni)"
@@ -3523,7 +3863,7 @@ msgid "Prefix"
msgstr "Prefisso"
msgid "Prefix for the motion identifier of amendments"
-msgstr ""
+msgstr "Prefisso per l'identificativo della mozione degli emendamenti"
msgid "Presence"
msgstr "Presenza"
@@ -3541,7 +3881,10 @@ msgid "Previous slides"
msgstr "Ultime slides"
msgid "Primary color"
-msgstr ""
+msgstr "Colore primario"
+
+msgid "Principals"
+msgstr "Mandanti"
msgid "Privacy Policy"
msgstr "Dichiarazione protezione dati"
@@ -3550,43 +3893,40 @@ msgid "Privacy policy"
msgstr "Dichiarazione protezione dati"
msgid "Private key of Service Provider (SP)"
-msgstr ""
+msgstr "Chiave privata del fornitore di servizi (SP)"
msgid "Process handling"
-msgstr ""
+msgstr "Gestione del processo"
msgid "Project"
msgstr "Proiettare"
msgid "Project active structure level"
-msgstr ""
+msgstr "Livello di struttura attiva del progetto"
msgid "Project all structure levels"
-msgstr ""
+msgstr "Progetto di tutti i livelli della struttura"
msgid "Project selection?"
msgstr "Proiettare selezione?"
msgid "Projection"
-msgstr ""
+msgstr "Proiezione"
msgid "Projection defaults"
msgstr "Impostazione proiezione"
msgid "Projections"
-msgstr ""
+msgstr "Proiezioni"
msgid "Projector"
msgstr "Proiettore"
-msgid "Projector and countdown"
-msgstr "Proiettore e conto alla rovescia"
-
msgid "Projector h1"
-msgstr ""
+msgstr "Proiettore h1"
msgid "Projector h2"
-msgstr ""
+msgstr "Proiettore h2"
msgid "Projector header image"
msgstr "Intestazione grafica proiettore"
@@ -3600,6 +3940,9 @@ msgstr "Proiettori"
msgid "Pronoun"
msgstr "Pronome"
+msgid "Proxy holders"
+msgstr "Delegati"
+
msgid "Public"
msgstr "Publico"
@@ -3609,6 +3952,9 @@ msgstr "Contributo publico"
msgid "Public template"
msgstr "Modello pubblico"
+msgid "Public template required for creating new meeting"
+msgstr "Modello pubblico richiesto per la creazione di una nuova riunione"
+
msgid "Publish"
msgstr "Pubblicare"
@@ -3622,13 +3968,13 @@ msgid "Queue"
msgstr "Lista d'attesa"
msgid "Rank"
-msgstr ""
+msgstr "Ordine"
msgid "Re-add last speaker"
msgstr "Ripristinare l'ultima lista dei relatori"
msgid "Re-count logged-in users"
-msgstr ""
+msgstr "Riconteggio degli utenti connessi"
msgid "Reason"
msgstr "Motivazione"
@@ -3637,10 +3983,10 @@ msgid "Reason required for creating new motion"
msgstr "Motivazione necessaria per creazione nuovo mozione"
msgid "Receipt of contributions"
-msgstr ""
+msgstr "Ricezione di contributi"
msgid "Receive motions"
-msgstr "Ricevere mozioni"
+msgstr "Ricevi mozioni"
msgid "Receive motions from"
msgstr "Ricevere mozioni da"
@@ -3661,6 +4007,8 @@ msgid ""
"Recommendation of motions in such a state can only be seen by motion "
"managers."
msgstr ""
+"La raccomandazione di mozioni in questo stato può essere vista solo dai "
+"gestori di mozione."
msgid "Recommendation reset"
msgstr "Ripristino raccomandazione"
@@ -3668,6 +4016,9 @@ msgstr "Ripristino raccomandazione"
msgid "Recommendation set to {}"
msgstr "Raccomandazione impostata su {}"
+msgid "Redo"
+msgstr "Rifare"
+
msgid "Reenter to conference room"
msgstr "Rientra nella stanza della conferenza"
@@ -3684,10 +4035,10 @@ msgid "Rejected"
msgstr "Respinto"
msgid "Relevant information could not be accessed"
-msgstr ""
+msgstr "Impossibile accedere alle informazioni pertinenti"
msgid "Reload page"
-msgstr ""
+msgstr "Ricarica pagina"
msgid "Remove"
msgstr "Cancellare"
@@ -3700,10 +4051,10 @@ msgstr ""
"gruppo extra per poter vedere il livestream."
msgid "Remove all next speakers"
-msgstr ""
+msgstr "Rimuovi tutti i prossimi oratori"
msgid "Remove all previous speakers"
-msgstr ""
+msgstr "Rimuovi tutti i precedenti oratori"
msgid "Remove all speakers"
msgstr "Cancellare tutti i realtori"
@@ -3723,12 +4074,18 @@ msgstr "Cancellare dall'ordine del giorno"
msgid "Remove from motion block"
msgstr "Cancellare dal gruppo mozioni"
+msgid "Remove link"
+msgstr "Rimuovi link"
+
msgid "Remove me"
msgstr "Cancellarmi"
msgid "Remove option"
msgstr "Rimuovere opzione"
+msgid "Remove point of order"
+msgstr "Rimuovere mozione d'ordine"
+
msgid "Reopen"
msgstr "Riaprire"
@@ -3742,7 +4099,7 @@ msgid "Request"
msgstr "Richiesta."
msgid "Request \"WhoAmI\" failed"
-msgstr ""
+msgstr "Richiesta \"WhoAmI\" fallita"
msgid "Requests to speak"
msgstr "Richieste di parola"
@@ -3761,7 +4118,7 @@ msgid "Required permissions to view this page:"
msgstr "Permessi necessari per visualizzare questa pagina:"
msgid "Requires permission to manage lists of speakers"
-msgstr ""
+msgstr "Richiede l'autorizzazione a gestire elenchi di relatori"
msgid "Reset"
msgstr "Resettare"
@@ -3781,8 +4138,11 @@ msgstr "Raccomandazione resettare"
msgid "Reset state"
msgstr "Resettare stato"
+msgid "Reset timer"
+msgstr "Risetta il timer"
+
msgid "Reset to default settings"
-msgstr ""
+msgstr "Risetta le impostazioni di default"
msgid "Resolution and size"
msgstr "Risoluzione e grandezza"
@@ -3790,20 +4150,35 @@ msgstr "Risoluzione e grandezza"
msgid "Restart livestream"
msgstr "Riavviare livestream"
+msgid ""
+"Restrict delegation principals from adding themselves to the list of "
+"speakers"
+msgstr ""
+"Impedire ai capi delle delegazioni di aggiungersi all'elenco degli oratori."
+
+msgid "Restrict delegation principals from creating motions/amendments"
+msgstr "Impedire ai capi delle delegazioni di creare mozioni/modifiche"
+
+msgid "Restrict delegation principals from supporting motions"
+msgstr "Impedire ai capi delle delegazioni di supportare mozioni"
+
+msgid "Restrict delegation principals from voting"
+msgstr "Impedire ai capi delegazione di votare"
+
msgid "Restrictions"
msgstr "Restrizioni"
msgid "Result"
-msgstr ""
+msgstr "Risultato"
msgid "Results"
msgstr "Risultati"
msgid "Resume speech"
-msgstr ""
+msgstr "Riprendi il discorso"
msgid "Retrieving vote status... Please wait!"
-msgstr ""
+msgstr "Recupero dello stato dei voti... Attendere prego!"
msgid "Right"
msgstr "Diritto"
@@ -3812,22 +4187,22 @@ msgid "Roman"
msgstr "Romano"
msgid "Rows with warnings"
-msgstr ""
+msgstr "Righe con avvertimenti"
msgid "SSO"
msgstr ""
msgid "SSO Identification"
-msgstr ""
+msgstr "Identificazione SSO"
msgid "SSO identification"
-msgstr ""
+msgstr "Identificazione SSO"
msgid "Same email"
-msgstr ""
+msgstr "Stessa email"
-msgid "Same first/last name"
-msgstr ""
+msgid "Same given and surname"
+msgstr "Stesso dato e cognome"
msgid "Save"
msgstr "Salvare"
@@ -3836,7 +4211,7 @@ msgid "Save all changes"
msgstr "Salvare tutte le modifiche"
msgid "Save editorial final version"
-msgstr ""
+msgstr "Salva version editoriale finale"
msgid "Scan this QR code to open URL."
msgstr "Scannerizzare questo codice QR per aprire URL."
@@ -3863,7 +4238,7 @@ msgid "Searching for candidates"
msgstr "A ricerca candidati"
msgid "Searching for players ..."
-msgstr ""
+msgstr "Alla ricerca di giocatori"
msgid "Secret voting can not be guaranteed"
msgstr "Non possono essere assicurati voti segreti"
@@ -3874,9 +4249,6 @@ msgstr "Seleziona"
msgid "Select all"
msgstr "Selezionare tutto"
-msgid "Select exactly two participants to swap mandates"
-msgstr ""
-
msgid "Select file"
msgstr "Selezionare file"
@@ -3887,10 +4259,10 @@ msgid "Select paragraphs"
msgstr "Selezionare paragrafi"
msgid "Select participant"
-msgstr ""
+msgstr "Seleziona partecipanti"
msgid "Select speaker"
-msgstr ""
+msgstr "Seleziona speaker"
msgid "Send"
msgstr "Inviare"
@@ -3918,86 +4290,86 @@ msgstr ""
"Impostazione server necessario per attivare integrazione di Jitsi Meet."
msgid "Set active"
-msgstr ""
+msgstr "Impostare attivo"
msgid "Set as favorite"
-msgstr "Selezionare come favorito"
+msgstr "Impostare come favorito"
msgid "Set as not favorite"
-msgstr "Selezionare come non favorito"
+msgstr "Impostare come non favorito"
msgid "Set as parent"
-msgstr "Selezionare come parente"
+msgstr "Impostare come parente"
msgid "Set as reference projector"
-msgstr "Selezionare come proiettore di riferimento"
+msgstr "Impostare come proiettore di riferimento"
msgid "Set category"
-msgstr "Selezionare categoria"
+msgstr "Impostare categoria"
msgid "Set favorite"
-msgstr "Selezionare favorito"
+msgstr "Impostare favorito"
msgid "Set forward"
msgstr "Impostare l'inoltro"
msgid "Set hidden"
-msgstr "Selezionare nascosto"
+msgstr "Impostare nascosto"
msgid "Set identifier"
-msgstr ""
+msgstr "Impostare l'identificatore"
msgid "Set inactive"
-msgstr ""
+msgstr "Impostare inattivo"
msgid "Set internal"
-msgstr "Selezionare interno"
+msgstr "Impostare interno"
msgid "Set it manually"
-msgstr "Selezionare manuale"
+msgstr "Impostazione manuale"
msgid "Set motion block"
-msgstr "Selezionare sezione di mozione"
+msgstr "Impostare il blocco di mozioni"
msgid "Set natural person ..."
msgstr "Impostare persona naturale"
msgid "Set not present in meeting {}"
-msgstr ""
+msgstr "Impostare non presente al meeting {}"
msgid "Set or remove motion forwarding from the selected committees to:"
msgstr ""
"Impostare o rimuovere l'inoltro della mozione dai comitati selezionati:"
msgid "Set presence ..."
-msgstr "Selezionare presenza ..."
+msgstr "Impostare presenza ..."
msgid "Set present in meeting {}"
-msgstr ""
+msgstr "Impostare presente al meeting {}"
msgid "Set public"
-msgstr "Selezionare publico ..."
+msgstr "Impostare publico ..."
msgid "Set recommendation"
-msgstr "Selezionare raccomandazione"
+msgstr "Impostare raccomandazione"
msgid "Set status"
-msgstr "Selezionare status"
+msgstr "Impostare lo stato"
msgid "Set status for selected accounts"
msgstr "Impostare lo stato per i conti selezionati"
msgid "Set status for selected participants:"
-msgstr "Selezionare status per partecipanti selezionati:"
+msgstr "Imposta status per partecipanti selezionati:"
msgid "Set submission timestamp"
-msgstr ""
+msgstr "Imposta data e ora dell' invio"
msgid "Set submitters"
-msgstr ""
+msgstr "Impostare il presentatore/la presentatrice di mozione"
msgid "Set tags"
-msgstr ""
+msgstr "Imposta tags"
msgid "Set workflow"
msgstr "Imposta il flusso di lavoro"
@@ -4013,6 +4385,9 @@ msgstr ""
msgid "Settings"
msgstr "Impostazioni"
+msgid "Short form for amendments"
+msgstr "Modulo breve per le modifiche"
+
msgid "Show all"
msgstr "Visualizzare tutto"
@@ -4073,7 +4448,7 @@ msgid "Show logo"
msgstr "Visualizzare logo"
msgid "Show main menu"
-msgstr ""
+msgstr "Mostra il menù principale"
msgid "Show meta information box beside the title on projector"
msgstr ""
@@ -4102,14 +4477,11 @@ msgstr "Visualizzare campo extra per raccomandazioni"
msgid "Show recommendation on projector"
msgstr "Mostra la raccomandazione sul proiettore"
-msgid "Show recommendations not public"
-msgstr ""
-
msgid "Show referring motions"
msgstr "Mostra le mozioni di riferimento"
msgid "Show report"
-msgstr ""
+msgstr "Mostra report"
msgid "Show state extension field"
msgstr "Visualizzare status campo extra"
@@ -4133,6 +4505,10 @@ msgstr "Visualizzare questo testo nella pagina login"
msgid "Show title"
msgstr "Visualizzare titolo"
+msgid "Show topic navigation in detail view"
+msgstr ""
+"Mostra la navigazione degli argomenti nella modalità vista dettagliata"
+
msgid ""
"Shows a button with help icon to connect to an extra Jitsi conference room "
"for technical audio/video tests."
@@ -4155,21 +4531,20 @@ msgstr ""
"image format: 24x24Px, PNG, JPG or SVG"
msgid "Single Sign-On settings"
-msgstr ""
+msgstr "Impostazioni di Single Sign-On"
msgid "Single votes"
msgstr "Voto singolo"
-msgid "Slide"
-msgstr "Diapositiva"
-
msgid "Some csv values could not be read correctly."
-msgstr ""
+msgstr "Non si è potuto leggere correttamente alcuni valori csv."
msgid ""
"Some mails could not be sent. There may be a problem communicating with the "
"mail server, please contact your admin."
msgstr ""
+"Non è stato possibile inviare alcune e-mail. Potrebbe esserci un problema di"
+" comunicazione con il server di posta, prego contattare l'amministratore."
msgid "Sort"
msgstr "Ordinare"
@@ -4178,7 +4553,7 @@ msgid "Sort agenda"
msgstr "Ordinare ordine del giorno"
msgid "Sort by identifier"
-msgstr ""
+msgstr "Ordina per identificatore"
msgid "Sort categories"
msgstr "Ordinare secondo categorie"
@@ -4198,38 +4573,51 @@ msgstr "Ordinare mozioni secondo"
msgid "Sort workflow"
msgstr "Selezionario per flusso di lavoro"
+msgid "Source"
+msgstr "Sorgente"
+
+msgid "Source code"
+msgstr "Codice sorgente"
+
msgid "Speaker"
-msgstr ""
+msgstr "Relatore"
msgid "Speakers"
-msgstr "Oratori"
+msgstr "Relatori"
+
+msgid "Speaking time – current contribution"
+msgstr "Durata dell'intervento - contributo attuale"
msgid "Speaking times"
-msgstr ""
+msgstr "Durata degli interventi "
+
+msgid "Speaking times – overview structure levels"
+msgstr "Durata degli interventi - Panoramica livelli di struttura"
msgid "Speech start time"
-msgstr ""
+msgstr "Orario di inizio del discorso"
msgid "Speech type"
-msgstr ""
+msgstr "Tipo di discorso"
msgid "Spokesperson"
-msgstr ""
+msgstr "Portavoce"
msgid "Spokespersons"
-msgstr ""
+msgstr "Persone portavoce"
msgid "Stalemate! It's a draw!"
-msgstr ""
+msgstr "Pareggio! E' un pareggio!"
msgid "Start and end time must either both be set or both be empty"
msgstr ""
+"L'ora di inizio e di fine devono essere entrambe impostate o entrambe vuote."
msgid "Start date"
msgstr "Data d'inizio"
msgid "Start line number"
-msgstr ""
+msgstr "Numero della linea di partenza"
msgid "Start time"
msgstr "Tempo di partenza"
@@ -4249,20 +4637,8 @@ msgstr "Stato impostato su {}"
msgid "Statistics"
msgstr "Statistiche"
-msgid "Statute"
-msgstr "Statuto"
-
-msgid "Statute amendment"
-msgstr "Richiesta di modifica statuto"
-
-msgid "Statute amendment for"
-msgstr "Richiesta di modifica statuto riguardante"
-
-msgid "Statute paragraph"
-msgstr "Paragrafo statuto"
-
-msgid "Statute paragraphs"
-msgstr "Paragrafi statuto"
+msgid "Status"
+msgstr "Stato"
msgid "Stop"
msgstr "Stop"
@@ -4277,22 +4653,22 @@ msgid "Stop voting"
msgstr "Stop votazione"
msgid "Stop waiting"
-msgstr ""
+msgstr "Smettere di aspettare"
msgid "Strikethrough"
-msgstr ""
+msgstr "Barrato"
msgid "Structure level"
msgstr "Livello struttura"
msgid "Structure levels"
-msgstr ""
+msgstr "Livelli di struttura"
msgid "Subcategory"
msgstr "Categoria secondaria"
msgid "Submission date"
-msgstr ""
+msgstr "Data di presentazione"
msgid "Submit selection now?"
msgstr "Inviare la selezione ora?"
@@ -4301,13 +4677,13 @@ msgid "Submit vote now"
msgstr "Inviare il voto ora?"
msgid "Submitter"
-msgstr ""
+msgstr "Presentatore/Presentatrici di mozioni"
msgid "Submitter (in target meeting)"
msgstr "Richiedente (nella riunione target)"
msgid "Submitter may set state to"
-msgstr ""
+msgstr "Chi presenta la mozione può impostare lo stato su"
msgid "Submitters"
msgstr "Richiedenti"
@@ -4316,7 +4692,10 @@ msgid "Submitters changed"
msgstr "I richiedenti sono cambiati"
msgid "Subscript"
-msgstr ""
+msgstr "Pedice"
+
+msgid "Subtract"
+msgstr "Sottrarre"
msgid "Suitable accounts found"
msgstr "Trovati accounts idonei "
@@ -4337,7 +4716,7 @@ msgid "Summary of changes:"
msgstr "Sommario di cambiamenti:"
msgid "Superadmin"
-msgstr "Superamministratore"
+msgstr "Super-amministratore"
msgid "Superadmin actions"
msgstr "Azioni del super amministratore"
@@ -4346,7 +4725,7 @@ msgid "Superadmin settings"
msgstr "Impostazioni super amministratore"
msgid "Superscript"
-msgstr ""
+msgstr "Apice"
msgid "Support"
msgstr "Supporto"
@@ -4360,11 +4739,11 @@ msgstr "Supportatori cambiati"
msgid "Surname"
msgstr "Cognome"
-msgid "Swap mandates ..."
-msgstr ""
+msgid "Swap mandates"
+msgstr "Cambio di mandato"
msgid "Switch"
-msgstr ""
+msgstr "Cambiare"
msgid "Table of contents"
msgstr "Tabella dei contenuti."
@@ -4378,6 +4757,9 @@ msgstr "Etichette"
msgid "Text"
msgstr "Testo"
+msgid "Text color"
+msgstr "Colore del testo"
+
msgid "Text for this option couldn't load."
msgstr "Non è stato possibile caricare il testo per questa opzione."
@@ -4387,11 +4769,14 @@ msgstr "Importo testo"
msgid "Text separator"
msgstr "Divisore testo"
+msgid "Text to display"
+msgstr "Testo da visualizzare"
+
msgid "The account is deactivated."
msgstr "L'account è disattivato"
msgid "The affected columns will not be imported."
-msgstr ""
+msgstr "Le colonne interessate non saranno importate"
msgid "The assembly may decide:"
msgstr "L'assemblea voglia decidere:"
@@ -4404,7 +4789,7 @@ msgstr ""
"Il gestore dell'evento non ha ancora impostato una politica sulla privacy "
msgid "The fields are defined as follows"
-msgstr ""
+msgstr "I campi sono definiti come segue"
msgid "The file has too few columns to be parsed properly."
msgstr "Il file ha troppo poche colonne per essere analizzato correttamente."
@@ -4421,9 +4806,11 @@ msgid ""
"The import can not proceed. There is likely a problem with the import data, "
"please check the preview for details."
msgstr ""
+"L'importazione non può procedere. È probabile che ci sia un problema con i "
+"dati di importazione; controllare l'anteprima per i dettagli."
msgid "The import is in progress, please wait ..."
-msgstr ""
+msgstr "L'importazione è in corso, attendere..."
msgid ""
"The import returned warnings. This does not mean that it failed, but some "
@@ -4431,12 +4818,17 @@ msgid ""
"same as during the preview, but as there is a possibility that new ones have"
" arisen, the relevant rows will be displayed below."
msgstr ""
+"L'importazione ha restituito degli avvertimenti. Ciò non significa che "
+"l'importazione sia fallita, ma che alcuni dati potrebbero essere stati "
+"importati in modo diverso. Di solito gli avvertimenti sono gli stessi "
+"comparsi nell'anteprima, ma poiché è possibile che ne siano sorte di nuove, "
+"le righe pertinenti vengono visualizzate di seguito"
msgid "The import was successful."
-msgstr ""
+msgstr "L'importazione è avvenuta con successo."
msgid "The input data for voting is invalid."
-msgstr ""
+msgstr "I dati inseriti per la votazione non sono validi."
msgid "The link is broken. Please contact your system administrator."
msgstr "Il link è rotto. Contatta il tuo amministratore di sistema."
@@ -4444,33 +4836,30 @@ msgstr "Il link è rotto. Contatta il tuo amministratore di sistema."
msgid "The list of speakers is closed."
msgstr "La lista degli oratori è chiusa."
-msgid "The list of speakers is open."
-msgstr "La lista dei realtori è aperta."
-
msgid ""
"The maximum number of characters per line. Relevant when line numbering is "
-"enabled. Min: 40"
+"enabled. Min: 40. Note: Check PDF export and font."
msgstr ""
-"Il numero massimo di caratteri per linea. Rilevante quando la numerazione "
-"delle righe è abilitata. Min. 40"
+"Il numero massimo di caratteri per riga. Rilevante quando la numerazione "
+"delle righe è abilitata. Min: 40. Nota: Controllare esportazione PDF e font."
msgid "The number has to be greater than 0."
msgstr "Il numero deve essere maggiore di 0."
msgid "The parent motion is not available."
-msgstr ""
+msgstr "La mozione principale non è disponibile."
msgid "The process is still running. Please wait!"
-msgstr ""
+msgstr "Il processo è ancora in corso. Siete pregati di aspettare!"
msgid "The process may have stopped running."
-msgstr ""
+msgstr "Il processo potrebbe aver smesso di funzionare."
msgid "The process will be started. Please wait!"
-msgstr ""
+msgstr "Il processo avrà inizio. Siete pregati di aspettare!"
msgid "The request could not be sent. Check your connection."
-msgstr ""
+msgstr "Non è stato possibile inviare la richiesta. Controlla la connessione."
msgid "The server could not be reached."
msgstr "Il server non è raggiugibile."
@@ -4482,6 +4871,8 @@ msgid ""
"The server may still be processing them, but you will probably not get a "
"result."
msgstr ""
+"Il server potrebbe ancora elaborarli, ma probabilmente non si otterrà alcun "
+"risultato."
msgid "The title is required"
msgstr "Il titol è richiesto"
@@ -4489,10 +4880,14 @@ msgstr "Il titol è richiesto"
msgid ""
"The user %user% has no email, so the invitation email could not be sent."
msgstr ""
+"%u L'utente ser% non ha un indirizzo e-mail, pertanto non è stato possibile "
+"inviare l'e-mail di invito."
msgid ""
"The users %user% have no email, so the invitation emails could not be sent."
msgstr ""
+"%uGli utenti ser% non hanno un'e-mail, quindi non è stato possibile inviare"
+" le e-mail di invito."
msgid "There are not enough options."
msgstr "Non ci sono abbastanza opzioni."
@@ -4512,14 +4907,19 @@ msgstr "C'è un problema di voto sconosciuto"
msgid "There is an unspecified error in this line, which prevents the import."
msgstr ""
+"ThIl server potrebbe ancora elaborarli, ma probabilmente non si otterrà "
+"alcun risultato.ere is an unspecified error in this line, which prevents the"
+" import."
msgid ""
"There seems to be a problem connecting to our services. Check your "
"connection or try again later."
msgstr ""
+"Sembra che ci sia un problema di connessione ai nostri servizi. Controllare "
+"la connessione o riprovare più tardi."
msgid "There should be at least 2 options."
-msgstr ""
+msgstr "Dovrebbero esserci almeno 2 opzioni."
msgid "Thereof point of orders"
msgstr "Mozioni d'ordine relative"
@@ -4528,7 +4928,11 @@ msgid "These accounts will be deleted:"
msgstr "Questi accounts saranno eliminati"
msgid "These participants will be removed:"
+msgstr "Questi partecipanti saranno rimossi:"
+
+msgid "These settings are only applied locally on this browser."
msgstr ""
+"Queste impostazioni vengono applicate solo localmente su questo browser."
msgid "This account has relations to meetings or committees"
msgstr "Questo account ha relazioni con riunioni o comitati"
@@ -4541,13 +4945,13 @@ msgstr ""
"nessuna riunione e non è responsabile di nessun comitato."
msgid "This action will diminish your organization management level"
-msgstr ""
+msgstr "Questa azione ridurrà il livello di gestione dell'organizzazione"
msgid "This action will remove you from one or more groups."
-msgstr ""
+msgstr "Questa azione vi rimuoverà da uno o più gruppi."
msgid "This action will remove you from one or more meetings."
-msgstr ""
+msgstr "Questa azione vi allontanerà da una o più riunioni."
msgid "This ballot contains deleted users."
msgstr "Questo sondaggio contiene utenti cancellati."
@@ -4571,9 +4975,12 @@ msgid ""
"This may diminish your ability to do things in this meeting and you may not "
"be able to revert it by youself. Are you sure you want to do this?"
msgstr ""
+"Questo potrebbe ridurre la vostra capacità di fare cose in questa riunione e"
+" potreste non essere in grado di tornare indietro da soli. È sicuro di "
+"volerlo fare?"
msgid "This meeting"
-msgstr ""
+msgstr "Questa riunione"
msgid "This meeting is archived"
msgstr "Questa riunione è archiviata"
@@ -4582,13 +4989,13 @@ msgid "This paragraph does not exist in the main motion anymore:"
msgstr "Questo paragrafo non esiste più nella mozione principale:"
msgid "This participant will only be removed from this meeting"
-msgstr ""
+msgstr "Questo partecipante sarà rimosso solo da questo incontro "
msgid "This prefix already exists"
-msgstr ""
+msgstr "Questo prefisso esiste già "
msgid "This prefix already exists."
-msgstr ""
+msgstr "Questo prefisso esiste già "
msgid "This prefix will be set if you run the automatic agenda numbering."
msgstr ""
@@ -4600,6 +5007,9 @@ msgid ""
" projectors will automatically set them to visible. Do you really want to do"
" this?"
msgstr ""
+"Questo proiettore è attualmente interno. Selezionando tali proiettori come "
+"proiettori di riferimento, verranno automaticamente impostati come visibili."
+" Volete davvero farlo?"
msgid ""
"This will add or remove the following groups for all selected participants:"
@@ -4611,6 +5021,8 @@ msgid ""
"This will add or remove the following structure levels for all selected "
"participants:"
msgstr ""
+"Questa operazione aggiunge o rimuove i seguenti livelli di struttura per "
+"tutti i partecipanti selezionati:"
msgid ""
"This will add or remove the following submitters for all selected motions:"
@@ -4638,6 +5050,8 @@ msgid ""
"This will diminish your ability to do things on the organization level and "
"you will not be able to revert this yourself."
msgstr ""
+"Questo ridurrà la vostra capacità di agire a livello organizzativo e non "
+"sarete in grado di ripristinare la situazione da soli."
msgid "This will move all selected motions as childs to:"
msgstr "Questo sposterà tutti i movimenti selezionati come figli a:"
@@ -4684,22 +5098,22 @@ msgid "Thoroughly check datastore (unsafe)"
msgstr "Controllo approfondito del datastore (non sicuro)"
msgid "Threefold repetition! It's a draw!"
-msgstr ""
+msgstr "Triplice ripetizione! E un pareggio!"
msgid "Tile view"
msgstr "Visualizzazione in modalità gallery"
msgid "Time"
-msgstr ""
+msgstr "Tempo"
msgid "Time and traffic light"
-msgstr ""
+msgstr "Tempo e semaforo"
msgid "Timer"
-msgstr ""
+msgstr "Timer"
msgid "Timers"
-msgstr ""
+msgstr "Timer"
msgid "Timestamp"
msgstr "Data e ora"
@@ -4717,13 +5131,13 @@ msgid "Title for access data and welcome PDF"
msgstr "Titolo PDF data d'accesso e benvenuto"
msgid "To start your search press Enter or the search icon"
-msgstr ""
+msgstr "Per avviare la ricerca, premere Invio o l'icona di ricerca."
msgid "Toggle to list of speakers"
-msgstr ""
+msgstr "Vai all'elenco dei relatori"
msgid "Toggle to parent item"
-msgstr ""
+msgstr "Cambiare in elemento genitore"
msgid "Too many votes on one option."
msgstr "Troppi voti per una unica opzione"
@@ -4735,31 +5149,31 @@ msgid "Topics"
msgstr "Temi"
msgid "Topics created"
-msgstr ""
+msgstr "Argomenti creati"
msgid "Topics skipped"
-msgstr ""
+msgstr "Argomenti saltati"
msgid "Topics updated"
-msgstr ""
+msgstr "Argomenti aggiornati"
msgid "Topics with warnings (will be skipped)"
-msgstr ""
+msgstr "Argomenti con avvertenze (saranno saltati)"
msgid "Total accounts"
-msgstr ""
+msgstr "Accounts totali"
msgid "Total committees"
-msgstr ""
+msgstr "Totale comitati"
msgid "Total participants"
-msgstr ""
+msgstr "Totale partecipanti"
msgid "Total time"
-msgstr ""
+msgstr "Tempo totale"
msgid "Total topics"
-msgstr ""
+msgstr "Totale argomenti"
msgid "Total votes cast"
msgstr "Voti totali espressi"
@@ -4776,9 +5190,15 @@ msgstr "Risoluzione dei problemi"
msgid "Try reconnect"
msgstr "Prova a ricollegare"
-msgid "Underline"
+msgid "URL"
msgstr ""
+msgid "Underline"
+msgstr "Sottolineato"
+
+msgid "Undo"
+msgstr "Annulla"
+
msgid "Undone"
msgstr "annullato"
@@ -4786,7 +5206,10 @@ msgid "Unique speakers"
msgstr "Altoparlanti unici"
msgid "Unknown participant"
-msgstr ""
+msgstr "Partecipante sconosciuto"
+
+msgid "Unknown user"
+msgstr "Utente sconosciuto"
msgid "Unsaved changes will not be applied."
msgstr "Le modifiche non salvate non verranno applicate."
@@ -4804,9 +5227,11 @@ msgid ""
"Use JSON key:value structure (key = OpenSlides attribute name, value = IdP "
"attribute name)."
msgstr ""
+"Utilizzare la struttura JSON chiave: valore (chiave = nome dell'attributo "
+"OpenSlides, valore = nome dell'attributo IdP)."
msgid "Use color"
-msgstr ""
+msgstr "Utilizzare colore"
msgid "Use the following custom number"
msgstr "Usa il seguente numero cliente"
@@ -4819,13 +5244,16 @@ msgstr ""
"L'url si riferisce all'url del sistema."
msgid "Used for WLAN QRCode projection."
-msgstr ""
+msgstr "Utilizzato per la proiezione del QRCode WLAN."
msgid "Used for invitation emails and QRCode in PDF of access data."
msgstr ""
"Utilizzato per le e-mail di invito e per i QRCode nei PDF dei dati di "
"accesso."
+msgid "User"
+msgstr "Utente"
+
msgid "User not found."
msgstr "Utente non trovato."
@@ -4833,18 +5261,22 @@ msgid "Username"
msgstr "Nome utente"
msgid "Username may not contain spaces"
-msgstr ""
+msgstr "Il nome utente non può contenere spazi"
msgid "Username or password is incorrect."
msgstr "Il nome utente o la password non sono corretti."
msgid "Uses leading zeros to sort motions correctly by identifier."
msgstr ""
+"Utilizza gli zeri iniziali per ordinare correttamente i movimenti in base "
+"all'identificatore."
msgid ""
"Using OpenSlides over HTTP 1.1 or lower is not supported. Make sure you can "
"use HTTP 2 to continue."
msgstr ""
+"L'uso di OpenSlides su HTTP 1.1 o inferiore non è supportato. Assicurarsi di"
+" poter utilizzare HTTP 2 per continuare."
msgid "Using OpenSlides over HTTP is not supported. Enable HTTPS to continue."
msgstr ""
@@ -4854,6 +5286,9 @@ msgstr ""
msgid "Valid votes"
msgstr "Voti validi"
+msgid "View"
+msgstr "Vista"
+
msgid "Virtual applause"
msgstr "Applauso virtuale"
@@ -4861,13 +5296,16 @@ msgid "Visibility"
msgstr "Visibilità "
msgid "Visibility on agenda"
-msgstr ""
+msgstr "Visibilità in agenda"
msgid "Vote"
msgstr "Voto"
+msgid "Vote delegation"
+msgstr "Delega di voto"
+
msgid "Vote submitted"
-msgstr ""
+msgstr "Voto presentato"
msgid "Vote weight"
msgstr "Peso del voto"
@@ -4878,20 +5316,20 @@ msgstr "Votato"
msgid "Votes"
msgstr "Voti"
-msgid "Voting and ballot papers"
-msgstr "Voto e schede elettorali"
+msgid "Voting"
+msgstr "Votazione"
msgid "Voting anonymized"
msgstr "Voto reso anonimo"
msgid "Voting colors"
-msgstr ""
+msgstr "Colori per la votazione"
msgid "Voting created"
-msgstr ""
+msgstr "Votazione creata"
msgid "Voting deleted"
-msgstr ""
+msgstr "Votazione cancellata"
msgid "Voting duration"
msgstr "Durata della votazione"
@@ -4958,26 +5396,23 @@ msgid "WLAN password"
msgstr "WLAN password"
msgid "Wait"
-msgstr ""
+msgstr "Attendere"
msgid "Wait for response ..."
-msgstr ""
+msgstr "Attendere la risposta ..."
msgid "Waiting for response ..."
-msgstr ""
+msgstr "In attesa di risposta ..."
msgid "Warn color"
-msgstr ""
-
-msgid ""
-"Warning: Amendments exist for at least one of the selected motions. Are you "
-"sure you want to delete these motions regardless?"
-msgstr ""
+msgstr "Colore di avvertimento"
msgid ""
"Warning: Amendments exist for this motion. Are you sure you want to delete "
"this motion regardless?"
msgstr ""
+"Attenzione: Esistono emendamenti per questa mozione. Siete sicuri di voler "
+"cancellare questa mozione a prescindere?"
msgid ""
"Warning: Amendments exist for this motion. Editing this text will likely "
@@ -4989,9 +5424,20 @@ msgstr ""
"emendamenti potrebbero diventare inutilizzabili se il paragrafo che "
"interessano viene cancellato."
-msgid "Warning: This projector will be set to visible"
+msgid ""
+"Warning: At least one of the selected motions has amendments, these will be "
+"deleted as well. Do you want to delete anyway?"
+msgstr ""
+"Attenzione: Almeno una delle mozioni selezionate ha degli emendamenti; anche"
+" questi saranno cancellati. Volete cancellare comunque?"
+
+msgid ""
+"Warning: Data loss is possible because accounts are in the same meeting."
msgstr ""
+msgid "Warning: This projector will be set to visible"
+msgstr "Attenzione: Questo proiettore sarà impostato su visibile"
+
msgid "Was forwarded to this meeting"
msgstr "È stato trasmesso a questa riunione"
@@ -5008,20 +5454,13 @@ msgid "Which version?"
msgstr "Quale versione?"
msgid "Wifi"
-msgstr ""
+msgstr "Wifi"
msgid "Wifi access data"
-msgstr ""
+msgstr "Dati di accesso Wifi"
msgid "Wifi name"
-msgstr ""
-
-msgid ""
-"Will be displayed as label before selected recommendation in statute "
-"amendments."
-msgstr ""
-"Sarà visualizzato come etichetta prima della raccomandazione selezionata "
-"negli emendamenti allo statuto."
+msgstr "Nome del Wifi"
msgid ""
"Will be displayed as label before selected recommendation. Use an empty "
@@ -5042,9 +5481,6 @@ msgstr "Flusso di lavoro dei nuovi emendamenti"
msgid "Workflow of new motions"
msgstr "Flusso di lavoro delle nuove mozioni"
-msgid "Workflow of new statute amendments"
-msgstr "Flusso di lavoro dei nuovi emendamenti allo statuto"
-
msgid "Workflows"
msgstr "Lavorazioni"
@@ -5073,7 +5509,7 @@ msgid "Yes/No/Abstain per candidate"
msgstr "Si/No/Astensione per candidato"
msgid "Yes/No/Abstain per list"
-msgstr ""
+msgstr "Sì/No/Assenso per elenco"
msgid "You are not allowed to see all entitled users."
msgstr "Non ti è permesso di vedere tutti gli utenti autorizzati."
@@ -5085,13 +5521,7 @@ msgid "You are not supposed to be here..."
msgstr "Non dovresti essere qui..."
msgid "You are using an incompatible client version."
-msgstr ""
-
-msgid ""
-"You are using the history mode of OpenSlides. Changes will not be saved."
-msgstr ""
-"Stai usando la modalità storia di OpenSlides. Le modifiche non saranno "
-"salvate."
+msgstr "Si sta utilizzando una versione del client non compatibile."
msgid "You can change this option only in the forwarding section."
msgstr "È possibile modificare questa opzione solo nella sezione di inoltro."
@@ -5104,7 +5534,7 @@ msgstr ""
"Non puoi votare in questo momento perché la votazione non è ancora iniziata."
msgid "You can only anonymize named polls."
-msgstr ""
+msgstr "È possibile anonimizzare solo i sondaggi nominativi."
msgid "You can use {event_name} and {username} as placeholder."
msgstr "Puoi usare {event_name} e {username} come segnaposto."
@@ -5116,28 +5546,30 @@ msgstr ""
"lavoro!"
msgid "You cannot delete the last workflow of a meeting."
-msgstr ""
+msgstr "Non è possibile eliminare l'ultimo flusso di lavoro di una riunione."
msgid ""
"You cannot delete the workflow as long as it is selected as default workflow"
" for new amendments in the settings. Please set another workflow as default "
"in the settings and try to delete the workflow again."
msgstr ""
+"Non è possibile eliminare il flusso di lavoro finché è selezionato come "
+"flusso di lavoro predefinito per le nuove modifiche nelle impostazioni. "
+"Impostate un altro flusso di lavoro come predefinito nelle impostazioni e "
+"provate di nuovo a eliminare il flusso di lavoro."
msgid ""
"You cannot delete the workflow as long as it is selected as default workflow"
" for new motions in the settings. Please set another workflow as default in "
"the settings and try to delete the workflow again."
msgstr ""
-
-msgid ""
-"You cannot delete the workflow as long as it is selected as default workflow"
-" for new statute amendments in the settings. Please set another workflow as "
-"default in the settings and try to delete the workflow again."
-msgstr ""
+"Non è possibile eliminare il flusso di lavoro finché è selezionato come "
+"flusso di lavoro predefinito per le nuove modifiche nelle impostazioni. "
+"Impostate un altro flusso di lavoro come predefinito nelle impostazioni e "
+"provate di nuovo a eliminare il flusso di lavoro."
msgid "You cannot delete yourself."
-msgstr ""
+msgstr "Non puoi cancellare te stesso"
msgid ""
"You cannot enter this meeting because you are not assigned to any group."
@@ -5158,7 +5590,7 @@ msgid "You have to be logged in to be able to vote."
msgstr "Devi essere registrato per poter votare."
msgid "You have to be present to add yourself."
-msgstr ""
+msgstr "Bisogna essere presenti per aggiungere se stessi."
msgid "You have to be present to vote."
msgstr "Bisogna essere presenti per votare."
@@ -5183,7 +5615,7 @@ msgid "You reached the maximum amount of votes. Deselect somebody first."
msgstr "Hai raggiunto il numero massimo di voti. Deseleziona prima qualcuno."
msgid "You won!"
-msgstr ""
+msgstr "Hai vinto!"
msgid "Your browser"
msgstr "Browser"
@@ -5201,13 +5633,13 @@ msgid "Your input does not match the following structure: \"hh:mm\""
msgstr "Il tuo input non corrisponde alla seguente struttura: \"hh:mm\""
msgid "Your opponent couldn't stand it anymore... You are the winner!"
-msgstr ""
+msgstr "Il tuo avversario non è riuscito a resistere ... Tu sei il vincitore!"
msgid "Your opponent has won!"
-msgstr ""
+msgstr "Il tuo avversario ha vinto!"
msgid "Your password has been reset successfully!"
-msgstr ""
+msgstr "La password è stata reimpostata con successo!"
msgid "Your votes"
msgstr "I suoi voti"
@@ -5242,16 +5674,16 @@ msgid "add group(s)"
msgstr "aggiungere gruppo(i)"
msgid "already exists"
-msgstr ""
+msgstr "Esiste già "
msgid "analog"
msgstr "analogo"
msgid "and"
-msgstr ""
+msgstr "e"
msgid "anonymized"
-msgstr ""
+msgstr "Reso anonimo"
msgid "are required"
msgstr "sono necessari"
@@ -5263,7 +5695,7 @@ msgid "by"
msgstr "da"
msgid "challenged you to a chess match!"
-msgstr ""
+msgstr "Ti ha sfidato a giocare una partita di scacchi!"
msgid "committee-example"
msgstr "comitato-esempio"
@@ -5290,10 +5722,10 @@ msgid "dateless"
msgstr "Senza data"
msgid "delete"
-msgstr ""
+msgstr "Cancellare"
msgid "deleted"
-msgstr ""
+msgstr "Cancellato"
msgid "disabled"
msgstr "disabilitato"
@@ -5377,13 +5809,13 @@ msgid "items selected"
msgstr "Inserimento selezionati"
msgid "last updated"
-msgstr ""
+msgstr "Ultimo aggiornamento"
msgid "lightblue"
msgstr "azzurro"
msgid "logged-in users"
-msgstr ""
+msgstr "Utenti connessi"
msgid "long running"
msgstr "A lungo termine"
@@ -5395,10 +5827,10 @@ msgid "male"
msgstr "maschile"
msgid "max. 32 characters allowed"
-msgstr ""
+msgstr "Ammessi un massimo di 32 caratteri"
msgid "modified"
-msgstr ""
+msgstr "Modificato"
msgid "motions"
msgstr "mozioni"
@@ -5416,7 +5848,7 @@ msgid "nominal"
msgstr "nominale"
msgid "non-binary"
-msgstr ""
+msgstr "Non binario"
msgid "non-nominal"
msgstr "non nominale"
@@ -5425,7 +5857,7 @@ msgid "none"
msgstr "niente"
msgid "not specified"
-msgstr ""
+msgstr "Non specificato"
msgid "of"
msgstr "di"
@@ -5440,7 +5872,7 @@ msgid "outside"
msgstr "fuori"
msgid "partially forwarded"
-msgstr ""
+msgstr "Inoltrato parzialmente"
msgid "participants"
msgstr "partecipanti"
@@ -5461,7 +5893,7 @@ msgid "red"
msgstr "rosso"
msgid "remove"
-msgstr ""
+msgstr "Eliminare"
msgid "remove group(s)"
msgstr "rimuovere gruppo(gruppi)"
@@ -5469,8 +5901,11 @@ msgstr "rimuovere gruppo(gruppi)"
msgid "represented by"
msgstr "rappresentato da"
+msgid "represented by old account of"
+msgstr "rappresentato dal vecchio account di"
+
msgid "reset"
-msgstr ""
+msgstr "Ripristinare"
msgid "selected"
msgstr "selezionato"
@@ -5482,7 +5917,7 @@ msgid "started"
msgstr "iniziato"
msgid "stopped"
-msgstr ""
+msgstr "Terminato"
msgid "successfully forwarded"
msgstr "Inoltrato con successo"
@@ -5500,7 +5935,7 @@ msgid "undocumented"
msgstr "Non documentato"
msgid "updated"
-msgstr ""
+msgstr "Aggiornato"
msgid "version"
msgstr "versione"
@@ -5508,6 +5943,9 @@ msgstr "versione"
msgid "votes per candidate"
msgstr "Voti per candidato"
+msgid "votes per option"
+msgstr "voti per opzione"
+
msgid "will be created"
msgstr "Sarà creato"
@@ -5515,19 +5953,19 @@ msgid "will be imported"
msgstr "Sarà importato"
msgid "will be updated"
-msgstr ""
+msgstr "Sarà aggiornato"
msgid "yellow"
msgstr "giallo"
msgid "{{amount}} interposed questions will be cleared"
-msgstr ""
+msgstr "{{amount}} Le domande intermedie saranno eliminate"
msgid "{{amount}} of them will be saved with 'unknown' speaker"
-msgstr ""
+msgstr "{{amount}} di essi saranno salvati con \"relatore sconosciuto\"."
msgid "{{amount}} will be saved"
-msgstr ""
+msgstr "{{amount}} saranno salvati"
msgid "Acceptance"
msgstr "Accettazione"
@@ -5576,22 +6014,22 @@ msgid "No decision"
msgstr "Nessuna decisione"
msgid "Presentation and assembly system"
-msgstr ""
+msgstr "Sistema di presentazione e assemblea"
msgid "Referral to"
-msgstr ""
+msgstr "Inoltro a"
msgid "Rejection"
msgstr "Rigetto"
msgid "Reset your OpenSlides password"
-msgstr ""
+msgstr "Reimpostare la password di OpenSlides"
msgid "Simple Workflow"
msgstr "Lavorazione semplice"
msgid "Space for your welcome text."
-msgstr ""
+msgstr "Spazio per il tuo testo di benvenuto."
msgid "Speaking time"
msgstr "Tempo per l'intervento"
@@ -5599,9 +6037,6 @@ msgstr "Tempo per l'intervento"
msgid "Staff"
msgstr "Collaboratori"
-msgid "Voting"
-msgstr "Votazione"
-
msgid ""
"You are receiving this email because you have requested a new password for your OpenSlides account.\n"
"\n"
@@ -5610,6 +6045,10 @@ msgid ""
"\n"
"The link will be valid for 10 minutes."
msgstr ""
+"Stai ricevendo questa e-mail perché hai richiesto una nuova password per il "
+"tuo account OpenSlides. Per favore apri il seguente link e scegli una nuova "
+"password: {url}/login/forget-password-"
+"confirm?user_id={user_id}&token={token}. Il link sarà valido per 10 minuti."
msgid "accepted"
msgstr "accettato"
@@ -5621,7 +6060,7 @@ msgid "in progress"
msgstr "in corso"
msgid "name"
-msgstr ""
+msgstr "Nome"
msgid "not concerned"
msgstr "non elaborato"
@@ -5630,13 +6069,13 @@ msgid "not decided"
msgstr "Non deciso"
msgid "not permitted"
-msgstr ""
+msgstr "Non consentito"
msgid "permitted"
msgstr "permesso"
msgid "referred to"
-msgstr ""
+msgstr "Riferito a "
msgid "rejected"
msgstr "rifiutato"
diff --git a/openslides_backend/migrations/migrations/0062_unset_presence_of_removed_users.py b/openslides_backend/migrations/migrations/0062_unset_presence_of_removed_users.py
new file mode 100644
index 0000000000..4ee49676de
--- /dev/null
+++ b/openslides_backend/migrations/migrations/0062_unset_presence_of_removed_users.py
@@ -0,0 +1,87 @@
+from collections import defaultdict
+
+from datastore.migrations import BaseModelMigration
+from datastore.reader.core import GetManyRequestPart
+from datastore.writer.core import BaseRequestEvent, RequestUpdateEvent
+
+from openslides_backend.shared.patterns import fqid_from_collection_and_id
+
+from ...shared.filters import And, FilterOperator
+
+
+class Migration(BaseModelMigration):
+ """
+ This migration removes the presence status if the user is not part of the meeting anymore.
+ """
+
+ target_migration_index = 63
+
+ def migrate_models(self) -> list[BaseRequestEvent] | None:
+ present_users_per_meeting: dict[int, list[int]] = defaultdict(list)
+ meetings_per_present_user: dict[int, list[int]] = defaultdict(list)
+
+ meeting_users = self.reader.filter(
+ "meeting_user",
+ And(
+ FilterOperator("group_ids", "=", "[]"),
+ FilterOperator("meta_deleted", "!=", True),
+ ),
+ ["user_id", "meeting_id"],
+ )
+ for meeting_user in meeting_users.values():
+ user_id = meeting_user["user_id"]
+ meeting_id = meeting_user["meeting_id"]
+ meetings_per_present_user[user_id].append(meeting_id)
+ present_users_per_meeting[meeting_id].append(user_id)
+
+ meetings = self.reader.get_many(
+ [
+ GetManyRequestPart(
+ "meeting",
+ [meeting_id for meeting_id in present_users_per_meeting],
+ ["present_user_ids"],
+ )
+ ]
+ ).get("meeting", dict())
+ users = self.reader.get_many(
+ [
+ GetManyRequestPart(
+ "user",
+ [present_user_id for present_user_id in meetings_per_present_user],
+ ["is_present_in_meeting_ids"],
+ )
+ ]
+ ).get("user", dict())
+ return [
+ *[
+ RequestUpdateEvent(
+ fqid_from_collection_and_id("meeting", meeting_id),
+ {
+ "present_user_ids": [
+ id_
+ for id_ in meetings.get(meeting_id, dict()).get(
+ "present_user_ids", []
+ )
+ if id_ not in user_ids
+ ]
+ },
+ )
+ for meeting_id, user_ids in present_users_per_meeting.items()
+ if meetings
+ ],
+ *[
+ RequestUpdateEvent(
+ fqid_from_collection_and_id("user", user_id),
+ {
+ "is_present_in_meeting_ids": [
+ id_
+ for id_ in users.get(user_id, dict()).get(
+ "is_present_in_meeting_ids", []
+ )
+ if id_ not in meeting_ids
+ ]
+ },
+ )
+ for user_id, meeting_ids in meetings_per_present_user.items()
+ ],
+ ]
diff --git a/openslides_backend/migrations/migrations/0062_remove_forwarding_user.py b/openslides_backend/migrations/migrations/0063_remove_forwarding_user.py
similarity index 96%
rename from openslides_backend/migrations/migrations/0062_remove_forwarding_user.py
rename to openslides_backend/migrations/migrations/0063_remove_forwarding_user.py
index 84534bd026..41b6ff9bc7 100644
--- a/openslides_backend/migrations/migrations/0062_remove_forwarding_user.py
+++ b/openslides_backend/migrations/migrations/0063_remove_forwarding_user.py
@@ -8,7 +8,7 @@ class Migration(BaseModelMigration):
This migration removes the forwarding user relation
"""
- target_migration_index = 63
+ target_migration_index = 64
def migrate_models(self) -> list[BaseRequestEvent] | None:
events: list[BaseRequestEvent] = []
diff --git a/openslides_backend/models/fields.py b/openslides_backend/models/fields.py
index d2c920427d..1f4b875acd 100644
--- a/openslides_backend/models/fields.py
+++ b/openslides_backend/models/fields.py
@@ -23,6 +23,9 @@
validate_html,
)
+TRUE_VALUES = ("1", "true", "yes", "t", "y")
+FALSE_VALUES = ("0", "false", "no", "f", "n")
+
class OnDelete(str, Enum):
PROTECT = "PROTECT"
@@ -136,8 +139,6 @@ def check_required_not_fulfilled(
return instance[self.own_field_name] is None
def validate(self, value: Any, payload: dict[str, Any] = {}) -> Any:
- TRUE_VALUES = ("1", "true", "yes", "t", "y")
- FALSE_VALUES = ("0", "false", "no", "f", "n")
if isinstance(value, bool):
return value
elif isinstance(value, str):
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 01fd9cd466..3735ff1880 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -482,6 +482,7 @@ class Meeting(Model, MeetingModelMixin):
motions_hide_metadata_background = fields.BooleanField(default=False)
motions_show_referring_motions = fields.BooleanField(default=True)
motions_show_sequential_number = fields.BooleanField(default=True)
+ motions_create_enable_additional_submitter_text = fields.BooleanField()
motions_recommendations_by = fields.CharField()
motions_block_slide_columns = fields.IntegerField(constraints={"minimum": 1})
motions_recommendation_text_mode = fields.CharField(
@@ -979,6 +980,7 @@ class Group(Model):
"motion.can_see_origin",
"motion.can_support",
"poll.can_manage",
+ "poll.can_see_progress",
"projector.can_manage",
"projector.can_see",
"tag.can_manage",
diff --git a/openslides_backend/permissions/permission_helper.py b/openslides_backend/permissions/permission_helper.py
index 4d79cd83da..58840eb033 100644
--- a/openslides_backend/permissions/permission_helper.py
+++ b/openslides_backend/permissions/permission_helper.py
@@ -206,6 +206,7 @@ def is_admin(datastore: DatastoreService, user_id: int, meeting_id: int) -> bool
Permissions.Projector.CAN_SEE,
Permissions.User.CAN_SEE,
Permissions.User.CAN_SEE_SENSITIVE_DATA,
+ Permissions.Poll.CAN_SEE_PROGRESS,
}
diff --git a/openslides_backend/permissions/permissions.py b/openslides_backend/permissions/permissions.py
index 3caa90864e..c5dc6def3c 100644
--- a/openslides_backend/permissions/permissions.py
+++ b/openslides_backend/permissions/permissions.py
@@ -59,6 +59,7 @@ class _Motion(str, Permission, Enum):
class _Poll(str, Permission, Enum):
CAN_MANAGE = "poll.can_manage"
+ CAN_SEE_PROGRESS = "poll.can_see_progress"
class _Projector(str, Permission, Enum):
@@ -140,6 +141,7 @@ class Permissions:
_Motion.CAN_MANAGE: [],
_Motion.CAN_SUPPORT: [],
_Motion.CAN_SEE_ORIGIN: [],
+ _Poll.CAN_SEE_PROGRESS: [_Poll.CAN_MANAGE],
_Poll.CAN_MANAGE: [],
_Projector.CAN_SEE: [_Projector.CAN_MANAGE],
_Projector.CAN_MANAGE: [],
diff --git a/openslides_backend/presenter/__init__.py b/openslides_backend/presenter/__init__.py
index 432c547183..f06f93cb07 100644
--- a/openslides_backend/presenter/__init__.py
+++ b/openslides_backend/presenter/__init__.py
@@ -7,6 +7,7 @@
get_forwarding_meetings,
get_history_information,
get_mediafile_context,
+ get_user_editable,
get_user_related_models,
get_user_scope,
get_users,
diff --git a/openslides_backend/presenter/base.py b/openslides_backend/presenter/base.py
index 72e44e971e..81a0ff7994 100644
--- a/openslides_backend/presenter/base.py
+++ b/openslides_backend/presenter/base.py
@@ -17,6 +17,7 @@ class BasePresenter(BaseServiceProvider):
Base class for presenters.
"""
+ internal: bool = False
data: Any
schema: Callable[[Any], None] | None = None
diff --git a/openslides_backend/presenter/check_database.py b/openslides_backend/presenter/check_database.py
index 624e9b2821..b051ee690c 100644
--- a/openslides_backend/presenter/check_database.py
+++ b/openslides_backend/presenter/check_database.py
@@ -41,7 +41,7 @@ def check_meetings(
errors: dict[int, str] = {}
for meeting_id in meeting_ids:
- export = export_meeting(datastore, meeting_id)
+ export = export_meeting(datastore, meeting_id, True)
try:
Checker(
data=export,
diff --git a/openslides_backend/presenter/get_user_editable.py b/openslides_backend/presenter/get_user_editable.py
new file mode 100644
index 0000000000..7104da3190
--- /dev/null
+++ b/openslides_backend/presenter/get_user_editable.py
@@ -0,0 +1,85 @@
+from collections import defaultdict
+from typing import Any
+
+import fastjsonschema
+
+from openslides_backend.permissions.permissions import Permissions
+from openslides_backend.shared.exceptions import (
+ ActionException,
+ MissingPermission,
+ PermissionDenied,
+ PresenterException,
+)
+from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
+ CreateUpdatePermissionsMixin,
+)
+from openslides_backend.shared.schema import id_list_schema, str_list_schema
+
+from ..shared.schema import schema_version
+from .base import BasePresenter
+from .presenter import register_presenter
+
+get_user_editable_schema = fastjsonschema.compile(
+ {
+ "$schema": schema_version,
+ "type": "object",
+ "title": "get_user_editable",
+ "description": "get user editable",
+ "properties": {
+ "user_ids": id_list_schema,
+ "fields": str_list_schema,
+ },
+ "required": ["user_ids", "fields"],
+ "additionalProperties": False,
+ }
+)
+
+
+@register_presenter("get_user_editable")
+class GetUserEditable(CreateUpdatePermissionsMixin, BasePresenter):
+ """
+ Checks for each given user whether the given fields are editable by calling user on a per payload group basis.
+ """
+
+ schema = get_user_editable_schema
+ name = "get_user_editable"
+ permission = Permissions.User.CAN_MANAGE
+
+ def get_result(self) -> Any:
+ if not self.data["fields"]:
+ raise PresenterException(
+ "Need at least one field name to check editability."
+ )
+ reversed_field_rights = {
+ field: group
+ for group, fields in self.field_rights.items()
+ for field in fields
+ }
+ one_field_per_group = {
+ group_fields[0]
+ for field_name in self.data["fields"]
+ for group_fields in self.field_rights.values()
+ if field_name in group_fields
+ }
+ result: defaultdict[str, dict[str, tuple[bool, str]]] = defaultdict(dict)
+ for user_id in self.data["user_ids"]:
+ result[str(user_id)] = {}
+ groups_editable = {}
+ for field_name in one_field_per_group:
+ try:
+ self.check_permissions({"id": user_id, field_name: None})
+ groups_editable[reversed_field_rights[field_name]] = (True, "")
+ except (PermissionDenied, MissingPermission, ActionException) as e:
+ groups_editable[reversed_field_rights[field_name]] = (
+ False,
+ e.message,
+ )
+ result[str(user_id)].update(
+ {
+ data_field_name: groups_editable[
+ reversed_field_rights[data_field_name]
+ ]
+ for data_field_name in self.data["fields"]
+ }
+ )
+ return result
diff --git a/openslides_backend/presenter/get_user_scope.py b/openslides_backend/presenter/get_user_scope.py
index 8c07f485a9..fa609bad92 100644
--- a/openslides_backend/presenter/get_user_scope.py
+++ b/openslides_backend/presenter/get_user_scope.py
@@ -36,7 +36,10 @@ def get_result(self) -> Any:
result: dict[str, Any] = {}
user_ids = self.data["user_ids"]
for user_id in user_ids:
- scope, scope_id, user_oml, committee_ids = self.get_user_scope(user_id)
+ scope, scope_id, user_oml, committee_meeting_ids = self.get_user_scope(
+ user_id
+ )
+ committee_ids = [ci for ci in committee_meeting_ids.keys()]
result[str(user_id)] = {
"collection": scope,
"id": scope_id,
diff --git a/openslides_backend/presenter/search_users.py b/openslides_backend/presenter/search_users.py
index 07004b472d..a9500ac7fb 100644
--- a/openslides_backend/presenter/search_users.py
+++ b/openslides_backend/presenter/search_users.py
@@ -72,9 +72,11 @@ class SearchUsers(BasePresenter):
def get_result(self) -> list[list[dict[str, Any]]]:
self.check_permissions(self.data["permission_type"], self.data["permission_id"])
filters: set[Filter] = set()
+ jehova = False
for search in self.data["search"]:
# strip all fields and use "" if no value was given
for field in all_fields:
+ jehova = jehova or (search.get(field, "") in ["Jehova", "Jehovah"])
search[field] = search.get(field, "").strip().lower()
for search_def in search_fields:
if all(search.get(field) for field in search_def):
@@ -111,6 +113,8 @@ def get_result(self) -> list[list[dict[str, Any]]]:
current_result.append(instance)
break
result.append(current_result)
+ if jehova and not any(x for x in result):
+ raise PresenterException("Oooh! He said it again! Oooh!...")
return result
def get_filter(self, field: str, value: str) -> Filter:
diff --git a/openslides_backend/services/auth/adapter.py b/openslides_backend/services/auth/adapter.py
index c95dcd598d..1c3f231380 100644
--- a/openslides_backend/services/auth/adapter.py
+++ b/openslides_backend/services/auth/adapter.py
@@ -68,3 +68,6 @@ def clear_all_sessions(self) -> None:
self.auth_handler.clear_all_sessions(
self.access_token, parse.unquote(self.refresh_id)
)
+
+ def clear_sessions_by_user_id(self, user_id: int) -> None:
+ self.auth_handler.clear_sessions_by_user_id(user_id)
diff --git a/openslides_backend/services/auth/interface.py b/openslides_backend/services/auth/interface.py
index df16cedd25..41fa1cfcb5 100644
--- a/openslides_backend/services/auth/interface.py
+++ b/openslides_backend/services/auth/interface.py
@@ -55,7 +55,13 @@ def verify_authorization_token(self, user_id: int, token: str) -> bool:
def clear_all_sessions(self) -> None:
"""
- Clears all sessions of the user.
+ Clears all sessions of the requesting user.
Authentication data must be set beforehand via set_authentication.
"""
+
+ def clear_sessions_by_user_id(self, user_id: int) -> None:
+ """
+ Clears all sessions of the given user.
+ Use with caution as the auth-service uses its internal route for this.
+ """
diff --git a/openslides_backend/shared/exceptions.py b/openslides_backend/shared/exceptions.py
index 59404c29a6..94ea1811d6 100644
--- a/openslides_backend/shared/exceptions.py
+++ b/openslides_backend/shared/exceptions.py
@@ -131,16 +131,29 @@ def __init__(self, action_name: str) -> None:
class MissingPermission(PermissionDenied):
def __init__(
self,
- permissions: AnyPermission | dict[AnyPermission, int],
+ permissions: AnyPermission | dict[AnyPermission, int | set[int]],
) -> None:
if isinstance(permissions, dict):
- self.message = (
- "Missing permission" + ("s" if len(permissions) > 1 else "") + ": "
- )
+ to_remove = []
+ for permission, id_or_ids in permissions.items():
+ if isinstance(id_or_ids, set) and not id_or_ids:
+ to_remove.append(permission)
+ for permission in to_remove:
+ del permissions[permission]
+ self.message = "Missing permission" + self._plural_s(permissions) + ": "
self.message += " or ".join(
- f"{permission.get_verbose_type()} {permission} in {permission.get_base_model()} {id}"
- for permission, id in permissions.items()
+ f"{permission.get_verbose_type()} {permission} in {permission.get_base_model()}{self._plural_s(id_or_ids)} {id_or_ids}"
+ for permission, id_or_ids in permissions.items()
)
else:
self.message = f"Missing {permissions.get_verbose_type()}: {permissions}"
super().__init__(self.message)
+
+ def _plural_s(self, permission_or_id_or_ids: dict | int | set[int]) -> str:
+ if (
+ isinstance(permission_or_id_or_ids, set)
+ or (isinstance(permission_or_id_or_ids, dict))
+ ) and len(permission_or_id_or_ids) > 1:
+ return "s"
+ else:
+ return ""
diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py
index 17d987ceb5..5e1bd944a2 100644
--- a/openslides_backend/shared/export_helper.py
+++ b/openslides_backend/shared/export_helper.py
@@ -20,6 +20,8 @@
FORBIDDEN_FIELDS = ["forwarded_motion_ids"]
+NON_CASCADING_MEETING_RELATION_LISTS = ["poll_candidate_list_ids", "poll_candidate_ids"]
+
def export_meeting(
datastore: DatastoreService, meeting_id: int, internal_target: bool = False
@@ -165,11 +167,9 @@ def add_users(
user["is_present_in_meeting_ids"] = [meeting_id]
else:
user["is_present_in_meeting_ids"] = None
- if not internal_target:
+ if not internal_target and (gender_id := user.pop("gender_id", None)):
gender_dict = datastore.get_all("gender", ["name"], lock_result=False)
- if user.get("gender_id"):
- user["gender"] = gender_dict.get(user["gender_id"], {}).get("name")
- del user["gender_id"]
+ user["gender"] = gender_dict.get(gender_id, {}).get("name")
# limit user fields to exported objects
collection_field_tupels = [
("meeting_user", "meeting_user_ids"),
@@ -202,10 +202,12 @@ def remove_meta_fields(res: dict[str, Any]) -> dict[str, Any]:
def get_relation_fields() -> Iterable[RelationListField]:
for field in Meeting().get_relation_fields():
- if (
- isinstance(field, RelationListField)
- and field.on_delete == OnDelete.CASCADE
- and field.get_own_field_name().endswith("_ids")
+ if isinstance(field, RelationListField) and (
+ (
+ field.on_delete == OnDelete.CASCADE
+ and field.get_own_field_name().endswith("_ids")
+ )
+ or field.get_own_field_name() in NON_CASCADING_MEETING_RELATION_LISTS
):
yield field
diff --git a/openslides_backend/action/actions/user/create_update_permissions_mixin.py b/openslides_backend/shared/mixins/user_create_update_permissions_mixin.py
similarity index 83%
rename from openslides_backend/action/actions/user/create_update_permissions_mixin.py
rename to openslides_backend/shared/mixins/user_create_update_permissions_mixin.py
index 70aa8a02c8..630e442293 100644
--- a/openslides_backend/action/actions/user/create_update_permissions_mixin.py
+++ b/openslides_backend/shared/mixins/user_create_update_permissions_mixin.py
@@ -3,7 +3,6 @@
from functools import reduce
from typing import Any, cast
-from openslides_backend.action.action import Action
from openslides_backend.action.relations.relation_manager import RelationManager
from openslides_backend.permissions.base_classes import Permission
from openslides_backend.permissions.management_levels import (
@@ -13,8 +12,10 @@
from openslides_backend.permissions.permissions import Permissions, permission_parents
from openslides_backend.services.datastore.commands import GetManyRequest
from openslides_backend.services.datastore.interface import DatastoreService
+from openslides_backend.shared.base_service_provider import BaseServiceProvider
from openslides_backend.shared.exceptions import (
ActionException,
+ AnyPermission,
MissingPermission,
PermissionDenied,
)
@@ -24,8 +25,6 @@
from openslides_backend.shared.mixins.user_scope_mixin import UserScope, UserScopeMixin
from openslides_backend.shared.patterns import fqid_from_collection_and_id
-from .user_mixins import UserMixin
-
class PermissionVarStore:
permission: Permission
@@ -171,9 +170,10 @@ def _get_user_meetings_with_permission(
return user_meetings
-class CreateUpdatePermissionsMixin(UserMixin, UserScopeMixin, Action):
+class CreateUpdatePermissionsMixin(UserScopeMixin, BaseServiceProvider):
permstore: PermissionVarStore
permission: Permission
+ internal: bool
field_rights: dict[str, list] = {
"A": [
@@ -203,7 +203,7 @@ class CreateUpdatePermissionsMixin(UserMixin, UserScopeMixin, Action):
"is_present", # participant import
],
"C": ["meeting_id", "group_ids"],
- "D": ["committee_ids", "committee_management_ids"],
+ "D": ["committee_management_ids"],
"E": ["organization_management_level"],
"F": ["default_password"],
"G": ["is_demo_user"],
@@ -213,10 +213,12 @@ class CreateUpdatePermissionsMixin(UserMixin, UserScopeMixin, Action):
def check_permissions(self, instance: dict[str, Any]) -> None:
"""
Checks the permissions on a per field and user.scope base, details see
- https://github.com/OpenSlides/OpenSlides/wiki/user.update or user.create
+ https://github.com/OpenSlides/OpenSlides/wiki/Users
+ https://github.com/OpenSlides/OpenSlides/wiki/Permission-System
+ https://github.com/OpenSlides/OpenSlides/wiki/Restrictions-Overview
The fields groups and their necessary permissions are also documented there.
+ Returns true if permissions are given.
"""
- self.assert_not_anonymous()
if not hasattr(self, "permstore"):
self.permstore = PermissionVarStore(
@@ -224,12 +226,12 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
)
actual_group_fields = self._get_actual_grouping_from_instance(instance)
- # store scope, id and OML-permission for requested user
+ # store scope, scope id, OML-permission and committee ids including the the respective meetings for requested user
(
self.instance_user_scope,
self.instance_user_scope_id,
self.instance_user_oml_permission,
- self.instance_committee_ids,
+ self.instance_committee_meeting_ids,
) = self.get_user_scope(instance.get("id") or instance)
if self.permstore.user_oml != OrganizationManagementLevel.SUPERADMIN:
@@ -244,40 +246,38 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
lock_result=False,
).get("locked_from_inside", False)
- # Ordered by supposed velocity advantages. Changing order can only effect the sequence of detected errors for tests
+ # Ordered by supposed speed advantages. Changing order can only effect the sequence of detected errors for tests
self.check_group_H(actual_group_fields["H"])
self.check_group_E(actual_group_fields["E"], instance)
self.check_group_D(actual_group_fields["D"], instance)
self.check_group_C(actual_group_fields["C"], instance, locked_from_inside)
self.check_group_B(actual_group_fields["B"], instance, locked_from_inside)
- self.check_group_A(actual_group_fields["A"])
- self.check_group_F(actual_group_fields["F"])
+ self.check_group_A(actual_group_fields["A"], instance)
+ self.check_group_F(actual_group_fields["F"], instance)
self.check_group_G(actual_group_fields["G"])
- def check_group_A(
- self,
- fields: list[str],
- ) -> None:
+ def check_group_A(self, fields: list[str], instance: dict[str, Any]) -> None:
"""Check Group A: Depending on scope of user to act on"""
if (
- self.permstore.user_oml == OrganizationManagementLevel.SUPERADMIN
- or not fields
+ not fields
or self.permstore.user_oml >= OrganizationManagementLevel.CAN_MANAGE_USERS
):
return
+ missing_permissions: dict[AnyPermission, int | set[int]] = dict()
if self.instance_user_scope == UserScope.Organization:
- if self.permstore.user_committees.intersection(self.instance_committee_ids):
- return
- raise MissingPermission({OrganizationManagementLevel.CAN_MANAGE_USERS: 1})
- if self.instance_user_scope == UserScope.Committee:
- if self.instance_user_scope_id not in self.permstore.user_committees:
- raise MissingPermission(
- {
- OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
- CommitteeManagementLevel.CAN_MANAGE: self.instance_user_scope_id,
- }
+ if not (
+ self.permstore.user_committees.intersection(
+ self.instance_committee_meeting_ids
)
+ ):
+ missing_permissions = {OrganizationManagementLevel.CAN_MANAGE_USERS: 1}
+ elif self.instance_user_scope == UserScope.Committee:
+ if self.instance_user_scope_id not in self.permstore.user_committees:
+ missing_permissions = {
+ OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
+ CommitteeManagementLevel.CAN_MANAGE: self.instance_user_scope_id,
+ }
elif (
self.instance_user_scope_id not in self.permstore.user_committees_meetings
and self.instance_user_scope_id not in self.permstore.user_meetings
@@ -287,13 +287,26 @@ def check_group_A(
["committee_id"],
lock_result=False,
)
- raise MissingPermission(
+ missing_permissions = {
+ OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
+ CommitteeManagementLevel.CAN_MANAGE: meeting["committee_id"],
+ self.permission: self.instance_user_scope_id,
+ }
+ if missing_permissions and not self.check_for_admin_in_all_meetings(
+ instance.get("id", 0)
+ ):
+ missing_permissions.update(
{
- OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
- CommitteeManagementLevel.CAN_MANAGE: meeting["committee_id"],
- self.permission: self.instance_user_scope_id,
+ Permissions.User.CAN_UPDATE: {
+ meeting_id
+ for meeting_ids in self.instance_committee_meeting_ids.values()
+ if meeting_ids is not None
+ for meeting_id in meeting_ids
+ if meeting_id is not None
+ },
}
)
+ raise MissingPermission(missing_permissions)
def check_group_B(
self, fields: list[str], instance: dict[str, Any], locked_from_inside: bool
@@ -357,10 +370,7 @@ def check_group_E(self, fields: list[str], instance: dict[str, Any]) -> None:
f"Your organization management level is not high enough to set a Level of {instance.get('organization_management_level', OrganizationManagementLevel.CAN_MANAGE_USERS.get_verbose_type())}."
)
- def check_group_F(
- self,
- fields: list[str],
- ) -> None:
+ def check_group_F(self, fields: list[str], instance: dict[str, Any]) -> None:
"""Check F common fields: scoped permissions necessary, but if instance user has
an oml-permission, that of the request user must be higher"""
if (
@@ -369,6 +379,7 @@ def check_group_F(
):
return
+ missing_permissions: dict[AnyPermission, int | set[int]] = dict()
if (
self.instance_user_oml_permission
or self.instance_user_scope == UserScope.Organization
@@ -379,25 +390,22 @@ def check_group_F(
)
else:
if self.permstore.user_committees.intersection(
- self.instance_committee_ids
+ self.instance_committee_meeting_ids
):
return
expected_oml_permission = OrganizationManagementLevel.CAN_MANAGE_USERS
if expected_oml_permission > self.permstore.user_oml:
- raise MissingPermission({expected_oml_permission: 1})
+ missing_permissions = {expected_oml_permission: 1}
else:
return
- else:
- if self.permstore.user_oml >= OrganizationManagementLevel.CAN_MANAGE_USERS:
- return
- if self.instance_user_scope == UserScope.Committee:
+ elif self.permstore.user_oml >= OrganizationManagementLevel.CAN_MANAGE_USERS:
+ return
+ elif self.instance_user_scope == UserScope.Committee:
if self.instance_user_scope_id not in self.permstore.user_committees:
- raise MissingPermission(
- {
- OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
- CommitteeManagementLevel.CAN_MANAGE: self.instance_user_scope_id,
- }
- )
+ missing_permissions = {
+ OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
+ CommitteeManagementLevel.CAN_MANAGE: self.instance_user_scope_id,
+ }
elif (
self.instance_user_scope_id not in self.permstore.user_committees_meetings
and self.instance_user_scope_id not in self.permstore.user_meetings
@@ -407,13 +415,26 @@ def check_group_F(
["committee_id"],
lock_result=False,
)
- raise MissingPermission(
+ missing_permissions = {
+ OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
+ CommitteeManagementLevel.CAN_MANAGE: meeting["committee_id"],
+ self.permission: self.instance_user_scope_id,
+ }
+ if missing_permissions and not self.check_for_admin_in_all_meetings(
+ instance.get("id", 0)
+ ):
+ missing_permissions.update(
{
- OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
- CommitteeManagementLevel.CAN_MANAGE: meeting["committee_id"],
- self.permission: self.instance_user_scope_id,
+ Permissions.User.CAN_UPDATE: {
+ meeting_id
+ for meeting_ids in self.instance_committee_meeting_ids.values()
+ if meeting_ids is not None
+ for meeting_id in meeting_ids
+ if meeting_id is not None
+ },
}
)
+ raise MissingPermission(missing_permissions)
def check_group_G(self, fields: list[str]) -> None:
"""Group G: OML SUPERADMIN necessary"""
@@ -472,8 +493,9 @@ def _get_actual_grouping_from_instance(
"""
Returns a dictionary with an entry for each field group A-E with
a list of fields from payload instance.
- The field groups A-F refer to https://github.com/OpenSlides/OpenSlides/wiki/user.create
- or user.update
+ The field groups A-F refer to https://github.com/OpenSlides/openslides-meta/blob/main/models.yml
+ or https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.create.md
+ or https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.update.md
"""
act_grouping: dict[str, list[str]] = defaultdict(list)
for key, _ in instance.items():
@@ -516,7 +538,9 @@ def _meetings_from_group_B_fields_from_instance(
any other group B field.
"""
meetings: set[int] = set(instance.get("is_present_in_meeting_ids", []))
- meetings.add(cast(int, instance.get("meeting_id")))
+ meeting_id = cast(int, instance.get("meeting_id"))
+ if meeting_id:
+ meetings.add(meeting_id)
return meetings
@@ -525,6 +549,7 @@ class CreateUpdatePermissionsFailingFields(CreateUpdatePermissionsMixin):
def __init__(
self,
+ user_id: int,
permstore: PermissionVarStore,
services: Services,
datastore: DatastoreService,
@@ -535,14 +560,11 @@ def __init__(
use_meeting_ids_for_archived_meeting_check: bool | None = None,
) -> None:
self.permstore = permstore
+ self.user_id = user_id
super().__init__(
services,
datastore,
- relation_manager,
logging,
- env,
- skip_archived_meeting_check,
- use_meeting_ids_for_archived_meeting_check,
)
def get_failing_fields(self, instance: dict[str, Any]) -> list[str]:
@@ -567,7 +589,7 @@ def get_failing_fields(self, instance: dict[str, Any]) -> list[str]:
self.instance_user_scope,
self.instance_user_scope_id,
self.instance_user_oml_permission,
- self.instance_committee_ids,
+ self.instance_committee_meeting_ids,
) = self.get_user_scope(instance.get("id") or instance)
instance_meeting_id = instance.get("meeting_id")
@@ -595,8 +617,8 @@ def get_failing_fields(self, instance: dict[str, Any]) -> list[str]:
instance,
locked_from_inside,
),
- (self.check_group_A, actual_group_fields["A"], None, None),
- (self.check_group_F, actual_group_fields["F"], None, None),
+ (self.check_group_A, actual_group_fields["A"], instance, None),
+ (self.check_group_F, actual_group_fields["F"], instance, None),
(self.check_group_G, actual_group_fields["G"], None, None),
]:
try:
diff --git a/openslides_backend/shared/mixins/user_scope_mixin.py b/openslides_backend/shared/mixins/user_scope_mixin.py
index b84b6050fa..47c22b5c3e 100644
--- a/openslides_backend/shared/mixins/user_scope_mixin.py
+++ b/openslides_backend/shared/mixins/user_scope_mixin.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
from enum import Enum
from typing import Any
@@ -29,21 +30,26 @@ def __repr__(self) -> str:
class UserScopeMixin(BaseServiceProvider):
+ instance_committee_meeting_ids: dict
+ name: str
+
def get_user_scope(
self, id_or_instance: int | dict[str, Any]
- ) -> tuple[UserScope, int, str, list[int]]:
+ ) -> tuple[UserScope, int, str, dict[int, Any]]:
"""
Parameter id_or_instance: id for existing user or instance for user to create
Returns the scope of the given user id together with the relevant scope id (either meeting,
committee or organization), the OML level of the user as string (empty string if the user
- has none) and the ids of all committees that the user is either a manager in or a member of.
+ has none) and the ids of all committees that the user is either a manager in or a member of
+ together with their respective meetings the user being part of. A committee can have no meetings if the
+ user just has committee management rights and is not part of any of its meetings.
"""
- meetings: set[int] = set()
+ meeting_ids: set[int] = set()
committees_manager: set[int] = set()
if isinstance(id_or_instance, dict):
if "group_ids" in id_or_instance:
if "meeting_id" in id_or_instance:
- meetings.add(id_or_instance["meeting_id"])
+ meeting_ids.add(id_or_instance["meeting_id"])
committees_manager.update(
set(id_or_instance.get("committee_management_ids", []))
)
@@ -56,15 +62,16 @@ def get_user_scope(
"organization_management_level",
"committee_management_ids",
],
+ lock_result=False,
)
- meetings.update(user.get("meeting_ids", []))
+ meeting_ids.update(user.get("meeting_ids", []))
committees_manager.update(set(user.get("committee_management_ids") or []))
oml_right = user.get("organization_management_level", "")
result = self.datastore.get_many(
[
GetManyRequest(
"meeting",
- list(meetings),
+ list(meeting_ids),
["committee_id", "is_active_in_organization_id"],
)
]
@@ -76,25 +83,32 @@ def get_user_scope(
if meeting_data.get("is_active_in_organization_id")
}
committees = committees_manager | set(meetings_committee.values())
+ committee_meetings: dict[int, Any] = defaultdict(list)
+ for meeting, committee in meetings_committee.items():
+ committee_meetings[committee].append(meeting)
+ for committee in committees:
+ if committee not in committee_meetings.keys():
+ committee_meetings[committee] = None
+
if len(meetings_committee) == 1 and len(committees) == 1:
return (
UserScope.Meeting,
next(iter(meetings_committee)),
oml_right,
- list(committees),
+ committee_meetings,
)
elif len(committees) == 1:
return (
UserScope.Committee,
next(iter(committees)),
oml_right,
- list(committees),
+ committee_meetings,
)
- return UserScope.Organization, 1, oml_right, list(committees)
+ return UserScope.Organization, 1, oml_right, committee_meetings
def check_permissions_for_scope(
self,
- id: int,
+ instance_id: int,
always_check_user_oml: bool = True,
meeting_permission: Permission = Permissions.User.CAN_MANAGE,
) -> None:
@@ -105,7 +119,9 @@ def check_permissions_for_scope(
Reason: A user with OML-level-permission has scope "meeting" or "committee" if
he belongs to only 1 meeting or 1 committee.
"""
- scope, scope_id, user_oml, committees = self.get_user_scope(id)
+ scope, scope_id, user_oml, committees_to_meetings = self.get_user_scope(
+ instance_id
+ )
if (
always_check_user_oml
and user_oml
@@ -160,7 +176,166 @@ def check_permissions_for_scope(
self.datastore,
self.user_id,
CommitteeManagementLevel.CAN_MANAGE,
- committees,
+ [ci for ci in committees_to_meetings.keys()],
):
return
- raise MissingPermission({OrganizationManagementLevel.CAN_MANAGE_USERS: 1})
+ meeting_ids = {
+ meeting_id
+ for mids in committees_to_meetings.values()
+ for meeting_id in mids
+ }
+ if not meeting_ids or not self.check_for_admin_in_all_meetings(
+ instance_id, meeting_ids
+ ):
+ raise MissingPermission(
+ {
+ OrganizationManagementLevel.CAN_MANAGE_USERS: 1,
+ **{
+ Permissions.User.CAN_UPDATE: meeting_id
+ for meeting_id in meeting_ids
+ },
+ }
+ )
+
+ def check_for_admin_in_all_meetings(
+ self,
+ instance_id: int,
+ b_meeting_ids: set[int] | None = None,
+ ) -> bool:
+ """
+ This function checks the special permission condition for scope request, user.update/create with
+ payload fields A and F and other user altering actions like user.delete or set_default_password.
+ This function returns true if:
+ * requested user is no committee manager and
+ * requested user doesn't have any admin/user.can_update/user.can_manage rights in his meetings and
+ * requesting user has those permissions in all of those meetings
+ """
+ if not self._check_not_committee_manager(instance_id):
+ return False
+
+ if not (meetings := self._get_meetings_if_subset(b_meeting_ids)):
+ return False
+ admin_meeting_users = self._collect_admin_meeting_users(meetings)
+ return self._analyze_meeting_admins(admin_meeting_users, meetings)
+
+ def _check_not_committee_manager(self, instance_id: int) -> bool:
+ """
+ Helper function used in method check_for_admin_in_all_meetings.
+ Checks that requested user is not a committee manager.
+ """
+ if not (hasattr(self, "name") and self.name == "user.create"):
+ if self.datastore.get(
+ fqid_from_collection_and_id("user", instance_id),
+ ["committee_management_ids"],
+ lock_result=False,
+ use_changed_models=False,
+ ).get("committee_management_ids", []):
+ return False
+ return True
+
+ def _get_meetings_if_subset(self, b_meeting_ids: set[int] | None) -> dict[int, Any]:
+ """
+ Helper function used in method check_for_admin_in_all_meetings.
+ Gets the requested users meetings if these are subset of requesting user. Returns False if this is not possible.
+ """
+ if not b_meeting_ids and not (
+ b_meeting_ids := {
+ m_id
+ for m_ids in self.instance_committee_meeting_ids.values()
+ for m_id in m_ids
+ }
+ ):
+ return {}
+ if not (
+ a_meeting_ids := set(
+ self.datastore.get(
+ fqid_from_collection_and_id("user", self.user_id),
+ ["meeting_ids"],
+ lock_result=False,
+ ).get("meeting_ids", [])
+ )
+ ):
+ return {}
+ if not b_meeting_ids.issubset(a_meeting_ids):
+ return {}
+ return self.datastore.get_many(
+ [
+ GetManyRequest(
+ "meeting",
+ list(b_meeting_ids),
+ ["admin_group_id", "group_ids"],
+ )
+ ],
+ lock_result=False,
+ ).get("meeting", {})
+
+ def _collect_admin_meeting_users(self, meetings: dict[int, Any]) -> set[int]:
+ """
+ Gets the admin groups and those groups with permission User.CAN_UPDATE and USER.CAN_MANAGE of the meetings.
+ Returns a set of the groups meeting_user_ids.
+ """
+ group_ids = [
+ group_id
+ for meeting_id, meeting_dict in meetings.items()
+ for group_id in meeting_dict.get("group_ids", [])
+ ]
+ return {
+ mu_id
+ for group_id, group in self.datastore.get_many(
+ [
+ GetManyRequest(
+ "group",
+ group_ids,
+ [
+ "meeting_user_ids",
+ "permissions",
+ "admin_group_for_meeting_id",
+ ],
+ )
+ ],
+ lock_result=False,
+ )
+ .get("group", {})
+ .items()
+ if (
+ group.get("admin_group_for_meeting_id")
+ or "user.can_update" in group.get("permissions", [])
+ or "user.can_manage" in group.get("permissions", [])
+ )
+ for mu_id in group.get("meeting_user_ids", [])
+ }
+
+ def _analyze_meeting_admins(
+ self,
+ admin_meeting_user_ids: set[int],
+ all_meetings: dict[int, Any],
+ ) -> bool:
+ """
+ Helper function used in method check_for_admin_in_all_meetings.
+ Compares the users of admin meeting users of all meetings with the ids of requested user and requesting user.
+ Requesting user must be admin in all meetings. Requested user cannot be admin in any.
+ """
+ meeting_id_to_admin_user_ids: dict[int, set[int]] = {
+ meeting_id: set() for meeting_id in all_meetings
+ }
+ for meeting_user in (
+ self.datastore.get_many(
+ [
+ GetManyRequest(
+ "meeting_user",
+ list(admin_meeting_user_ids),
+ ["user_id", "meeting_id"],
+ )
+ ],
+ lock_result=False,
+ )
+ .get("meeting_user", {})
+ .values()
+ ):
+ meeting_id_to_admin_user_ids[meeting_user["meeting_id"]].add(
+ meeting_user["user_id"]
+ )
+ return all(
+ self.user_id in admin_users
+ for admin_users in meeting_id_to_admin_user_ids.values()
+ )
diff --git a/openslides_backend/shared/util.py b/openslides_backend/shared/util.py
index b77c02cddd..a36c564c51 100644
--- a/openslides_backend/shared/util.py
+++ b/openslides_backend/shared/util.py
@@ -74,8 +74,8 @@
"word-wrap",
]
-INITIAL_DATA_FILE = "global/data/initial-data.json"
-EXAMPLE_DATA_FILE = "global/data/example-data.json"
+INITIAL_DATA_FILE = "data/initial-data.json"
+EXAMPLE_DATA_FILE = "data/example-data.json"
ONE_ORGANIZATION_ID = 1
ONE_ORGANIZATION_FQID = fqid_from_collection_and_id("organization", ONE_ORGANIZATION_ID)
diff --git a/requirements/export_service_commits.sh b/requirements/export_service_commits.sh
index f647b4bc96..e2647c71af 100755
--- a/requirements/export_service_commits.sh
+++ b/requirements/export_service_commits.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-export DATASTORE_COMMIT_HASH=ff32410426631356f67013ebdd607c099cd676a1
-export AUTH_COMMIT_HASH=31bf8d3e965f59ab4203223fff1d9a640f3a3444
+export DATASTORE_COMMIT_HASH=e84907944c9a64aa5f7c05855dfad15fe952c4c6
+export AUTH_COMMIT_HASH=f1d720a100e52cdf1cf473969c3c3d0df69af22c
diff --git a/requirements/partial/requirements_development.txt b/requirements/partial/requirements_development.txt
index 479338e0b7..2feed75085 100644
--- a/requirements/partial/requirements_development.txt
+++ b/requirements/partial/requirements_development.txt
@@ -1,22 +1,22 @@
aiosmtpd==1.4.6
autoflake==2.3.1
black==24.10.0
-debugpy==1.8.8
+debugpy==1.8.12
flake8==7.1.1
isort==5.13.2
-mypy==1.13.0
+mypy==1.14.1
pip-check==2.9
-pytest==8.3.3
+pytest==8.3.4
pytest-cov==6.0.0
-pytest-profiling==1.7.0
-pyupgrade==3.19.0
+pytest-profiling==1.8.1
+pyupgrade==3.19.1
pyyaml==6.0.2
# typing
types-babel==2.11.0.15
-types-beautifulsoup4==4.12.0.20240907
-types-bleach==6.1.0.20240331
-types-PyYAML==6.0.12.20240917
+types-beautifulsoup4==4.12.0.20241020
+types-bleach==6.2.0.20241123
+types-PyYAML==6.0.12.20241230
types-requests==2.32.0.20241016
-types-simplejson==3.19.0.20240801
-types-Pygments==2.18.0.20240506
+types-simplejson==3.19.0.20241221
+types-Pygments==2.19.0.20250107
diff --git a/requirements/partial/requirements_production.txt b/requirements/partial/requirements_production.txt
index fb1fc89c85..269689cc0b 100644
--- a/requirements/partial/requirements_production.txt
+++ b/requirements/partial/requirements_production.txt
@@ -1,20 +1,20 @@
Babel==2.16.0
beautifulsoup4==4.12.3
-bleach[css]==6.1.0
-dependency_injector==4.42.0
-fastjsonschema==2.20.0
+bleach[css]==6.2.0
+dependency_injector==4.45.0
+fastjsonschema==2.21.1
gunicorn==23.0.0
-pypdf[crypto]==5.0.1
+pypdf[crypto]==5.1.0
requests==2.32.3
-roman==4.2
+roman==5.0
simplejson==3.19.3
-Werkzeug==3.0.4
+Werkzeug==3.1.3
python-magic==0.4.27
-pygments==2.18.0
+pygments==2.19.1
# opentelemetry
-opentelemetry-api==1.27.0
-opentelemetry-sdk==1.27.0
-opentelemetry-exporter-otlp==1.27.0
-opentelemetry-instrumentation-flask==0.48b0
-opentelemetry-instrumentation-requests==0.48b0
+opentelemetry-api==1.29.0
+opentelemetry-sdk==1.29.0
+opentelemetry-exporter-otlp==1.29.0
+opentelemetry-instrumentation-flask==0.50b0
+opentelemetry-instrumentation-requests==0.50b0
diff --git a/scripts/fetch-example-data.sh b/scripts/fetch-example-data.sh
index 9f223e327e..25d7271e3d 100755
--- a/scripts/fetch-example-data.sh
+++ b/scripts/fetch-example-data.sh
@@ -3,4 +3,4 @@
set -e
file=${1:-example-data.json}
-curl https://raw.githubusercontent.com/OpenSlides/openslides-backend/main/global/data/example-data.json --output $file
+curl https://raw.githubusercontent.com/OpenSlides/openslides-backend/main/data/example-data.json --output $file
diff --git a/setup.cfg b/setup.cfg
index dc886bf457..78e91862eb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,4 +1,4 @@
-# The following sections must be kept in sync with global/meta/dev/setup.cfg
+# The following sections must be kept in sync with meta/dev/setup.cfg
[autoflake]
verbose = true
diff --git a/tests/system/action/assignment/test_update.py b/tests/system/action/assignment/test_update.py
index d4b8f50b65..a338152b65 100644
--- a/tests/system/action/assignment/test_update.py
+++ b/tests/system/action/assignment/test_update.py
@@ -26,6 +26,7 @@ def test_update_correct_full_fields(self) -> None:
"name": "name_sdurqw12",
"is_active_in_organization_id": 1,
"meeting_mediafile_ids": [11],
+ "assignment_poll_add_candidates_to_list_of_speakers": True,
},
"assignment/111": {"title": "title_srtgb123", "meeting_id": 110},
"mediafile/1": {
@@ -71,6 +72,91 @@ def test_update_wrong_id(self) -> None:
model = self.get_model("assignment/111")
assert model.get("title") == "title_srtgb123"
+ def prepare_voting_phase_test(
+ self, number_of_speakers: int, phase: str = "search"
+ ) -> None:
+ self.create_meeting()
+ for i in range(number_of_speakers):
+ self.create_user(f"user{i+1}", [3])
+ ids = list(range(1, number_of_speakers + 1))
+ self.set_models(
+ {
+ "meeting/1": {
+ "assignment_ids": [1],
+ "list_of_speakers_ids": [1],
+ "assignment_candidate_ids": ids,
+ "assignment_poll_add_candidates_to_list_of_speakers": True,
+ },
+ "assignment/1": {
+ "title": "assignment_with_candidates",
+ "meeting_id": 1,
+ "list_of_speakers_id": 1,
+ "candidate_ids": ids,
+ "phase": phase,
+ },
+ "list_of_speakers/1": {
+ "content_object_id": "assignment/1",
+ "meeting_id": 1,
+ },
+ **{
+ f"assignment_candidate/{i}": {
+ "meeting_id": 1,
+ "assignment_id": 1,
+ "meeting_user_id": i,
+ }
+ for i in ids
+ },
+ **{f"meeting_user/{i}": {"assignment_candidate_ids": [i]} for i in ids},
+ }
+ )
+
+ def test_update_phase_to_voting(self) -> None:
+ self.prepare_voting_phase_test(3)
+ self.request("speaker.create", {"meeting_user_id": 1, "list_of_speakers_id": 1})
+ response = self.request(
+ "assignment.update",
+ {"id": 1, "phase": "voting"},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("list_of_speakers/1", {"speaker_ids": [1, 2, 3]})
+ for id_ in [1, 2, 3]:
+ self.assert_model_exists(
+ f"speaker/{id_}",
+ {"list_of_speakers_id": 1, "meeting_user_id": id_, "meeting_id": 1},
+ )
+
+ def test_update_phase_to_voting_empty_los(self) -> None:
+ self.prepare_voting_phase_test(3)
+ response = self.request(
+ "assignment.update",
+ {"id": 1, "phase": "voting"},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("list_of_speakers/1", {"speaker_ids": [1, 2, 3]})
+ for id_ in [1, 2, 3]:
+ self.assert_model_exists(
+ f"speaker/{id_}",
+ {"list_of_speakers_id": 1, "meeting_user_id": id_, "meeting_id": 1},
+ )
+
+ def test_update_phase_to_voting_no_candidates(self) -> None:
+ self.prepare_voting_phase_test(0)
+ response = self.request(
+ "assignment.update",
+ {"id": 1, "phase": "voting"},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_not_exists("speaker/1")
+
+ def test_update_phase_from_voting_to_voting(self) -> None:
+ self.prepare_voting_phase_test(3, "voting")
+ response = self.request(
+ "assignment.update",
+ {"id": 1, "phase": "voting"},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_not_exists("speaker/1")
+
def test_update_no_permission(self) -> None:
self.base_permission_test(
{
diff --git a/tests/system/action/base.py b/tests/system/action/base.py
index 90a638d7df..a964b4f7b5 100644
--- a/tests/system/action/base.py
+++ b/tests/system/action/base.py
@@ -177,6 +177,9 @@ def create_meeting(self, base: int = 1) -> None:
"motions_default_workflow_id": base,
"committee_id": committee_id,
"is_active_in_organization_id": 1,
+ "language": "en",
+ "motion_state_ids": [base],
+ "motion_workflow_ids": [base],
},
f"group/{base}": {
"meeting_id": base,
diff --git a/tests/system/action/mediafile/test_upload.py b/tests/system/action/mediafile/test_upload.py
index df3dffeee9..3f50a901bf 100644
--- a/tests/system/action/mediafile/test_upload.py
+++ b/tests/system/action/mediafile/test_upload.py
@@ -6,7 +6,11 @@
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.permissions.permissions import Permissions
-from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, get_initial_data_file
+from openslides_backend.shared.util import (
+ INITIAL_DATA_FILE,
+ ONE_ORGANIZATION_FQID,
+ get_initial_data_file,
+)
from tests.system.action.base import BaseActionTestCase
@@ -472,9 +476,7 @@ def test_upload_json_detect_html(self) -> None:
"meeting/110", {"name": "name_DsJFXoot", "is_active_in_organization_id": 1}
)
filename = "test.json"
- data = json.dumps(
- get_initial_data_file("global/data/initial-data.json")
- ).encode()
+ data = json.dumps(get_initial_data_file(INITIAL_DATA_FILE)).encode()
json_content = base64.b64encode(data).decode()
response = self.request(
"mediafile.upload",
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index 2ce6016894..9efede8ff0 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -3,6 +3,7 @@
from unittest.mock import MagicMock
from openslides_backend.action.action_worker import ActionWorkerState
+from openslides_backend.models.mixins import MeetingModelMixin
from openslides_backend.models.models import AgendaItem, Meeting
from openslides_backend.shared.util import (
ONE_ORGANIZATION_FQID,
@@ -2139,3 +2140,145 @@ def test_clone_non_template_and_committee_change_not_allowed(self) -> None:
response.json["message"]
== "Cannot clone meeting to a different committee if it is a non-template meeting."
)
+
+ def test_clone_with_list_election(self) -> None:
+ self.create_meeting()
+ self.set_user_groups(1, [2])
+ self.create_user("Huey", [3])
+ self.create_user("Dewey", [3])
+ self.create_user("Louie", [3])
+ self.set_models(
+ {
+ "user/2": {
+ "organization_id": 1,
+ "poll_candidate_ids": [1],
+ },
+ "user/3": {
+ "organization_id": 1,
+ "poll_candidate_ids": [2],
+ },
+ "user/4": {
+ "organization_id": 1,
+ "poll_candidate_ids": [3],
+ },
+ "organization/1": {
+ "user_ids": [1, 2, 3, 4],
+ },
+ "motion_workflow/1": {
+ "name": "Workflow",
+ "sequential_number": 1,
+ "default_amendment_workflow_meeting_id": 1,
+ },
+ "motion_state/1": {
+ "name": "State",
+ "weight": 1,
+ },
+ "projector/1": {
+ "name": "default",
+ "meeting_id": 1,
+ "used_as_reference_projector_meeting_id": 1,
+ "sequential_number": 1,
+ **{
+ key: 1 for key in MeetingModelMixin.reverse_default_projectors()
+ },
+ },
+ "list_of_speakers/1": {
+ "id": 1,
+ "closed": False,
+ "meeting_id": 1,
+ "content_object_id": "assignment/1",
+ "sequential_number": 1,
+ },
+ "meeting/1": {
+ "id": 1,
+ "name": "Duckburg town government",
+ "poll_ids": [1],
+ "option_ids": [1, 2],
+ "projector_ids": [1],
+ "assignment_ids": [1],
+ "poll_candidate_ids": [1, 2, 3],
+ "list_of_speakers_ids": [1],
+ "reference_projector_id": 1,
+ "poll_candidate_list_ids": [1],
+ **{key: [1] for key in MeetingModelMixin.all_default_projectors()},
+ "motions_default_amendment_workflow_id": 1,
+ },
+ "assignment/1": {
+ "id": 1,
+ "phase": "search",
+ "title": "Duckburg town council",
+ "poll_ids": [1],
+ "meeting_id": 1,
+ "open_posts": 0,
+ "sequential_number": 1,
+ "list_of_speakers_id": 1,
+ },
+ "poll_candidate/1": {
+ "id": 1,
+ "weight": 1,
+ "user_id": 2,
+ "meeting_id": 1,
+ "poll_candidate_list_id": 1,
+ },
+ "poll_candidate/2": {
+ "id": 2,
+ "weight": 2,
+ "user_id": 3,
+ "meeting_id": 1,
+ "poll_candidate_list_id": 1,
+ },
+ "poll_candidate/3": {
+ "id": 3,
+ "weight": 3,
+ "user_id": 4,
+ "meeting_id": 1,
+ "poll_candidate_list_id": 1,
+ },
+ "poll_candidate_list/1": {
+ "id": 1,
+ "option_id": 1,
+ "meeting_id": 1,
+ "poll_candidate_ids": [1, 2, 3],
+ },
+ "option/1": {
+ "id": 1,
+ "weight": 1,
+ "poll_id": 1,
+ "meeting_id": 1,
+ "content_object_id": "poll_candidate_list/1",
+ },
+ "option/2": {
+ "id": 2,
+ "text": "global option",
+ "weight": 1,
+ "meeting_id": 1,
+ "used_as_global_option_in_poll_id": 1,
+ },
+ "poll/1": {
+ "id": 1,
+ "type": "pseudoanonymous",
+ "state": "created",
+ "title": "First election",
+ "backend": "fast",
+ "global_no": False,
+ "votescast": "0.000000",
+ "global_yes": False,
+ "meeting_id": 1,
+ "option_ids": [1],
+ "pollmethod": "YNA",
+ "votesvalid": "0.000000",
+ "votesinvalid": "0.000000",
+ "global_abstain": False,
+ "global_option_id": 2,
+ "max_votes_amount": 1,
+ "min_votes_amount": 1,
+ "content_object_id": "assignment/1",
+ "sequential_number": 1,
+ "is_pseudoanonymized": True,
+ "max_votes_per_option": 1,
+ "onehundred_percent_base": "disabled",
+ },
+ }
+ )
+ response = self.request("meeting.clone", {"meeting_id": 1})
+ self.assert_status_code(response, 200)
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index a2cb61383c..40a0f1b8d9 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -2240,7 +2240,7 @@ def test_all_migrations(self) -> None:
@performance
def test_big_file(self) -> None:
data = {
- "meeting": get_initial_data_file("global/data/put_your_file.json"),
+ "meeting": get_initial_data_file("data/put_your_file.json"),
"committee_id": 1,
}
with Profiler("test_meeting_import.prof"):
@@ -2613,7 +2613,7 @@ def test_delete_statutes(self) -> None:
@pytest.mark.skip()
def test_import_os3_data(self) -> None:
- data_raw = get_initial_data_file("global/data/export-OS3-demo.json")
+ data_raw = get_initial_data_file("data/export-OS3-demo.json")
data = {"committee_id": 1, "meeting": data_raw}
response = self.request("meeting.import", data)
self.assert_status_code(response, 200)
diff --git a/tests/system/action/meeting/test_settings.py b/tests/system/action/meeting/test_settings.py
index 4b5c22b156..1174712428 100644
--- a/tests/system/action/meeting/test_settings.py
+++ b/tests/system/action/meeting/test_settings.py
@@ -8,6 +8,7 @@ def test_group_ids(self) -> None:
"meeting/1": {
"motion_poll_default_group_ids": [1],
"is_active_in_organization_id": 1,
+ "language": "en",
},
"group/1": {"used_as_motion_poll_default_id": 1},
"group/2": {"name": "2", "used_as_motion_poll_default_id": None},
@@ -29,7 +30,8 @@ def test_group_ids(self) -> None:
def test_html_field_iframe(self) -> None:
self.create_model(
- "meeting/1", {"welcome_text": "Hi", "is_active_in_organization_id": 1}
+ "meeting/1",
+ {"welcome_text": "Hi", "is_active_in_organization_id": 1, "language": "en"},
)
response = self.request(
"meeting.update", {"id": 1, "welcome_text": '