diff --git a/data/example-data.json b/data/example-data.json index 6379615bf5..4feeefbb50 100644 --- a/data/example-data.json +++ b/data/example-data.json @@ -97,10 +97,6 @@ 1 ], "committee_management_ids": [1], - "poll_voted_ids": [5], - "option_ids": [5, 7], - "vote_ids": [9], - "delegated_vote_ids": [9], "meeting_user_ids": [1], "meeting_ids": [ 1 @@ -121,7 +117,6 @@ "committee_ids": [ 1 ], - "option_ids": [9, 12], "meeting_user_ids": [2], "meeting_ids": [ 1 @@ -139,12 +134,11 @@ "can_change_own_password": true, "gender_id": 3, "default_vote_weight": "1.000000", - "option_ids": [8, 11], "meeting_user_ids": [3], "meeting_ids": [ 1 ], - "organization_id": 1, + "organization_id": 1, "committee_ids": [ 1 ] @@ -164,7 +158,11 @@ "motion_submitter_ids": [1, 2, 3, 4], "assignment_candidate_ids": [1], "structure_level_ids": [1], - "group_ids": [2] + "group_ids": [2], + "poll_voted_ids": [3], + "poll_option_ids": [1, 8], + "acting_ballot_ids": [9], + "represented_ballot_ids": [9] }, "2": { "id": 2, @@ -176,6 +174,7 @@ "vote_weight": "1.000000", "speaker_ids": [2, 3, 7, 10, 11, 13], "assignment_candidate_ids": [3, 5], + "poll_option_ids": [9], "structure_level_ids": [2], "group_ids": [5] }, @@ -190,6 +189,7 @@ "speaker_ids": [4, 8, 9], "supported_motion_ids": [3], "assignment_candidate_ids": [2, 4], + "poll_option_ids": [2, 10], "structure_level_ids": [3], "group_ids": [5] } @@ -391,7 +391,7 @@ 3 ], "poll_default_live_voting_enabled": false, - "poll_default_backend": "fast", + "poll_default_allow_invalid": false, "poll_couple_countdown": true, "meeting_user_ids": [1, 2, 3], "projector_ids": [ @@ -543,32 +543,6 @@ 4, 5 ], - "option_ids": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13 - ], - "vote_ids": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], "assignment_ids": [ 1, 2 @@ -1104,7 +1078,7 @@ "speaker": { "11": { "id": 11, - "weight": 11, + "weight": 11, "begin_time": 1584512636, "end_time": 1584512638, "list_of_speakers_id": 1, @@ -1203,7 +1177,11 @@ "sequential_number": 1, "agenda_item_id": 3, "list_of_speakers_id": 3, - "meeting_id": 1 + "meeting_id": 1, + "poll_ids": [ + 4 + ] + }, "2": { "id": 2, @@ -1286,10 +1264,6 @@ 1 ], "poll_ids": [ - 1, - 2 - ], - "option_ids": [ 1, 3 ], @@ -1646,7 +1620,7 @@ "allow_support": false, "set_workflow_timestamp": true, "allow_motion_forwarding": true, - "allow_amendment_forwarding": true, + "allow_amendment_forwarding": true, "meeting_id": 1 }, "6": { @@ -1937,359 +1911,213 @@ "1": { "id": 1, "title": "1", - "type": "analog", - "backend": "fast", - "pollmethod": "YNA", + "config_id": "poll_config_approval/1", + "visibility": "manually", "state": "finished", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "onehundred_percent_base": "YNA", - "votesvalid": "2.000000", - "votesinvalid": "9.000000", - "votescast": "2.000000", - "sequential_number": 1, + "published": true, "content_object_id": "motion/1", - "option_ids": [ - 1 + "ballot_ids": [ + 1, + 2, + 3 ], - "global_option_id": 2, - "global_abstain": false, - "global_no": false, - "global_yes": false, - "meeting_id": 1, - "live_voting_enabled": false + "meeting_id": 1 }, "2": { "id": 2, "title": "2", - "type": "analog", - "backend": "fast", - "pollmethod": "YNA", - "state": "created", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "onehundred_percent_base": "YNA", - "sequential_number": 2, - "content_object_id": "motion/1", - "option_ids": [ - 3 - ], - "global_option_id": 4, - "global_abstain": false, - "global_no": false, - "global_yes": false, - "meeting_id": 1, - "live_voting_enabled": false + "config_id": "poll_config_approval/2", + "visibility": "manually", + "state": "finished", + "published": true, + "content_object_id": "assignment/1", + "ballot_ids": [], + "meeting_id": 1 }, "3": { "id": 3, - "title": "1", - "type": "analog", - "backend": "fast", - "pollmethod": "YNA", - "state": "created", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "global_yes": false, - "global_no": true, - "global_abstain": true, - "onehundred_percent_base": "YNA", - "sequential_number": 3, - "content_object_id": "assignment/1", - "option_ids": [ - 5 - ], - "global_option_id": 6, - "meeting_id": 1, - "live_voting_enabled": false + "title": "3", + "config_id": "poll_config_selection/1", + "visibility": "open", + "state": "started", + "published": false, + "content_object_id": "motion/1", + "ballot_ids": [9], + "meeting_id": 1 }, "4": { "id": 4, - "title": "2", - "type": "analog", - "backend": "fast", - "pollmethod": "Y", - "state": "finished", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "global_yes": false, - "global_no": true, - "global_abstain": true, - "onehundred_percent_base": "Y", - "votesvalid": "9.000000", - "votesinvalid": "2.000000", - "votescast": "16.000000", - "sequential_number": 4, - "content_object_id": "assignment/1", - "option_ids": [ - 7, - 8, - 9 - ], - "global_option_id": 10, - "meeting_id": 1, - "live_voting_enabled": false + "title": "4", + "config_id": "poll_config_rating_score/1", + "visibility": "secret", + "state": "created", + "content_object_id": "topic/1", + "ballot_ids": [], + "meeting_id": 1 }, "5": { "id": 5, - "title": "Wahlgang", - "type": "named", - "backend": "fast", - "pollmethod": "Y", + "title": "5", + "config_id": "poll_config_rating_approval/1", + "visibility": "manually", "state": "finished", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "global_yes": false, - "global_no": true, - "global_abstain": false, - "onehundred_percent_base": "valid", - "votesvalid": "1.000000", - "votesinvalid": "0.000000", - "votescast": "1.000000", - "sequential_number": 5, - "content_object_id": "assignment/2", - "voted_ids": [ - 1 - ], - "entitled_group_ids": [ - 2 - ], - "option_ids": [ - 11, - 12 + "content_object_id": "assignment/1", + "ballot_ids": [ + 4, + 5, + 6, + 7, + 8 ], - "global_option_id": 13, - "meeting_id": 1, - "live_voting_enabled": false + "meeting_id": 1 } }, - "option": { + "poll_config_approval": { + "1": { + "poll_id": 1, + "allow_abstain": false + }, + "2": { + "poll_id": 2, + "option_ids": [1, 2] + } + }, + "poll_config_selection": { + "1": { + "poll_id": 3, + "option_ids": [3, 4, 5], + "max_options_amount": 2, + "min_options_amount": 1, + "allow_nota": true + } + }, + "poll_config_rating_score": { + "1": { + "poll_id": 4, + "option_ids": [6, 7], + "max_options_amount": 2, + "min_options_amount": 2, + "max_vote_sum": 5, + "min_vote_sum": 1 + } + }, + "poll_config_rating_approval": { + "1": { + "poll_id": 5, + "allow_abstain": true, + "option_ids": [8, 9, 10] + } + }, + "poll_config_option": { "1": { "id": 1, - "yes": "2.000000", - "no": "4.000000", - "abstain": "1.000000", "weight": 1, - "poll_id": 1, - "content_object_id": "motion/1", - "vote_ids": [ - 1, - 2, - 3 - ], - "meeting_id": 1 + "poll_config_id": "poll_config_approval/2", + "meeting_user_id": 1 }, "2": { "id": 2, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "used_as_global_option_in_poll_id": 1, - "meeting_id": 1 + "weight": 2, + "poll_config_id": "poll_config_approval/2", + "meeting_user_id": 3 }, "3": { "id": 3, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "poll_id": 2, - "content_object_id": "motion/1", - "meeting_id": 1 + "poll_config_id": "poll_config_selection/1", + "text": "Option 3" }, "4": { "id": 4, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "used_as_global_option_in_poll_id": 2, - "meeting_id": 1 + "poll_config_id": "poll_config_selection/1", + "text": "Option 4" }, "5": { "id": 5, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "poll_id": 3, - "content_object_id": "user/1", - "meeting_id": 1 + "poll_config_id": "poll_config_selection/1", + "text": "Option 5" }, "6": { "id": 6, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "used_as_global_option_in_poll_id": 3, - "meeting_id": 1 + "poll_config_id": "poll_config_rating_score/1", + "text": "option 6" }, "7": { "id": 7, - "yes": "3.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "poll_id": 4, - "content_object_id": "user/1", - "vote_ids": [ - 4 - ], - "meeting_id": 1 + "poll_config_id": "poll_config_rating_score/1", + "text": "option 7" }, "8": { "id": 8, - "yes": "7.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 2, - "poll_id": 4, - "content_object_id": "user/3", - "vote_ids": [ - 5 - ], - "meeting_id": 1 + "poll_config_id": "poll_config_rating_approval/1", + "meeting_user_id": 1 }, "9": { "id": 9, - "yes": "2.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 3, - "poll_id": 4, - "content_object_id": "user/2", - "vote_ids": [ - 6 - ], - "meeting_id": 1 + "poll_config_id": "poll_config_rating_approval/1", + "meeting_user_id": 2 }, "10": { "id": 10, - "yes": "0.000000", - "no": "2.000000", - "abstain": "1.000000", - "weight": 1, - "used_as_global_option_in_poll_id": 4, - "vote_ids": [ - 7, - 8 - ], - "meeting_id": 1 - }, - "11": { - "id": 11, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "poll_id": 5, - "content_object_id": "user/3", - "meeting_id": 1 - }, - "12": { - "id": 12, - "yes": "1.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 2, - "poll_id": 5, - "content_object_id": "user/2", - "vote_ids": [ - 9 - ], - "meeting_id": 1 - }, - "13": { - "id": 13, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - "used_as_global_option_in_poll_id": 5, - "meeting_id": 1 + "poll_config_id": "poll_config_rating_approval/1", + "meeting_user_id": 3 } }, - "vote": { + "ballot": { "1": { "id": 1, "weight": "2.000000", - "value": "Y", - "user_token": "SNuxJc7W93bnhAiA", - "option_id": 1, - "meeting_id": 1 + "value": "yes", + "poll_id": 1 }, "2": { "id": 2, "weight": "4.000000", - "value": "N", - "user_token": "4bgn4RBjNlIeO7vj", - "option_id": 1, - "meeting_id": 1 + "value": "no", + "poll_id": 1 }, "3": { "id": 3, "weight": "1.000000", - "value": "A", - "user_token": "xLBFgo3O1pAfGZ0h", - "option_id": 1, - "meeting_id": 1 + "value": "abstain", + "poll_id": 1 }, "4": { "id": 4, - "value": "Y", + "value": {"1":"yes","2":"abstain","3":"yes"}, "weight": "3.000000", - "user_token": "neT9r5YkT9U8yJfa", - "option_id": 7, - "meeting_id": 1 + "poll_id": 5 }, "5": { "id": 5, - "value": "Y", + "value": {"1":"no","2":"yes","3":"yes"}, "weight": "7.000000", - "user_token": "U5YSuLUI1G5rNOHn", - "option_id": 8, - "meeting_id": 1 + "poll_id": 5 }, "6": { "id": 6, - "value": "Y", + "value": {"1":"yes","2":"no","3":"no"}, "weight": "2.000000", - "user_token": "jkNKIiJr8Dl0yOXI", - "option_id": 9, - "meeting_id": 1 + "poll_id": 5 }, "7": { "id": 7, - "value": "N", + "value": {"1":"no","2":"no","3":"yes"}, "weight": "2.000000", - "user_token": "Z1cxOviuelzPT2rm", - "option_id": 10, - "meeting_id": 1 + "poll_id": 5 }, "8": { "id": 8, - "value": "A", + "value": {"1":"abstain","2":"yes","3":"abstain"}, "weight": "1.000000", - "user_token": "daUZh16fXCAu5DBL", - "option_id": 10, - "meeting_id": 1 + "poll_id": 5 }, "9": { "id": 9, - "value": "Y", + "value": "1", "weight": "1.000000", - "user_token": "ivgipZ18D9Xac8pd", - "user_id": 1, - "delegated_user_id": 1, - "option_id": 12, - "meeting_id": 1 + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + "poll_id": 3 } }, "assignment": { @@ -2305,8 +2133,8 @@ 3 ], "poll_ids": [ - 3, - 4 + 2, + 5 ], "agenda_item_id": 11, "list_of_speakers_id": 11, diff --git a/docs/Actions-Overview.md b/docs/Actions-Overview.md index 8a30c617aa..250f27adbd 100644 --- a/docs/Actions-Overview.md +++ b/docs/Actions-Overview.md @@ -242,21 +242,3 @@ A more general format description see in [Action-Service](https://github.com/Ope - [account.import](actions/account.import.md) - [participant.json_upload](actions/participant.json_upload.md) - [participant.import](actions/participant.import.md) - -## Voting - -- [option.update](actions/option.update.md) -- [poll.create](actions/poll.create.md) -- [poll.delete](actions/poll.delete.md) -- [poll.update](actions/poll.update.md) -- [poll.start](actions/poll.start.md) -- [poll.stop](actions/poll.stop.md) -- [poll.publish](actions/poll.publish.md) -- [poll.reset](actions/poll.reset.md) -- [poll.anonymize](actions/poll.anonymize.md) -- [poll.vote](actions/poll.vote.md) -- [poll_candidate_list.create](actions/poll_candidate_list.create.md) -- [poll_candidate_list.delete](actions/poll_candidate_list.delete.md) -- [poll_candidate.create](actions/poll_candidate.create.md) -- [poll_candidate.delete](actions/poll_candidate.delete.md) - diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md index fd49bc7cc6..71f8376b77 100644 --- a/docs/actions/meeting.update.md +++ b/docs/actions/meeting.update.md @@ -159,8 +159,8 @@ topic_poll_default_group_ids: Id[]; - poll_default_backend: string; poll_default_live_voting_enabled: boolean; + poll_default_allow_invalid: boolean; // Group B present_user_ids: user/is_present_in_meeting_ids; diff --git a/docs/actions/meeting_user.update.md b/docs/actions/meeting_user.update.md index ea953a352a..ea4cdf7b2a 100644 --- a/docs/actions/meeting_user.update.md +++ b/docs/actions/meeting_user.update.md @@ -5,6 +5,11 @@ id: Id; // Optional + poll_option_ids: Id[]; + poll_voted_ids: Id[]; + acting_ballot_ids: Id[]; + represented_ballot_ids: Id[]; + // Group A number: string; structure_level_ids: Id[]; diff --git a/docs/actions/option.update.md b/docs/actions/option.update.md deleted file mode 100644 index d34d66b473..0000000000 --- a/docs/actions/option.update.md +++ /dev/null @@ -1,23 +0,0 @@ -## Payload -```js -{ -// Required - id: Id; -// Optional. - Y: number; - N: number; - A: number; - publish_immediately: boolean; -} -``` - -## Action -This action is only allowed for analog polls. Updating this option changes the associated `vote` objects for `Y`, `N` and `A` to the given values. - -If the poll's state is *created* and at least one vote value is given (`Y`, `N` or `A`), the state must be set to *finished*. if additionally `publish_immediately` is given, the state must be set to *published*. - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.anonymize.md b/docs/actions/poll.anonymize.md deleted file mode 100644 index 019faa4d54..0000000000 --- a/docs/actions/poll.anonymize.md +++ /dev/null @@ -1,15 +0,0 @@ -## Payload -```js -{id: Id;} -``` - -## Action -Only for non-analog polls in the state *finished* or *published*. Sets all `vote/user_id` and `vote/delegated_user_id` references to `None` for each vote of each option of the poll (including the global option). - -`is_pseudoanonymized` has to be set to `true`. - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.create.md b/docs/actions/poll.create.md deleted file mode 100644 index 3af4954a49..0000000000 --- a/docs/actions/poll.create.md +++ /dev/null @@ -1,77 +0,0 @@ -## 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 - - // Optionally and only for type==analog, votes can be given - 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, - - 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, - max_votes_per_option: number, - global_yes: boolean, - global_no: boolean, - global_abstain: boolean, - onehundred_percent_base: string, - backend: string, - -// Optional, only for type==named - live_voting_enabled: boolean, - -// Only for non analog types - entitled_group_ids: Id[], - -// Only for type==analog - publish_immediately: boolean, - -// Optionally and only for type==analog, 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) -} -``` - -## Action -If an analog poll with votes is given, the state is set to `finished` if at least one vote value is given. if `publish_immediately` is true and some vote value is given, the state is set to `published`. All options given are created as instances of the `option` model. If some options have values (for analog polls), `vote` objects have to be created, one for each option and vote value (`Y`, `N`, `A`). - -The options must be unique in the way that each non-empty `text` and non-empty `content_object_id` can only exist once. The `option/weight` is set in the order the options are given in the payload. A global option is created. - -If the `type` is `pseudoanonymous`, `is_pseudoanonymized` is set to `true`. - -If the `content_object_id` points to a `motion` and the `motion_state` of the motion misses `allow_create_poll`, it is forbidden to create a poll. - -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` if the poll's content object is a topic diff --git a/docs/actions/poll.delete.md b/docs/actions/poll.delete.md deleted file mode 100644 index 7e06aea316..0000000000 --- a/docs/actions/poll.delete.md +++ /dev/null @@ -1,13 +0,0 @@ -## Payload -```js -{ id: Id; } -``` - -## Action -Deletes the given poll and all linked options with all votes, too. - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.publish.md b/docs/actions/poll.publish.md deleted file mode 100644 index 67899ccf89..0000000000 --- a/docs/actions/poll.publish.md +++ /dev/null @@ -1,13 +0,0 @@ -## Payload -```js -{id: Id;} -``` - -## Action -Sets the state to *published*. Only allowed for polls in the *finished* state. - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.reset.md b/docs/actions/poll.reset.md deleted file mode 100644 index d628476aec..0000000000 --- a/docs/actions/poll.reset.md +++ /dev/null @@ -1,15 +0,0 @@ -## Payload -```js -{id: Id;} -``` - -## Action -Sets the state to *created*. Only allowed for polls in the *finished* or *published* state. All vote objects of all options (including the global option) are deleted. - -If `type != "pseudoanonymous"`, `is_pseudoanonymized` may be reset to `false` (if it was previously `true`). - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.start.md b/docs/actions/poll.start.md deleted file mode 100644 index c28c1063a5..0000000000 --- a/docs/actions/poll.start.md +++ /dev/null @@ -1,15 +0,0 @@ -## Payload -```js -{id: Id;} -``` - -## Action -Sets the state to *started*. Only allowed for polls in the *created* state. - -If `meeting/poll_couple_countdown` is true and the poll is an electronic poll, the countdown given by `meeting/poll_countdown_id` must be *restarted* (see [Countdowns](https://github.com/OpenSlides/OpenSlides/wiki/Countdowns#restart-a-countdown)). - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.stop.md b/docs/actions/poll.stop.md deleted file mode 100644 index 42d9d1f86f..0000000000 --- a/docs/actions/poll.stop.md +++ /dev/null @@ -1,19 +0,0 @@ -## Payload -```js -{id: Id;} -``` - -## Action -Sets the state to *finished*. Only allowed for polls in the *started* state. - -If `meeting/poll_couple_countdown` is true, the countdown given by `meeting/poll_countdown_id` is *reset* (see [Countdowns](https://github.com/OpenSlides/OpenSlides/wiki/Countdowns#reset-a-countdown)). - -Some fields are calculated upon stopping a poll: -- The fields `votescast`, `votesvalid` and `votesinvalid` are filled (see [poll results](https://github.com/OpenSlides/OpenSlides/wiki/Voting#poll-results)). They are only filled once when the poll stops to prevent any changes e.g. from deleting users. -- `entitled_users_at_stop` is filled. It is an array of objects which represents all users entitled to vote at the stopping point of the poll. The syntax is `{"user_id": Id, "voted": boolean, "vote_delegated_to_id": Id | null}`. The fields should be self-explanatory. This field is also a snapshot like the ones above. - -## 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` if the poll's content object is a topic diff --git a/docs/actions/poll.update.md b/docs/actions/poll.update.md deleted file mode 100644 index ffed841fd2..0000000000 --- a/docs/actions/poll.update.md +++ /dev/null @@ -1,52 +0,0 @@ -## Payload -```js -{ -// Required - id: Id, - -// Optional, only if state == created - pollmethod: string, - min_votes_amount: number, - max_votes_amount: number, - max_votes_per_option: number, - 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[], - -// Optional, every state - title: string, - description: string, - onehundred_percent_base: string, - -// Optional, type==analog, every state - votesvalid: number, - votesinvalid: number, - votescast: number, - publish_immediately: boolean, - -// Optional, type==named - live_voting_enabled: boolean - -// action called internally - entitled_users_at_stop: json -} -``` - -## Action -For analog polls: If the state is created and at least one vote value is given (`votesvalid`/`votesinvalid`/`votescast`), the state is set to finished. If additionally `publish_immediately` is given, the state is set to published. - -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 has 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` if the poll's content object is a topic diff --git a/docs/actions/poll_candidate.create.md b/docs/actions/poll_candidate.create.md deleted file mode 100644 index fdf4c0b674..0000000000 --- a/docs/actions/poll_candidate.create.md +++ /dev/null @@ -1,14 +0,0 @@ -## Payload - -```js -{ - // Required - user_id: Id; - poll_candidate_list_id: Id; - weight: number; - meeting_id: Id; -} -``` - -## Internal action -Creates a poll candidate. diff --git a/docs/actions/poll_candidate.delete.md b/docs/actions/poll_candidate.delete.md deleted file mode 100644 index 1862c0eee8..0000000000 --- a/docs/actions/poll_candidate.delete.md +++ /dev/null @@ -1,13 +0,0 @@ -## Payload - -Payload: -```js -{ - // Required - id: Id; -} -``` - -## Internal action -Deletes a poll candidate with the given id. - \ No newline at end of file diff --git a/docs/actions/poll_candidate_list.create.md b/docs/actions/poll_candidate_list.create.md deleted file mode 100644 index b11a0e3597..0000000000 --- a/docs/actions/poll_candidate_list.create.md +++ /dev/null @@ -1,17 +0,0 @@ -## Payload - -Payload: -```js -{ - // Required - option_id: Id, - meeting_id: Id, - entries: [{ - user_id: Id, - weight: number - }] -} -``` - -## Internal action -Creates poll candidates for the entries and creates a `poll_candidate_list`. diff --git a/docs/actions/poll_candidate_list.delete.md b/docs/actions/poll_candidate_list.delete.md deleted file mode 100644 index ff65d8eda2..0000000000 --- a/docs/actions/poll_candidate_list.delete.md +++ /dev/null @@ -1,12 +0,0 @@ -## Payload - -Payload: -```js -{ - // Required - id: Id; -} -``` - -## Internal action -Deletes the `poll_candidate_list` with the given id and all poll candidates which belong to this list. \ No newline at end of file diff --git a/docs/actions/user.merge_together.md b/docs/actions/user.merge_together.md index 2dacb90eea..0f052932e0 100644 --- a/docs/actions/user.merge_together.md +++ b/docs/actions/user.merge_together.md @@ -42,11 +42,10 @@ An error is thrown if: - Any of the secondary users have a `saml_id` - There are multiple different `member_number`s between the selected users (empty does not count) - There are conflicts regarding polls, i.e. two or more of the selected users... - - are option content_objects (not counting poll_candidate_list membership) on the same poll - - are candidates on the same poll_candidate_list - - have voted on the same poll (delegated or not) - Any affected meeting_users have groups that are currently entitled to work on any poll - Any affected meeting_users _who share a meeting_: + - are meeting_users of the poll_config_option on the same poll + - have voted on the same poll (delegated or not) - have running speakers - are in a meeting without `list_of_speakers_allow_multiple_speakers` and have waiting speakers on the same list who cannot be merged together into at most one point_of_order and one normal speech. Speeches may not be merged if there are multiple different values (empty does count) within any of the fields: - `speech_state` @@ -70,8 +69,6 @@ Any date in the custom payload data from the request overwrites anything that wo Data validity of the results is checked according to user.update rules. The secondary users are deleted. - -Any poll that contains the id of any secondary user in its `entitled_users_at_stop` list will have it re-written to _additionally_ contain the new user id. This means that a line `{"voted": false, "present": true, "user_id": 4, "vote_delegated_to_user_id": 7}` becomes @@ -80,7 +77,7 @@ after two merges where for the first `user/4` was merged into `user/2` and for t This is to ensure that the client can recognize where users were merged, as simply replacing the ids may cause situations where a user is present on a list twice and not replacing them would mean that the user that voted would not be recognizable anymore. #### Merging of sub-collections -Relation lists where simple unification does not suffice (usually because the target collections function mostly as a type of m:n connection between two other collections) are merged. +Relation lists where simple unification does not suffice (usually because the target collections function mostly as a type of m:n connection between two other collections) are merged. For that purpose, target models for these relations are compared and those that are judged to fulfill equivalent roles are grouped together. @@ -141,7 +138,7 @@ He also needs a organization management level that is equal or higher than that The client could/should fill the optional fields from a chosen "main" user to not force the editor to rewrite all the data. -Warnings should be shown alerting the user that +Warnings should be shown alerting the user that - this action is not reversable, - will potentially change/overwrite data in archived meetings and - will neither port the history information of the secondary users to the new ones nor rewrite the user id in the "Changed by" column \ No newline at end of file diff --git a/docs/actions/user.update.md b/docs/actions/user.update.md index 057e4353d5..b15080cf8a 100644 --- a/docs/actions/user.update.md +++ b/docs/actions/user.update.md @@ -58,11 +58,6 @@ // only internal is_present_in_meeting_ids: Id[]; - option_ids: Id[]; - poll_candidate_ids: Id[]; - poll_voted_ids: Id[]; - vote_ids: Id[]; - delegated_vote_ids: Id[]; } ``` diff --git a/meta b/meta index b2e83a19cd..a13f31d8b4 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit b2e83a19cd9d6fd69e7e080bce986b2c25a90fdd +Subproject commit a13f31d8b44e531c48e45e75153f16c0ac0fbfd8 diff --git a/openslides_backend/action/actions/__init__.py b/openslides_backend/action/actions/__init__.py index 98bf124578..b943557d7d 100644 --- a/openslides_backend/action/actions/__init__.py +++ b/openslides_backend/action/actions/__init__.py @@ -30,14 +30,10 @@ def prepare_actions_map() -> None: motion_submitter, motion_workflow, motion_working_group_speaker, - option, organization, organization_tag, personal_note, point_of_order_category, - poll, - poll_candidate, - poll_candidate_list, projection, projector, projector_countdown, @@ -49,7 +45,6 @@ def prepare_actions_map() -> None: theme, topic, user, - vote, ) diff --git a/openslides_backend/action/actions/meeting/delete.py b/openslides_backend/action/actions/meeting/delete.py index 4968b73213..79389e30f6 100644 --- a/openslides_backend/action/actions/meeting/delete.py +++ b/openslides_backend/action/actions/meeting/delete.py @@ -25,3 +25,15 @@ def get_committee_id(self, instance: dict[str, Any]) -> int: ["committee_id"], ) return meeting["committee_id"] + + def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: + """ + Handle deletion of polls and all the related instances in the vote service. + """ + poll_ids = self.datastore.get(f"meeting/{instance['id']}", ["poll_ids"])[ + "poll_ids" + ] + for poll_id in poll_ids: + self.vote_service.delete(poll_id) + self.datastore.apply_to_be_deleted(f"poll/{poll_id}") + return instance diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py index 6fb472aca8..3d934d3eff 100644 --- a/openslides_backend/action/actions/meeting/update.py +++ b/openslides_backend/action/actions/meeting/update.py @@ -169,8 +169,8 @@ "assignment_poll_default_group_ids", "assignment_poll_default_backend", "topic_poll_default_group_ids", - "poll_default_backend", "poll_default_live_voting_enabled", + "poll_default_allow_invalid", ] diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py index 3a4a8c72e7..b7075a02bf 100644 --- a/openslides_backend/action/actions/meeting_user/update.py +++ b/openslides_backend/action/actions/meeting_user/update.py @@ -39,6 +39,10 @@ class MeetingUserUpdate( optional_properties=[ "about_me", "group_ids", + "poll_option_ids", + "poll_voted_ids", + "acting_ballot_ids", + "represented_ballot_ids", *meeting_user_standard_fields, *merge_fields, ], diff --git a/openslides_backend/action/actions/option/__init__.py b/openslides_backend/action/actions/option/__init__.py deleted file mode 100644 index a27b3acfe9..0000000000 --- a/openslides_backend/action/actions/option/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import create, delete, set_auto_fields, update # noqa diff --git a/openslides_backend/action/actions/option/create.py b/openslides_backend/action/actions/option/create.py deleted file mode 100644 index 854f75768e..0000000000 --- a/openslides_backend/action/actions/option/create.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Any - -from openslides_backend.shared.schema import id_list_schema - -from ....models.models import Option -from ....shared.exceptions import ActionException -from ...generics.create import CreateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ..poll_candidate_list.create import PollCandidateListCreate -from ..vote.create import VoteCreate -from ..vote.user_token_helper import get_user_token - - -@register_action("option.create", action_type=ActionType.BACKEND_INTERNAL) -class OptionCreateAction(CreateAction): - """ - (internal) Action to create an option - """ - - model = Option() - schema = DefaultSchema(Option()).get_create_schema( - required_properties=["meeting_id"], - optional_properties=[ - "text", - "poll_id", - "used_as_global_option_in_poll_id", - "content_object_id", - "yes", - "no", - "abstain", - "weight", - ], - additional_optional_fields={"poll_candidate_user_ids": id_list_schema}, - ) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - keyword = self.check_one_of_three_keywords(instance) - action_data = [] - user_token = get_user_token() - yes_data = self.get_vote_action_data(instance, "Y", "yes", user_token) - if yes_data is not None: - action_data.append(yes_data) - no_data = self.get_vote_action_data(instance, "N", "no", user_token) - if no_data is not None: - action_data.append(no_data) - abstain_data = self.get_vote_action_data(instance, "A", "abstain", user_token) - if abstain_data is not None: - action_data.append(abstain_data) - if action_data: - self.apply_instance(instance) - self.execute_other_action(VoteCreate, action_data) - if keyword == "poll_candidate_user_ids": - self.apply_instance(instance) - user_ids = instance.pop("poll_candidate_user_ids") - self.execute_other_action( - PollCandidateListCreate, - [ - { - "option_id": instance["id"], - "meeting_id": instance["meeting_id"], - "entries": [ - {"user_id": user_id, "weight": i} - for i, user_id in enumerate(user_ids, start=1) - ], - } - ], - ) - - return instance - - def get_vote_action_data( - self, instance: dict[str, Any], value: str, prop: str, user_token: str - ) -> dict[str, Any] | None: - if instance.get(prop): - return { - "value": value, - "weight": instance[prop], - "option_id": instance["id"], - "user_token": user_token, - } - return None - - @staticmethod - def check_one_of_three_keywords(instance: dict[str, Any]) -> str: - keys = [ - key - for key in ("text", "content_object_id", "poll_candidate_user_ids") - if key in instance - ] - if len(keys) != 1: - raise ActionException( - "Need one of text, content_object_id or poll_candidate_user_ids." - ) - return keys[0] diff --git a/openslides_backend/action/actions/option/delete.py b/openslides_backend/action/actions/option/delete.py deleted file mode 100644 index b2bc679e67..0000000000 --- a/openslides_backend/action/actions/option/delete.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Any - -from ....models.models import Option -from ....shared.patterns import ( - collection_from_fqid, - fqid_from_collection_and_id, - id_from_fqid, -) -from ...generics.delete import DeleteAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ..poll_candidate_list.delete import PollCandidateListDelete - - -@register_action("option.delete", action_type=ActionType.BACKEND_INTERNAL) -class OptionDelete(DeleteAction): - """ - Action to delete options. - """ - - model = Option() - schema = DefaultSchema(Option()).get_delete_schema() - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - option = self.datastore.get( - fqid_from_collection_and_id("option", instance["id"]), ["content_object_id"] - ) - if ( - option.get("content_object_id") - and collection_from_fqid(option["content_object_id"]) - == "poll_candidate_list" - ): - self.execute_other_action( - PollCandidateListDelete, - [{"id": id_from_fqid(option["content_object_id"])}], - ) - return instance diff --git a/openslides_backend/action/actions/option/set_auto_fields.py b/openslides_backend/action/actions/option/set_auto_fields.py deleted file mode 100644 index f297d50e70..0000000000 --- a/openslides_backend/action/actions/option/set_auto_fields.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any - -from ....models.models import Option -from ...generics.update import UpdateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("option.set_auto_fields", action_type=ActionType.BACKEND_INTERNAL) -class OptionSetAutoFields(UpdateAction): - """ - Action to calculate auto fields for options (yes, no, abstain) - """ - - model = Option() - schema = DefaultSchema(Option()).get_update_schema( - optional_properties=["yes", "no", "abstain"] - ) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - set_without_calc = ( - instance.get("yes") or instance.get("no") or instance.get("abstain") - ) - if not set_without_calc: - # TODO in this case we should autogenerate the fields. - raise NotImplementedError() - return instance diff --git a/openslides_backend/action/actions/option/update.py b/openslides_backend/action/actions/option/update.py deleted file mode 100644 index 9c1225d8d0..0000000000 --- a/openslides_backend/action/actions/option/update.py +++ /dev/null @@ -1,185 +0,0 @@ -from typing import Any - -from ....models.models import Option, Poll -from ....services.database.commands import GetManyRequest -from ....shared.exceptions import ActionException -from ....shared.patterns import fqid_from_collection_and_id -from ....shared.schema import decimal_schema -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ..poll.functions import check_poll_or_option_perms -from ..poll.set_state import PollSetState -from ..vote.create import VoteCreate -from ..vote.update import VoteUpdate -from ..vote.user_token_helper import get_user_token - -option_keys = ("yes", "no", "abstain") -option_keys_map = {key[0].upper(): key for key in option_keys} - - -@register_action("option.update") -class OptionUpdateAction(UpdateAction): - """ - Action to update an option. - """ - - model = Option() - schema = DefaultSchema(Option()).get_update_schema( - additional_optional_fields={ - "Y": decimal_schema, - "N": decimal_schema, - "A": decimal_schema, - "publish_immediately": {"type": "boolean"}, - } - ) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - """Update votes and auto calculate yes, no, abstain.""" - - option, poll = self._get_option_and_poll(instance["id"]) - state_change = self.check_state_change(instance, poll) - - if option.get("used_as_global_option_in_poll_id"): - self._handle_global_option_data(instance, poll) - else: - self._handle_poll_option_data(instance, poll) - - id_to_vote = self._fetch_votes(option.get("vote_ids", [])) - - action_data_create = [] - action_data_update = [] - user_token = get_user_token() - - for letter, option_key in option_keys_map.items(): - if option_key in instance: - vote_id = self._get_vote_id(letter, id_to_vote) - if vote_id is None: - action_data_create.append( - { - "option_id": instance["id"], - "value": letter, - "weight": instance[option_key], - "user_token": user_token, - } - ) - else: - action_data_update.append( - {"id": vote_id, "weight": instance[option_key]} - ) - if action_data_create: - self.execute_other_action(VoteCreate, action_data_create) - if action_data_update: - self.execute_other_action(VoteUpdate, action_data_update) - - execute_other_action = False - if state_change: - state = Poll.STATE_FINISHED - execute_other_action = True - if ( - execute_other_action - or ( - poll["state"] == Poll.STATE_FINISHED - and poll["type"] == Poll.TYPE_ANALOG - ) - ) and instance.get("publish_immediately"): - state = Poll.STATE_PUBLISHED - execute_other_action = True - if execute_other_action: - self.execute_other_action( - PollSetState, [{"id": poll["id"], "state": state}] - ) - - instance.pop("publish_immediately", None) - return instance - - def _get_option_and_poll( - self, option_id: int - ) -> tuple[dict[str, Any], dict[str, Any]]: - option = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, option_id), - ["poll_id", "used_as_global_option_in_poll_id", "vote_ids", "meeting_id"], - ) - return ( - option, - self.datastore.get( - fqid_from_collection_and_id("poll", option["poll_id"]), - [ - "id", - "state", - "type", - "pollmethod", - "global_yes", - "global_no", - "global_abstain", - "meeting_id", - "content_object_id", - ], - lock_result=["type"], - ), - ) - - def _handle_poll_option_data( - self, instance: dict[str, Any], poll: dict[str, Any] - ) -> None: - if poll.get("type") == "analog": - data = self._get_data(instance) - pollmethod = poll["pollmethod"] - for letter, key in option_keys_map.items(): - if letter in pollmethod: - instance[key] = data.get(key, "-2.000000") - elif data.get(key) is not None: - raise ActionException( - f"Pollmethod {pollmethod} does not support {key} votes" - ) - - def _handle_global_option_data( - self, instance: dict[str, Any], poll: dict[str, Any] - ) -> None: - if poll.get("type") == "analog": - data = self._get_data(instance) - for key in option_keys: - if poll.get(f"global_{key}") and poll.get("pollmethod") in ("Y", "N"): - instance[key] = data.get(key, "-2.000000") - elif key in data: - raise ActionException( - f"Global {key} votes are not allowed for this poll" - ) - - def _get_data(self, instance: dict[str, Any]) -> dict[str, Any]: - return { - key: instance.pop(letter) - for letter, key in option_keys_map.items() - if letter in instance - } - - def _fetch_votes(self, vote_ids: list[int]) -> dict[int, dict[str, Any]]: - get_many_request = GetManyRequest("vote", vote_ids, ["value"]) - gm_result = self.datastore.get_many([get_many_request]) - votes = gm_result.get("vote", {}) - return votes - - def _get_vote_id( - self, search_value: str, id_to_vote: dict[int, dict[str, Any]] - ) -> int | None: - for key, item in id_to_vote.items(): - if item["value"] == search_value: - return key - return None - - def check_state_change( - self, instance: dict[str, Any], poll: dict[str, Any] - ) -> bool: - return ( - poll.get("type") == Poll.TYPE_ANALOG - and poll.get("state") == Poll.STATE_CREATED - and any(letter in instance for letter in option_keys_map.keys()) - ) - - def check_permissions(self, instance: dict[str, Any]) -> None: - _, poll = self._get_option_and_poll(instance["id"]) - content_object_id = poll.get("content_object_id", "") - meeting_id = poll["meeting_id"] - check_poll_or_option_perms( - content_object_id, self.datastore, self.user_id, meeting_id - ) diff --git a/openslides_backend/action/actions/poll/__init__.py b/openslides_backend/action/actions/poll/__init__.py deleted file mode 100644 index 4a66fcc507..0000000000 --- a/openslides_backend/action/actions/poll/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from . import ( # noqa - anonymize, - create, - delete, - publish, - reset, - set_state, - start, - stop, - update, -) diff --git a/openslides_backend/action/actions/poll/anonymize.py b/openslides_backend/action/actions/poll/anonymize.py deleted file mode 100644 index 280d020e31..0000000000 --- a/openslides_backend/action/actions/poll/anonymize.py +++ /dev/null @@ -1,76 +0,0 @@ -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin - -from ....models.models import Poll -from ....services.database.commands import GetManyRequest -from ....shared.exceptions import ActionException -from ....shared.patterns import fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ...util.typing import ActionData -from ..vote.anonymize import VoteAnonymize -from .mixins import PollHistoryMixin, PollPermissionMixin - - -@register_action("poll.anonymize") -class PollAnonymize( - ExtendHistoryMixin, UpdateAction, PollPermissionMixin, PollHistoryMixin -): - """ - Action to anonymize a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema() - poll_history_information = "anonymized" - extend_history_to = "content_object_id" - - def get_updated_instances(self, action_data: ActionData) -> ActionData: - for instance in action_data: - self.check_allowed(instance["id"]) - option_ids = self._get_option_ids(instance["id"]) - options = self._get_options(option_ids) - - for option_id in options: - option = options[option_id] - if option.get("vote_ids"): - self._remove_user_id_from(option["vote_ids"]) - - instance["is_pseudoanonymized"] = True - yield instance - - def check_allowed(self, poll_id: int) -> None: - poll = self.datastore.get( - fqid_from_collection_and_id("poll", poll_id), ["type", "state"] - ) - - if not poll.get("state") in (Poll.STATE_FINISHED, Poll.STATE_PUBLISHED): - raise ActionException( - "Anonymizing can only be done after finishing a poll." - ) - if poll.get("type") != Poll.TYPE_NAMED: - raise ActionException("You can only anonymize named polls.") - - def _get_option_ids(self, poll_id: int) -> list[int]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, poll_id), - ["option_ids", "global_option_id"], - ) - option_ids = poll.get("option_ids", []) - if poll.get("global_option_id"): - option_ids.append(poll["global_option_id"]) - return option_ids - - def _get_options(self, option_ids: list[int]) -> dict[int, dict[str, Any]]: - get_many_request = GetManyRequest("option", option_ids, ["vote_ids"]) - gm_result = self.datastore.get_many([get_many_request]) - options: dict[int, dict[str, Any]] = gm_result.get("option", {}) - return options - - def _remove_user_id_from(self, vote_ids: list[int]) -> None: - action_data = [] - for id_ in vote_ids: - action_data.append({"id": id_}) - self.execute_other_action(VoteAnonymize, action_data) diff --git a/openslides_backend/action/actions/poll/base.py b/openslides_backend/action/actions/poll/base.py deleted file mode 100644 index 62784f4d0f..0000000000 --- a/openslides_backend/action/actions/poll/base.py +++ /dev/null @@ -1,26 +0,0 @@ -from openslides_backend.models.models import Poll - -from ....shared.exceptions import ActionException - - -def base_check_onehundred_percent_base( - pollmethod: str | None, onehundred_percent_base: str | None -) -> None: - error_msg = "This onehundred_percent_base not allowed in this pollmethod." - if pollmethod == "Y" and onehundred_percent_base in ( - Poll.ONEHUNDRED_PERCENT_BASE_N, - Poll.ONEHUNDRED_PERCENT_BASE_YN, - Poll.ONEHUNDRED_PERCENT_BASE_YNA, - ): - raise ActionException(error_msg) - elif pollmethod == "N" and onehundred_percent_base in ( - Poll.ONEHUNDRED_PERCENT_BASE_Y, - Poll.ONEHUNDRED_PERCENT_BASE_YN, - Poll.ONEHUNDRED_PERCENT_BASE_YNA, - ): - raise ActionException(error_msg) - elif ( - pollmethod == "YN" - and onehundred_percent_base == Poll.ONEHUNDRED_PERCENT_BASE_YNA - ): - raise ActionException(error_msg) diff --git a/openslides_backend/action/actions/poll/create.py b/openslides_backend/action/actions/poll/create.py deleted file mode 100644 index d7f60f48cc..0000000000 --- a/openslides_backend/action/actions/poll/create.py +++ /dev/null @@ -1,242 +0,0 @@ -from typing import Any - -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID - -from ....models.models import Poll -from ....shared.exceptions import ActionException -from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id -from ....shared.schema import decimal_schema, id_list_schema, optional_fqid_schema -from ...generics.create import CreateAction -from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin -from ...mixins.sequential_numbers_mixin import SequentialNumbersMixin -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ..option.create import OptionCreateAction -from .base import base_check_onehundred_percent_base -from .mixins import PollHistoryMixin, PollPermissionMixin, PollValidationMixin - -options_schema = { - "description": "A option inside a poll create schema", - "type": "object", - "properties": { - "text": {"type": "string", "description": "the text of an option"}, - "content_object_id": optional_fqid_schema, - "poll_candidate_user_ids": id_list_schema, - "Y": decimal_schema, - "N": decimal_schema, - "A": decimal_schema, - }, - "additionalProperties": False, -} - - -@register_action("poll.create") -class PollCreateAction( - PollValidationMixin, - SequentialNumbersMixin, - CreateAction, - PollPermissionMixin, - PollHistoryMixin, - ForbidAnonymousGroupMixin, -): - """ - Action to create a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_create_schema( - required_properties=["title", "type", "pollmethod", "meeting_id"], - additional_required_fields={ - "options": { - "type": "array", - "items": options_schema, - "minItems": 1, - } - }, - optional_properties=[ - "content_object_id", - "description", - "min_votes_amount", - "max_votes_amount", - "max_votes_per_option", - "global_yes", - "global_no", - "global_abstain", - "onehundred_percent_base", - "votesvalid", - "votesinvalid", - "votescast", - "entitled_group_ids", - "backend", - "live_voting_enabled", - ], - additional_optional_fields={ - "publish_immediately": {"type": "boolean"}, - "amount_global_yes": decimal_schema, - "amount_global_no": decimal_schema, - "amount_global_abstain": decimal_schema, - }, - ) - poll_history_information = "created" - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - instance = super().update_instance(instance) - action_data = [] - - state_change = self.check_state_change(instance) - is_motion_poll = collection_from_fqid(instance["content_object_id"]) == "motion" - - # check enabled_electronic_voting - if instance["type"] in (Poll.TYPE_NAMED, Poll.TYPE_PSEUDOANONYMOUS): - organization = self.datastore.get( - ONE_ORGANIZATION_FQID, - ["enable_electronic_voting"], - ) - if not organization.get("enable_electronic_voting"): - raise ActionException("Electronic voting is not allowed.") - - # check named and live_voting_enabled - if instance.get("live_voting_enabled") and not ( - instance["type"] == Poll.TYPE_NAMED and is_motion_poll - ): - raise ActionException( - "live_voting_enabled only allowed for named motion polls." - ) - - # check entitled_group_ids and analog - if instance["type"] == Poll.TYPE_ANALOG and "entitled_group_ids" in instance: - raise ActionException("entitled_group_ids is not allowed for analog.") - # check analog and onehundredpercentbase entitled - if instance["type"] == Poll.TYPE_ANALOG and ( - base := instance.get("onehundred_percent_base") - ) in ( - Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED, - Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT, - ): - raise ActionException( - f"onehundred_percent_base: value {base} is not allowed for analog." - ) - self.check_onehundred_percent_base(instance) - - # check non-analog and publish_immediately - if instance["type"] != Poll.TYPE_ANALOG and "publish_immediately" in instance: - raise ActionException("publish_immediately only allowed for analog polls.") - - # check content_object_id motion and state allow_create_poll - if is_motion_poll: - motion = self.datastore.get(instance["content_object_id"], ["state_id"]) - if not motion.get("state_id"): - raise ActionException("Motion doesn't have a state.") - state = self.datastore.get( - fqid_from_collection_and_id("motion_state", motion["state_id"]), - ["allow_create_poll"], - ) - if not state.get("allow_create_poll"): - raise ActionException("Motion state doesn't allow to create poll.") - - # handle non-global options - unique_set = set() - - for weight, option in enumerate(instance.get("options", []), start=1): - # check the keys with staticmethod from option.create, where they belong - key = OptionCreateAction.check_one_of_three_keywords(option) - data: dict[str, Any] = { - "poll_id": instance["id"], - "meeting_id": instance["meeting_id"], - "weight": weight, - key: option[key], - } - - o_obj = f"{key},{option[key]}" - if o_obj in unique_set: - raise ActionException( - f"Duplicated option in poll.options: {option[key]}" - ) - else: - unique_set.add(o_obj) - - if instance["type"] == "analog": - if instance["pollmethod"] == "N": - data["no"] = self.parse_vote_value(option, "N") - else: - data["yes"] = self.parse_vote_value(option, "Y") - if instance["pollmethod"] in ("YN", "YNA"): - data["no"] = self.parse_vote_value(option, "N") - if instance["pollmethod"] == "YNA": - data["abstain"] = self.parse_vote_value(option, "A") - - action_data.append(data) - - # handle global option - global_data = { - "text": "global option", - "used_as_global_option_in_poll_id": instance["id"], - "meeting_id": instance["meeting_id"], - "weight": 1, - } - if instance["type"] == "analog": - for option in ["yes", "no", "abstain"]: - if instance["type"] == "analog" and instance.get(f"global_{option}"): - global_data[option] = self.parse_vote_value( - instance, f"amount_global_{option}" - ) - instance.pop(f"amount_global_{option}", None) - action_data.append(global_data) - - # Execute the create option actions - self.apply_instance(instance) - self.execute_other_action( - OptionCreateAction, - action_data, - ) - - # set state - instance["state"] = Poll.STATE_CREATED - if state_change: - instance["state"] = Poll.STATE_FINISHED - if ( - instance["type"] == Poll.TYPE_ANALOG - and instance["state"] == Poll.STATE_FINISHED - and instance.get("publish_immediately") - ): - instance["state"] = Poll.STATE_PUBLISHED - - # set votescast, votesvalid, votesinvalid defaults - for field in ("votescast", "votesvalid", "votesinvalid"): - instance[field] = ( - self.parse_vote_value(instance, field) - if instance["type"] == Poll.TYPE_ANALOG - else instance.get(field, "0.000000") - ) - - # calculate is_pseudoanonymized - instance["is_pseudoanonymized"] = instance["type"] == Poll.TYPE_PSEUDOANONYMOUS - - instance.pop("options", None) - instance.pop("publish_immediately", None) - self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"]) - return instance - - def parse_vote_value(self, data: dict[str, Any], field: str) -> Any: - return data.get(field, "-2.000000") - - def check_onehundred_percent_base(self, instance: dict[str, Any]) -> None: - pollmethod = instance["pollmethod"] - onehundred_percent_base = instance.get("onehundred_percent_base") - base_check_onehundred_percent_base(pollmethod, onehundred_percent_base) - - def check_state_change(self, instance: dict[str, Any]) -> bool: - if instance["type"] != Poll.TYPE_ANALOG: - return False - check_fields = ( - "votesvalid", - "votesinvalid", - "votescast", - ) - for field in check_fields: - if instance.get(field): - return True - for option in instance.get("options", []): - if option.get("Y") or option.get("N") or option.get("A"): - return True - return False diff --git a/openslides_backend/action/actions/poll/delete.py b/openslides_backend/action/actions/poll/delete.py deleted file mode 100644 index 5d6e878828..0000000000 --- a/openslides_backend/action/actions/poll/delete.py +++ /dev/null @@ -1,105 +0,0 @@ -from collections.abc import Callable - -from ....models.models import Poll -from ....services.database.interface import GetManyRequest -from ....shared.exceptions import VoteServiceException -from ...generics.delete import DeleteAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ...util.typing import ActionData -from .mixins import PollHistoryMixin, PollPermissionMixin - - -@register_action("poll.delete") -class PollDelete(DeleteAction, PollPermissionMixin, PollHistoryMixin): - """ - Action to delete polls. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_delete_schema() - poll_history_information = "deleted" - - def prefetch(self, action_data: ActionData) -> None: - result = self.datastore.get_many( - [ - GetManyRequest( - "poll", - list({instance["id"] for instance in action_data}), - [ - "content_object_id", - "meeting_id", - "entitled_group_ids", - "voted_ids", - "option_ids", - "global_option_id", - "projection_ids", - "meta_position", - "state", - ], - ), - ], - use_changed_models=False, - ) - polls = result["poll"].values() - self.started_polls = [ - id_ - for id_, poll in result["poll"].items() - if poll.get("state") == "started" - ] - meeting_ids = list({poll["meeting_id"] for poll in polls}) - group_ids = list( - { - group_id - for poll in polls - for group_id in poll.get("entitled_group_ids", ()) - } - ) - option_ids = [ - option_id - for poll in polls - if poll.get("option_ids") - for option_id in poll["option_ids"] - ] - requests = [ - GetManyRequest( - "meeting", - meeting_ids, - [ - "is_active_in_organization_id", - "name", - "option_ids", - "poll_ids", - ], - ), - GetManyRequest( - "option", - option_ids, - [ - "meeting_id", - "vote_ids", - "content_object_id", - "poll_id", - "used_as_global_option_in_poll_id", - "vote_ids", - "meta_position", - ], - ), - GetManyRequest( - "group", - group_ids, - ["poll_ids"], - ), - ] - self.datastore.get_many(requests, use_changed_models=False) - - def get_on_success(self, action_data: ActionData) -> Callable[[], None]: - def on_success() -> None: - for instance in action_data: - if (id_ := instance["id"]) in self.started_polls: - try: - self.vote_service.clear(id_) - except VoteServiceException as e: - self.logger.error(f"Error clearing vote {id_}: {str(e)}") - - return on_success diff --git a/openslides_backend/action/actions/poll/functions.py b/openslides_backend/action/actions/poll/functions.py deleted file mode 100644 index 25c2eb432a..0000000000 --- a/openslides_backend/action/actions/poll/functions.py +++ /dev/null @@ -1,25 +0,0 @@ -from ....permissions.permission_helper import has_perm -from ....permissions.permissions import Permission, Permissions -from ....services.database.interface import Database -from ....shared.exceptions import ActionException, MissingPermission -from ....shared.patterns import KEYSEPARATOR, collection_from_fqid - - -def check_poll_or_option_perms( - content_object_id: str, - datastore: Database, - user_id: int, - meeting_id: int, -) -> None: - if content_object_id.startswith("motion" + KEYSEPARATOR): - perm: Permission = Permissions.Motion.CAN_MANAGE_POLLS - elif content_object_id.startswith("assignment" + KEYSEPARATOR): - perm = Permissions.Assignment.CAN_MANAGE - 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 deleted file mode 100644 index e4ffdf0fa0..0000000000 --- a/openslides_backend/action/actions/poll/mixins.py +++ /dev/null @@ -1,285 +0,0 @@ -from collections import defaultdict -from decimal import Decimal -from typing import Any, cast - -from openslides_backend.shared.typing import HistoryInformation - -from ....services.database.commands import GetManyRequest -from ....shared.exceptions import ActionException, VoteServiceException -from ....shared.interfaces.write_request import WriteRequest -from ....shared.patterns import ( - collection_from_fqid, - collectionfield_and_fqid_from_fqfield, - fqid_from_collection_and_id, -) -from ...action import Action -from ..option.set_auto_fields import OptionSetAutoFields -from ..projector_countdown.mixins import CountdownCommand, CountdownControl -from ..vote.create import VoteCreate -from ..vote.user_token_helper import get_user_token -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 = cast( - int, - instance.get( - "max_votes_amount", poll["max_votes_amount"] if poll_id else 1 - ), - ) - min_votes_amount = cast( - int, - instance.get( - "min_votes_amount", poll["min_votes_amount"] if poll_id else 1 - ), - ) - max_votes_per_option = cast( - int, - 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: - content_object_id = instance.get("content_object_id", "") - meeting_id = instance["meeting_id"] - else: - poll = self.datastore.get( - fqid_from_collection_and_id("poll", instance["id"]), - ["content_object_id", "meeting_id"], - lock_result=False, - ) - 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 - ) - - -class StopControl(CountdownControl, Action): - def build_write_request(self) -> WriteRequest | None: - """ - Reduce locked fields - """ - self.datastore.locked_fields = { - k: v - for k, v in self.datastore.locked_fields.items() - if collectionfield_and_fqid_from_fqfield(k)[0] - not in ( - "meeting_user/user_id", - "meeting_user/vote_delegated_to_id", - "poll/pollmethod", - "poll/global_option_id", - "poll/meeting_id", - "poll/content_object_id", - "meeting/users_enable_vote_weight", - "meeting/poll_couple_countdown", - "meeting/poll_countdown_id", - "option/meeting_id", - ) - } - return super().build_write_request() - - def on_stop(self, instance: dict[str, Any]) -> None: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - [ - "state", - "meeting_id", - "pollmethod", - "global_option_id", - "entitled_group_ids", - ], - ) - # reset countdown given by meeting - meeting = self.datastore.get( - fqid_from_collection_and_id("meeting", poll["meeting_id"]), - [ - "poll_couple_countdown", - "poll_countdown_id", - "users_enable_vote_weight", - "users_enable_vote_delegations", - ], - ) - if meeting.get("poll_couple_countdown") and meeting.get("poll_countdown_id"): - self.control_countdown(meeting["poll_countdown_id"], CountdownCommand.RESET) - - # stop poll in vote service and create vote objects - results = self.vote_service.stop(instance["id"]) - action_data = [] - votesvalid = Decimal("0.000000") - option_results: dict[int, dict[str, Decimal]] = defaultdict( - lambda: defaultdict(lambda: Decimal("0.000000")) - ) # maps options to their respective YNA sums - for ballot in results["votes"]: - user_token = get_user_token() - vote_weight = Decimal(ballot["weight"]) - votesvalid += vote_weight - vote_template: dict[str, str | int] = {"user_token": user_token} - if "vote_user_id" in ballot: - vote_template["user_id"] = ballot["vote_user_id"] - if "request_user_id" in ballot: - vote_template["delegated_user_id"] = ballot["request_user_id"] - - if isinstance(ballot["value"], dict): - for option_id_str, value in ballot["value"].items(): - option_id = int(option_id_str) - - vote_value = value - vote_weighted = vote_weight # use new variable vote_weighted because pollmethod=Y/N does not imply anymore that only one loop is done (see max_votes_per_option) - if poll["pollmethod"] in ("Y", "N"): - if value == 0: - continue - vote_value = poll["pollmethod"] - vote_weighted *= value - - option_results[option_id][vote_value] += vote_weighted - action_data.append( - { - "value": vote_value, - "option_id": option_id, - "weight": str(vote_weighted), - **vote_template, - } - ) - elif isinstance(ballot["value"], str): - vote_value = ballot["value"] - option_id = poll["global_option_id"] - option_results[option_id][vote_value] += vote_weight - action_data.append( - { - "value": vote_value, - "option_id": option_id, - "weight": str(vote_weight), - **vote_template, - } - ) - else: - raise VoteServiceException("Invalid response from vote service") - self.execute_other_action(VoteCreate, action_data) - # update results into option - self.execute_other_action( - OptionSetAutoFields, - [ - { - "id": _id, - "yes": str(option["Y"]), - "no": str(option["N"]), - "abstain": str(option["A"]), - } - for _id, option in option_results.items() - ], - ) - # set voted ids - voted_ids = results["user_ids"] - instance["voted_ids"] = voted_ids - - # set votescast, votesvalid, votesinvalid - instance["votesvalid"] = str(votesvalid) - instance["votescast"] = str(Decimal("0.000000") + Decimal(len(voted_ids))) - instance["votesinvalid"] = "0.000000" - - # set entitled users at stop. - instance["entitled_users_at_stop"] = self.get_entitled_users( - poll | instance, meeting - ) - - def get_entitled_users( - self, poll: dict[str, Any], meeting: dict[str, Any] - ) -> list[dict[str, Any]]: - entitled_users = [] - all_voted_users = set(poll.get("voted_ids", [])) - - # get all users from the groups. - gmr = GetManyRequest( - "group", poll.get("entitled_group_ids", []), ["meeting_user_ids"] - ) - gm_result = self.datastore.get_many([gmr]) - groups = gm_result.get("group", {}).values() - - # fetch presence status - meeting_user_ids = set() - for group in groups: - meeting_user_ids.update(group.get("meeting_user_ids", [])) - gmr = GetManyRequest( - "meeting_user", list(meeting_user_ids), ["user_id", "vote_delegated_to_id"] - ) - gm_result = self.datastore.get_many([gmr]) - meeting_users = gm_result.get("meeting_user", {}).values() - - mu_to_user_id = {} - if meeting.get("users_enable_vote_delegations"): - # fetch vote delegations - delegated_to_mu_ids = list( - {id_ for mu in meeting_users if (id_ := mu.get("vote_delegated_to_id"))} - ) - if delegated_to_mu_ids: - gmr = GetManyRequest("meeting_user", delegated_to_mu_ids, ["user_id"]) - mu_to_user_id = self.datastore.get_many([gmr]).get("meeting_user", {}) - - gmr = GetManyRequest( - "user", - [mu["user_id"] for mu in meeting_users], - ["is_present_in_meeting_ids"], - ) - users = self.datastore.get_many([gmr]).get("user", {}) - - for mu in meeting_users: - entitled_users.append( - { - "voted": mu["user_id"] in all_voted_users, - "present": poll["meeting_id"] - in users[mu["user_id"]].get("is_present_in_meeting_ids", []), - "user_id": mu["user_id"], - "vote_delegated_to_user_id": ( - mu_to_user_id[vote_mu_id]["user_id"] - if (vote_mu_id := mu.get("vote_delegated_to_id")) - and meeting.get("users_enable_vote_delegations") - else None - ), - } - ) - - return entitled_users - - -class PollHistoryMixin(Action): - poll_history_information: str - - def get_history_information(self) -> HistoryInformation | None: - # no datastore access necessary if information is in payload - polls = self.get_instances_with_fields(["content_object_id"]) - return { - poll["content_object_id"]: [ - f"{self.get_history_title(poll)} {self.poll_history_information}" - ] - for poll in polls - } - - def get_history_title(self, poll: dict[str, Any]) -> str: - content_collection = collection_from_fqid(poll["content_object_id"]) - if content_collection == "assignment": - return "Ballot" - return "Voting" diff --git a/openslides_backend/action/actions/poll/publish.py b/openslides_backend/action/actions/poll/publish.py deleted file mode 100644 index 972820483a..0000000000 --- a/openslides_backend/action/actions/poll/publish.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin -from openslides_backend.shared.typing import HistoryInformation - -from ....models.models import Poll -from ....shared.exceptions import ActionException -from ....shared.patterns import fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from .mixins import PollHistoryMixin, PollPermissionMixin, StopControl - - -@register_action("poll.publish") -class PollPublishAction( - ExtendHistoryMixin, - StopControl, - UpdateAction, - PollPermissionMixin, - PollHistoryMixin, -): - """ - Action to publish a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema() - extend_history_to = "content_object_id" - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - ["state"], - ) - if poll.get("state") not in [Poll.STATE_FINISHED, Poll.STATE_STARTED]: - raise ActionException( - f"Cannot publish poll {instance['id']}, because it is not in state finished or started." - ) - if poll["state"] == Poll.STATE_STARTED: - self.on_stop(instance) - - instance["state"] = Poll.STATE_PUBLISHED - return instance - - def get_history_information(self) -> HistoryInformation | None: - polls = self.get_instances_with_fields(["content_object_id", "state"]) - return { - poll["content_object_id"]: [ - f"{self.get_history_title(poll)} {'stopped/' if poll['state'] != Poll.STATE_FINISHED else ''}published" - ] - for poll in polls - } diff --git a/openslides_backend/action/actions/poll/reset.py b/openslides_backend/action/actions/poll/reset.py deleted file mode 100644 index 3a71209753..0000000000 --- a/openslides_backend/action/actions/poll/reset.py +++ /dev/null @@ -1,138 +0,0 @@ -from collections.abc import Callable -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin - -from ....models.models import Poll -from ....services.database.interface import GetManyRequest -from ....shared.patterns import fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ...util.typing import ActionData -from ..option.set_auto_fields import OptionSetAutoFields -from ..vote.delete import VoteDelete -from .mixins import PollHistoryMixin, PollPermissionMixin - - -@register_action("poll.reset") -class PollResetAction( - ExtendHistoryMixin, UpdateAction, PollPermissionMixin, PollHistoryMixin -): - """ - Action to reset a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema() - poll_history_information = "reset" - extend_history_to = "content_object_id" - - def prefetch(self, action_data: ActionData) -> None: - result = self.datastore.get_many( - [ - GetManyRequest( - "poll", - list({instance["id"] for instance in action_data}), - [ - "content_object_id", - "meeting_id", - "type", - "voted_ids", - "option_ids", - "global_option_id", - ], - ), - ], - use_changed_models=False, - ) - polls = result["poll"].values() - meeting_ids = list({poll["meeting_id"] for poll in polls}) - option_ids = [ - option_id - for poll in polls - if poll.get("option_ids") - for option_id in poll["option_ids"] - ] - requests = [ - GetManyRequest( - "meeting", - meeting_ids, - [ - "is_active_in_organization_id", - "name", - ], - ), - GetManyRequest( - "option", - option_ids, - ["vote_ids"], - ), - ] - self.datastore.get_many(requests, use_changed_models=False) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - instance["state"] = Poll.STATE_CREATED - self.delete_all_votes(instance["id"]) - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), ["type"] - ) - instance["is_pseudoanonymized"] = poll.get("type") == Poll.TYPE_PSEUDOANONYMOUS - instance["voted_ids"] = [] - instance["entitled_users_at_stop"] = None - instance["votesvalid"] = None - instance["votesinvalid"] = None - instance["votescast"] = None - return instance - - def delete_all_votes(self, poll_id: int) -> None: - option_ids = self._get_option_ids(poll_id) - options = self._get_options(option_ids) - for option_id in options: - option = options[option_id] - if option.get("vote_ids"): - self._delete_votes(option["vote_ids"]) - self._clear_option_auto_fields(option_id) - - def _get_option_ids(self, poll_id: int) -> list[int]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, poll_id), - ["option_ids", "global_option_id"], - ) - option_ids = poll.get("option_ids", []) - if poll.get("global_option_id"): - option_ids.append(poll["global_option_id"]) - return option_ids - - def _get_options(self, option_ids: list[int]) -> dict[int, dict[str, Any]]: - get_many_request = GetManyRequest("option", option_ids, ["vote_ids"]) - gm_result = self.datastore.get_many( - [get_many_request], use_changed_models=False - ) - options: dict[int, dict[str, Any]] = gm_result.get("option", {}) - - return options - - def _delete_votes(self, vote_ids: list[int]) -> None: - action_data = [] - for id_ in vote_ids: - action_data.append({"id": id_}) - self.execute_other_action(VoteDelete, action_data) - - def _clear_option_auto_fields(self, option_id: int) -> None: - action_data = [ - { - "id": option_id, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - } - ] - self.execute_other_action(OptionSetAutoFields, action_data) - - def get_on_success(self, action_data: ActionData) -> Callable[[], None]: - def on_success() -> None: - for instance in action_data: - self.vote_service.clear(instance["id"]) - - return on_success diff --git a/openslides_backend/action/actions/poll/set_state.py b/openslides_backend/action/actions/poll/set_state.py deleted file mode 100644 index 997ff8ccae..0000000000 --- a/openslides_backend/action/actions/poll/set_state.py +++ /dev/null @@ -1,17 +0,0 @@ -from ....models.models import Poll -from ...generics.update import UpdateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("poll.set_state", action_type=ActionType.BACKEND_INTERNAL) -class PollSetState(UpdateAction): - """ - Internal action to set state of a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema( - required_properties=["state"], - ) diff --git a/openslides_backend/action/actions/poll/start.py b/openslides_backend/action/actions/poll/start.py deleted file mode 100644 index 394c35b281..0000000000 --- a/openslides_backend/action/actions/poll/start.py +++ /dev/null @@ -1,74 +0,0 @@ -from collections.abc import Callable -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin - -from ....models.models import Poll -from ....shared.exceptions import ActionException, VoteServiceException -from ....shared.patterns import fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ...util.typing import ActionData -from ..projector_countdown.mixins import CountdownCommand, CountdownControl -from .mixins import PollHistoryMixin, PollPermissionMixin - - -@register_action("poll.start") -class PollStartAction( - ExtendHistoryMixin, - CountdownControl, - UpdateAction, - PollPermissionMixin, - PollHistoryMixin, -): - """ - Action to start a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema() - poll_history_information = "started" - extend_history_to = "content_object_id" - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - ["state", "meeting_id", "type"], - ) - if poll.get("type") == Poll.TYPE_ANALOG: - raise ActionException( - "Analog polls cannot be started. Please use poll.update instead to give votes." - ) - if poll.get("state") != Poll.STATE_CREATED: - raise ActionException( - f"Cannot start poll {instance['id']}, because it is not in state created." - ) - instance["state"] = Poll.STATE_STARTED - - # restart projector countdown given by the meeting - meeting = self.datastore.get( - fqid_from_collection_and_id("meeting", poll["meeting_id"]), - [ - "poll_couple_countdown", - "poll_countdown_id", - ], - ) - if meeting.get("poll_couple_countdown") and meeting.get("poll_countdown_id"): - self.control_countdown( - meeting["poll_countdown_id"], CountdownCommand.RESTART - ) - - self.vote_service.start(instance["id"]) - - return instance - - def get_on_failure(self, action_data: ActionData) -> Callable[[], None]: - def on_failure() -> None: - for instance in action_data: - try: - self.vote_service.clear(instance["id"]) - except VoteServiceException as e: - self.logger.error(f"Error clearing vote {instance['id']}: {str(e)}") - - return on_failure diff --git a/openslides_backend/action/actions/poll/stop.py b/openslides_backend/action/actions/poll/stop.py deleted file mode 100644 index e1884b7c6e..0000000000 --- a/openslides_backend/action/actions/poll/stop.py +++ /dev/null @@ -1,133 +0,0 @@ -from collections.abc import Callable -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin - -from ....models.models import Poll -from ....services.database.commands import GetManyRequest -from ....shared.exceptions import ActionException, VoteServiceException -from ....shared.patterns import fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ...util.typing import ActionData -from .mixins import PollHistoryMixin, PollPermissionMixin, StopControl - - -@register_action("poll.stop") -class PollStopAction( - ExtendHistoryMixin, - StopControl, - UpdateAction, - PollPermissionMixin, - PollHistoryMixin, -): - """ - Action to stop a poll. - """ - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema() - poll_history_information = "stopped" - extend_history_to = "content_object_id" - - def prefetch(self, action_data: ActionData) -> None: - result = self.datastore.get_many( - [ - GetManyRequest( - "poll", - list({instance["id"] for instance in action_data}), - [ - "content_object_id", - "meeting_id", - "state", - "voted_ids", - "pollmethod", - "global_option_id", - "entitled_group_ids", - ], - ), - ], - use_changed_models=False, - ) - polls = result["poll"].values() - meeting_ids = list({poll["meeting_id"] for poll in polls}) - requests = [ - GetManyRequest( - "meeting", - meeting_ids, - [ - "poll_couple_countdown", - "poll_countdown_id", - "users_enable_vote_weight", - "vote_ids", - ], - ), - GetManyRequest( - "group", - list( - { - group_id - for poll in polls - for group_id in poll.get("entitled_group_ids", []) - } - ), - ["meeting_user_ids"], - ), - ] - result = self.datastore.get_many(requests, use_changed_models=False) - groups = result["group"].values() - result = self.datastore.get_many( - [ - GetManyRequest( - "meeting_user", - list( - { - meeting_user_id - for group in groups - for meeting_user_id in group.get("meeting_user_ids", []) - } - ), - ["user_id"], - ), - ], - use_changed_models=False, - ) - meeting_users = result["meeting_user"].values() - self.datastore.get_many( - [ - GetManyRequest( - "user", - list({mu["user_id"] for mu in meeting_users}), - [ - "poll_voted_ids", - "delegated_vote_ids", - "vote_ids", - ], - ), - ], - use_changed_models=False, - ) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - ["state", "meeting_id", "voted_ids"], - ) - if poll.get("state") != Poll.STATE_STARTED: - raise ActionException( - f"Cannot stop poll {instance['id']}, because it is not in state started." - ) - instance["state"] = Poll.STATE_FINISHED - self.on_stop(instance) - return instance - - def get_on_success(self, action_data: ActionData) -> Callable[[], None]: - def on_success() -> None: - for instance in action_data: - try: - self.vote_service.clear(instance["id"]) - except VoteServiceException as e: - self.logger.error(f"Error clearing vote {instance['id']}: {str(e)}") - - return on_success diff --git a/openslides_backend/action/actions/poll/update.py b/openslides_backend/action/actions/poll/update.py deleted file mode 100644 index c8603aea5e..0000000000 --- a/openslides_backend/action/actions/poll/update.py +++ /dev/null @@ -1,224 +0,0 @@ -from typing import Any - -from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin -from openslides_backend.services.database.interface import PartialModel - -from ....models.models import Poll -from ....shared.exceptions import ActionException -from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id -from ...generics.update import UpdateAction -from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin -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, PollValidationMixin - - -@register_action("poll.update") -class PollUpdateAction( - PollValidationMixin, - ExtendHistoryMixin, - UpdateAction, - PollPermissionMixin, - PollHistoryMixin, - ForbidAnonymousGroupMixin, -): - """ - Action to update a poll. - """ - - internal_fields = [ - "entitled_users_at_stop", - ] - - model = Poll() - schema = DefaultSchema(Poll()).get_update_schema( - optional_properties=[ - "pollmethod", - "min_votes_amount", - "max_votes_amount", - "max_votes_per_option", - "global_yes", - "global_no", - "global_abstain", - "entitled_group_ids", - "title", - "description", - "onehundred_percent_base", - "votesvalid", - "votesinvalid", - "votescast", - "backend", - "live_voting_enabled", - *internal_fields, - ], - additional_optional_fields={ - "publish_immediately": {"type": "boolean"}, - }, - ) - poll_history_information = "updated" - extend_history_to = "content_object_id" - - def validate_fields(self, instance: dict[str, Any]) -> dict[str, Any]: - super().validate_instance(instance) - if not self.internal and any( - forbidden := {key for key in instance if key in self.internal_fields} - ): - raise ActionException(f"data must not contain {forbidden} properties") - return super().validate_fields(instance) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - ["state", "type", "entitled_users_at_stop", "content_object_id"], - ) - - self.check_entitled_users_at_stop(instance, poll) - - state_change = self.check_state_change(instance, poll) - - self.check_onehundred_percent_base(instance) - - not_allowed = [] - if not poll.get("state") == Poll.STATE_CREATED: - for key in ( - "pollmethod", - "type", - "min_votes_amount", - "max_votes_amount", - "max_votes_per_option", - "global_yes", - "global_no", - "global_abstain", - "backend", - "live_voting_enabled", - ): - if key in instance: - not_allowed.append(key) - if ( - poll.get("state") != Poll.STATE_CREATED - or poll.get("type") == Poll.TYPE_ANALOG - ): - if "entitled_group_ids" in instance: - not_allowed.append("entitled_group_ids") - if not poll.get("type") == Poll.TYPE_ANALOG: - for key in ( - "votesvalid", - "votesinvalid", - "votescast", - "publish_immediately", - ): - if key in instance: - not_allowed.append(key) - - if not_allowed: - raise ActionException( - "Following options are not allowed in this state and type: " - + ", ".join(not_allowed) - ) - - # check named and live_voting_enabled - if instance.get("live_voting_enabled") and not ( - poll["type"] == Poll.TYPE_NAMED - and collection_from_fqid(poll["content_object_id"]) == "motion" - ): - raise ActionException( - "live_voting_enabled only allowed for named motion polls." - ) - - if poll["type"] == Poll.TYPE_ANALOG and ( - base := instance.get("onehundred_percent_base") - ) in ( - Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED, - Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT, - ): - raise ActionException( - f"onehundred_percent_base: value {base} is not allowed for analog." - ) - if state_change: - instance["state"] = Poll.STATE_FINISHED - if ( - poll["type"] == Poll.TYPE_ANALOG - and ( - instance.get("state") == Poll.STATE_FINISHED - or poll["state"] == Poll.STATE_FINISHED - ) - and instance.get("publish_immediately") - ): - instance["state"] = Poll.STATE_PUBLISHED - - # set votescast, votesvalid, votesinvalid defaults - if poll["type"] == Poll.TYPE_ANALOG: - for field in ("votescast", "votesvalid", "votesinvalid"): - if field not in instance: - instance[field] = "-2.000000" - - instance.pop("publish_immediately", None) - self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"]) - return instance - - def check_onehundred_percent_base(self, instance: dict[str, Any]) -> None: - onehundred_percent_base = instance.get("onehundred_percent_base") - if "pollmethod" in instance: - pollmethod = instance["pollmethod"] - else: - poll = self.datastore.get( - fqid_from_collection_and_id(self.model.collection, instance["id"]), - ["pollmethod"], - ) - pollmethod = poll.get("pollmethod") - base_check_onehundred_percent_base(pollmethod, onehundred_percent_base) - - def check_state_change( - self, instance: dict[str, Any], poll: dict[str, Any] - ) -> bool: - if poll.get("type") != Poll.TYPE_ANALOG: - return False - if poll.get("state") != Poll.STATE_CREATED: - return False - check_fields = ( - "votesvalid", - "votesinvalid", - "votescast", - ) - for field in check_fields: - if instance.get(field): - return True - return False - - def check_entitled_users_at_stop( - self, instance: dict[str, Any], poll: PartialModel - ) -> None: - field = "entitled_users_at_stop" - if field not in instance: - return - if field not in poll: - raise ActionException( - "Can not set 'entitled_users_at_stop' via poll.update" - ) - if not isinstance(instance[field], list) or any( - entry - for entry in instance[field] - if not isinstance(entry, dict) or not entry.get("user_id") - ): - raise ActionException("'entitled_users_at_stop' has the wrong format") - original_main_user_id_to_entry: dict[int, dict[str, Any]] = { - date["user_id"]: date for date in poll[field] - } - new_main_user_id_to_entry: dict[int, dict[str, Any]] = { - date["user_id"]: date for date in instance[field] - } - original_ids = {user_id for user_id in original_main_user_id_to_entry} - new_ids = {user_id for user_id in new_main_user_id_to_entry} - if (len(original_ids) != len(new_ids)) or len(original_ids.difference(new_ids)): - raise ActionException( - "Can not change essential 'entitled_users_at_stop' data via poll.update" - ) - if any( - original_entry.get(field) != new_main_user_id_to_entry[user_id].get(field) - for user_id, original_entry in original_main_user_id_to_entry.items() - for field in ["voted", "present", "user_id", "vote_delegated_to_user_id"] - ): - raise ActionException( - "Can not change essential 'entitled_users_at_stop' data via poll.update" - ) diff --git a/openslides_backend/action/actions/poll_candidate/__init__.py b/openslides_backend/action/actions/poll_candidate/__init__.py deleted file mode 100644 index e2b11ce4af..0000000000 --- a/openslides_backend/action/actions/poll_candidate/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import create, delete # noqa diff --git a/openslides_backend/action/actions/poll_candidate/create.py b/openslides_backend/action/actions/poll_candidate/create.py deleted file mode 100644 index 933b72cb1f..0000000000 --- a/openslides_backend/action/actions/poll_candidate/create.py +++ /dev/null @@ -1,23 +0,0 @@ -from ....models.models import PollCandidate -from ...generics.create import CreateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("poll_candidate.create", action_type=ActionType.BACKEND_INTERNAL) -class PollCandidateCreate(CreateAction): - """ - Internal action to create a poll candiate. It gets the meeting_id from - its poll candidate list, - """ - - model = PollCandidate() - schema = DefaultSchema(PollCandidate()).get_create_schema( - required_properties=[ - "user_id", - "poll_candidate_list_id", - "weight", - "meeting_id", - ] - ) diff --git a/openslides_backend/action/actions/poll_candidate/delete.py b/openslides_backend/action/actions/poll_candidate/delete.py deleted file mode 100644 index 8677db3f6d..0000000000 --- a/openslides_backend/action/actions/poll_candidate/delete.py +++ /dev/null @@ -1,15 +0,0 @@ -from ....models.models import PollCandidate -from ...generics.delete import DeleteAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("poll_candidate.delete", action_type=ActionType.BACKEND_INTERNAL) -class PollCandidateDelete(DeleteAction): - """ - Internal action to delete a poll candidate. - """ - - model = PollCandidate() - schema = DefaultSchema(PollCandidate()).get_delete_schema() diff --git a/openslides_backend/action/actions/poll_candidate_list/__init__.py b/openslides_backend/action/actions/poll_candidate_list/__init__.py deleted file mode 100644 index e2b11ce4af..0000000000 --- a/openslides_backend/action/actions/poll_candidate_list/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import create, delete # noqa diff --git a/openslides_backend/action/actions/poll_candidate_list/create.py b/openslides_backend/action/actions/poll_candidate_list/create.py deleted file mode 100644 index 5aaa2ebad0..0000000000 --- a/openslides_backend/action/actions/poll_candidate_list/create.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Any - -from ....models.models import PollCandidateList -from ....shared.schema import required_id_schema -from ...generics.create import CreateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from ..poll_candidate.create import PollCandidateCreate - -entry_schema = { - "description": "An entry in a poll_candidate_list create schema", - "type": "object", - "properties": { - "user_id": required_id_schema, - "weight": {"type": "integer"}, - }, - "additionalProperties": False, -} - - -@register_action("poll_candidate_list.create", action_type=ActionType.BACKEND_INTERNAL) -class PollCandidateListCreate(CreateAction): - """ - Internal action to create a poll_candidate_list. - """ - - model = PollCandidateList() - schema = DefaultSchema(PollCandidateList()).get_create_schema( - required_properties=["option_id", "meeting_id"], - additional_required_fields={ - "entries": {"type": "array", "items": entry_schema, "minItems": 1} - }, - ) - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - instance = super().update_instance(instance) - entries = instance.pop("entries") - self.apply_instance(instance) - res = self.execute_other_action( - PollCandidateCreate, - [ - { - "user_id": entry["user_id"], - "weight": entry["weight"], - "poll_candidate_list_id": instance["id"], - "meeting_id": instance["meeting_id"], - } - for entry in entries - ], - ) - instance["poll_candidate_ids"] = [r["id"] for r in res] # type: ignore - return instance diff --git a/openslides_backend/action/actions/poll_candidate_list/delete.py b/openslides_backend/action/actions/poll_candidate_list/delete.py deleted file mode 100644 index 4b87e9c187..0000000000 --- a/openslides_backend/action/actions/poll_candidate_list/delete.py +++ /dev/null @@ -1,15 +0,0 @@ -from ....models.models import PollCandidateList -from ...generics.delete import DeleteAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("poll_candidate_list.delete", action_type=ActionType.BACKEND_INTERNAL) -class PollCandidateListDelete(DeleteAction): - """ - Internal action to delete a poll candidate. - """ - - model = PollCandidateList() - schema = DefaultSchema(PollCandidateList()).get_delete_schema() diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py index 3a0cc6b408..809fa47e82 100644 --- a/openslides_backend/action/actions/user/merge_mixins.py +++ b/openslides_backend/action/actions/user/merge_mixins.py @@ -10,8 +10,10 @@ MotionWorkingGroupSpeaker, PersonalNote, Speaker, + Poll, + Ballot, + PollConfigOption, ) -from ....services.database.commands import GetManyRequest from ....shared.exceptions import ActionException from ....shared.filters import And, FilterOperator, Or from ....shared.patterns import Collection, fqid_from_collection_and_id @@ -111,6 +113,7 @@ def get_full_history_information(self) -> HistoryInformation | None: class MotionSubmitterMergeMixin(BaseMergeMixin): + # ??? def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.add_collection_field_groups( @@ -163,12 +166,45 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ) +class PollMergeMixin(BaseMergeMixin): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.add_collection_field_groups( + Poll, + {}, + "meeting_user_id", + ) + + +class PollConfigOptionMergeMixin(BaseMergeMixin): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.add_collection_field_groups( + PollConfigOption, + {}, + "meeting_user_id", + ) + + +class BallotMergeMixin(BaseMergeMixin): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.add_collection_field_groups( + Ballot, + {}, + "meeting_user_id", + ) + + class MeetingUserMergeMixin( PersonalNoteMergeMixin, MotionWorkingGroupSpeakerMergeMixin, MotionEditorMergeMixin, MotionSubmitterMergeMixin, AssignmentCandidateMergeMixin, + PollMergeMixin, + PollConfigOptionMergeMixin, + BallotMergeMixin, SpeakerMergeMixin, ): def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -194,6 +230,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "chat_message_ids", "group_ids", "structure_level_ids", + "poll_voted_ids", + "poll_option_ids", + "acting_ballot_ids", + "represented_ballot_ids", ], "deep_merge": { "assignment_candidate_ids": "assignment_candidate", @@ -240,23 +280,8 @@ def get_merge_comparison_hash( case _: return super().get_merge_comparison_hash(collection, model) - def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]: + def check_polls_helper(self, meeting_users: list[int]) -> list[str]: messages: list[str] = [] - meeting_users = self.datastore.get_many( - [ - GetManyRequest( - "meeting_user", - meeting_user_ids, - [ - "vote_delegations_from_ids", - "vote_delegated_to_id", - "meeting_id", - "group_ids", - ], - ) - ] - ).get("meeting_user", {}) - group_ids: set[int] = set() meeting_ids: set[int] = set() meeting_id_by_group_ids: dict[int, int] = {} @@ -271,7 +296,7 @@ def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]: polls = self.datastore.filter( "poll", And( - FilterOperator("state", "=", "started"), + FilterOperator("state", "=", Poll.STATE_STARTED), Or( FilterOperator("meeting_id", "=", meeting_id) for meeting_id in meeting_ids @@ -324,7 +349,7 @@ def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]: if len( bad_users := [ id_ - for id_ in meeting_user_ids + for id_ in list(meeting_users.keys()) if id_ in {*delegator_meeting_user_ids, *proxy_meeting_user_ids} ] ): diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py index 06355485b6..176b24f47a 100644 --- a/openslides_backend/action/actions/user/merge_together.py +++ b/openslides_backend/action/actions/user/merge_together.py @@ -1,8 +1,7 @@ from typing import Any, cast -from psycopg.types.json import Jsonb - from openslides_backend.services.database.interface import PartialModel +from openslides_backend.models.models import Poll from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin from ....models.models import User @@ -29,7 +28,6 @@ from ..motion_working_group_speaker.update import MotionWorkingGroupSpeakerUpdateAction from ..personal_note.create import PersonalNoteCreateAction from ..personal_note.update import PersonalNoteUpdateAction -from ..poll.update import PollUpdateAction from ..speaker.create_for_merge import SpeakerCreateForMerge from ..speaker.delete import SpeakerDeleteAction from ..speaker.update import SpeakerUpdate @@ -104,11 +102,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ], "merge": [ "committee_management_ids", - "option_ids", # throw error if conflict on same poll - "poll_voted_ids", # throw error if conflict on same poll - "vote_ids", # throw error if conflict on same poll - "delegated_vote_ids", # throw error if conflict on same poll - "poll_candidate_ids", # error if multiple of the users are on the same list, else merge ], "deep_create_merge": { "meeting_user_ids": "meeting_user", @@ -168,43 +161,6 @@ def get_updated_instances(self, action_data: ActionData) -> ActionData: raise ActionException( "Users cannot be part of different merges at the same time" ) - secondary_id_to_main_ids = { - user_id: instance["id"] - for instance in action_data - for user_id in instance.get("user_ids", []) - } - polls = self.datastore.filter( - "poll", - And( - FilterOperator("entitled_users_at_stop", "!=", None), - FilterOperator("entitled_users_at_stop", "!=", Jsonb([])), - ), - ["entitled_users_at_stop"], - ) - poll_payloads: list[dict[str, Any]] = [] - for id_, poll in polls.items(): - entitled: list[dict[str, Any]] = poll["entitled_users_at_stop"] - changed = False - for vote in entitled: - if ( - user_id := (vote.get("user_merged_into_id") or vote.get("user_id")) - ) in secondary_id_to_main_ids: - vote["user_merged_into_id"] = secondary_id_to_main_ids[user_id] - changed = True - if ( - user_id := ( - vote.get("delegation_user_merged_into_id") - or vote.get("vote_delegated_to_user_id") - ) - ) in secondary_id_to_main_ids: - vote["delegation_user_merged_into_id"] = secondary_id_to_main_ids[ - user_id - ] - changed = True - if changed: - poll_payloads.append({"id": id_, "entitled_users_at_stop": entitled}) - if len(poll_payloads): - self.execute_other_action(PollUpdateAction, poll_payloads) return super().get_updated_instances(action_data) def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: @@ -398,88 +354,161 @@ def call_other_actions( def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> None: all_models = [into, *other_models] - vote_poll_ids_per_user_id: dict[int, set[int]] = { - user["id"]: {poll_id for poll_id in user.get("poll_voted_ids", [])} - for user in [into, *other_models] + # user -> meeting user, same name + mu_ids_per_user = { + user["id"]: user.get("meeting_user_ids", []) for user in all_models + } + meeting_user_ids = [ + mu_id for user in all_models for mu_id in user.get("meeting_user_ids", []) + ] + poll_conflict_fields = [ + "poll_voted_ids", + "poll_option_ids", + "acting_ballot_ids", + "represented_ballot_ids", + ] + helper_fields = [ + "vote_delegations_from_ids", + "vote_delegated_to_id", + "meeting_id", + "group_ids", + ] + mu_data = self.datastore.get_many( + [ + GetManyRequest( + "meeting_user", + meeting_user_ids, + ["user_id"] + poll_conflict_fields + helper_fields, + ) + ] + )["meeting_user"] + + vote_poll_ids_per_meeting_user_id: dict[int, set[int]] = { + meeting_user["id"]: { + "user_id": meeting_user["user_id"], + "poll_voted_ids": poll_voted_ids, + } + for id_, meeting_user in mu_data.items() + if (poll_voted_ids := meeting_user.get("poll_voted_ids")) } + + ballot_poll_ids_per_user_id: dict[int, set[int]] = {} + for meeting_user in vote_poll_ids_per_meeting_user_id.values(): + ballot_poll_ids_per_user_id.setdefault( + meeting_user["user_id"], set() + ).update(set(meeting_user["poll_voted_ids"])) + # acting_vote_ids, represented_vote_ids -> mu + # meeting_user.poll_option_ids + # list: option of config-approval with options option_poll_ids_per_user_id: dict[int, set[int]] = {} candidate_list_ids_per_user_id: dict[int, set[int]] = {} meeting_user_ids: list[int] = [] - for model in all_models: + for model in mu_data.values(): if len( - (pc_ids := model.get("poll_candidate_ids", [])) - + (o_ids := model.get("option_ids", [])) + # (pc_ids := model.get("poll_candidate_ids", [])) + # +(o_ids := model.get("poll_option_ids", [])) + (o_ids := model.get("poll_option_ids", [])) + ( v_ids := list( { id_ for id_ in [ - *model.get("vote_ids", []), - *model.get("delegated_vote_ids", []), + *model.get("acting_ballot_ids", []), + *model.get("represented_ballot_ids", []), ] } ) ) ): get_many_requests = [ + # GetManyRequest( + # "poll_candidate", pc_ids, ["poll_candidate_list_id"] + # ), GetManyRequest( - "poll_candidate", pc_ids, ["poll_candidate_list_id"] - ), - GetManyRequest( - "option", + "poll_config_option", o_ids, - ["poll_id"], + ["poll_config_id"], ), GetManyRequest( - "vote", + "ballot", v_ids, - ["option_id"], + ["poll_id"], ), ] many_models = self.datastore.get_many(get_many_requests) - if pc_ids: - candidate_list_ids_per_user_id[model["id"]] = { - poll_candidate["poll_candidate_list_id"] - for poll_candidate in many_models["poll_candidate"].values() - if poll_candidate.get("poll_candidate_list_id") - } + # if pc_ids: + # candidate_list_ids_per_user_id[model["id"]] = { + # poll_candidate["poll_candidate_list_id"] + # for poll_candidate in many_models["poll_candidate"].values() + # if poll_candidate.get("poll_candidate_list_id") + # } + # if o_ids: + # option_poll_ids_per_user_id[model["id"]] = { + # option["poll_id"] + # for option in many_models["option"].values() + # if option.get("poll_id") + # } if o_ids: - option_poll_ids_per_user_id[model["id"]] = { - option["poll_id"] - for option in many_models["option"].values() - if option.get("poll_id") + option_data = many_models["poll_config_option"] + config_ids_per_collection = {} + for config in option_data.values(): + collection, id_ = config["poll_config_id"].split("/") + config_ids_per_collection.setdefault(collection, set()).update( + id_ + ) + gmr = [ + GetManyRequest( + collection, + [int(id_) for id_ in ids], + ["poll_id"], + ) + for collection, ids in config_ids_per_collection.items() + ] + r = self.datastore.get_many(gmr) + option_poll_ids_per_user_id[model["user_id"]] = { + *option_poll_ids_per_user_id.get(model["user_id"], set()), + *{ + cast(int, option["poll_id"]) + for collection, config in self.datastore.get_many( + gmr + ).items() + for option in config.values() + if collection != Poll.CONFIG_TYPE_APPROVAL + }, } - vote_data = many_models["vote"] - vote_poll_ids_per_user_id[model["id"]] = { - *vote_poll_ids_per_user_id.get(model["id"], set()), + candidate_list_ids_per_user_id[model["user_id"]] = { + *candidate_list_ids_per_user_id.get(model["user_id"], set()), + *{ + cast(int, option["poll_id"]) + for collection, config in self.datastore.get_many( + gmr + ).items() + for option in config.values() + if collection == Poll.CONFIG_TYPE_APPROVAL + }, + } + + ballot_data = many_models["ballot"] + ballot_poll_ids_per_user_id[model["user_id"]] = { + *ballot_poll_ids_per_user_id.get(model["user_id"], set()), *{ - cast(int, option["poll_id"]) - for option in self.datastore.get_many( - [ - GetManyRequest( - "option", - list( - { - id_ - for id_ in [ - vote["option_id"] - for vote in vote_data.values() - if vote.get("option_id") - ] - } - ), - ["poll_id"], - ) - ] - )["option"].values() - if option.get("poll_id") + id_ + for id_ in [ + ballot["poll_id"] for ballot in ballot_data.values() + ] }, } - meeting_user_ids += model.get("meeting_user_ids", []) + # if v_ids: + # vote_poll_ids_per_user_id[model["id"]] = { + # vote["poll_id"] + # for vote in many_models["vote"].values() + # if vote.get("poll_id") + # } + # meeting_user_ids += model.get("meeting_user_ids", []) voting_conflicts = { poll_id - for id1, poll_ids1 in vote_poll_ids_per_user_id.items() - for id2, poll_ids2 in vote_poll_ids_per_user_id.items() + for id1, poll_ids1 in ballot_poll_ids_per_user_id.items() + for id2, poll_ids2 in ballot_poll_ids_per_user_id.items() for poll_id in poll_ids1.intersection(poll_ids2) if id1 != id2 } @@ -497,7 +526,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N for list_id in list_ids1.intersection(list_ids2) if id1 != id2 } - messages: list[str] = self.check_polls_helper(meeting_user_ids) + messages: list[str] = self.check_polls_helper(mu_data) if len(voting_conflicts): messages.append( f"among the selected users multiple voted in poll(s) {', '.join([str(id_) for id_ in voting_conflicts])}" @@ -507,24 +536,8 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N f"multiple of the selected users are among the options in poll(s) {', '.join([str(id_) for id_ in option_conflicts])}" ) if len(candidate_list_conflicts): - lists = self.datastore.get_many( - [ - GetManyRequest( - "poll_candidate_list", - list(candidate_list_conflicts), - ["option_id"], - ) - ], - lock_result=False, - )["poll_candidate_list"] - option_ids = {c_list["option_id"] for c_list in lists.values()} - options = self.datastore.get_many( - [GetManyRequest("option", list(option_ids), ["poll_id"])], - lock_result=False, - )["option"] - poll_ids = {option["poll_id"] for option in options.values()} messages.append( - f"multiple of the selected users are in the same candidate list in poll(s) {', '.join([str(id_) for id_ in poll_ids])}" + f"multiple of the selected users are in the same candidate list in poll(s) {', '.join([str(id_) for id_ in candidate_list_conflicts])}" ) if len(messages): raise ActionException( diff --git a/openslides_backend/action/actions/user/update.py b/openslides_backend/action/actions/user/update.py index 4a9ba87440..ef9612dddc 100644 --- a/openslides_backend/action/actions/user/update.py +++ b/openslides_backend/action/actions/user/update.py @@ -45,14 +45,7 @@ class UserUpdate( Action to update a user. """ - internal_id_fields = [ - "is_present_in_meeting_ids", - "option_ids", - "poll_candidate_ids", - "poll_voted_ids", - "vote_ids", - "delegated_vote_ids", - ] + internal_id_fields = ["is_present_in_meeting_ids"] model = User() schema = DefaultSchema(User()).get_update_schema( diff --git a/openslides_backend/action/actions/vote/__init__.py b/openslides_backend/action/actions/vote/__init__.py deleted file mode 100644 index 2d608629cc..0000000000 --- a/openslides_backend/action/actions/vote/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import anonymize, create, delete, update # noqa diff --git a/openslides_backend/action/actions/vote/anonymize.py b/openslides_backend/action/actions/vote/anonymize.py deleted file mode 100644 index 3a0ea659a8..0000000000 --- a/openslides_backend/action/actions/vote/anonymize.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Any - -from ....models.models import Vote -from ...generics.update import UpdateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("vote.anonymize", action_type=ActionType.BACKEND_INTERNAL) -class VoteAnonymize(UpdateAction): - """ - Action to anonymize a vote by removing the user ids. - """ - - model = Vote() - schema = DefaultSchema(Vote()).get_update_schema() - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - instance["user_id"] = None - instance["delegated_user_id"] = None - return instance diff --git a/openslides_backend/action/actions/vote/create.py b/openslides_backend/action/actions/vote/create.py deleted file mode 100644 index 2846ce44cb..0000000000 --- a/openslides_backend/action/actions/vote/create.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import cast - -from openslides_backend.action.util.typing import ActionData -from openslides_backend.services.database.commands import GetManyRequest - -from ....models.models import Vote -from ...mixins.create_action_with_inferred_meeting import ( - CreateActionWithInferredMeeting, -) -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("vote.create", action_type=ActionType.BACKEND_INTERNAL) -class VoteCreate(CreateActionWithInferredMeeting): - """ - Internal action to create a vote. - """ - - model = Vote() - schema = DefaultSchema(Vote()).get_create_schema( - required_properties=[ - "weight", - "value", - "option_id", - "user_token", - ], - optional_properties=["delegated_user_id", "user_id"], - ) - - relation_field_for_meeting = "option_id" - - def prefetch(self, action_data: ActionData) -> None: - self.datastore.get_many( - [ - GetManyRequest( - "option", - list({instance["option_id"] for instance in action_data}), - ["meeting_id", "vote_ids"], - ), - ], - use_changed_models=False, - ) - meeting_users = self.datastore.get_many( - [ - GetManyRequest( - "meeting_user", - list( - { - cast(int, instance.get(fname)) - for instance in action_data - for fname in ( - "meeting_user_id", - "delegated_meeting_user_id", - ) - if instance.get(fname) - } - ), - ["id", "user_id", "vote_ids", "delegated_vote_ids"], - ), - ], - use_changed_models=False, - )["meeting_user"] - - self.datastore.get_many( - [ - GetManyRequest( - "user", - list({mu["user_id"] for mu in meeting_users.values()}), - ["id", "poll_voted_ids"], - ), - ], - use_changed_models=False, - ) diff --git a/openslides_backend/action/actions/vote/delete.py b/openslides_backend/action/actions/vote/delete.py deleted file mode 100644 index 099e24459c..0000000000 --- a/openslides_backend/action/actions/vote/delete.py +++ /dev/null @@ -1,15 +0,0 @@ -from ....models.models import Vote -from ...generics.delete import DeleteAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("vote.delete", action_type=ActionType.BACKEND_INTERNAL) -class VoteDelete(DeleteAction): - """ - Action to delete votes. - """ - - model = Vote() - schema = DefaultSchema(Vote()).get_delete_schema() diff --git a/openslides_backend/action/actions/vote/update.py b/openslides_backend/action/actions/vote/update.py deleted file mode 100644 index fa34189f1c..0000000000 --- a/openslides_backend/action/actions/vote/update.py +++ /dev/null @@ -1,15 +0,0 @@ -from ....models.models import Vote -from ...generics.update import UpdateAction -from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema -from ...util.register import register_action - - -@register_action("vote.update", action_type=ActionType.BACKEND_INTERNAL) -class VoteUpdate(UpdateAction): - """ - Internal action to update a vote. - """ - - model = Vote() - schema = DefaultSchema(Vote()).get_update_schema(required_properties=["weight"]) diff --git a/openslides_backend/action/actions/vote/user_token_helper.py b/openslides_backend/action/actions/vote/user_token_helper.py deleted file mode 100644 index e993d0922f..0000000000 --- a/openslides_backend/action/actions/vote/user_token_helper.py +++ /dev/null @@ -1,5 +0,0 @@ -from ...util.crypto import get_random_string - - -def get_user_token() -> str: - return get_random_string(16) diff --git a/openslides_backend/models/checker.py b/openslides_backend/models/checker.py index 5dabac5b92..547fe71cc4 100644 --- a/openslides_backend/models/checker.py +++ b/openslides_backend/models/checker.py @@ -173,7 +173,17 @@ def check_timestamp(value: Any) -> bool: collection for collection, model in model_registry.items() if model().has_field("meeting_id") -} | {"meeting", "user", "mediafile"} +} | { + "meeting", + "user", + "mediafile", + "vote", + "poll_config_approval", + "poll_config_selection", + "poll_config_rating_score", + "poll_config_rating_approval", + "poll_config_option", +} class Checker: @@ -454,6 +464,13 @@ def check_relation( if collection == "user" and field == "organization_id": return + if ( + collection == "vote" + and field == "poll_id" + and str(model["poll_id"]) in self.data.get("poll", {}) + ): + return + if isinstance(field_type, RelationField): foreign_id = model[field] if not foreign_id: diff --git a/openslides_backend/models/mixins.py b/openslides_backend/models/mixins.py index ee78890774..01afc8729e 100644 --- a/openslides_backend/models/mixins.py +++ b/openslides_backend/models/mixins.py @@ -78,11 +78,16 @@ class PollModelMixin: STATE_CREATED = "created" STATE_STARTED = "started" STATE_FINISHED = "finished" - STATE_PUBLISHED = "published" - TYPE_ANALOG = "analog" - TYPE_NAMED = "named" - TYPE_PSEUDOANONYMOUS = "pseudoanonymous" + VISIBILITY_MANUALLY = "manually" + VISIBILITY_NAMED = "named" + VISIBILITY_OPEN = "open" + VISIBILITY_SECRET = "secret" + + CONFIG_TYPE_APPROVAL = "poll_config_approval" + CONFIG_TYPE_SELECTION = "poll_config_selection" + CONFIG_TYPE_RATING_SCORE = "poll_config_rating_score" + CONFIG_TYPE_RATING_APPROVAL = "poll_config_rating_approval" ONEHUNDRED_PERCENT_BASE_Y = "Y" ONEHUNDRED_PERCENT_BASE_YN = "YN" @@ -93,3 +98,6 @@ class PollModelMixin: ONEHUNDRED_PERCENT_BASE_ENTITLED = "entitled" ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT = "entitled_present" ONEHUNDRED_PERCENT_BASE_DISABLED = "disabled" + + OPTION_TYPE_TEXT = "text" + OPTION_TYPE_MEETING_USER = "meeting_user" diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py index 047f6658ed..a34da079a9 100644 --- a/openslides_backend/models/models.py +++ b/openslides_backend/models/models.py @@ -46,11 +46,6 @@ class Organization(Model): saml_metadata_idp = fields.TextField() saml_metadata_sp = fields.TextField() saml_private_key = fields.TextField() - vote_decrypt_public_main_key = fields.CharField( - constraints={ - "description": "Public key from vote decrypt to validate cryptographic votes." - } - ) committee_ids = fields.RelationListField( to={"committee": "organization_id"}, is_view_field=True, is_primary=True ) @@ -157,21 +152,6 @@ class User(Model): on_delete=fields.OnDelete.CASCADE, is_view_field=True, ) - poll_voted_ids = fields.RelationListField( - to={"poll": "voted_ids"}, - is_view_field=True, - write_fields=("nm_poll_voted_ids_user_t", "user_id", "poll_id", []), - ) - option_ids = fields.RelationListField( - to={"option": "content_object_id"}, is_view_field=True - ) - vote_ids = fields.RelationListField(to={"vote": "user_id"}, is_view_field=True) - delegated_vote_ids = fields.RelationListField( - to={"vote": "delegated_user_id"}, is_view_field=True - ) - poll_candidate_ids = fields.RelationListField( - to={"poll_candidate": "user_id"}, is_view_field=True - ) home_committee_id = fields.RelationField(to={"committee": "native_user_ids"}) history_position_ids = fields.RelationListField( to={"history_position": "user_id"}, is_view_field=True @@ -260,6 +240,28 @@ class MeetingUser(Model): is_view_field=True, equal_fields="meeting_id", ) + poll_voted_ids = fields.RelationListField( + to={"poll": "voted_ids"}, + is_view_field=True, + is_primary=True, + write_fields=( + "nm_meeting_user_poll_voted_ids_poll_t", + "meeting_user_id", + "poll_id", + [], + ), + ) + poll_option_ids = fields.RelationListField( + to={"poll_config_option": "meeting_user_id"}, + is_view_field=True, + is_primary=True, + ) + acting_ballot_ids = fields.RelationListField( + to={"ballot": "acting_meeting_user_id"}, is_view_field=True + ) + represented_ballot_ids = fields.RelationListField( + to={"ballot": "represented_meeting_user_id"}, is_view_field=True + ) chat_message_ids = fields.RelationListField( to={"chat_message": "meeting_user_id"}, is_view_field=True, @@ -720,12 +722,6 @@ class Meeting(Model, MeetingModelMixin): constraints={"enum": ["first_name", "last_name"]}, ) motion_poll_projection_max_columns = fields.IntegerField(required=True, default=6) - poll_candidate_list_ids = fields.RelationListField( - to={"poll_candidate_list": "meeting_id"}, is_view_field=True - ) - poll_candidate_ids = fields.RelationListField( - to={"poll_candidate": "meeting_id"}, is_view_field=True - ) meeting_user_ids = fields.RelationListField( to={"meeting_user": "meeting_id"}, on_delete=fields.OnDelete.CASCADE, @@ -828,13 +824,22 @@ class Meeting(Model, MeetingModelMixin): poll_default_group_ids = fields.RelationListField( to={"group": "used_as_poll_default_id"}, is_view_field=True ) - poll_default_backend = fields.CharField( - default="fast", constraints={"enum": ["long", "fast"]} - ) poll_default_live_voting_enabled = fields.BooleanField( default=False, constraints={ - "description": "Defines default 'poll.live_voting_enabled' option suggested to user. Is not used in the validations." + "description": "Defines default 'poll.published' before finished option suggested to user. Is not used in the validations." + }, + ) + poll_default_allow_invalid = fields.BooleanField( + default=False, + constraints={ + "description": "Defines defaut `poll.allow_invalid` option suggested to user." + }, + ) + poll_default_allow_vote_split = fields.BooleanField( + default=False, + constraints={ + "description": "Defines defaut `poll.allow_vote_split` option suggested to user." }, ) poll_couple_countdown = fields.BooleanField(default=True) @@ -972,14 +977,6 @@ class Meeting(Model, MeetingModelMixin): poll_ids = fields.RelationListField( to={"poll": "meeting_id"}, on_delete=fields.OnDelete.CASCADE, is_view_field=True ) - option_ids = fields.RelationListField( - to={"option": "meeting_id"}, - on_delete=fields.OnDelete.CASCADE, - is_view_field=True, - ) - vote_ids = fields.RelationListField( - to={"vote": "meeting_id"}, on_delete=fields.OnDelete.CASCADE, is_view_field=True - ) assignment_ids = fields.RelationListField( to={"assignment": "meeting_id"}, on_delete=fields.OnDelete.CASCADE, @@ -1523,7 +1520,8 @@ class ListOfSpeakers(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) moderator_notes = fields.HTMLStrictField() @@ -1685,7 +1683,8 @@ class Topic(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) attachment_meeting_mediafile_ids = fields.RelationListField( @@ -1749,7 +1748,8 @@ class Motion(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) title = fields.CharField(required=True) @@ -1918,12 +1918,6 @@ class Motion(Model): is_view_field=True, equal_fields="meeting_id", ) - option_ids = fields.RelationListField( - to={"option": "content_object_id"}, - on_delete=fields.OnDelete.CASCADE, - is_view_field=True, - equal_fields="meeting_id", - ) change_recommendation_ids = fields.RelationListField( to={"motion_change_recommendation": "motion_id"}, on_delete=fields.OnDelete.CASCADE, @@ -2082,7 +2076,8 @@ class MotionCommentSection(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) submitter_can_write = fields.BooleanField() @@ -2135,7 +2130,8 @@ class MotionCategory(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) parent_id = fields.RelationField( @@ -2166,7 +2162,8 @@ class MotionBlock(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) motion_ids = fields.RelationListField( @@ -2329,7 +2326,8 @@ class MotionWorkflow(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) state_ids = fields.RelationListField( @@ -2359,107 +2357,71 @@ class Poll(Model, PollModelMixin): verbose_name = "poll" id = fields.IntegerField(required=True, constant=True) - description = fields.TextField() title = fields.CharField(required=True) - type = fields.CharField( + config_id = fields.GenericRelationField( + to={ + "poll_config_rating_approval": "poll_id", + "poll_config_rating_score": "poll_id", + "poll_config_selection": "poll_id", + "poll_config_approval": "poll_id", + }, required=True, - constraints={"enum": ["analog", "named", "pseudoanonymous", "cryptographic"]}, - ) - backend = fields.CharField( - required=True, default="fast", constraints={"enum": ["long", "fast"]} ) - is_pseudoanonymized = fields.BooleanField() - pollmethod = fields.CharField( - required=True, constraints={"enum": ["Y", "YN", "YNA", "N"]} + visibility = fields.CharField( + required=True, constraints={"enum": ["manually", "named", "open", "secret"]} ) state = fields.CharField( - default="created", - constraints={"enum": ["created", "started", "finished", "published"]}, - ) - min_votes_amount = fields.IntegerField(default=1, constraints={"minimum": 1}) - max_votes_amount = fields.IntegerField(default=1, constraints={"minimum": 1}) - max_votes_per_option = fields.IntegerField(default=1, constraints={"minimum": 1}) - global_yes = fields.BooleanField(default=False) - global_no = fields.BooleanField(default=False) - global_abstain = fields.BooleanField(default=False) - onehundred_percent_base = fields.CharField( - required=True, - default="disabled", + default="created", constraints={"enum": ["created", "started", "finished"]} + ) + result = fields.TextField( constraints={ - "enum": [ - "Y", - "YN", - "YNA", - "N", - "valid", - "cast", - "entitled", - "entitled_present", - "disabled", - ] - }, + "description": "Calculated result. The format depends on the value in poll/method. Can be manually set when visibility is set to manually." + } + ) + published = fields.BooleanField( + default=False, constraints={"description": "If true, users can see the result."} ) - votesvalid = fields.DecimalField() - votesinvalid = fields.DecimalField() - votescast = fields.DecimalField() - entitled_users_at_stop = fields.JSONField() - live_voting_enabled = fields.BooleanField( + allow_invalid = fields.BooleanField( default=False, constraints={ - "description": "If true, the vote service sends the votes of the users to the autoupdate service." + "description": "If true, the vote service does not validate. This is always the case for secret polls." }, ) - live_votes = fields.JSONField( - constraints={ - "description": "dict from user to their vote. The value is null, when live voting is disabled." - } + allow_vote_split = fields.BooleanField( + default=False, + constraints={"description": "If true, users can split there vote."}, ) sequential_number = fields.IntegerField( required=True, read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) - crypt_key = fields.CharField( - read_only=True, - constraints={"description": "base64 public key to cryptographic votes."}, - ) - crypt_signature = fields.CharField( - read_only=True, - constraints={"description": "base64 signature of cryptographic_key."}, - ) - votes_raw = fields.TextField( - read_only=True, constraints={"description": "original form of decrypted votes."} - ) - votes_signature = fields.CharField( - read_only=True, - constraints={"description": "base64 signature of votes_raw field."}, - ) content_object_id = fields.GenericRelationField( to={"motion": "poll_ids", "assignment": "poll_ids", "topic": "poll_ids"}, required=True, constant=True, equal_fields="meeting_id", ) - option_ids = fields.RelationListField( - to={"option": "poll_id"}, + ballot_ids = fields.RelationListField( + to={"ballot": "poll_id"}, on_delete=fields.OnDelete.CASCADE, is_view_field=True, - equal_fields="meeting_id", - ) - global_option_id = fields.RelationField( - to={"option": "used_as_global_option_in_poll_id"}, - on_delete=fields.OnDelete.CASCADE, - constant=True, + is_primary=True, equal_fields="meeting_id", ) voted_ids = fields.RelationListField( - to={"user": "poll_voted_ids"}, + to={"meeting_user": "poll_voted_ids"}, is_view_field=True, - is_primary=True, - write_fields=("nm_poll_voted_ids_user_t", "poll_id", "user_id", []), + write_fields=( + "nm_meeting_user_poll_voted_ids_poll_t", + "poll_id", + "meeting_user_id", + [], + ), ) entitled_group_ids = fields.RelationListField( to={"group": "poll_ids"}, @@ -2478,63 +2440,108 @@ class Poll(Model, PollModelMixin): ) -class Option(Model): - collection = "option" - verbose_name = "option" +class PollConfigApproval(Model): + collection = "poll_config_approval" + verbose_name = "poll config approval" id = fields.IntegerField(required=True, constant=True) - weight = fields.IntegerField(default=10000) - text = fields.HTMLStrictField() - yes = fields.DecimalField() - no = fields.DecimalField() - abstain = fields.DecimalField() - poll_id = fields.RelationField( - to={"poll": "option_ids"}, constant=True, equal_fields="meeting_id" + poll_id = fields.RelationField(to={"poll": "config_id"}, required=True) + option_ids = fields.RelationListField( + to={"poll_config_option": "poll_config_id"}, + on_delete=fields.OnDelete.CASCADE, + is_view_field=True, ) - used_as_global_option_in_poll_id = fields.RelationField( - to={"poll": "global_option_id"}, + allow_abstain = fields.BooleanField(default=True) + + +class PollConfigSelection(Model): + collection = "poll_config_selection" + verbose_name = "poll config selection" + + id = fields.IntegerField(required=True, constant=True) + poll_id = fields.RelationField(to={"poll": "config_id"}, required=True) + option_ids = fields.RelationListField( + to={"poll_config_option": "poll_config_id"}, + on_delete=fields.OnDelete.CASCADE, is_view_field=True, - constant=True, - equal_fields="meeting_id", ) - vote_ids = fields.RelationListField( - to={"vote": "option_id"}, + max_options_amount = fields.IntegerField(default=0) + min_options_amount = fields.IntegerField(default=0) + allow_nota = fields.BooleanField(default=False) + + +class PollConfigRatingScore(Model): + collection = "poll_config_rating_score" + verbose_name = "poll config rating score" + + id = fields.IntegerField(required=True, constant=True) + poll_id = fields.RelationField(to={"poll": "config_id"}, required=True) + option_ids = fields.RelationListField( + to={"poll_config_option": "poll_config_id"}, on_delete=fields.OnDelete.CASCADE, is_view_field=True, - equal_fields="meeting_id", ) - content_object_id = fields.GenericRelationField( + max_options_amount = fields.IntegerField(default=0) + min_options_amount = fields.IntegerField(default=0) + max_votes_per_option = fields.IntegerField(default=0) + max_vote_sum = fields.IntegerField(default=0) + min_vote_sum = fields.IntegerField(default=0) + + +class PollConfigRatingApproval(Model): + collection = "poll_config_rating_approval" + verbose_name = "poll config rating approval" + + id = fields.IntegerField(required=True, constant=True) + poll_id = fields.RelationField(to={"poll": "config_id"}, required=True) + option_ids = fields.RelationListField( + to={"poll_config_option": "poll_config_id"}, + on_delete=fields.OnDelete.CASCADE, + is_view_field=True, + ) + max_options_amount = fields.IntegerField(default=0) + min_options_amount = fields.IntegerField(default=0) + allow_abstain = fields.BooleanField(default=True) + + +class PollConfigOption(Model): + collection = "poll_config_option" + verbose_name = "poll config option" + + id = fields.IntegerField(required=True, constant=True) + poll_config_id = fields.GenericRelationField( to={ - "poll_candidate_list": "option_id", - "user": "option_ids", - "motion": "option_ids", + "poll_config_rating_approval": "option_ids", + "poll_config_rating_score": "option_ids", + "poll_config_selection": "option_ids", + "poll_config_approval": "option_ids", }, - constant=True, - equal_fields="meeting_id", - ) - meeting_id = fields.RelationField( - to={"meeting": "option_ids"}, required=True, constant=True + required=True, ) + weight = fields.IntegerField() + text = fields.CharField() + meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"}) -class Vote(Model): - collection = "vote" - verbose_name = "vote" +class Ballot(Model): + collection = "ballot" + verbose_name = "ballot" id = fields.IntegerField(required=True, constant=True) - weight = fields.DecimalField(constant=True) - value = fields.CharField(constant=True) - user_token = fields.CharField(required=True, constant=True) - option_id = fields.RelationField( - to={"option": "vote_ids"}, + weight = fields.DecimalField(constant=True, default="1.000000") + split = fields.BooleanField(default=False) + value = fields.TextField(constant=True) + poll_id = fields.RelationField( + to={"poll": "ballot_ids"}, required=True, constant=True, equal_fields="meeting_id", ) - user_id = fields.RelationField(to={"user": "vote_ids"}) - delegated_user_id = fields.RelationField(to={"user": "delegated_vote_ids"}) - meeting_id = fields.RelationField( - to={"meeting": "vote_ids"}, required=True, constant=True + acting_meeting_user_id = fields.RelationField( + to={"meeting_user": "acting_ballot_ids"} + ) + represented_meeting_user_id = fields.RelationField( + to={"meeting_user": "represented_ballot_ids"} ) @@ -2556,7 +2563,8 @@ class Assignment(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) candidate_ids = fields.RelationListField( @@ -2641,47 +2649,6 @@ class AssignmentCandidate(Model): ) -class PollCandidateList(Model): - collection = "poll_candidate_list" - verbose_name = "poll candidate list" - - id = fields.IntegerField(required=True, constant=True) - poll_candidate_ids = fields.RelationListField( - to={"poll_candidate": "poll_candidate_list_id"}, - on_delete=fields.OnDelete.CASCADE, - is_view_field=True, - equal_fields="meeting_id", - ) - meeting_id = fields.RelationField( - to={"meeting": "poll_candidate_list_ids"}, required=True, constant=True - ) - option_id = fields.RelationField( - to={"option": "content_object_id"}, - is_view_field=True, - required=True, - constant=True, - equal_fields="meeting_id", - ) - - -class PollCandidate(Model): - collection = "poll_candidate" - verbose_name = "poll candidate" - - id = fields.IntegerField(required=True, constant=True) - poll_candidate_list_id = fields.RelationField( - to={"poll_candidate_list": "poll_candidate_ids"}, - required=True, - constant=True, - equal_fields="meeting_id", - ) - user_id = fields.RelationField(to={"user": "poll_candidate_ids"}, constant=True) - weight = fields.IntegerField(required=True) - meeting_id = fields.RelationField( - to={"meeting": "poll_candidate_ids"}, required=True, constant=True - ) - - class Mediafile(Model): collection = "mediafile" verbose_name = "mediafile" @@ -2876,7 +2843,8 @@ class Projector(Model): read_only=True, constant=True, constraints={ - "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only." + "sequence_scope": "meeting_id", + "description": "The (positive) serial number of this model in its meeting. This number is auto-generated and read-only.", }, ) current_projection_ids = fields.RelationListField( diff --git a/openslides_backend/services/vote/adapter.py b/openslides_backend/services/vote/adapter.py index 67a000bbc8..740a1cfdc9 100644 --- a/openslides_backend/services/vote/adapter.py +++ b/openslides_backend/services/vote/adapter.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Literal import requests import simplejson as json @@ -54,21 +54,35 @@ def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) -> ) raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.") - def start(self, id: int) -> None: - endpoint = self.get_endpoint("start", id) - self.retrieve(endpoint) + def create(self, payload: dict[str, Any]) -> dict[str, Any]: + endpoint = self.get_endpoint("create") + return self.retrieve(endpoint, payload) + + def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: + endpoint = self.get_endpoint("update", id) + return self.retrieve(endpoint, payload) - def stop(self, id: int) -> dict[str, Any]: - endpoint = self.get_endpoint("stop", id) + def delete(self, id: int) -> dict[str, Any]: + endpoint = self.get_endpoint("delete", id) return self.retrieve(endpoint) - def clear(self, id: int) -> None: - endpoint = self.get_endpoint("clear", id) - self.retrieve(endpoint) + def start(self, id: int) -> dict[str, Any]: + endpoint = self.get_endpoint("start", id) + return self.retrieve(endpoint) - def clear_all(self) -> None: - endpoint = self.get_endpoint("clear_all") - self.retrieve(endpoint) + def finalize( + self, + id: int, + optional_attributes: list[Literal["publish", "anonymize"]] = [], + ) -> dict[str, Any]: + endpoint = self.get_endpoint("finalize", id) + if optional_attributes: + endpoint += f"&{'&'.join(optional_attributes)}" + return self.retrieve(endpoint) + + def reset(self, id: int) -> dict[str, Any]: + endpoint = self.get_endpoint("reset", id) + return self.retrieve(endpoint) def get_endpoint(self, route: str, id: int | None = None) -> str: return f"{self.url}/{route}" + (f"?id={id}" if id else "") diff --git a/openslides_backend/services/vote/interface.py b/openslides_backend/services/vote/interface.py index 7c22cb3af4..7a468a62f0 100644 --- a/openslides_backend/services/vote/interface.py +++ b/openslides_backend/services/vote/interface.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Any, Protocol +from typing import Any, Literal, Protocol from ..shared.authenticated_service import AuthenticatedServiceInterface @@ -10,14 +10,21 @@ class VoteService(AuthenticatedServiceInterface, Protocol): """ @abstractmethod - def start(self, id: int) -> None: ... + def create(self, payload: dict[str, Any]) -> dict[str, Any]: ... @abstractmethod - def stop(self, id: int) -> dict[str, Any]: ... + def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ... @abstractmethod - def clear(self, id: int) -> None: ... + def delete(self, id: int) -> dict[str, Any]: ... @abstractmethod - def clear_all(self) -> None: - """Only for testing purposes.""" + def start(self, id: int) -> dict[str, Any]: ... + + @abstractmethod + def finalize( + self, id: int, optional_attributes: list[Literal["publish", "anonymize"]] = [] + ) -> dict[str, Any]: ... + + @abstractmethod + def reset(self, id: int) -> dict[str, Any]: ... diff --git a/openslides_backend/shared/env.py b/openslides_backend/shared/env.py index 01dca41d14..f0098c149d 100644 --- a/openslides_backend/shared/env.py +++ b/openslides_backend/shared/env.py @@ -45,7 +45,7 @@ class Environment(Env): "OPENTELEMETRY_ENABLED": "false", "PRESENTER_PORT": "9003", "VOTE_HOST": "vote", - "VOTE_PATH": "/internal/vote", + "VOTE_PATH": "/system/vote", "VOTE_PORT": "9013", "VOTE_PROTOCOL": "http", "DATABASE_HOST": "", diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py index 84b542dea5..b368fe382b 100644 --- a/openslides_backend/shared/export_helper.py +++ b/openslides_backend/shared/export_helper.py @@ -12,15 +12,13 @@ RelationField, RelationListField, ) -from ..models.models import Meeting +from ..models.models import Meeting, Ballot from ..services.database.commands import GetManyRequest from ..services.database.interface import Database from .patterns import collection_from_fqid, fqid_from_collection_and_id, id_from_fqid FORBIDDEN_FIELDS = ["forwarded_motion_ids"] -NON_CASCADING_MEETING_RELATION_LISTS = ["poll_candidate_list_ids", "poll_candidate_ids"] - def export_meeting( datastore: Database, meeting_id: int, internal_target: bool = False @@ -136,6 +134,59 @@ def export_meeting( user_ids.add(results["meeting_user"][id_]["user_id"]) add_users(list(user_ids), export, meeting_id, datastore, internal_target) + # Get related ballots + if polls := export.get("poll"): + ballot_ids = [ + ballot_id + for poll in polls.values() + for ballot_id in poll.get("ballot_ids", []) + ] + if ballot_ids: + ballots = datastore.get_many( + [GetManyRequest("ballot", ballot_ids, get_fields_for_export("ballot"))], + lock_result=False, + use_changed_models=False, + )["ballot"] + for ballot_id, instance in ballots.items(): + export.setdefault("ballot", {})[str(ballot_id)] = instance + for field_name, value in instance.items(): + model_field = Ballot().get_field(field_name) + if ( + not isinstance(model_field, RelationField) + or model_field.get_own_field_name() == "poll_id" + ): + continue + collection, relation_field = next(iter(model_field.to.items())) + export[collection][str(value)].setdefault( + relation_field, [] + ).append(ballot_id) + + config_ids = [poll.get("config_id") for poll in polls.values()] + + for config_fqid in config_ids: + collection, id_ = config_fqid.split("/") + instance = datastore.get( + config_fqid, + get_fields_for_export(collection), + lock_result=False, + use_changed_models=False, + ) + export.setdefault(collection, {})[str(id_)] = instance + if option_ids := instance.get("option_ids"): + options = datastore.get_many( + [ + GetManyRequest( + "poll_config_option", + option_ids, + get_fields_for_export("poll_config_option"), + ) + ], + lock_result=False, + use_changed_models=False, + )["poll_config_option"] + for id_, option in options.items(): + export.setdefault("poll_config_option", {})[str(id_)] = option + # Sort instances by id within each collection for collection, instances in export.items(): if collection == "_migration_index": @@ -201,15 +252,9 @@ def add_users( gender_dict = datastore.get_all("gender", ["name"], lock_result=False) user["gender"] = gender_dict.get(gender_id, {}).get("name") # limit user fields to exported objects - collection_field_tupels = [ - ("meeting_user", "meeting_user_ids"), - ("poll", "poll_voted_ids"), - ("option", "option_ids"), - ("vote", "vote_ids"), - ("poll_candidate", "poll_candidate_ids"), - ("vote", "delegated_vote_ids"), - ] - for collection, fname in collection_field_tupels: + # Check voting related fields for mu + collection_field_tuples = [("meeting_user", "meeting_user_ids")] + for collection, fname in collection_field_tuples: user[fname] = [ id_ for id_ in user.get(fname, []) @@ -237,7 +282,6 @@ def get_relation_fields() -> Iterable[RelationListField]: 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/tests/database/reader/system/util.py b/tests/database/reader/system/util.py index 853e6293ae..b6ada342a2 100644 --- a/tests/database/reader/system/util.py +++ b/tests/database/reader/system/util.py @@ -77,13 +77,12 @@ "meeting_ids": None, "is_present_in_meeting_ids": None, "meeting_user_ids": None, - "option_ids": None, - "poll_candidate_ids": None, - "poll_voted_ids": None, - "vote_ids": None, + # "poll_candidate_ids": None, + # "poll_voted_ids": None, + # "acting_vote_ids": None, "committee_ids": None, "committee_management_ids": None, - "delegated_vote_ids": None, + # "represented_vote_ids": None, "organization_id": 1, }, 2: { @@ -114,13 +113,12 @@ "meeting_ids": None, "is_present_in_meeting_ids": None, "meeting_user_ids": None, - "option_ids": None, - "poll_candidate_ids": None, - "poll_voted_ids": None, - "vote_ids": None, + # "poll_candidate_ids": None, + # "poll_voted_ids": None, + # "acting_vote_ids": None, "committee_ids": None, "committee_management_ids": None, - "delegated_vote_ids": None, + # "represented_vote_ids": None, "organization_id": 1, }, 3: { @@ -151,13 +149,12 @@ "meeting_ids": None, "is_present_in_meeting_ids": None, "meeting_user_ids": None, - "option_ids": None, - "poll_candidate_ids": None, - "poll_voted_ids": None, - "vote_ids": None, + # "poll_candidate_ids": None, + # "poll_voted_ids": None, + # "acting_vote_ids": None, "committee_ids": None, "committee_management_ids": None, - "delegated_vote_ids": None, + # "represented_vote_ids": None, "organization_id": 1, }, }, diff --git a/tests/system/action/base.py b/tests/system/action/base.py index f074dd90ea..7c9bab1957 100644 --- a/tests/system/action/base.py +++ b/tests/system/action/base.py @@ -557,6 +557,29 @@ def create_mediafile( self.set_models({f"mediafile/{base}": model_data}) + def create_topic( + self, base: int, meeting_id: int, topic_data: PartialModel = {} + ) -> None: + self.set_models( + { + f"topic/{base}": { + "title": "test", + "sequential_number": base, + "meeting_id": meeting_id, + **topic_data, + }, + f"agenda_item/{base}": { + "meeting_id": meeting_id, + "content_object_id": f"topic/{base}", + }, + f"list_of_speakers/{base}": { + "content_object_id": f"topic/{base}", + "sequential_number": 1, + "meeting_id": meeting_id, + }, + } + ) + def base_permission_test( self, models: dict[str, dict[str, Any]], diff --git a/tests/system/action/base_generic.py b/tests/system/action/base_generic.py index a21f4cc91a..be77f82d5a 100644 --- a/tests/system/action/base_generic.py +++ b/tests/system/action/base_generic.py @@ -54,6 +54,7 @@ def create_table_view(cls, yml: str) -> None: final_info_code, missing_handled_attributes, im_table_code, + create_trigger_partitioned_sequences_code, create_trigger_1_1_relation_not_null_code, create_trigger_relationlistnotnull_code, create_trigger_unique_ids_pair_code, diff --git a/tests/system/action/meeting/test_archive.py b/tests/system/action/meeting/test_archive.py index 295d7f5035..bc58cc6345 100644 --- a/tests/system/action/meeting/test_archive.py +++ b/tests/system/action/meeting/test_archive.py @@ -103,20 +103,22 @@ def test_archive_meeting_with_inactive_speakers(self) -> None: response = self.request("meeting.archive", {"id": 1}) self.assert_status_code(response, 200) - def create_poll(self, base: int, state: str) -> None: + def create_poll(self, base: int, state: str, published: bool = False) -> None: self.set_models( { f"poll/{base}": { "title": f"Poll {base}", - "type": Poll.TYPE_NAMED, - "backend": "fast", - "pollmethod": "YNA", - "onehundred_percent_base": "YNA", + "config_id": f"poll_config_approval/{base}", + "visibility": Poll.VISIBILITY_NAMED, "meeting_id": 1, "sequential_number": base, "content_object_id": "motion/1", "state": state, - } + "published": published, + }, + f"poll_config_approval/{base}": { + "poll_id": base, + }, } ) @@ -124,7 +126,7 @@ def test_archive_meeting_with_inactive_polls(self) -> None: self.create_motion(1) self.create_poll(1, Poll.STATE_CREATED) self.create_poll(2, Poll.STATE_FINISHED) - self.create_poll(3, Poll.STATE_PUBLISHED) + self.create_poll(3, Poll.STATE_FINISHED, True) response = self.request("meeting.archive", {"id": 1}) self.assert_status_code(response, 200) diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py index 0360840eb2..9c38cc1c88 100644 --- a/tests/system/action/meeting/test_clone.py +++ b/tests/system/action/meeting/test_clone.py @@ -9,7 +9,7 @@ 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.models.models import AgendaItem, Meeting, Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from openslides_backend.permissions.permissions import Permissions from openslides_backend.shared.patterns import fqid_from_collection_and_id @@ -657,16 +657,6 @@ def test_clone_with_personal_note(self) -> None: "personal_note/2", {"meeting_user_id": 2, "meeting_id": 2} ) - def test_clone_with_option(self) -> None: - self.set_test_data_with_admin() - self.set_models({"option/1": {"content_object_id": "user/1", "meeting_id": 1}}) - response = self.request("meeting.clone", {"meeting_id": 1}) - self.assert_status_code(response, 200) - self.assert_model_exists("user/1", {"option_ids": [1, 2]}) - self.assert_model_exists( - "option/2", {"content_object_id": "user/1", "meeting_id": 2} - ) - def test_clone_with_mediafile(self) -> None: self.set_test_data_with_admin() self.set_models( @@ -1331,41 +1321,46 @@ def test_clone_vote_delegation(self) -> None: ) def test_clone_vote_delegated_vote(self) -> None: - self.set_test_data_with_admin() + self.set_test_data() self.create_meeting(4) + self.create_motion(1, 1) + self.create_motion(4, 4) self.set_user_groups(1, [2, 5]) - self.set_models( - { - "vote/1": { - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 1, - "option_id": 1, - "user_token": "asdfgh", - }, - "vote/2": { - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 4, - "option_id": 2, - "user_token": "hjkl", + for poll_id in range(1, 3): + self.set_models( + { + f"poll/{poll_id}": { + "title": f"Poll {poll_id}", + "meeting_id": 1 if poll_id == 1 else 4, + "content_object_id": f"motion/{poll_id}", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": f"poll_config_rating_approval/{poll_id}", + "state": Poll.STATE_STARTED, + }, + f"poll_config_rating_approval/{poll_id}": {"poll_id": poll_id}, + f"ballot/{poll_id}": { + "acting_meeting_user_id": poll_id, + "represented_meeting_user_id": poll_id, + "poll_id": poll_id, + }, }, - "option/1": {"meeting_id": 1}, - "option/2": {"meeting_id": 4}, - }, - ) + ) response = self.request("meeting.clone", {"meeting_id": 1}) self.assert_status_code(response, 200) self.assert_model_exists( - "vote/3", - {"user_id": 1, "delegated_user_id": 1, "option_id": 3, "meeting_id": 5}, + "ballot/3", + { + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + "poll_id": 3, + }, ) self.assert_model_exists( "user/1", { "meeting_user_ids": [1, 2, 3], - "vote_ids": [1, 2, 3], - "delegated_vote_ids": [1, 2, 3], + # "acting_vote_ids": [1, 2, 3, 4], + # "represented_vote_ids": [1, 2, 3, 4], "meeting_ids": [1, 4, 5], }, ) @@ -1571,73 +1566,31 @@ def test_clone_with_list_election(self) -> None: "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", + "visibility": Poll.VISIBILITY_SECRET, + "config_id": "poll_config_approval/1", + "state": Poll.STATE_CREATED, }, + "poll_config_approval/1": {"id": 1, "poll_id": 1}, } ) + for id_ in range(1, 4): + self.set_models( + { + f"poll_config_option/{id_}": { + "id": id_, + "weight": id_, + "poll_config_id": "poll_config_approval/1", + "meeting_user_id": id_ + 1, + } + } + ) + response = self.request("meeting.clone", {"meeting_id": 1}) self.assert_status_code(response, 200) self.assert_model_exists( @@ -1646,9 +1599,6 @@ def test_clone_with_list_election(self) -> None: "committee_id": 60, "list_of_speakers_ids": [2], "assignment_ids": [2], - "poll_candidate_list_ids": [2], - "poll_candidate_ids": [4, 5, 6], - "option_ids": [3, 4], "poll_ids": [2], "projector_ids": [2], "reference_projector_id": 2, @@ -1656,3 +1606,28 @@ def test_clone_with_list_election(self) -> None: "motions_default_amendment_workflow_id": 2, }, ) + self.assert_model_exists( + "poll/2", + { + "title": "First election", + "meeting_id": 2, + "content_object_id": "assignment/2", + "sequential_number": 1, + "visibility": Poll.VISIBILITY_SECRET, + "config_id": "poll_config_approval/2", + "state": Poll.STATE_CREATED, + }, + ) + self.assert_model_exists( + "poll_config_approval/2", + {"poll_id": 2, "option_ids": [4, 5, 6]}, + ) + for id_ in range(4, 7): + self.assert_model_exists( + f"poll_config_option/{id_}", + { + "weight": id_ - 3, + "poll_config_id": "poll_config_approval/2", + "meeting_user_id": id_ + 2, + }, + ) diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py index ba077575f4..6853324d10 100644 --- a/tests/system/action/meeting/test_delete.py +++ b/tests/system/action/meeting/test_delete.py @@ -1,8 +1,8 @@ from datetime import datetime -import pytest from psycopg.types.json import Jsonb +from openslides_backend.models.models import Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from openslides_backend.permissions.permissions import Permissions from openslides_backend.shared.util import ONE_ORGANIZATION_FQID @@ -42,7 +42,6 @@ def test_delete_permissions_can_manage_committee(self) -> None: self.assert_status_code(response, 200) self.assert_model_not_exists("meeting/1") - @pytest.mark.skip(reason="Requires poll.") def test_delete_full_meeting(self) -> None: self.load_example_data() self.set_models( @@ -95,10 +94,15 @@ def test_delete_full_meeting(self) -> None: self.assert_model_not_exists(f"motion_workflow/{i+1}") for i in range(5): self.assert_model_not_exists(f"poll/{i+1}") - for i in range(13): - self.assert_model_not_exists(f"option/{i+1}") + for i in range(2): + self.assert_model_not_exists(f"poll_config_approval/{i+1}") + self.assert_model_not_exists("poll_config_approval/1") + self.assert_model_not_exists("poll_config_selection/1") + self.assert_model_not_exists("poll_config_rating_score/1") + for i in range(10): + self.assert_model_not_exists(f"poll_config_option/{i+1}") for i in range(9): - self.assert_model_not_exists(f"vote/{i+1}") + self.assert_model_not_exists(f"ballot/{i+1}") for i in range(2): self.assert_model_not_exists(f"assignment/{i+1}") for i in range(5): @@ -192,7 +196,6 @@ def test_delete_archived_meeting(self) -> None: self.assert_status_code(response, 200) self.assert_model_not_exists("meeting/1") - @pytest.mark.skip(reason="Requires poll.") def test_delete_with_poll_candidates_and_speakers(self) -> None: self.set_committee_management_level([60]) self.create_user("user/2", [3]) @@ -209,31 +212,20 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None: "meeting_id": 1, "content_object_id": "assignment/140", "title": "Analog poll 150", - "type": "analog", - "pollmethod": "YNA", + "state": Poll.STATE_CREATED, + "config_id": "poll_config_approval/160", + "visibility": Poll.VISIBILITY_MANUALLY, "meeting_id": 1, "sequential_number": 150, }, - "option/160": { - "meeting_id": 1, - "poll_id": 150, - "content_object_id": "poll_candidate_list/170", - }, - "poll_candidate_list/170": { - "meeting_id": 1, - "option_id": 160, - }, - "poll_candidate/180": { - "meeting_id": 1, - "weight": 1, - "poll_candidate_list_id": 170, - "user_id": 2, + "poll_config_approval/160": {"poll_id": 150}, + "poll_config_option/180": { + "poll_config_id": "poll_config_approval/160", + "meeting_user_id": 1, }, - "poll_candidate/181": { - "meeting_id": 1, - "weight": 1, - "poll_candidate_list_id": 170, - "user_id": 3, + "poll_config_option/181": { + "poll_config_id": "poll_config_approval/160", + "meeting_user_id": 2, }, "list_of_speakers/190": { "meeting_id": 1, @@ -272,10 +264,9 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None: "group/3", "assignment/140", "poll/150", - "option/160", - "poll_candidate_list/170", - "poll_candidate/180", - "poll_candidate/181", + "poll_config_approval/160", + "poll_config_option/180", + "poll_config_option/181", "list_of_speakers/190", "speaker/210", "speaker/211", diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py index 3907bcb41e..9619194bc0 100644 --- a/tests/system/action/meeting/test_import.py +++ b/tests/system/action/meeting/test_import.py @@ -5,7 +5,7 @@ import pytest from openslides_backend.action.action_worker import ActionWorkerState -from openslides_backend.models.models import Meeting +from openslides_backend.models.models import Meeting, Poll from openslides_backend.shared.util import ( ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID, @@ -187,8 +187,8 @@ def create_request_data( "poll_default_method": "votes", "poll_default_onehundred_percent_base": "valid", "poll_default_group_ids": [], - "poll_default_backend": "fast", "poll_default_live_voting_enabled": False, + "poll_default_allow_invalid": False, "poll_couple_countdown": True, "projector_ids": [1], "all_projection_ids": [], @@ -211,8 +211,6 @@ def create_request_data( "motion_workflow_ids": [1], "motion_change_recommendation_ids": [], "poll_ids": [], - "option_ids": [], - "vote_ids": [], "assignment_ids": [], "assignment_candidate_ids": [], "personal_note_ids": [], @@ -2223,50 +2221,39 @@ def test_import_with_wrong_decimal(self) -> None: ) def test_import_new_user_with_vote(self) -> None: + self.set_user_groups(1, [1]) self.set_models( { - "vote/1": { - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 1, - "option_id": 10, - "user_token": "asdfgh", - }, - "option/10": { - "vote_ids": [1], + "poll/1": { + "title": "pull", + "config_id": "poll_config_approval/1", + "visibility": Poll.VISIBILITY_MANUALLY, + "state": Poll.STATE_STARTED, "meeting_id": 1, + "content_object_id": "meeting/1", }, - "user/1": { - "vote_ids": [1], - "delegated_vote_ids": [1], + "poll_config_approval/1": {"poll_id": 1}, + "ballot/1": { + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + "poll_id": 1, }, } ) data = self.create_request_data( { - "vote": { + "ballot": { "1": { "id": 1, - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 1, - "option_id": 1, - "user_token": "asdfgh", - }, - }, - "option": { - "1": { - "id": 1, - "vote_ids": [1], - "meeting_id": 1, + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + "poll_id": 1, }, }, } ) - data["meeting"]["meeting"]["1"]["vote_ids"] = [1] - data["meeting"]["meeting"]["1"]["option_ids"] = [1] - data["meeting"]["user"]["1"]["vote_ids"] = [1] - data["meeting"]["user"]["1"]["delegated_vote_ids"] = [1] + data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1] + data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1] response = self.request("meeting.import", data) self.assert_status_code(response, 200) self.assert_model_exists( @@ -2274,19 +2261,31 @@ def test_import_new_user_with_vote(self) -> None: { "username": "admin", "meeting_user_ids": [2], - "vote_ids": [1], - "delegated_vote_ids": [1], }, ) self.assert_model_exists( "user/2", { "username": "test", - "vote_ids": [2], - "delegated_vote_ids": [2], "meeting_user_ids": [1], }, ) + self.assert_model_exists( + "meeting_user/1", + { + "user_id": 2, + "acting_ballot_ids": [1], + "represented_ballot_ids": [1], + }, + ) + self.assert_model_exists( + "meeting_user/2", + { + "user_id": 1, + "acting_ballot_ids": [2], + "represented_ballot_ids": [2], + }, + ) def test_gender_import(self) -> None: """ @@ -2426,52 +2425,40 @@ def test_gender_import(self) -> None: ) def test_import_existing_user_with_vote(self) -> None: + self.set_user_groups(1, [1]) self.set_models( { - "vote/1": { - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 1, - "option_id": 10, - "user_token": "asdfgh", - }, - "option/10": { - "vote_ids": [1], + "poll/1": { + "title": "pull", + "config_id": "poll_config_approval/1", + "visibility": Poll.VISIBILITY_MANUALLY, + "state": Poll.STATE_STARTED, "meeting_id": 1, + "content_object_id": "meeting/1", }, - "user/1": { - "vote_ids": [1], - "delegated_vote_ids": [1], + "poll_config_approval/1": {"poll_id": 1}, + "vote/1": { + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + "poll_id": 1, }, } ) data = self.create_request_data( { - "vote": { - "1": { - "id": 1, - "user_id": 1, - "delegated_user_id": 1, - "meeting_id": 1, - "option_id": 1, - "user_token": "asdfgh", - }, - }, - "option": { + "ballot": { "1": { "id": 1, - "vote_ids": [1], - "meeting_id": 1, + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, }, }, } ) - data["meeting"]["meeting"]["1"]["vote_ids"] = [1] - data["meeting"]["meeting"]["1"]["option_ids"] = [1] data["meeting"]["user"]["1"]["username"] = "admin" data["meeting"]["user"]["1"]["last_name"] = "" - data["meeting"]["user"]["1"]["vote_ids"] = [1] - data["meeting"]["user"]["1"]["delegated_vote_ids"] = [1] + data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1] + data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1] response = self.request("meeting.import", data) self.assert_status_code(response, 200) self.assert_model_exists( @@ -2479,8 +2466,14 @@ def test_import_existing_user_with_vote(self) -> None: { "username": "admin", "meeting_user_ids": [1], - "vote_ids": [1, 2], - "delegated_vote_ids": [1, 2], + }, + ) + self.assert_model_exists( + "meeting_user_ids/1", + { + "user_id": 1, + "acting_ballot_ids": [1, 2], + "represented_ballot_ids": [1, 2], }, ) self.assert_model_not_exists("user/2") diff --git a/tests/system/action/meeting/test_update.py b/tests/system/action/meeting/test_update.py index 7a7b061f92..6e19aed196 100644 --- a/tests/system/action/meeting/test_update.py +++ b/tests/system/action/meeting/test_update.py @@ -283,7 +283,6 @@ def test_update_poll_default_backend_fields(self) -> None: data = { "motion_poll_default_backend": "long", "assignment_poll_default_backend": "long", - "poll_default_backend": "long", } self.basic_test(data) self.assert_model_exists("meeting/1", data) @@ -295,6 +294,10 @@ def test_update_poll_default_live_voting_enabled(self) -> None: {"poll_default_live_voting_enabled": True}, ) + def test_update_poll_default_allow_invalid(self) -> None: + self.basic_test({"poll_default_allow_invalid": True}) + self.assert_model_exists("meeting/1", {"poll_default_allow_invalid": True}) + def test_update_motions_block_slide_columns(self) -> None: self.basic_test({"motions_block_slide_columns": 2}) self.assert_model_exists("meeting/1", {"motions_block_slide_columns": 2}) diff --git a/tests/system/action/meeting_user/test_update.py b/tests/system/action/meeting_user/test_update.py index 18df90981d..82f09dccb5 100644 --- a/tests/system/action/meeting_user/test_update.py +++ b/tests/system/action/meeting_user/test_update.py @@ -1,7 +1,8 @@ from datetime import datetime from decimal import Decimal - +from openslides_backend.models.models import Poll from tests.system.action.base import BaseActionTestCase +from typing import Any class MeetingUserUpdate(BaseActionTestCase): @@ -104,6 +105,84 @@ def test_update_merge_fields_correct(self) -> None: }, ) + def test_update_with_vote_related_fields(self) -> None: + self.create_meeting() + self.set_user_groups(1, [1]) + self.create_user("dummy2", [1]) + self.create_user("dummy3", [1]) + self.set_models( + { + "meeting_user/1": { + "acting_ballot_ids": [1, 2], + "represented_ballot_ids": [1], + }, + "poll/1": { + "title": "Poll 1", + "meeting_id": 1, + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/1", + "state": Poll.STATE_FINISHED, + }, + "list_of_speakers/1": { + "id": 1, + "closed": False, + "meeting_id": 1, + "content_object_id": "assignment/1", + "sequential_number": 1, + }, + "assignment/1": { + "id": 1, + "phase": "search", + "title": "Duckburg town council", + "meeting_id": 1, + "sequential_number": 1, + "list_of_speakers_id": 1, + }, + "poll_config_option/1": { + "poll_config_id": "poll_config_approval/1", + "meeting_user_id": 2, + }, + "poll_config_approval/1": {"poll_id": 1}, + "ballot/1": { + "poll_id": 1, + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 1, + }, + "ballot/2": { + "poll_id": 1, + "acting_meeting_user_id": 1, + "represented_meeting_user_id": 2, + }, + } + ) + response = self.request( + "meeting_user.update", + { + "id": 3, + "poll_option_ids": [1], + "poll_voted_ids": [1], + "acting_ballot_ids": [1], + "represented_ballot_ids": [2], + }, + internal=True, + ) + self.assert_status_code(response, 200) + expected: dict[str, dict[str, Any]] = { + "meeting_user/3": { + "poll_option_ids": [1], + "poll_voted_ids": [1], + "acting_ballot_ids": [1], + "represented_ballot_ids": [2], + }, + "poll/1": {"voted_ids": [3]}, + "poll_config_option/1": {"meeting_user_id": 3}, + "ballot/1": {"acting_meeting_user_id": 3}, + "ballot/2": {"represented_meeting_user_id": 3}, + } + for fqid, model in expected.items(): + self.assert_model_exists(fqid, model) + def test_update_anonymous_group_id(self) -> None: self.create_meeting() self.set_models( diff --git a/tests/system/action/option/__init__.py b/tests/system/action/option/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/system/action/option/test_create.py b/tests/system/action/option/test_create.py deleted file mode 100644 index 61e22bb15d..0000000000 --- a/tests/system/action/option/test_create.py +++ /dev/null @@ -1,82 +0,0 @@ -from decimal import Decimal - -from tests.system.action.base import BaseActionTestCase - - -class OptionCreateActionTest(BaseActionTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting(111) - - def test_create(self) -> None: - response = self.request( - "option.create", {"text": "testtesttest", "meeting_id": 111, "weight": 10} - ) - self.assert_status_code(response, 200) - model = self.get_model("option/1") - assert model.get("text") == "testtesttest" - assert model.get("meeting_id") == 111 - assert model.get("weight") == 10 - - def test_create_without_text_and_content_object_id(self) -> None: - response = self.request("option.create", {"meeting_id": 111, "weight": 10}) - self.assert_status_code(response, 400) - assert ( - "Need one of text, content_object_id or poll_candidate_user_ids." - in response.json["message"] - ) - - def test_create_with_both_text_and_content_object_id(self) -> None: - self.set_models( - { - "motion/112": { - "sequential_number": 11, - "title": "mosh pit", - "state_id": 111, - "meeting_id": 111, - }, - "list_of_speakers/23": { - "content_object_id": "motion/112", - "sequential_number": 11, - "meeting_id": 111, - }, - } - ) - response = self.request( - "option.create", - { - "text": "test", - "content_object_id": "motion/112", - "meeting_id": 111, - "weight": 10, - }, - ) - self.assert_status_code(response, 400) - assert ( - "Need one of text, content_object_id or poll_candidate_user_ids." - in response.json["message"] - ) - - def test_create_yna_votes(self) -> None: - response = self.request( - "option.create", - { - "text": "test", - "meeting_id": 111, - "weight": 10, - "yes": "1.000000", - "no": "2.500000", - "abstain": "0.666667", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("option/1", {"vote_ids": [1, 2, 3], "text": "test"}) - self.assert_model_exists( - "vote/1", {"value": "Y", "weight": Decimal("1.000000"), "option_id": 1} - ) - self.assert_model_exists( - "vote/2", {"value": "N", "weight": Decimal("2.500000"), "option_id": 1} - ) - self.assert_model_exists( - "vote/3", {"value": "A", "weight": Decimal("0.666667"), "option_id": 1} - ) diff --git a/tests/system/action/option/test_delete.py b/tests/system/action/option/test_delete.py deleted file mode 100644 index 3b6088c6e3..0000000000 --- a/tests/system/action/option/test_delete.py +++ /dev/null @@ -1,44 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class OptionDeleteTest(BaseActionTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting() - self.create_motion(1) - self.set_models( - { - "option/111": {"meeting_id": 1, "content_object_id": "motion/1"}, - } - ) - - def test_delete_correct(self) -> None: - response = self.request("option.delete", {"id": 111}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("option/111") - self.assert_model_exists("motion/1") - - def test_delete_wrong_id(self) -> None: - response = self.request("option.delete", {"id": 112}) - self.assert_status_code(response, 400) - self.assert_model_exists("option/111") - - def test_delete_correct_cascading(self) -> None: - self.set_models( - { - "option/112": { - "meeting_id": 1, - "content_object_id": "poll_candidate_list/2", - }, - "vote/42": { - "user_token": "imnotapropertoken", - "option_id": 112, - "meeting_id": 1, - }, - "poll_candidate_list/2": {"option_id": 112, "meeting_id": 1}, - } - ) - response = self.request("option.delete", {"id": 112}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("option/112") - self.assert_model_not_exists("vote/42") diff --git a/tests/system/action/option/test_update.py b/tests/system/action/option/test_update.py deleted file mode 100644 index b4d336880e..0000000000 --- a/tests/system/action/option/test_update.py +++ /dev/null @@ -1,280 +0,0 @@ -from decimal import Decimal -from typing import Any - -from openslides_backend.permissions.permissions import Permissions -from tests.system.action.base import BaseActionTestCase - - -class OptionUpdateActionTest(BaseActionTestCase): - def setUp(self) -> None: - super().setUp() - self.vote_models: dict[str, dict[str, Any]] = { - "option/57": { - "vote_ids": [22], - }, - "vote/22": { - "value": "Y", - "user_token": "imnotapropertoken", - "weight": "0.000000", - "meeting_id": 1, - "option_id": 57, - }, - } - self.create_meeting() - self.set_models( - { - "topic/1": { - "title": "to pic", - "sequential_number": 1, - "meeting_id": 1, - }, - "agenda_item/1": {"content_object_id": "topic/1", "meeting_id": 1}, - "list_of_speakers/23": { - "content_object_id": "topic/1", - "sequential_number": 11, - "meeting_id": 1, - }, - "poll/65": { - "title": "pool", - "sequential_number": 1, - "content_object_id": "topic/1", - "type": "analog", - "state": "created", - "pollmethod": "YNA", - "max_votes_amount": 1, - "max_votes_per_option": 1, - "min_votes_amount": 1, - "meeting_id": 1, - "option_ids": [57], - }, - "option/57": { - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "meeting_id": 1, - "poll_id": 65, - }, - } - ) - - def test_update(self) -> None: - self.set_models(self.vote_models) - response = self.request( - "option.update", - {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "option/57", - { - "yes": Decimal("1.000000"), - "no": Decimal("2.000000"), - "abstain": Decimal("3.000000"), - "vote_ids": [22, 23, 24], - }, - ) - self.assert_model_exists( - "vote/22", {"option_id": 57, "value": "Y", "weight": Decimal("1.000000")} - ) - self.assert_model_exists( - "vote/23", {"option_id": 57, "value": "N", "weight": Decimal("2.000000")} - ) - self.assert_model_exists( - "vote/24", {"option_id": 57, "value": "A", "weight": Decimal("3.000000")} - ) - poll = self.get_model("poll/65") - assert poll.get("state") == "finished" - - def test_update_Y(self) -> None: - response = self.request( - "option.update", - { - "id": 57, - "Y": "1.000000", - "N": "2.000000", - "A": "3.000000", - "publish_immediately": True, - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "option/57", - { - "yes": Decimal("1.000000"), - "no": Decimal("2.000000"), - "abstain": Decimal("3.000000"), - "publish_immediately": None, - }, - ) - poll = self.get_model("poll/65") - assert poll.get("state") == "published" - - def test_update_default_values(self) -> None: - self.set_models( - { - "poll/65": { - "pollmethod": "YN", - }, - "option/57": { - "abstain": None, - }, - } - ) - response = self.request( - "option.update", - { - "id": 57, - "Y": "1.000000", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "option/57", - {"yes": Decimal("1.000000"), "no": Decimal("-2.000000"), "abstain": None}, - ) - - def test_update_invalid_keys(self) -> None: - self.set_models( - { - "poll/65": { - "pollmethod": "YN", - }, - } - ) - response = self.request( - "option.update", - { - "id": 57, - "A": "0.000000", - }, - ) - self.assert_status_code(response, 400) - assert ( - "Pollmethod YN does not support abstain votes" in response.json["message"] - ) - - def test_update_global_option(self) -> None: - self.set_models( - { - "poll/65": { - "pollmethod": "Y", - "global_option_id": 57, - "global_yes": True, - "global_no": True, - "global_abstain": True, - }, - } - ) - response = self.request( - "option.update", - {"id": 57, "Y": "1.000000", "N": "2.000000"}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "option/57", - { - "yes": Decimal("1.000000"), - "no": Decimal("2.000000"), - "abstain": Decimal("-2.000000"), - }, - ) - - def test_update_global_option_invalid(self) -> None: - self.set_models({"poll/65": {"global_option_id": 57}}) - response = self.request( - "option.update", - {"id": 57, "Y": "1.000000"}, - ) - self.assert_status_code(response, 400) - assert ( - "Global yes votes are not allowed for this poll" in response.json["message"] - ) - - def test_update_no_permissions(self) -> None: - self.base_permission_test( - self.vote_models, - "option.update", - {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"}, - ) - - def test_update_permissions(self) -> None: - self.base_permission_test( - self.vote_models, - "option.update", - {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_update_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - self.vote_models, - "option.update", - {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"}, - ) - - def test_update_together_with_poll(self) -> None: - self.set_models( - { - "poll/65": { - "pollmethod": "YN", - "option_ids": [57, 58], - }, - "option/58": { - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "meeting_id": 1, - "poll_id": 65, - }, - } - ) - response = self.request_json( - [ - { - "action": "poll.update", - "data": [ - { - "id": 65, - "onehundred_percent_base": "valid", - "pollmethod": "YNA", - "title": "Ballot", - } - ], - }, - { - "action": "option.update", - "data": [ - {"id": 57, "Y": "1.000000", "A": "3.000000", "N": "2.000000"}, - {"id": 58, "Y": "4.000000", "N": "-1.000000"}, - ], - }, - ] - ) - self.assert_status_code(response, 200) - - def test_update_together_with_poll_2(self) -> None: - response = self.request_json( - [ - { - "action": "poll.update", - "data": [ - { - "id": 65, - "onehundred_percent_base": "YNA", - "pollmethod": "YNA", - "title": "Abstimmung", - "votescast": "10.000000", - "votesinvalid": "10.000000", - "votesvalid": "10.000000", - } - ], - }, - { - "action": "option.update", - "data": [ - {"id": 57, "Y": "10.000000", "A": "10.000000", "N": "10.000000"} - ], - }, - ] - ) - self.assert_status_code(response, 200) diff --git a/tests/system/action/organization/test_delete_history_information.py b/tests/system/action/organization/test_delete_history_information.py index e5bc622dbf..24323e94e9 100644 --- a/tests/system/action/organization/test_delete_history_information.py +++ b/tests/system/action/organization/test_delete_history_information.py @@ -1,3 +1,4 @@ +from openslides_backend.models.models import Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from tests.system.action.base import BaseActionTestCase @@ -27,18 +28,19 @@ def test_delete_history_information_correct(self) -> None: }, } ) - response = self.request( - "poll.create", + vote_service_response = self.vote_service.create( { "title": "test", - "type": "analog", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], + "visibility": Poll.VISIBILITY_MANUALLY, + "method": Poll.METHOD_RATING_APPROVAL, + "state": Poll.STATE_CREATED, "meeting_id": 1, "content_object_id": "assignment/1", + "config": {"allow_abstain": True}, + "result": {"yes": "3", "no": "2", "abstain": "1"}, }, ) - self.assert_status_code(response, 200) + self.assertIsNotNone(vote_service_response) self.assert_history_information("assignment/1", ["Ballot created"]) response = self.request("organization.delete_history_information", {"id": 1}) diff --git a/tests/system/action/poll/__init__.py b/tests/system/action/poll/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/system/action/poll/base_poll_test.py b/tests/system/action/poll/base_poll_test.py deleted file mode 100644 index 89a7f69208..0000000000 --- a/tests/system/action/poll/base_poll_test.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from tests.system.action.base import BaseActionTestCase - - -@pytest.mark.skip(reason="During development of relational DB not necessary") -class BasePollTestCase(BaseActionTestCase): - def setUp(self) -> None: - super().setUp() - self.vote_service.clear_all() diff --git a/tests/system/action/poll/poll_test_mixin.py b/tests/system/action/poll/poll_test_mixin.py deleted file mode 100644 index 7f35fc16f6..0000000000 --- a/tests/system/action/poll/poll_test_mixin.py +++ /dev/null @@ -1,66 +0,0 @@ -from openslides_backend.models.models import Poll -from tests.system.action.base import DEFAULT_PASSWORD, BaseActionTestCase -from tests.system.base import ADMIN_PASSWORD, ADMIN_USERNAME - - -class PollTestMixin(BaseActionTestCase): - def start_poll(self, id: int) -> None: - self.vote_service.start(id) - - def prepare_users_and_poll(self, user_count: int) -> list[int]: - user_ids = list(range(2, user_count + 2)) - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "motion/1", - "type": Poll.TYPE_NAMED, - "pollmethod": "YNA", - "backend": "fast", - "state": Poll.STATE_STARTED, - "option_ids": [1], - "meeting_id": 1, - "entitled_group_ids": [3], - "sequential_number": 1, - "onehundred_percent_base": "YNA", - "title": "Poll 1", - }, - "option/1": {"meeting_id": 1, "poll_id": 1}, - **{ - f"user/{i}": { - **self._get_user_data(f"user{i}"), - "is_present_in_meeting_ids": [1], - "meeting_ids": [1], - "meeting_user_ids": [i + 10], - } - for i in user_ids - }, - **{ - f"meeting_user/{i+10}": { - "meeting_id": 1, - "user_id": i, - "group_ids": [3], - } - for i in user_ids - }, - "group/3": { - "meeting_user_ids": [id_ + 10 for id_ in user_ids], - "meeting_id": 1, - }, - "meeting/1": { - "user_ids": user_ids, - "group_ids": [3], - "name": "test", - }, - } - ) - self.start_poll(1) - for i in user_ids: - self.client.login(f"user{i}", DEFAULT_PASSWORD, i) - response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}}) - self.assert_status_code(response, 200) - self.client.login(ADMIN_USERNAME, ADMIN_PASSWORD, 1) - return user_ids diff --git a/tests/system/action/poll/test_anonymize.py b/tests/system/action/poll/test_anonymize.py deleted file mode 100644 index 58bcc2909d..0000000000 --- a/tests/system/action/poll/test_anonymize.py +++ /dev/null @@ -1,120 +0,0 @@ -from openslides_backend.models.models import Poll -from openslides_backend.permissions.permissions import Permissions - -from .base_poll_test import BasePollTestCase - - -class PollAnonymize(BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting() - self.set_models( - { - "poll/1": { - "option_ids": [1], - "global_option_id": 2, - "meeting_id": 1, - "state": Poll.STATE_FINISHED, - "type": Poll.TYPE_NAMED, - "content_object_id": "topic/1", - }, - "topic/1": {"meeting_id": 1}, - "option/1": {"vote_ids": [1], "meeting_id": 1}, - "option/2": {"vote_ids": [2], "meeting_id": 1}, - "vote/1": { - "user_id": 1, - "meeting_id": 1, - "delegated_user_id": 1, - }, - "vote/2": { - "user_id": 1, - "meeting_id": 1, - "delegated_user_id": 1, - }, - "user/1": { - "meeting_user_ids": [11], - "delegated_vote_ids": [1, 2], - "vote_ids": [1, 2], - }, - "meeting_user/11": { - "meeting_id": 1, - "user_id": 1, - }, - } - ) - - def assert_anonymize(self) -> None: - poll = self.get_model("poll/1") - assert poll.get("is_pseudoanonymized") is True - for fqid in ("vote/1", "vote/2"): - vote = self.get_model(fqid) - assert vote.get("user_id") is None - assert vote.get("delegated_user_id") is None - self.assert_model_exists("user/1", {"vote_ids": [], "delegated_vote_ids": []}) - - def test_anonymize(self) -> None: - response = self.request("poll.anonymize", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_anonymize() - self.assert_history_information("topic/1", ["Voting anonymized"]) - - def test_anonymize_assignment_poll(self) -> None: - self.set_models( - { - "assignment/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "assignment/1", - }, - } - ) - response = self.request("poll.anonymize", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_history_information("assignment/1", ["Ballot anonymized"]) - - def test_anonymize_publish_state(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_PUBLISHED}) - response = self.request("poll.anonymize", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_anonymize() - - def test_anonymize_wrong_state(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_CREATED}) - response = self.request("poll.anonymize", {"id": 1}) - self.assert_status_code(response, 400) - for vote_fqid in ("vote/1", "vote/2"): - vote = self.get_model(vote_fqid) - assert vote.get("user_id") == 1 - assert vote.get("delegated_user_id") == 1 - - def test_anonymize_wrong_type(self) -> None: - self.update_model("poll/1", {"type": Poll.TYPE_ANALOG}) - response = self.request("poll.anonymize", {"id": 1}) - self.assert_status_code(response, 400) - for vote_fqid in ("vote/1", "vote/2"): - vote = self.get_model(vote_fqid) - assert vote.get("user_id") == 1 - assert vote.get("delegated_user_id") == 1 - - def test_anonymize_no_permissions(self) -> None: - self.base_permission_test( - {}, - "poll.anonymize", - {"id": 1}, - ) - - def test_anonymize_permissions(self) -> None: - self.base_permission_test( - {}, - "poll.anonymize", - {"id": 1}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_anonymize_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - {}, - "poll.anonymize", - {"id": 1}, - ) diff --git a/tests/system/action/poll/test_create.py b/tests/system/action/poll/test_create.py deleted file mode 100644 index 2022474387..0000000000 --- a/tests/system/action/poll/test_create.py +++ /dev/null @@ -1,1204 +0,0 @@ -from openslides_backend.models.models import Poll -from openslides_backend.permissions.permissions import Permissions -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID - -from .base_poll_test import BasePollTestCase - - -class CreatePoll(BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting() - self.set_models( - { - "assignment/1": { - "title": "test_assignment_ohneivoh9caiB8Yiungo", - "open_posts": 1, - "meeting_id": 1, - }, - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "user/3": {"username": "User3"}, - }, - ) - - def test_create_correct(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "min_votes_amount": 5, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "backend": "long", - "amount_global_yes": "2.000000", - }, - ) - self.assert_status_code(response, 200) - poll = self.assert_model_exists( - "poll/1", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "meeting_id": 1, - "option_ids": [1], - "global_option_id": 2, - "state": "finished", - "onehundred_percent_base": "Y", - "is_pseudoanonymized": False, - "min_votes_amount": 5, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "backend": "long", - "sequential_number": 1, - }, - ) - assert "options" not in poll - self.assert_model_exists( - "option/1", - {"text": "test2", "poll_id": 1, "meeting_id": 1, "yes": "10.000000"}, - ) - self.assert_model_exists( - "option/2", - { - "text": "global option", - "used_as_global_option_in_poll_id": 1, - "meeting_id": 1, - "yes": "2.000000", - "no": "-2.000000", - }, - ) - self.assert_history_information("assignment/1", ["Ballot created"]) - - def test_create_correct_publish_immediately(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "min_votes_amount": 5, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "publish_immediately": True, - }, - ) - self.assert_status_code(response, 200) - poll = self.assert_model_exists("poll/1", {"state": "published"}) - assert "publish_immediately" not in poll - - def test_create_correct_default_values(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "YN", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "onehundred_percent_base": "Y", - "votesvalid": "3.000000", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", {"votesvalid": "3.000000", "votesinvalid": "-2.000000"} - ) - self.assert_model_exists("vote/1", {"value": "Y", "weight": "10.000000"}) - self.assert_model_exists("vote/2", {"value": "N", "weight": "-2.000000"}) - - def test_create_correct_with_topic(self) -> None: - self.set_models( - { - "topic/12": { - "title": "Wichtiges Topic", - "text": "blablabla", - "meeting_id": 1, - } - } - ) - - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "topic/12", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "min_votes_amount": 5, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "publish_immediately": True, - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"content_object_id": "topic/12"}) - self.assert_model_exists( - "topic/12", - { - "title": "Wichtiges Topic", - "text": "blablabla", - "meeting_id": 1, - "poll_ids": [1], - }, - ) - - def test_create_three_options(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "options": [ - {"text": "test2", "Y": "10.000000"}, - {"text": "test3", "N": "0.999900"}, - {"text": "test4", "N": "11.000000"}, - ], - "meeting_id": 1, - "onehundred_percent_base": "YNA", - "content_object_id": "assignment/1", - "amount_global_yes": "5.000000", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "meeting_id": 1, - "option_ids": [1, 2, 3], - "global_option_id": 4, - "onehundred_percent_base": "YNA", - }, - ) - self.assert_model_exists( - "option/1", - { - "text": "test2", - "poll_id": 1, - "meeting_id": 1, - "yes": "10.000000", - "weight": 1, - }, - ) - self.assert_model_exists( - "option/2", - { - "text": "test3", - "poll_id": 1, - "meeting_id": 1, - "no": "0.999900", - "weight": 2, - }, - ) - self.assert_model_exists( - "option/3", - { - "text": "test4", - "poll_id": 1, - "meeting_id": 1, - "no": "11.000000", - "weight": 3, - }, - ) - option_4 = self.assert_model_exists( - "option/4", - { - "text": "global option", - "used_as_global_option_in_poll_id": 1, - "meeting_id": 1, - "weight": 1, - }, - ) - assert option_4.get("yes") is None - - def test_all_fields(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_ahThai4pae1pi4xoogoo", - "pollmethod": "YN", - "type": "pseudoanonymous", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "global_yes": False, - "global_no": False, - "global_abstain": False, - "description": "test_description_ieM8ThuasoSh8aecai8p", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "title": "test_title_ahThai4pae1pi4xoogoo", - "pollmethod": "YN", - "type": "pseudoanonymous", - "is_pseudoanonymized": True, - "global_yes": False, - "global_no": False, - "global_abstain": False, - "description": "test_description_ieM8ThuasoSh8aecai8p", - "onehundred_percent_base": "YN", - }, - ) - - def test_create_wrong_publish_immediately(self) -> None: - response = self.request( - "poll.create", - { - "content_object_id": "assignment/1", - "title": "test_title_ahThai4pae1pi4xoogoo", - "pollmethod": "YN", - "type": "pseudoanonymous", - "meeting_id": 1, - "options": [{"text": "test"}], - "publish_immediately": True, - }, - ) - self.assert_status_code(response, 400) - assert ( - "publish_immediately only allowed for analog polls." - in response.json["message"] - ) - - def test_no_options(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_eing5eipue5cha2Iefai", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [], - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "data.options must contain at least 1 items", response.json["message"] - ) - self.assert_model_not_exists("poll/1") - - def test_invalid_options(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_eing5eipue5cha2Iefai", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{}], - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "Need one of text, content_object_id or poll_candidate_user_ids.", - response.json["message"], - ) - self.assert_model_not_exists("poll/1") - - def test_missing_keys(self) -> None: - complete_request_data = { - "title": "test_title_keugh8Iu9ciyooGaevoh", - "pollmethod": "YNA", - "type": "named", - "meeting_id": 1, - "options": [{"text": "test"}], - } - for key in complete_request_data.keys(): - request_data = { - _key: value - for _key, value in complete_request_data.items() - if _key != key - } - response = self.request("poll.create", request_data) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - - def test_with_groups(self) -> None: - self.set_models({"group/1": {"meeting_id": 1}, "group/2": {"meeting_id": 1}}) - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "entitled_group_ids": [1, 2], - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"entitled_group_ids": [1, 2]}) - - def test_with_empty_groups(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "entitled_group_ids": [], - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"entitled_group_ids": []}) - - def test_with_groups_and_analog(self) -> None: - self.set_models({"group/1": {"meeting_id": 1}, "group/2": {"meeting_id": 1}}) - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YNA", - "type": "analog", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "entitled_group_ids": [1, 2], - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - assert ( - "entitled_group_ids is not allowed for analog." in response.json["message"] - ) - - def test_with_100_percent_base_entitled_and_analog(self) -> None: - self.set_models({"group/1": {"meeting_id": 1}, "group/2": {"meeting_id": 1}}) - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YNA", - "type": "analog", - "content_object_id": "assignment/1", - "onehundred_percent_base": "entitled", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - assert ( - "onehundred_percent_base: value entitled is not allowed for analog." - in response.json["message"] - ) - - def test_not_supported_type(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_yaiyeighoh0Iraet3Ahc", - "pollmethod": "YNA", - "type": "not_existing", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - - def test_not_allowed_type(self) -> None: - self.update_model(ONE_ORGANIZATION_FQID, {"enable_electronic_voting": False}) - response = self.request( - "poll.create", - { - "title": "test_title_yaiyeighoh0Iraet3Ahc", - "pollmethod": "YNA", - "type": Poll.TYPE_NAMED, - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - - def test_not_supported_pollmethod(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_SeVaiteYeiNgie5Xoov8", - "pollmethod": "not_existing", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - - def test_not_supported_onehundred_percent_base(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "invalid base", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "data.onehundred_percent_base must be one of ['Y', 'YN', 'YNA', 'N', 'valid', 'cast', 'entitled', 'entitled_present', 'disabled']", - response.json["message"], - ) - self.assert_model_not_exists("poll/1") - - def test_wrong_pollmethod_onehundred_percent_base_combination_1(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "Y", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - assert ( - "This onehundred_percent_base not allowed in this pollmethod" - in response.json["message"] - ) - self.assert_model_not_exists("poll/1") - - def test_wrong_pollmethod_onehundred_percent_base_combination_2(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "YN", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YNA", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - assert ( - "This onehundred_percent_base not allowed in this pollmethod" - in response.json["message"] - ) - self.assert_model_not_exists("poll/1") - - def test_wrong_pollmethod_onehundred_percent_base_combination_3(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": "Y", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YNA", - "meeting_id": 1, - "options": [{"text": "test"}], - }, - ) - self.assert_status_code(response, 400) - assert ( - "This onehundred_percent_base not allowed in this pollmethod" - in response.json["message"] - ) - self.assert_model_not_exists("poll/1") - - def test_create_poll_for_option_with_wrong_content_object(self) -> None: - response = self.request( - "poll.create", - { - "meeting_id": 1, - "title": "Wahlgang (3)", - "onehundred_percent_base": "valid", - "pollmethod": "YN", - "type": "analog", - "options": [{"content_object_id": "assignment/1"}], - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "The collection 'assignment' is not available for field 'content_object_id' in collection 'option'.", - response.json["message"], - ) - - def test_unique_error_options_text(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "onehundred_percent_base": "valid", - "options": [ - {"text": "test", "Y": "10.000000"}, - {"text": "test", "A": "11.000000"}, - {"text": "test", "N": "12.000000"}, - ], - "meeting_id": 1, - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "Duplicated option in poll.options: test", response.json["message"] - ) - - def test_unique_error_options_content_object_id(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "options": [ - {"content_object_id": "user/1", "Y": "10.000000"}, - {"text": "test4", "N": "11.000000"}, - {"content_object_id": "user/1", "Y": "11.000000"}, - ], - "meeting_id": 1, - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "Duplicated option in poll.options: user/1", response.json["message"] - ) - - def test_unique_no_error_mixed_text_content_object_id_options(self) -> None: - self.create_meeting() - self.set_models( - { - "meeting_user/1": {"meeting_id": 1, "user_id": 1}, - "user/1": {"meeting_ids": [1]}, - } - ) - self.set_user_groups(1, [1]) - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YN", - "onehundred_percent_base": "valid", - "options": [ - { - "content_object_id": "user/1", - "Y": "10.000000", - "N": "5.000000", - }, - {"text": "text", "Y": "10.000000"}, - ], - "meeting_id": 1, - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 200) - - def test_analog_poll_without_YNA_values(self) -> None: - self.set_models( - { - "motion/3": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - } - ) - response = self.request( - "poll.create", - { - "meeting_id": 1, - "title": "Abstimmung", - "onehundred_percent_base": "YNA", - "pollmethod": "YNA", - "type": "analog", - "options": [{"content_object_id": "motion/3"}], - "content_object_id": "motion/3", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "option/1", - { - "content_object_id": "motion/3", - "vote_ids": [1, 2, 3], - "yes": "-2.000000", - "no": "-2.000000", - "abstain": "-2.000000", - "weight": 1, - }, - ) - self.assert_model_exists( - "option/2", - { - "text": "global option", - "used_as_global_option_in_poll_id": 1, - "weight": 1, - }, - ) - self.assert_history_information("motion/3", ["Voting created"]) - - def test_not_state_change(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_eing5eipue5cha2Iefai", - "pollmethod": "YNA", - "type": "named", - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test1"}], - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"state": "created"}) - - def test_create_user_option_valid(self) -> None: - self.create_meeting(42) - self.set_models( - { - "meeting/42": { - "meeting_user_ids": [1], - }, - "group/5": {"meeting_id": 42, "meeting_user_ids": [1]}, - "user/1": { - "meeting_user_ids": [1], - "meeting_ids": [42], - }, - "meeting_user/1": { - "meeting_id": 42, - "user_id": 1, - "group_ids": [5], - }, - "assignment/2": { - "meeting_id": 42, - }, - } - ) - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "options": [ - {"content_object_id": "user/1"}, - ], - "meeting_id": 42, - "onehundred_percent_base": "YN", - "content_object_id": "assignment/2", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "option_ids": [1], - "meeting_id": 42, - }, - ) - self.assert_model_exists( - "option/1", - {"content_object_id": "user/1", "poll_id": 1, "meeting_id": 42}, - ) - - def test_create_user_option_invalid(self) -> None: - self.create_meeting(7) - self.create_meeting(42) - self.set_models( - { - "meeting/42": {"meeting_user_ids": [1]}, - "group/5": {"meeting_id": 42, "meeting_user_ids": [1]}, - "user/1": { - "meeting_user_ids": [1], - "meeting_ids": [42], - }, - "meeting_user/1": { - "meeting_id": 42, - "user_id": 1, - "group_ids": [5], - }, - } - ) - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "YNA", - "options": [ - {"content_object_id": "user/1"}, - ], - "meeting_id": 7, - "onehundred_percent_base": "YN", - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 400) - assert ( - response.json["message"] - == "The following models do not belong to meeting 7: ['user/1']" - ) - - def test_create_without_content_object(self) -> None: - response = self.request( - "poll.create", - { - "title": "test_title_eing5eipue5cha2Iefai", - "pollmethod": "YNA", - "type": "named", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test1"}], - }, - ) - self.assert_status_code(response, 400) - assert response.json["message"] == "No 'content_object_id' was given" - - def test_create_no_permissions_assignment(self) -> None: - self.base_permission_test( - {}, - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - }, - ) - - def test_create_permissions_assignment(self) -> None: - self.base_permission_test( - {}, - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - }, - Permissions.Assignment.CAN_MANAGE, - ) - - def test_create_permissions_assignment_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - {}, - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - }, - ) - - def test_create_forbidden_to_create_poll(self) -> None: - self.set_models( - { - "motion/23": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": False}, - } - ) - response = self.request( - "poll.create", - { - "meeting_id": 1, - "title": "Abstimmung", - "onehundred_percent_base": "YNA", - "pollmethod": "YNA", - "type": "analog", - "options": [{"content_object_id": "motion/23"}], - "content_object_id": "motion/23", - }, - ) - self.assert_status_code(response, 400) - assert "Motion state doesn't allow to create poll." in response.json["message"] - - def test_create_no_permissions_motion(self) -> None: - self.base_permission_test( - {"motion/23": {"meeting_id": 1}}, - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "motion/23", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - }, - ) - - def test_create_permissions_motion(self) -> None: - self.base_permission_test( - { - "motion/23": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - }, - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "motion/23", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - }, - Permissions.Motion.CAN_MANAGE_POLLS, - ) - - def test_create_no_permissions_topic(self) -> None: - self.set_models( - {"meeting/1": {"topic_ids": [13]}, "topic/13": {"meeting_id": 1}} - ) - self.base_permission_test( - {}, - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "content_object_id": "topic/1", - }, - ) - - def test_create_permissions_topic(self) -> None: - self.set_models( - {"meeting/1": {"topic_ids": [13]}, "topic/13": {"meeting_id": 1}} - ) - self.base_permission_test( - {}, - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "content_object_id": "topic/1", - }, - ) - - def test_non_negative_max_votes_per_option(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "content_object_id": "assignment/1", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "global_yes": True, - "global_no": True, - "global_abstain": True, - "onehundred_percent_base": "Y", - "min_votes_amount": 5, - "max_votes_amount": 10, - "max_votes_per_option": -1, - "backend": "long", - }, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - - def test_max_votes_per_option_smaller_max_votes_amount(self) -> None: - """Also asserts that default values are respected.""" - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "max_votes_per_option": 2, - }, - ) - self.assert_status_code(response, 400) - assert ( - response.json["message"] - == "The maximum votes per option cannot be higher than the maximum amount of votes in total." - ) - self.assert_model_not_exists("poll/1") - - def test_max_votes_amount_smaller_min(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": "analog", - "pollmethod": "Y", - "options": [{"text": "test2", "Y": "10.000000"}], - "meeting_id": 1, - "min_votes_amount": 5, - "max_votes_amount": 2, - "max_votes_per_option": 2, - }, - ) - self.assert_status_code(response, 400) - assert ( - response.json["message"] - == "The minimum amount of votes cannot be higher than the maximum amount of votes." - ) - self.assert_model_not_exists("poll/1") - - def test_create_poll_candidate_list(self) -> None: - response = self.request( - "poll.create", - { - "title": "test", - "type": Poll.TYPE_NAMED, - "content_object_id": "assignment/1", - "pollmethod": "YNA", - "options": [{"poll_candidate_user_ids": [1, 3]}], - "meeting_id": 1, - "backend": "long", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("assignment/1", {"poll_ids": [1]}) - self.assert_model_exists( - "poll/1", - {"title": "test", "option_ids": [1], "content_object_id": "assignment/1"}, - ) - self.assert_model_exists( - "option/1", {"content_object_id": "poll_candidate_list/1", "poll_id": 1} - ) - self.assert_model_exists( - "poll_candidate_list/1", - {"option_id": 1, "meeting_id": 1, "poll_candidate_ids": [1, 2]}, - ) - self.assert_model_exists( - "poll_candidate/1", - {"user_id": 1, "weight": 1, "poll_candidate_list_id": 1, "meeting_id": 1}, - ) - self.assert_model_exists( - "poll_candidate/2", - {"user_id": 3, "weight": 2, "poll_candidate_list_id": 1, "meeting_id": 1}, - ) - self.assert_model_exists( - "meeting/1", {"poll_candidate_list_ids": [1], "poll_candidate_ids": [1, 2]} - ) - self.assert_model_exists( - "meeting/1", - { - "poll_ids": [1], - "option_ids": [1, 2], - "poll_candidate_ids": [1, 2], - "poll_candidate_list_ids": [1], - }, - ) - - def test_create_poll_candidate_lists(self) -> None: - self.set_models( - { - "user/2": {"username": "User2"}, - "user/4": {"username": "User4"}, - } - ) - response = self.request( - "poll.create", - { - "title": "test", - "type": Poll.TYPE_PSEUDOANONYMOUS, - "content_object_id": "assignment/1", - "pollmethod": "YNA", - "options": [ - {"poll_candidate_user_ids": [1, 3]}, - {"poll_candidate_user_ids": [2, 4]}, - ], - "meeting_id": 1, - "backend": "long", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "title": "test", - "option_ids": [1, 2], - "content_object_id": "assignment/1", - }, - ) - self.assert_model_exists( - "option/1", {"content_object_id": "poll_candidate_list/1", "poll_id": 1} - ) - self.assert_model_exists( - "option/2", {"content_object_id": "poll_candidate_list/2", "poll_id": 1} - ) - self.assert_model_exists( - "poll_candidate_list/1", - {"option_id": 1, "meeting_id": 1, "poll_candidate_ids": [1, 2]}, - ) - self.assert_model_exists( - "poll_candidate_list/2", - {"option_id": 2, "meeting_id": 1, "poll_candidate_ids": [3, 4]}, - ) - - def test_with_anonymous_in_entitled_group_ids(self) -> None: - self.create_meeting() - self.set_anonymous() - response = self.request( - "poll.create", - { - "meeting_id": 1, - "options": [{"text": "test"}], - "pollmethod": "YNA", - "title": "test", - "type": Poll.TYPE_NAMED, - "entitled_group_ids": [4], - "content_object_id": "assignment/1", - }, - ) - self.assert_status_code(response, 400) - self.assertIn( - "Anonymous group is not allowed in entitled_group_ids.", - response.json["message"], - ) - - def test_live_voting_named_motion_poll(self) -> None: - self.set_models( - { - "motion/3": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - } - ) - response = self.request( - "poll.create", - { - "title": "test_title_yaiyeighoh0Iraet3Ahc", - "pollmethod": "YNA", - "type": Poll.TYPE_NAMED, - "content_object_id": "motion/3", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - "live_voting_enabled": True, - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", {"type": Poll.TYPE_NAMED, "live_voting_enabled": True} - ) - - def test_live_voting_not_allowed_type_analog(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_ANALOG, True) - - def test_live_voting_not_allowed_type_pseudoanonymous(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_PSEUDOANONYMOUS, True) - - def test_live_voting_not_allowed_is_motion_poll_false(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_NAMED, False) - - def base_live_voting_not_allowed( - self, poll_type: str, is_motion_poll: bool - ) -> None: - request_data = { - "title": "test_title_yaiyeighoh0Iraet3Ahc", - "pollmethod": "YNA", - "type": poll_type, - "content_object_id": "assignment/1", - "onehundred_percent_base": "YN", - "meeting_id": 1, - "options": [{"text": "test"}], - "live_voting_enabled": True, - } - if is_motion_poll: - self.set_models( - { - "motion/3": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - } - ) - request_data["content_object_id"] = "motion/3" - - response = self.request("poll.create", request_data) - self.assert_status_code(response, 400) - self.assert_model_not_exists("poll/1") - assert ( - "live_voting_enabled only allowed for named motion polls." - ) in response.json["message"] diff --git a/tests/system/action/poll/test_delete.py b/tests/system/action/poll/test_delete.py deleted file mode 100644 index 20b9d3511d..0000000000 --- a/tests/system/action/poll/test_delete.py +++ /dev/null @@ -1,198 +0,0 @@ -from unittest.mock import Mock, patch - -from openslides_backend.permissions.permissions import Permissions -from tests.system.util import CountDatastoreCalls, Profiler, performance - -from .base_poll_test import BasePollTestCase -from .poll_test_mixin import PollTestMixin - - -class PollDeleteTest(PollTestMixin, BasePollTestCase): - @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear") - def test_delete_correct(self, clear: Mock) -> None: - """ - Also ensures that the vote service is notified on success if the poll is started. - """ - clear_called_on: list[int] = [] - - def add_to_list(id_: int) -> None: - clear_called_on.append(id_) - - clear.side_effect = add_to_list - self.create_meeting() - self.set_models( - { - "poll/111": { - "meeting_id": 1, - "content_object_id": "motion/1", - "state": "started", - }, - "motion/1": {"meeting_id": 1, "poll_ids": [111]}, - } - ) - response = self.request("poll.delete", {"id": 111}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll/111") - self.assert_history_information("motion/1", ["Voting deleted"]) - assert clear_called_on == [111] - - def test_delete_wrong_id(self) -> None: - self.create_meeting() - self.set_models( - { - "poll/112": {"meeting_id": 1}, - } - ) - response = self.request("poll.delete", {"id": 111}) - self.assert_status_code(response, 400) - self.assert_model_exists("poll/112") - - @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear") - def test_delete_correct_cascading(self, clear: Mock) -> None: - """ - Also ensures that the vote service is not notified if the poll wasn't started anyway. - """ - clear_called_on: list[int] = [] - - def add_to_list(id_: int) -> None: - clear_called_on.append(id_) - - clear.side_effect = add_to_list - self.create_meeting() - self.set_models( - { - "topic/1": {"poll_ids": [111], "meeting_id": 1}, - "poll/111": { - "option_ids": [42], - "meeting_id": 1, - "projection_ids": [1], - "content_object_id": "topic/1", - }, - "option/42": {"poll_id": 111, "meeting_id": 1}, - "meeting/1": { - "all_projection_ids": [1], - "topic_ids": [1], - }, - "projection/1": { - "content_object_id": "poll/111", - "current_projector_id": 1, - "meeting_id": 1, - }, - "projector/1": { - "current_projection_ids": [1], - "meeting_id": 1, - }, - } - ) - response = self.request("poll.delete", {"id": 111}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll/111") - self.assert_model_not_exists("option/42") - self.assert_model_not_exists("projection/1") - self.assert_model_exists("projector/1", {"current_projection_ids": []}) - assert clear_called_on == [] - - def test_delete_cascading_poll_candidate_list(self) -> None: - self.create_meeting() - self.set_models( - { - "topic/1": {"poll_ids": [111], "meeting_id": 1}, - "poll/111": { - "option_ids": [42], - "meeting_id": 1, - "content_object_id": "topic/1", - }, - "option/42": { - "poll_id": 111, - "meeting_id": 1, - "content_object_id": "poll_candidate_list/12", - }, - "meeting/1": { - "poll_candidate_list_ids": [12], - "poll_candidate_ids": [13], - "topic_ids": [1], - }, - "poll_candidate_list/12": { - "meeting_id": 1, - "option_id": 42, - "poll_candidate_ids": [13], - }, - "poll_candidate/13": { - "meeting_id": 1, - "user_id": 1, - "weight": 1, - "poll_candidate_list_id": 12, - }, - "user/1": {"poll_candidate_ids": [13]}, - } - ) - response = self.request("poll.delete", {"id": 111}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll/111") - self.assert_model_not_exists("option/42") - self.assert_model_not_exists("poll_candidate_list/12") - - @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear") - def test_delete_no_permissions(self, clear: Mock) -> None: - """ - Also ensures that the vote service is not notified if the action failed. - """ - clear_called_on: list[int] = [] - - def add_to_list(id_: int) -> None: - clear_called_on.append(id_) - - clear.side_effect = add_to_list - self.base_permission_test( - { - "poll/111": { - "meeting_id": 1, - "content_object_id": "topic/1", - "state": "started", - } - }, - "poll.delete", - {"id": 111}, - ) - - assert clear_called_on == [] - - def test_delete_permissions(self) -> None: - self.base_permission_test( - {"poll/111": {"meeting_id": 1, "content_object_id": "topic/1"}}, - "poll.delete", - {"id": 111}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_delete_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - {"poll/111": {"meeting_id": 1, "content_object_id": "topic/1"}}, - "poll.delete", - {"id": 111}, - ) - - def test_delete_datastore_calls(self) -> None: - self.prepare_users_and_poll(3) - - with CountDatastoreCalls() as counter: - response = self.request("poll.delete", {"id": 1}) - - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll/1") - assert counter.calls == 7 - - @performance - def test_delete_performance(self) -> None: - # TODO this needs a different idea - user_ids = self.prepare_users_and_poll(1000) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - self.datastore.reset(hard=True) - - with Profiler("test_delete_performance.prof"): - response = self.request("poll.delete", {"id": 1}) - - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll["voted_ids"] == user_ids diff --git a/tests/system/action/poll/test_publish.py b/tests/system/action/poll/test_publish.py deleted file mode 100644 index a397e8709e..0000000000 --- a/tests/system/action/poll/test_publish.py +++ /dev/null @@ -1,101 +0,0 @@ -from typing import Any - -from openslides_backend.permissions.permissions import Permissions - -from .base_poll_test import BasePollTestCase - - -class PollPublishActionTest(BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.test_models: dict[str, dict[str, Any]] = { - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "long", - "state": "finished", - "meeting_id": 1, - "content_object_id": "topic/1", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "YNA", - }, - "topic/1": {"meeting_id": 1}, - "committee/1": {"meeting_ids": [1]}, - "meeting/1": {"is_active_in_organization_id": 1, "committee_id": 1}, - } - - def test_publish_correct(self) -> None: - self.set_models(self.test_models) - response = self.request("poll.publish", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("state") == "published" - self.assert_history_information("topic/1", ["Voting published"]) - - def test_publish_assignment(self) -> None: - self.test_models["poll/1"]["content_object_id"] = "assignment/1" - self.test_models["assignment/1"] = { - "meeting_id": 1, - } - self.set_models(self.test_models) - response = self.request("poll.publish", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_history_information("assignment/1", ["Ballot published"]) - - def test_publish_wrong_state(self) -> None: - self.create_meeting() - self.set_models( - { - "topic/1": {"poll_ids": [111], "meeting_id": 1}, - "poll/1": { - "state": "created", - "meeting_id": 1, - "content_object_id": "topic/1", - }, - "meeting/1": {"topic_ids": [1]}, - } - ) - response = self.request("poll.publish", {"id": 1}) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - assert poll.get("state") == "created" - assert ( - "Cannot publish poll 1, because it is not in state finished or started." - in response.json["message"] - ) - - def test_publish_started(self) -> None: - self.test_models["poll/1"]["state"] = "started" - self.set_models(self.test_models) - self.vote_service.start(1) - response = self.request("poll.publish", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "votescast": "0.000000", - "votesinvalid": "0.000000", - "votesvalid": "0.000000", - "entitled_users_at_stop": [], - }, - ) - self.assert_history_information("topic/1", ["Voting stopped/published"]) - - def test_publish_no_permissions(self) -> None: - self.base_permission_test(self.test_models, "poll.publish", {"id": 1}) - - def test_publish_permissions(self) -> None: - self.base_permission_test( - self.test_models, - "poll.publish", - {"id": 1}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_publish_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - self.test_models, - "poll.publish", - {"id": 1}, - ) diff --git a/tests/system/action/poll/test_reset.py b/tests/system/action/poll/test_reset.py deleted file mode 100644 index 1a2795d9bb..0000000000 --- a/tests/system/action/poll/test_reset.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import Any - -from openslides_backend.models.models import Poll -from openslides_backend.permissions.permissions import Permissions -from tests.system.util import CountDatastoreCalls, Profiler, performance - -from .base_poll_test import BasePollTestCase -from .poll_test_mixin import PollTestMixin - - -class PollResetActionTest(PollTestMixin, BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.test_models: dict[str, dict[str, Any]] = { - "topic/1": { - "meeting_id": 1, - }, - "poll/1": { - "state": Poll.STATE_STARTED, - "option_ids": [1], - "global_option_id": 2, - "meeting_id": 1, - "content_object_id": "topic/1", - }, - "option/1": {"vote_ids": [1, 2], "poll_id": 1, "meeting_id": 1}, - "option/2": { - "vote_ids": [3], - "used_as_global_option_in_poll_id": 1, - "meeting_id": 1, - }, - "vote/1": {"option_id": 1, "meeting_id": 1}, - "vote/2": {"option_id": 1, "meeting_id": 1}, - "vote/3": {"option_id": 2, "meeting_id": 1}, - "committee/1": {"meeting_ids": [1]}, - "meeting/1": {"is_active_in_organization_id": 1, "committee_id": 1}, - } - - def test_reset_correct(self) -> None: - self.test_models["poll/1"] = { - **self.test_models["poll/1"], - "votesvalid": "3.000000", - "votesinvalid": "1.000000", - "votescast": "5.000000", - "entitled_users_at_stop": [{"user_id": 1, "voted": True}], - } - self.set_models(self.test_models) - - response = self.request("poll.reset", {"id": 1}) - self.assert_status_code(response, 200) - - # check if the state has been changed to 1 (Created). - poll = self.get_model("poll/1") - assert poll.get("state") == "created" - - # check if not is_pseudoanonymized - assert poll.get("is_pseudoanonymized") is False - - # check if voted_ids is cleared - assert poll.get("voted_ids") == [] - - # check if auto generated fields are cleared - assert poll.get("entitled_users_at_stop") is None - assert poll.get("votesvalid") is None - assert poll.get("votesinvalid") is None - assert poll.get("votescast") is None - - # check if the votes are deleted - self.assert_model_not_exists("vote/1") - self.assert_model_not_exists("vote/2") - self.assert_model_not_exists("vote/3") - - # check if the option.vote_ids fields are cleared - option_1 = self.get_model("option/1") - assert option_1.get("vote_ids") == [] - assert option_1.get("yes") == "0.000000" - assert option_1.get("no") == "0.000000" - assert option_1.get("abstain") == "0.000000" - option_2 = self.get_model("option/2") - assert option_2.get("vote_ids") == [] - assert option_2.get("yes") == "0.000000" - assert option_2.get("no") == "0.000000" - assert option_2.get("abstain") == "0.000000" - - # test history - self.assert_history_information("topic/1", ["Voting reset"]) - - def test_reset_assignment(self) -> None: - self.test_models["poll/1"]["content_object_id"] = "assignment/1" - self.test_models["assignment/1"] = { - "meeting_id": 1, - } - self.set_models(self.test_models) - response = self.request("poll.reset", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_history_information("assignment/1", ["Ballot reset"]) - - def test_reset_no_permissions(self) -> None: - self.base_permission_test(self.test_models, "poll.reset", {"id": 1}) - - def test_reset_permissions(self) -> None: - self.base_permission_test( - self.test_models, - "poll.reset", - {"id": 1}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_reset_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - self.test_models, - "poll.reset", - {"id": 1}, - ) - - def test_reset_not_allowed_to_vote_again(self) -> None: - self.set_models(self.test_models) - self.set_models( - { - "group/1": {"meeting_user_ids": [1]}, - "user/1": {"meeting_user_ids": [1], "is_present_in_meeting_ids": [1]}, - "meeting_user/1": {"meeting_id": 1, "user_id": 1, "group_ids": [1]}, - "poll/1": { - "state": "started", - "option_ids": [1], - "global_option_id": 2, - "meeting_id": 1, - "entitled_group_ids": [1], - "pollmethod": "Y", - "max_votes_per_option": 1, - "type": "named", - "backend": "long", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "Y", - "content_object_id": "topic/1", - }, - "topic/1": {"meeting_id": 1, "poll_ids": [1], "title": "Tim the topic"}, - } - ) - self.vote_service.start(1) - response = self.vote_service.vote({"id": 1, "value": {"1": 1}}) - self.assert_status_code(response, 200) - response = self.request("poll.reset", {"id": 1}) - self.assert_status_code(response, 200) - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - response = self.vote_service.vote({"id": 1, "value": {"1": 1}}) - self.assert_status_code(response, 200) - - def test_reset_datastore_calls(self) -> None: - self.prepare_users_and_poll(3) - - with CountDatastoreCalls() as counter: - response = self.request("poll.reset", {"id": 1}) - - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", {"voted_ids": [], "state": Poll.STATE_CREATED} - ) - assert counter.calls == 5 - - @performance - def test_reset_performance(self) -> None: - # TODO this needs a different idea - self.prepare_users_and_poll(100) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - self.datastore.reset(hard=True) - - with Profiler("test_reset_performance.prof"): - response = self.request("poll.reset", {"id": 1}) - - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", {"voted_ids": [], "state": Poll.STATE_CREATED} - ) diff --git a/tests/system/action/poll/test_set_state.py b/tests/system/action/poll/test_set_state.py deleted file mode 100644 index 9a362d93e3..0000000000 --- a/tests/system/action/poll/test_set_state.py +++ /dev/null @@ -1,32 +0,0 @@ -from .base_poll_test import BasePollTestCase - - -class PollSetState(BasePollTestCase): - def test_set_state(self) -> None: - self.set_models( - { - "meeting/110": { - "name": "meeting_110", - "is_active_in_organization_id": 1, - }, - "poll/65": { - "type": "analog", - "state": "created", - "pollmethod": "YNA", - "meeting_id": 110, - "option_ids": [57], - }, - "option/57": { - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "meeting_id": 110, - "poll_id": 65, - "vote_ids": [], - }, - } - ) - response = self.request("poll.set_state", {"id": 65, "state": "finished"}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/65") - assert poll.get("state") == "finished" diff --git a/tests/system/action/poll/test_start.py b/tests/system/action/poll/test_start.py deleted file mode 100644 index b0eb53dda8..0000000000 --- a/tests/system/action/poll/test_start.py +++ /dev/null @@ -1,208 +0,0 @@ -from typing import Any - -from openslides_backend.models.models import Poll -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID - -from .base_poll_test import BasePollTestCase - - -class VotePollBaseTestClass(BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting() - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "meeting/1": { - "name": "my meeting", - "poll_couple_countdown": True, - "poll_countdown_id": 11, - "meeting_user_ids": [11], - "present_user_ids": [1], - }, - "projector_countdown/11": { - "default_time": 60, - "running": False, - "countdown_time": 60, - "meeting_id": 1, - }, - "group/1": {"meeting_user_ids": [11]}, - "option/1": {"meeting_id": 1, "poll_id": 1}, - "option/2": {"meeting_id": 1, "poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [1], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "meeting_id": 1, - "user_id": 1, - "group_ids": [1], - }, - "assignment/1": { - "title": "test_assignment_tcLT59bmXrXif424Qw7K", - "open_posts": 1, - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "assignment/1", - "title": "test_title_04k0y4TwPLpJKaSvIGm1", - "state": Poll.STATE_CREATED, - "meeting_id": 1, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "votesvalid": "0.000000", - "votescast": "0.000000", - "backend": "fast", - **self.get_poll_data(), - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - } - ) - - def get_poll_data(self) -> dict[str, Any]: - # has to be implemented by subclasses - raise NotImplementedError() - - -class VotePollAnalogYNA(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "YNA", - "type": Poll.TYPE_ANALOG, - } - - def test_start_analog_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 400) - assert ( - "Analog polls cannot be started. Please use poll.update instead to give votes." - in response.json["message"] - ) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_CREATED) - - -class VotePollNamedYNA(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "YNA", - "type": Poll.TYPE_NAMED, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") - # test history - self.assert_history_information("assignment/1", ["Ballot started"]) - # test that votes can be given - response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}}) - self.assert_status_code(response, 200) - - def test_start_motion_poll(self) -> None: - self.set_models( - { - "poll/1": {"content_object_id": "motion/1"}, - "motion/1": {"meeting_id": 1}, - } - ) - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_history_information("motion/1", ["Voting started"]) - - -class VotePollNamedY(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "Y", - "type": Poll.TYPE_NAMED, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") - - -class VotePollNamedN(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "N", - "type": Poll.TYPE_NAMED, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoanonymousYNA(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "YNA", - "type": Poll.TYPE_PSEUDOANONYMOUS, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoanonymousY(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "Y", - "type": Poll.TYPE_PSEUDOANONYMOUS, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoAnonymousN(VotePollBaseTestClass): - def get_poll_data(self) -> dict[str, Any]: - return { - "pollmethod": "N", - "type": Poll.TYPE_PSEUDOANONYMOUS, - } - - def test_start_poll(self) -> None: - response = self.request("poll.start", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("state"), Poll.STATE_STARTED) - self.assertEqual(poll.get("votesvalid"), "0.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "0.000000") - self.assert_model_not_exists("vote/1") diff --git a/tests/system/action/poll/test_stop.py b/tests/system/action/poll/test_stop.py deleted file mode 100644 index 50de40a6fa..0000000000 --- a/tests/system/action/poll/test_stop.py +++ /dev/null @@ -1,507 +0,0 @@ -from typing import Any -from unittest.mock import Mock, patch - -from openslides_backend.models.models import Poll -from openslides_backend.permissions.permissions import Permissions -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID -from tests.system.util import CountDatastoreCalls, Profiler, performance - -from .base_poll_test import BasePollTestCase -from .poll_test_mixin import PollTestMixin - - -class PollStopActionTest(PollTestMixin, BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.test_models: dict[str, dict[str, Any]] = { - "topic/1": { - "meeting_id": 1, - }, - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "fast", - "content_object_id": "topic/1", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "Y", - }, - "committee/1": {"meeting_ids": [1]}, - "meeting/1": {"is_active_in_organization_id": 1, "committee_id": 1}, - } - - @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear") - def test_stop_correct(self, clear: Mock) -> None: - clear_called_on: list[int] = [] - - def add_to_list(id_: int) -> None: - clear_called_on.append(id_) - - clear.side_effect = add_to_list - self.create_meeting() - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "motion/1", - "type": Poll.TYPE_NAMED, - "pollmethod": "YN", - "backend": "fast", - "state": Poll.STATE_STARTED, - "option_ids": [1], - "meeting_id": 1, - "entitled_group_ids": [1], - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - "option/1": {"meeting_id": 1, "poll_id": 1}, - "group/1": {"meeting_id": 1}, - "meeting/1": { - "users_enable_vote_weight": True, - "default_group_id": 1, - "poll_couple_countdown": True, - "poll_countdown_id": 1, - "group_ids": [1], - "users_enable_vote_delegations": True, - }, - "projector_countdown/1": { - "running": True, - "default_time": 60, - "countdown_time": 30.0, - "meeting_id": 1, - }, - } - ) - user1 = self.create_user_for_meeting(1) - user2 = self.create_user_for_meeting(1) - user3 = self.create_user_for_meeting(1) - self.set_models( - { - f"user/{user1}": { - "meeting_user_ids": [1], - "default_vote_weight": "2.000000", - "is_present_in_meeting_ids": [1], - }, - f"user/{user2}": { - "meeting_user_ids": [2], - "default_vote_weight": "3.000000", - "is_present_in_meeting_ids": [1], - }, - f"user/{user3}": {"meeting_user_ids": [3]}, - "meeting_user/1": { - "user_id": 2, - "vote_weight": "2.600000", - "vote_delegations_from_ids": [4], - }, - "meeting_user/2": { - "user_id": 3, - "vote_weight": "3.600000", - }, - "meeting_user/3": { - "user_id": 4, - "vote_weight": "4.600000", - "vote_delegated_to_id": 1, - }, - } - ) - self.start_poll(1) - self.login(user1) - response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}}) - self.assert_status_code(response, 200) - response = self.vote_service.vote( - {"id": 1, "user_id": user3, "value": {"1": "N"}} - ) - self.assert_status_code(response, 200) - - self.login(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - countdown = self.get_model("projector_countdown/1") - assert countdown.get("running") is False - assert countdown.get("countdown_time") == 60 - poll = self.get_model("poll/1") - assert poll.get("voted_ids") == [2, 4] - assert poll.get("state") == Poll.STATE_FINISHED - assert poll.get("votescast") == "2.000000" - assert poll.get("votesinvalid") == "0.000000" - assert poll.get("votesvalid") == "7.200000" - assert poll.get("entitled_users_at_stop") == [ - { - "voted": True, - "present": True, - "user_id": user1, - "vote_delegated_to_user_id": None, - }, - { - "voted": False, - "present": True, - "user_id": user2, - "vote_delegated_to_user_id": None, - }, - { - "voted": True, - "present": False, - "user_id": user3, - "vote_delegated_to_user_id": user1, - }, - ] - # test history - self.assert_history_information("motion/1", ["Voting stopped"]) - assert clear_called_on == [1] - - def test_stop_assignment_poll(self) -> None: - self.create_meeting() - self.set_models( - { - "assignment/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "assignment/1", - "type": Poll.TYPE_NAMED, - "pollmethod": "YN", - "backend": "fast", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - } - ) - self.start_poll(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - self.assert_history_information("assignment/1", ["Ballot stopped"]) - - def test_stop_entitled_users_at_stop_user_only_once(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "fast", - "content_object_id": "motion/1", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "entitled_group_ids": [3, 4], - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - "user/2": { - "is_present_in_meeting_ids": [1], - "meeting_user_ids": [1], - }, - "meeting_user/1": { - "user_id": 2, - "meeting_id": 1, - "group_ids": [3, 4], - }, - "group/3": {"meeting_user_ids": [1]}, - "group/4": {"meeting_user_ids": [1]}, - "meeting/1": { - "group_ids": [3, 4], - "meeting_user_ids": [1], - }, - } - ) - self.start_poll(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("entitled_users_at_stop") == [ - { - "voted": False, - "present": True, - "user_id": 2, - "vote_delegated_to_user_id": None, - }, - ] - - def test_stop_entitled_users_not_present(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "fast", - "content_object_id": "motion/1", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "entitled_group_ids": [3], - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - "user/2": { - "meeting_user_ids": [12], - "meeting_ids": [1], - }, - "meeting_user/12": {"user_id": 2, "meeting_id": 1, "group_ids": [3]}, - "user/3": { - "meeting_user_ids": [13], - "meeting_ids": [1], - }, - "meeting_user/13": {"user_id": 3, "meeting_id": 1, "group_ids": [4]}, - "group/3": {"meeting_user_ids": [12], "meeting_id": 1}, - "group/4": {"meeting_user_ids": [13], "meeting_id": 1}, - "meeting/1": { - "user_ids": [2, 3], - "group_ids": [3, 4], - "meeting_user_ids": [12, 13], - }, - } - ) - self.start_poll(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("entitled_users_at_stop") == [ - { - "voted": False, - "present": False, - "user_id": 2, - "vote_delegated_to_user_id": None, - }, - ] - - def test_stop_entitled_users_with_delegations(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "fast", - "content_object_id": "motion/1", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "entitled_group_ids": [3], - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - "user/2": { - "meeting_user_ids": [12], - "meeting_ids": [1], - }, - "meeting_user/12": { - "user_id": 2, - "meeting_id": 1, - "group_ids": [3], - "vote_delegated_to_id": 13, - }, - "user/3": { - "meeting_user_ids": [13], - "meeting_ids": [1], - }, - "meeting_user/13": { - "user_id": 3, - "meeting_id": 1, - "group_ids": [4], - "vote_delegations_from_ids": [12], - }, - "group/3": {"meeting_user_ids": [12], "meeting_id": 1}, - "group/4": {"meeting_user_ids": [13], "meeting_id": 1}, - "meeting/1": { - "user_ids": [2, 3], - "group_ids": [3, 4], - "meeting_user_ids": [12, 13], - "users_enable_vote_delegations": True, - }, - } - ) - self.start_poll(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("entitled_users_at_stop") == [ - { - "voted": False, - "present": False, - "user_id": 2, - "vote_delegated_to_user_id": 3, - }, - ] - - def test_stop_entitled_users_with_delegations_turned_off(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "type": "named", - "pollmethod": "Y", - "backend": "fast", - "content_object_id": "motion/1", - "state": Poll.STATE_STARTED, - "meeting_id": 1, - "entitled_group_ids": [3], - "onehundred_percent_base": "Y", - "sequential_number": 1, - "title": "Poll 1", - }, - "user/2": { - "meeting_user_ids": [12], - "meeting_ids": [1], - }, - "meeting_user/12": { - "user_id": 2, - "meeting_id": 1, - "group_ids": [3], - "vote_delegated_to_id": 13, - }, - "user/3": { - "meeting_user_ids": [13], - "meeting_ids": [1], - }, - "meeting_user/13": { - "user_id": 3, - "meeting_id": 1, - "group_ids": [4], - "vote_delegations_from_ids": [12], - }, - "group/3": {"meeting_user_ids": [12], "meeting_id": 1}, - "group/4": {"meeting_user_ids": [13], "meeting_id": 1}, - "meeting/1": { - "user_ids": [2, 3], - "group_ids": [3, 4], - "meeting_user_ids": [12, 13], - "users_enable_vote_delegations": False, - }, - } - ) - self.start_poll(1) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("entitled_users_at_stop") == [ - { - "voted": False, - "present": False, - "user_id": 2, - "vote_delegated_to_user_id": None, - }, - ] - - def test_stop_published(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "motion/1", - "state": Poll.STATE_PUBLISHED, - "meeting_id": 1, - }, - } - ) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - assert poll.get("state") == Poll.STATE_PUBLISHED - assert ( - "Cannot stop poll 1, because it is not in state started." - in response.json["message"] - ) - - def test_stop_created(self) -> None: - self.create_meeting() - self.set_models( - { - "motion/1": { - "meeting_id": 1, - }, - "poll/1": { - "content_object_id": "motion/1", - "state": Poll.STATE_CREATED, - "meeting_id": 1, - }, - } - ) - response = self.request("poll.stop", {"id": 1}) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - assert poll.get("state") == Poll.STATE_CREATED - assert ( - "Cannot stop poll 1, because it is not in state started." - in response.json["message"] - ) - - @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear") - def test_stop_no_permissions(self, clear: Mock) -> None: - clear_called_on: list[int] = [] - - def add_to_list(id_: int) -> None: - clear_called_on.append(id_) - - clear.side_effect = add_to_list - self.set_models(self.test_models) - self.start_poll(1) - self.base_permission_test({}, "poll.stop", {"id": 1}) - - assert clear_called_on == [] - - def test_stop_permissions(self) -> None: - self.set_models(self.test_models) - self.start_poll(1) - self.base_permission_test( - {}, - "poll.stop", - {"id": 1}, - Permissions.Poll.CAN_MANAGE, - ) - - def test_stop_permissions_locked_meeting(self) -> None: - self.set_models(self.test_models) - self.start_poll(1) - self.base_locked_out_superadmin_permission_test( - {}, - "poll.stop", - {"id": 1}, - ) - - def test_stop_datastore_calls(self) -> None: - user_ids = self.prepare_users_and_poll(3) - - with CountDatastoreCalls() as counter: - response = self.request("poll.stop", {"id": 1}) - - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll["voted_ids"] == user_ids - # always 12 plus len(user_ids) calls, dependent of user count - assert counter.calls == 12 + len(user_ids) - - @performance - def test_stop_performance(self) -> None: - user_ids = self.prepare_users_and_poll(3) - - with Profiler("test_stop_performance.prof"): - response = self.request("poll.stop", {"id": 1}) - - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll["voted_ids"] == user_ids diff --git a/tests/system/action/poll/test_update.py b/tests/system/action/poll/test_update.py deleted file mode 100644 index 229598bcfe..0000000000 --- a/tests/system/action/poll/test_update.py +++ /dev/null @@ -1,667 +0,0 @@ -from typing import Any - -from openslides_backend.models.models import Poll -from openslides_backend.permissions.permissions import Permissions -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID - -from .base_poll_test import BasePollTestCase - - -class UpdatePollTestCase(BasePollTestCase): - def setUp(self) -> None: - super().setUp() - self.entitled_users_at_stop_data: list[dict[str, Any]] = [ - { - "voted": True, - "present": True, - "user_id": 2, - "vote_delegated_to_user_id": None, - }, - { - "voted": True, - "present": False, - "user_id": 3, - "vote_delegated_to_user_id": 2, - }, - ] - self.create_meeting() - self.set_models( - { - "assignment/1": { - "title": "test_assignment_ohneivoh9caiB8Yiungo", - "open_posts": 1, - }, - "meeting/1": { - "meeting_user_ids": [11], - }, - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11], "poll_ids": [1]}, - "poll/1": { - "content_object_id": "assignment/1", - "title": "test_title_beeFaihuNae1vej2ai8m", - "pollmethod": "Y", - "type": Poll.TYPE_NAMED, - "onehundred_percent_base": "Y", - "state": Poll.STATE_CREATED, - "meeting_id": 1, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - }, - "option/1": {"meeting_id": 1, "poll_id": 1}, - "option/2": {"meeting_id": 1, "poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [1], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 1, - "group_ids": [1], - }, - } - ) - - def test_catch_not_allowed(self) -> None: - self.update_model("poll/1", {"state": "started"}) - response = self.request( - "poll.update", - { - "id": 1, - "pollmethod": "Y", - "type": "analog", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "global_yes": False, - "global_no": True, - "global_abstain": True, - }, - ) - self.assert_status_code(response, 400) - assert ("data must not contain {'type'} properties") in response.json["message"] - - def test_optional_state_created(self) -> None: - response = self.request( - "poll.update", - { - "id": 1, - "pollmethod": "Y", - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "global_yes": False, - "global_no": True, - "global_abstain": True, - }, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("pollmethod") == "Y" - assert poll.get("min_votes_amount") == 1 - assert poll.get("max_votes_amount") == 1 - assert poll.get("max_votes_per_option") == 1 - assert poll.get("global_yes") is False - assert poll.get("global_no") is True - assert poll.get("global_abstain") is True - self.assert_history_information("assignment/1", ["Ballot updated"]) - - def test_not_allowed_for_analog(self) -> None: - self.update_model("poll/1", {"type": "analog"}) - response = self.request("poll.update", {"id": 1, "entitled_group_ids": []}) - self.assert_status_code(response, 400) - assert ( - "Following options are not allowed in this state and type: " - "entitled_group_ids" - ) in response.json["message"] - - def test_not_allowed_for_non_analog(self) -> None: - response = self.request( - "poll.update", - { - "id": 1, - "votesvalid": "10.000000", - "votesinvalid": "11.000000", - "votescast": "3.000000", - "publish_immediately": True, - }, - ) - self.assert_status_code(response, 400) - assert ( - "Following options are not allowed in this state and type: " - "votesvalid, votesinvalid, votescast, publish_immediately" - ) in response.json["message"] - - def test_update_title(self) -> None: - response = self.request( - "poll.update", - {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("title"), "test_title_Aishohh1ohd0aiSut7gi") - - def test_prevent_updating_content_object(self) -> None: - self.create_model( - "assignment/2", - {"title": "test_title_phohdah8quukooHeetuz", "open_posts": 1}, - ) - response = self.request( - "poll.update", - {"content_object_id": "assignment/2", "id": 1}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("content_object_id"), "assignment/1") # unchanged - - def test_update_pollmethod(self) -> None: - response = self.request( - "poll.update", - {"pollmethod": "YNA", "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("pollmethod"), "YNA") - self.assertEqual(poll.get("onehundred_percent_base"), "Y") - - def test_update_backend(self) -> None: - response = self.request( - "poll.update", - {"backend": "long", "id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"backend": "long"}) - - def test_update_backend_not_allowed(self) -> None: - self.set_models({"poll/1": {"state": Poll.STATE_FINISHED}}) - response = self.request( - "poll.update", - {"backend": "long", "id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - "Following options are not allowed in this state and type: backend" - in response.json["message"] - ) - - def test_update_invalid_pollmethod(self) -> None: - response = self.request( - "poll.update", - {"pollmethod": "invalid"}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("pollmethod"), "Y") - - def test_update_groups_to_empty(self) -> None: - response = self.request("poll.update", {"entitled_group_ids": [], "id": 1}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("entitled_group_ids") == [] - - def test_update_groups(self) -> None: - self.create_model("group/4", {"meeting_id": 1, "poll_ids": []}) - response = self.request( - "poll.update", - {"entitled_group_ids": [4], "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("entitled_group_ids"), [4]) - - def test_update_groups_with_anonymous(self) -> None: - group_id = self.set_anonymous() - response = self.request( - "poll.update", - {"entitled_group_ids": [group_id], "id": 1}, - ) - self.assert_status_code(response, 400) - self.assertIn( - "Anonymous group is not allowed in entitled_group_ids.", - response.json["message"], - ) - - def test_update_title_started(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED}) - response = self.request( - "poll.update", - {"title": "test_title_Oophah8EaLaequu3toh8", "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("title"), "test_title_Oophah8EaLaequu3toh8") - - def test_update_wrong_state(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED}) - response = self.request( - "poll.update", - {"type": Poll.TYPE_NAMED, "id": 1}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("type"), Poll.TYPE_NAMED) - - def test_update_max_votes_per_option(self) -> None: - response = self.request( - "poll.update", - {"max_votes_per_option": 5, "max_votes_amount": 5, "id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"max_votes_per_option": 5}) - - def test_max_votes_per_option_smaller_max_votes_amount(self) -> None: - response = self.request( - "poll.update", - {"max_votes_per_option": 5, "max_votes_amount": 1, "id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - response.json["message"] - == "The maximum votes per option cannot be higher than the maximum amount of votes in total." - ) - self.assert_model_exists("poll/1", {"max_votes_per_option": 1}) - - def test_max_votes_amount_smaller_min(self) -> None: - response = self.request( - "poll.update", - {"min_votes_amount": 5, "max_votes_amount": 1, "id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - response.json["message"] - == "The minimum amount of votes cannot be higher than the maximum amount of votes." - ) - self.assert_model_exists("poll/1", {"min_votes_amount": 1}) - - def test_update_negative_fields(self) -> None: - for field in ("max_votes_per_option", "max_votes_amount", "min_votes_amount"): - response = self.request( - "poll.update", - {field: -3, "id": 1}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get(field), 1) - - def test_update_100_percent_base(self) -> None: - response = self.request( - "poll.update", - {"onehundred_percent_base": "cast", "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("onehundred_percent_base"), "cast") - - def test_update_wrong_100_percent_base(self) -> None: - response = self.request( - "poll.update", - {"onehundred_percent_base": "invalid", "id": 1}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("onehundred_percent_base"), "Y") - - def test_update_multiple_fields(self) -> None: - response = self.request( - "poll.update", - { - "id": 1, - "title": "test_title_ees6Tho8ahheen4cieja", - "pollmethod": "Y", - "global_yes": True, - "global_no": True, - "global_abstain": False, - "max_votes_per_option": 2, - "max_votes_amount": 2, - }, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("title"), "test_title_ees6Tho8ahheen4cieja") - self.assertEqual(poll.get("pollmethod"), "Y") - self.assertTrue(poll.get("global_yes")) - self.assertTrue(poll.get("global_no")) - self.assertFalse(poll.get("global_abstain")) - self.assertEqual(poll.get("max_votes_per_option"), 2) - - def test_update_max_votes_per_option_state_not_created(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED}) - response = self.request( - "poll.update", - {"max_votes_per_option": 3, "id": 1}, - ) - self.assert_status_code(response, 400) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("max_votes_per_option"), 1) - - def test_update_100_percent_base_state_not_created(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED}) - response = self.request( - "poll.update", - {"onehundred_percent_base": "cast", "id": 1}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("onehundred_percent_base"), "cast") - - def test_update_wrong_100_percent_base_state_not_created(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED, "pollmethod": "YN"}) - response = self.request( - "poll.update", - {"onehundred_percent_base": "YNA", "id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - "This onehundred_percent_base not allowed in this pollmethod" - in response.json["message"] - ) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("onehundred_percent_base"), "Y") - - def test_update_wrong_100_percent_base_entitled_and_analog(self) -> None: - self.update_model( - "poll/1", - {"state": Poll.STATE_STARTED, "pollmethod": "YN", "type": Poll.TYPE_ANALOG}, - ) - response = self.request( - "poll.update", - {"onehundred_percent_base": "entitled", "id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - "onehundred_percent_base: value entitled is not allowed for analog." - in response.json["message"] - ) - - def test_state_change(self) -> None: - self.update_model("poll/1", {"type": Poll.TYPE_ANALOG}) - response = self.request("poll.update", {"id": 1, "votescast": "1.000000"}) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("state") == Poll.STATE_FINISHED - - def test_state_change_2_published(self) -> None: - self.update_model("poll/1", {"type": Poll.TYPE_ANALOG}) - response = self.request( - "poll.update", - {"id": 1, "votescast": "1.000000", "publish_immediately": True}, - ) - self.assert_status_code(response, 200) - poll = self.get_model("poll/1") - assert poll.get("state") == Poll.STATE_PUBLISHED - assert "publish_immediately" not in poll - - def test_default_vote_values(self) -> None: - self.update_model("poll/1", {"type": Poll.TYPE_ANALOG}) - response = self.request( - "poll.update", - {"id": 1, "votescast": "1.000000"}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "votescast": "1.000000", - "votesvalid": "-2.000000", - "votesinvalid": "-2.000000", - }, - ) - - def test_motion_history_information(self) -> None: - self.set_models( - { - "poll/1": {"content_object_id": "motion/1"}, - "motion/1": {"meeting_id": 1, "poll_ids": [1]}, - } - ) - response = self.request( - "poll.update", - {"id": 1, "title": "test"}, - ) - self.assert_status_code(response, 200) - self.assert_history_information("motion/1", ["Voting updated"]) - - def test_update_no_permissions(self) -> None: - self.base_permission_test( - {}, - "poll.update", - {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1}, - ) - - def test_update_permissions(self) -> None: - self.base_permission_test( - {}, - "poll.update", - {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1}, - Permissions.Assignment.CAN_MANAGE, - ) - - def test_update_permissions_locked_meeting(self) -> None: - self.base_locked_out_superadmin_permission_test( - {}, - "poll.update", - {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1}, - ) - - def test_update_entitled_users_at_stop_error(self) -> None: - response = self.request( - "poll.update", - { - "entitled_users_at_stop": self.entitled_users_at_stop_data, - "id": 1, - }, - internal=False, - ) - self.assert_status_code(response, 400) - assert ( - "data must not contain {'entitled_users_at_stop'} properties" - in response.json["message"] - ) - - def test_update_entitled_users_at_stop_fields_changed_error(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - { - "entitled_users_at_stop": [ - {**self.entitled_users_at_stop_data[0], "voted": False}, - self.entitled_users_at_stop_data[1], - ], - "id": 1, - }, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "Can not change essential 'entitled_users_at_stop' data via poll.update" - in response.json["message"] - ) - - def test_update_entitled_users_at_stop_initial_set_attempt_error(self) -> None: - response = self.request( - "poll.update", - {"entitled_users_at_stop": self.entitled_users_at_stop_data, "id": 1}, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "Can not set 'entitled_users_at_stop' via poll.update" - in response.json["message"] - ) - - def test_update_entitled_users_at_stop_list_shortened_error(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - {"entitled_users_at_stop": [self.entitled_users_at_stop_data[1]], "id": 1}, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "Can not change essential 'entitled_users_at_stop' data via poll.update" - in response.json["message"] - ) - - def test_update_entitled_users_at_stop_list_lengthened_error(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - { - "entitled_users_at_stop": [ - *self.entitled_users_at_stop_data, - {"voted": True, "present": True, "user_id": 5}, - ], - "id": 1, - }, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "Can not change essential 'entitled_users_at_stop' data via poll.update" - in response.json["message"] - ) - - def test_update_entitled_users_at_stop_wrong_format_error(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - {"entitled_users_at_stop": {"this": "shouldn't be a dict"}, "id": 1}, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "'entitled_users_at_stop' has the wrong format" in response.json["message"] - ) - - def test_update_entitled_users_at_stop_wrong_format_error_2(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - {"entitled_users_at_stop": ["this shouldn't be a string"], "id": 1}, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "'entitled_users_at_stop' has the wrong format" in response.json["message"] - ) - - def test_update_entitled_users_at_stop_wrong_format_error_3(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - { - "entitled_users_at_stop": [{"this": "is still the wrong format"}], - "id": 1, - }, - internal=True, - ) - self.assert_status_code(response, 400) - assert ( - "'entitled_users_at_stop' has the wrong format" in response.json["message"] - ) - - def test_update_entitled_users_at_stop_nothing_changed(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - {"entitled_users_at_stop": self.entitled_users_at_stop_data, "id": 1}, - internal=True, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", {"entitled_users_at_stop": self.entitled_users_at_stop_data} - ) - - def test_update_entitled_users_at_stop_fields_changed_success(self) -> None: - self.set_models( - {"poll/1": {"entitled_users_at_stop": self.entitled_users_at_stop_data}} - ) - response = self.request( - "poll.update", - { - "entitled_users_at_stop": [ - { - **self.entitled_users_at_stop_data[0], - "delegation_user_merged_into_id": 9, - "user_merged_into_id": 10, - }, - self.entitled_users_at_stop_data[1], - ], - "id": 1, - }, - internal=True, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll/1", - { - "entitled_users_at_stop": [ - { - **self.entitled_users_at_stop_data[0], - "delegation_user_merged_into_id": 9, - "user_merged_into_id": 10, - }, - self.entitled_users_at_stop_data[1], - ] - }, - ) - - def test_live_voting_named_motion_poll(self) -> None: - self.set_models( - { - "motion/3": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - } - ) - self.update_model( - "poll/1", {"type": Poll.TYPE_NAMED, "content_object_id": "motion/3"} - ) - - response = self.request("poll.update", {"id": 1, "live_voting_enabled": True}) - self.assert_status_code(response, 200) - self.assert_model_exists("poll/1", {"live_voting_enabled": True}) - - def test_live_voting_not_allowed_type_analog(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_ANALOG, True) - - def test_live_voting_not_allowed_type_pseudoanonymous(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_PSEUDOANONYMOUS, True) - - def test_live_voting_not_allowed_is_motion_poll_false(self) -> None: - self.base_live_voting_not_allowed(Poll.TYPE_NAMED, False) - - def base_live_voting_not_allowed( - self, poll_type: str, is_motion_poll: bool - ) -> None: - if is_motion_poll: - self.set_models( - { - "motion/3": {"meeting_id": 1, "state_id": 444}, - "motion_state/444": {"meeting_id": 1, "allow_create_poll": True}, - } - ) - self.update_model("poll/1", {"content_object_id": "motion/3"}) - self.update_model("poll/1", {"type": poll_type}) - - response = self.request("poll.update", {"id": 1, "live_voting_enabled": True}) - self.assert_status_code(response, 400) - self.assert_model_exists("poll/1", {"live_voting_enabled": None}) - assert ( - "live_voting_enabled only allowed for named motion polls." - ) in response.json["message"] diff --git a/tests/system/action/poll/test_vote.py b/tests/system/action/poll/test_vote.py deleted file mode 100644 index 8c2b0535a4..0000000000 --- a/tests/system/action/poll/test_vote.py +++ /dev/null @@ -1,2424 +0,0 @@ -from typing import Any - -import pytest -import requests -import simplejson as json - -from openslides_backend.models.models import Poll -from openslides_backend.shared.util import ONE_ORGANIZATION_FQID -from tests.system.util import convert_to_test_response -from tests.util import Response - -from .base_poll_test import BasePollTestCase - - -@pytest.mark.usefixtures("auth_mockers") -class BaseVoteTestCase(BasePollTestCase): - def setUp(self) -> None: - assert hasattr(self, "auth_mockers") - self.auth_mockers["login_patch"].stop() - self.auth_mockers["auth_http_adapter_patch"].stop() - super().setUp() - - def tearDown(self) -> None: - super().tearDown() - self.auth_mockers["login_patch"].start() - self.auth_mockers["auth_http_adapter_patch"].start() - - def request( - self, - action: str, - data: dict[str, Any], - anonymous: bool = False, - lang: str | None = None, - internal: bool | None = None, - start_poll_before_vote: bool = True, - stop_poll_after_vote: bool = True, - ) -> Response: - """Overwrite request method to reroute voting requests to the vote service.""" - if action == "poll.vote": - if start_poll_before_vote: - self.vote_service.start(data["id"]) - response = self.vote_service.vote(data) - if stop_poll_after_vote: - self.execute_action_internally("poll.stop", {"id": data["id"]}) - return response - else: - return super().request(action, data, anonymous, lang, internal) - - def anonymous_vote(self, payload: dict[str, Any], id: int = 1) -> Response: - # make request manually to prevent sending of cookie & header - payload_json = json.dumps(payload, separators=(",", ":")) - response = requests.post( - url=self.vote_service.url.replace("internal", "system") + f"?id={id}", - data=payload_json, - headers={ - "Content-Type": "application/json", - }, - ) - return convert_to_test_response(response) - - -class PollVoteTest(BaseVoteTestCase): - def setUp(self) -> None: - super().setUp() - self.create_model( - "meeting/113", - {"is_active_in_organization_id": 1}, - ) - - def test_vote_correct_pollmethod_Y(self) -> None: - user_id = self.create_user("test2") - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11, 12], "poll_ids": [1]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - "meeting_ids": [113], - }, - "meeting_user/11": { - "meeting_id": 113, - "user_id": 1, - "group_ids": [1], - }, - f"user/{user_id}": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [12], - "meeting_ids": [113], - }, - "meeting_user/12": { - "meeting_id": 113, - "user_id": user_id, - "vote_weight": "2.000000", - "group_ids": [1], - }, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "meeting/113": { - "users_enable_vote_weight": True, - "meeting_user_ids": [11, 12], - }, - } - ) - response = self.request( - "poll.vote", {"id": 1, "value": {"11": 1}}, stop_poll_after_vote=False - ) - self.assert_status_code(response, 200) - self.login(user_id) - response = self.request( - "poll.vote", {"id": 1, "value": {"11": 1}}, start_poll_before_vote=False - ) - self.assert_status_code(response, 200) - for i in range(1, 3): - vote = self.assert_model_exists( - f"vote/{i}", {"value": "Y", "option_id": 11, "meeting_id": 113} - ) - user_id = vote.get("user_id", 0) - assert user_id == vote.get("delegated_user_id") - self.assert_model_exists( - f"user/{user_id}", - { - "poll_voted_ids": [1], - "delegated_vote_ids": [i], - "vote_ids": [vote["id"]], - }, - ) - assert vote.get("weight") == f"{user_id}.000000" - self.assert_model_exists( - "option/11", - { - "vote_ids": [1, 2], - "yes": "3.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - ) - - def test_value_check(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "YN", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "Y", "12": "N", "13": "A"}, - }, - ) - self.assert_status_code(response, 400) - assert ( - "Data for option 13 does not fit the poll method." - in response.json["message"] - ) - - def test_vote_correct_pollmethod_YN(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "YN", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "Y", "12": "N"}, - }, - ) - self.assert_status_code(response, 200) - vote = self.assert_model_exists( - "vote/1", - { - "value": "Y", - "option_id": 11, - "weight": "1.000000", - "meeting_id": 113, - "user_id": 1, - "delegated_user_id": 1, - }, - ) - user_token = vote.get("user_token") - vote = self.assert_model_exists( - "vote/2", - { - "value": "N", - "option_id": 12, - "weight": "1.000000", - "meeting_id": 113, - "user_id": 1, - "delegated_user_id": 1, - }, - ) - assert vote.get("user_token") == user_token - self.assert_model_exists( - "option/11", - { - "vote_ids": [1], - "yes": "1.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - ) - self.assert_model_exists( - "option/12", - { - "vote_ids": [2], - "yes": "0.000000", - "no": "1.000000", - "abstain": "0.000000", - }, - ) - - def test_vote_wrong_votes_total(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": 1, "12": 1}, - }, - ) - self.assert_status_code(response, 400) - assert ( - "The sum of your answers has to be between 1 and 1" - in response.json["message"] - ) - self.assert_model_not_exists("vote/1") - - def test_vote_pollmethod_Y_wrong_value(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "option_ids": [11, 12, 13], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "value": {"11": "Y"}, - }, - ) - self.assert_status_code(response, 400) - assert "Your vote has a wrong format" in response.json["message"] - self.assert_model_not_exists("vote/1") - - def test_vote_no_votes_total_check_by_YNA(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "YNA", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 1, - "max_votes_amount": 2, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "Y", "12": "A"}, - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - - def test_vote_no_votes_total_check_by_YNA_max_votes_error(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "YNA", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "Y", "12": "A"}, - }, - ) - self.assert_status_code(response, 400) - assert "You have to select between 1 and 1 options" in response.json["message"] - - def test_vote_no_votes_total_check_by_YN(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "YN", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "Y", "12": "N"}, - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - - def test_vote_wrong_votes_total_min_case(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11, 12, 13], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "min_votes_amount": 2, - "max_votes_amount": 2, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": 1}, - }, - ) - self.assert_status_code(response, 400) - assert ( - "The sum of your answers has to be between 2 and 2" - in response.json["message"] - ) - self.assert_model_not_exists("vote/1") - - def test_vote_global(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11, 12]}, - "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1}, - "user/2": { - "username": "test2", - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [12], - }, - "meeting_user/12": { - "user_id": 2, - "meeting_id": 113, - "group_ids": [1], - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "global_option_id": 11, - "global_no": True, - "global_yes": False, - "global_abstain": False, - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "pollmethod": "YNA", - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - } - ) - response = self.request( - "poll.vote", - {"id": 1, "user_id": 1, "value": "N"}, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 200) - response = self.request("poll.vote", {"id": 1, "user_id": 2, "value": "Y"}) - self.assert_status_code(response, 400) - - self.assert_model_exists( - "vote/1", - { - "value": "N", - "option_id": 11, - "weight": "1.000000", - "meeting_id": 113, - "user_id": 1, - }, - ) - self.assert_model_exists( - "option/11", - { - "vote_ids": [1], - "yes": "0.000000", - "no": "1.000000", - "abstain": "0.000000", - }, - ) - self.assert_model_exists( - "user/1", - { - "poll_voted_ids": [1], - "delegated_vote_ids": [1], - "vote_ids": [1], - }, - ) - self.assert_model_not_exists("vote/2") - - def test_vote_schema_problems(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "entitled_group_ids": [1], - "meeting_id": 113, - "pollmethod": "YNA", - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "X"}) - self.assert_status_code(response, 400) - assert "Global vote X is not enabled" in response.json["message"] - - def test_vote_invalid_vote_value(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "option_ids": [11], - "pollmethod": "YNA", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "meeting_id": 113, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"11": "X"}, - }, - ) - self.assert_status_code(response, 400) - assert ( - "Data for option 11 does not fit the poll method." - in response.json["message"] - ) - - def test_vote_not_started_in_service(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "type": "named", - "meeting_id": 113, - "pollmethod": "YNA", - "global_yes": True, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "YNA", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - {"id": 1, "value": "Y"}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - assert "Poll does not exist" in response.json["message"] - - def test_vote_option_not_in_poll(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "motion/1": { - "meeting_id": 113, - }, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "option/12": {"meeting_id": 113, "poll_id": 1}, - "option/13": {"meeting_id": 113, "poll_id": 1}, - "poll/1": { - "content_object_id": "motion/1", - "option_ids": [11, 12, 13], - "title": "my test poll", - "type": "named", - "pollmethod": "YNA", - "entitled_group_ids": [1], - "meeting_id": 113, - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request( - "poll.vote", - { - "id": 1, - "user_id": 1, - "value": {"113": "Y"}, - }, - ) - self.assert_status_code(response, 400) - assert "Option_id 113 does not belong to the poll" in response.json["message"] - - def test_double_vote(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11, 12]}, - "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1}, - "user/2": { - "username": "test2", - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [12], - }, - "meeting_user/12": { - "user_id": 2, - "meeting_id": 113, - "group_ids": [1], - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "global_option_id": 11, - "global_no": True, - "global_yes": False, - "global_abstain": False, - "meeting_id": 113, - "entitled_group_ids": [1], - "pollmethod": "YN", - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - } - ) - response = self.request( - "poll.vote", - {"id": 1, "user_id": 1, "value": "N"}, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 200) - response = self.request( - "poll.vote", - {"id": 1, "user_id": 1, "value": "N"}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - assert "Not the first vote" in response.json["message"] - self.assert_model_exists( - "vote/1", - { - "value": "N", - "option_id": 11, - "weight": "1.000000", - "meeting_id": 113, - "user_id": 1, - "delegated_user_id": 1, - }, - ) - self.assert_model_exists("option/11", {"vote_ids": [1]}) - self.assert_model_exists( - "user/1", - {"poll_voted_ids": [1], "vote_ids": [1], "delegated_vote_ids": [1]}, - ) - - def test_check_user_in_entitled_group(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - "meeting_ids": [113], - }, - "meeting_user/11": {"user_id": 1, "meeting_id": 113, "group_ids": [1]}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "pollmethod": "YNA", - "global_option_id": 11, - "global_no": True, - "global_yes": False, - "global_abstain": False, - "meeting_id": 113, - "entitled_group_ids": [], - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "title": "Poll 1", - "onehundred_percent_base": "YNA", - }, - } - ) - response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "N"}) - self.assert_status_code(response, 400) - assert "User 1 is not allowed to vote" in response.json["message"] - - def test_check_user_present_in_meeting(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "user/1": {"meeting_user_ids": [11]}, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "global_option_id": 11, - "global_no": True, - "global_yes": False, - "global_abstain": False, - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "backend": "fast", - "type": "named", - "pollmethod": "YNA", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - } - ) - response = self.request("poll.vote", {"id": 1, "value": "N"}) - self.assert_status_code(response, 400) - assert "You have to be present in meeting 113" in response.json["message"] - - def test_check_str_validation(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "type": "named", - "meeting_id": 113, - "entitled_group_ids": [1], - "pollmethod": "Y", - "state": Poll.STATE_STARTED, - "backend": "fast", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - } - ) - response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "X"}) - self.assert_status_code(response, 400) - assert "Global vote X is not enabled" in response.json["message"] - - def test_default_vote_weight(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - "default_vote_weight": "3.000000", - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - "meeting/113": {"users_enable_vote_weight": True}, - } - ) - response = self.request( - "poll.vote", {"id": 1, "user_id": 1, "value": {"11": 1}} - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "vote/1", - { - "value": "Y", - "option_id": 11, - "weight": "3.000000", - "meeting_id": 113, - "user_id": 1, - }, - ) - self.assert_model_exists( - "option/11", - { - "vote_ids": [1], - "yes": "3.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - ) - self.assert_model_exists( - "user/1", - {"poll_voted_ids": [1], "delegated_vote_ids": [1], "vote_ids": [1]}, - ) - - def test_vote_weight_not_enabled(self) -> None: - self.set_models( - { - ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, - "group/1": {"meeting_user_ids": [11]}, - "option/11": {"meeting_id": 113, "poll_id": 1}, - "user/1": { - "is_present_in_meeting_ids": [113], - "default_vote_weight": "3.000000", - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "meeting_id": 113, - "user_id": 1, - "vote_weight": "4.200000", - "group_ids": [1], - }, - "motion/1": { - "meeting_id": 113, - }, - "poll/1": { - "content_object_id": "motion/1", - "title": "my test poll", - "option_ids": [11], - "pollmethod": "Y", - "meeting_id": 113, - "entitled_group_ids": [1], - "state": Poll.STATE_STARTED, - "max_votes_per_option": 1, - "backend": "fast", - "type": "named", - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - "meeting/113": { - "users_enable_vote_weight": False, - "meeting_user_ids": [11], - }, - } - ) - response = self.request( - "poll.vote", {"id": 1, "user_id": 1, "value": {"11": 1}} - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "vote/1", - { - "value": "Y", - "option_id": 11, - "weight": "1.000000", - "meeting_id": 113, - "user_id": 1, - }, - ) - self.assert_model_exists( - "option/11", - { - "vote_ids": [1], - "yes": "1.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - ) - self.assert_model_exists( - "user/1", - {"poll_voted_ids": [1], "delegated_vote_ids": [1], "vote_ids": [1]}, - ) - - -class VotePollBaseTestClass(BaseVoteTestCase): - def setUp(self) -> None: - super().setUp() - self.set_models( - { - "assignment/1": { - "title": "test_assignment_tcLT59bmXrXif424Qw7K", - "open_posts": 1, - "meeting_id": 113, - }, - "meeting/113": {"is_active_in_organization_id": 1}, - } - ) - self.create_poll() - self.set_models( - { - "group/1": {"meeting_user_ids": [11], "meeting_id": 113}, - "option/1": { - "meeting_id": 113, - "poll_id": 1, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - "option/2": { - "meeting_id": 113, - "poll_id": 1, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - }, - "user/1": { - "is_present_in_meeting_ids": [113], - "meeting_user_ids": [11], - }, - "meeting_user/11": { - "user_id": 1, - "meeting_id": 113, - "group_ids": [1], - }, - "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1}, - "poll/1": {"global_option_id": 11, "backend": "fast"}, - } - ) - - def create_poll(self) -> None: - # has to be implemented by subclasses - raise NotImplementedError() - - def start_poll(self) -> None: - self.update_model("poll/1", {"state": Poll.STATE_STARTED}) - - def add_option(self) -> None: - self.set_models( - { - "option/3": {"meeting_id": 113, "poll_id": 1}, - "poll/1": {"option_ids": [1, 2, 3]}, - } - ) - - -class VotePollNamedYNA(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_OkHAIvOSIcpFnCxbaL6v", - "pollmethod": "YNA", - "type": Poll.TYPE_NAMED, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votescast": "0.000000", - "votesvalid": "0.000000", - "votesinvalid": "0.000000", - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "YNA", - }, - ) - - def test_vote(self) -> None: - self.add_option() - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_count("vote", 113, 3) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertIn(1, poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - option3 = self.get_model("option/3") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "1.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - self.assertEqual(option3.get("yes"), "0.000000") - self.assertEqual(option3.get("no"), "0.000000") - self.assertEqual(option3.get("abstain"), "1.000000") - - def test_vote_with_voteweight(self) -> None: - self.set_models( - { - "user/1": {"meeting_user_ids": [11]}, - "meeting_user/11": { - "meeting_id": 113, - "user_id": 1, - "vote_weight": "4.200000", - }, - "meeting/113": {"users_enable_vote_weight": True}, - } - ) - self.add_option() - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_count("vote", 113, 3) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "4.200000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertEqual(poll.get("state"), Poll.STATE_FINISHED) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - option3 = self.get_model("option/3") - self.assertEqual(option1.get("yes"), "4.200000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "4.200000") - self.assertEqual(option2.get("abstain"), "0.000000") - self.assertEqual(option3.get("yes"), "0.000000") - self.assertEqual(option3.get("no"), "0.000000") - self.assertEqual(option3.get("abstain"), "4.200000") - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": "N"}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/2") - vote = self.get_model("vote/1") - self.assertEqual(vote.get("value"), "Y") - - def test_too_many_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_anonymous(self) -> None: - self.start_poll() - response = self.anonymous_vote({"value": {"1": "Y"}}) - self.assert_status_code(response, 401) - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_missing_data(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - poll = self.get_model("poll/1") - self.assertNotIn(1, poll.get("voted_ids", [])) - - def test_wrong_data_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": [1, 2, 5], "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"id": "Y"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_data(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": [None]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - -class VotePollNamedY(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_Zrvh146QAdq7t6iSDwZk", - "pollmethod": "Y", - "type": Poll.TYPE_NAMED, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "global_yes": True, - "global_no": True, - "global_abstain": True, - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - self.assert_model_not_exists("vote/2") - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertIn(1, poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_global_yes(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "1.000000") - self.assertEqual(option.get("no"), "0.000000") - self.assertEqual(option.get("abstain"), "0.000000") - - def test_global_yes_forbidden(self) -> None: - self.update_model("poll/1", {"global_yes": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_global_no(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "0.000000") - self.assertEqual(option.get("no"), "1.000000") - self.assertEqual(option.get("abstain"), "0.000000") - - def test_global_no_forbidden(self) -> None: - self.update_model("poll/1", {"global_no": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_global_abstain(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "0.000000") - self.assertEqual(option.get("no"), "0.000000") - self.assertEqual(option.get("abstain"), "1.000000") - - def test_global_abstain_forbidden(self) -> None: - self.update_model("poll/1", {"global_abstain": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_negative_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": -1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_too_many_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 1, "3": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"id": 1, "value": {"3": 1}}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_anonymous(self) -> None: - self.start_poll() - response = self.anonymous_vote({"value": {"1": 1}}) - self.assert_status_code(response, 401) - self.assert_model_not_exists("vote/1") - - def test_anonymous_as_vote_user(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 0}, - ) - self.assert_status_code(response, 400) - assert "Votes for anonymous user are not allowed" in response.json["message"] - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_missing_data(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - poll = self.get_model("poll/1") - self.assertNotIn(1, poll.get("voted_ids", [])) - - def test_wrong_data_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": [1, 2, 5], "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"id": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_data(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": [None]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - -class VotePollYMaxVotesPerOption(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_Zrvh146QAASDfVeidq7t6iSDwZk", - "pollmethod": "Y", - "type": Poll.TYPE_NAMED, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "global_yes": False, - "global_no": False, - "global_abstain": False, - "min_votes_amount": 1, - "max_votes_amount": 5, - "max_votes_per_option": 3, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 2, "2": 3}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertIn(1, poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "2.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "3.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 3}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 2, "2": 0}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "3.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_vote_weight(self) -> None: - self.update_model("user/1", {"default_vote_weight": "3.000000"}) - self.update_model("meeting/113", {"users_enable_vote_weight": True}) - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 3}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "3.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "9.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_vote_change_weight(self) -> None: - self.update_model("user/1", {"default_vote_weight": "3.000000"}) - self.update_model("meeting/113", {"users_enable_vote_weight": True}) - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 2, "2": 0}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 0, "2": 3}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "6.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - -class VotePollNamedN(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_4oi49ckKFk39SDIfj30s", - "pollmethod": "N", - "type": Poll.TYPE_NAMED, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "global_yes": True, - "global_no": True, - "global_abstain": True, - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - self.assert_model_not_exists("vote/2") - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertTrue(1 in poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "0.000000") - self.assertEqual(option1.get("no"), "1.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_change_vote(self) -> None: - self.add_option() - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "0.000000") - self.assertEqual(option1.get("no"), "1.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_global_yes(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "1.000000") - self.assertEqual(option.get("no"), "0.000000") - self.assertEqual(option.get("abstain"), "0.000000") - - def test_global_yes_forbidden(self) -> None: - self.update_model("poll/1", {"global_yes": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_global_no(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "0.000000") - self.assertEqual(option.get("no"), "1.000000") - self.assertEqual(option.get("abstain"), "0.000000") - - def test_global_no_forbidden(self) -> None: - self.update_model("poll/1", {"global_no": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_global_abstain(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1}) - self.assert_status_code(response, 200) - option = self.get_model("option/11") - self.assertEqual(option.get("yes"), "0.000000") - self.assertEqual(option.get("no"), "0.000000") - self.assertEqual(option.get("abstain"), "1.000000") - - def test_global_abstain_forbidden(self) -> None: - self.update_model("poll/1", {"global_abstain": False}) - self.start_poll() - response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_negative_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": -1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"3": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_anonymous(self) -> None: - self.start_poll() - response = self.anonymous_vote({"value": {"1": 1}}) - self.assert_status_code(response, 401) - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_missing_data(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - poll = self.get_model("poll/1") - self.assertNotIn(1, poll.get("voted_ids", [])) - - def test_wrong_data_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": [1, 2, 5], "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"id": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_data(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": [None]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoanonymousYNA(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_OkHAIvOSIcpFnCxbaL6v", - "pollmethod": "YNA", - "type": Poll.TYPE_PSEUDOANONYMOUS, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.add_option() - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_count("vote", 113, 3) - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - option3 = self.get_model("option/3") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "1.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - self.assertEqual(option3.get("yes"), "0.000000") - self.assertEqual(option3.get("no"), "0.000000") - self.assertEqual(option3.get("abstain"), "1.000000") - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": "N"}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - option1 = self.get_model("option/1") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - - def test_too_many_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_partial_vote(self) -> None: - self.add_option() - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - - def test_wrong_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_anonymous(self) -> None: - self.start_poll() - response = self.anonymous_vote({"value": {"1": "Y"}}) - self.assert_status_code(response, 401) - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - response = self.request( - "poll.vote", - {"value": {"1": "Y"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_missing_value(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - poll = self.get_model("poll/1") - self.assertNotIn(1, poll.get("voted_ids", [])) - - def test_wrong_value_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": [1, 2, 5], "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"id": "Y"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_value(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": [None]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoanonymousY(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_Zrvh146QAdq7t6iSDwZk", - "pollmethod": "Y", - "type": Poll.TYPE_PSEUDOANONYMOUS, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - self.assert_model_not_exists("vote/2") - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertTrue(1 in poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - vote = self.get_model("vote/1") - self.assertIsNone(vote.get("user_id")) - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - self.get_model("poll/1") - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "1.000000") - self.assertEqual(option1.get("no"), "0.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_negative_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": -1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_options(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"3": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_missing_data(self) -> None: - self.start_poll() - response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1}) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - poll = self.get_model("poll/1") - self.assertNotIn(1, poll.get("voted_ids", [])) - - def test_wrong_data_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"value": [1, 2, 5]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"id": 1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_data(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": [None]}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - -class VotePollPseudoanonymousN(VotePollBaseTestClass): - def create_poll(self) -> None: - self.create_model( - "poll/1", - { - "content_object_id": "assignment/1", - "title": "test_title_wWPOVJgL9afm83eamf3e", - "pollmethod": "N", - "type": Poll.TYPE_PSEUDOANONYMOUS, - "state": Poll.STATE_CREATED, - "meeting_id": 113, - "option_ids": [1, 2], - "entitled_group_ids": [1], - "votesinvalid": "0.000000", - "min_votes_amount": 1, - "max_votes_amount": 10, - "max_votes_per_option": 1, - "sequential_number": 1, - "onehundred_percent_base": "Y", - }, - ) - - def test_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"id": 1, "value": {"1": 1, "2": 0}, "user_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists("vote/1") - self.assert_model_not_exists("vote/2") - poll = self.get_model("poll/1") - self.assertEqual(poll.get("votesvalid"), "1.000000") - self.assertEqual(poll.get("votesinvalid"), "0.000000") - self.assertEqual(poll.get("votescast"), "1.000000") - self.assertTrue(1 in poll.get("voted_ids", [])) - option1 = self.get_model("option/1") - option2 = self.get_model("option/2") - self.assertEqual(option1.get("yes"), "0.000000") - self.assertEqual(option1.get("no"), "1.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - vote = self.get_model("vote/1") - self.assertIsNone(vote.get("user_id")) - - def test_change_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1}, - stop_poll_after_vote=False, - ) - response = self.request( - "poll.vote", - {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - ) - self.assert_status_code(response, 400) - self.get_model("poll/1") - option1 = self.get_model("option/1") - self.assertEqual(option1.get("yes"), "0.000000") - self.assertEqual(option1.get("no"), "1.000000") - self.assertEqual(option1.get("abstain"), "0.000000") - option2 = self.get_model("option/2") - self.assertEqual(option2.get("yes"), "0.000000") - self.assertEqual(option2.get("no"), "0.000000") - self.assertEqual(option2.get("abstain"), "0.000000") - - def test_negative_vote(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": -1}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_vote_not_present(self) -> None: - self.start_poll() - self.update_model("user/1", {"is_present_in_meeting_ids": []}) - - response = self.request( - "poll.vote", - {"id": 1, "user_id": 1, "value": {"1": 1}}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_state(self) -> None: - response = self.request( - "poll.vote", - {"value": {"1": 1}, "id": 1, "user_id": 1}, - start_poll_before_vote=False, - stop_poll_after_vote=False, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_data_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": [1, 2, 5], "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - assert ( - "decoding payload: unknown vote value: `[1,2,5]`" - in response.json["message"] - ) - self.assert_model_not_exists("vote/1") - - def test_wrong_option_format(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"value": {"1": "string"}, "id": 1, "user_id": 1}, - ) - self.assert_status_code(response, 400) - assert "Your vote has a wrong format" in response.json["message"] - self.assert_model_not_exists("vote/1") - - def test_wrong_option_id_type(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"id": 1}, - ) - self.assert_status_code(response, 400) - self.assert_model_not_exists("vote/1") - - def test_wrong_vote_data(self) -> None: - self.start_poll() - response = self.request( - "poll.vote", - {"id": 1, "value": {"1": [None]}, "user_id": 1}, - ) - self.assert_status_code(response, 400) - assert "decoding payload: unknown vote value:" in response.json["message"] - self.assert_model_not_exists("vote/1") diff --git a/tests/system/action/poll_candidate/__init__.py b/tests/system/action/poll_candidate/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/system/action/poll_candidate/test_create.py b/tests/system/action/poll_candidate/test_create.py deleted file mode 100644 index 8b755916d2..0000000000 --- a/tests/system/action/poll_candidate/test_create.py +++ /dev/null @@ -1,27 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class PollCandidateTest(BaseActionTestCase): - def test_create(self) -> None: - self.set_models( - { - "meeting/1": { - "name": "meeting_1", - "is_active_in_organization_id": 1, - "poll_candidate_list_ids": [2], - }, - "poll_candidate_list/2": {"meeting_id": 1}, - } - ) - response = self.request( - "poll_candidate.create", - {"user_id": 1, "poll_candidate_list_id": 2, "weight": 12, "meeting_id": 1}, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll_candidate/1", - {"meeting_id": 1, "user_id": 1, "poll_candidate_list_id": 2, "weight": 12}, - ) - self.assert_model_exists("user/1", {"poll_candidate_ids": [1]}) - self.assert_model_exists("meeting/1", {"poll_candidate_ids": [1]}) - self.assert_model_exists("poll_candidate_list/2", {"poll_candidate_ids": [1]}) diff --git a/tests/system/action/poll_candidate/test_delete.py b/tests/system/action/poll_candidate/test_delete.py deleted file mode 100644 index f613c13577..0000000000 --- a/tests/system/action/poll_candidate/test_delete.py +++ /dev/null @@ -1,29 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class PollCandidateDeleteTest(BaseActionTestCase): - def test_delete_correct(self) -> None: - self.set_models( - { - "meeting/1": { - "name": "meeting_1", - "poll_candidate_list_ids": [2], - "poll_candidate_ids": [3], - "is_active_in_organization_id": 1, - }, - "user/1": {"poll_candidate_ids": [3]}, - "poll_candidate_list/2": {"meeting_id": 1, "poll_candidate_ids": [3]}, - "poll_candidate/3": { - "meeting_id": 1, - "poll_candidate_list_id": 2, - "user_id": 1, - "weight": 1, - }, - } - ) - response = self.request("poll_candidate.delete", {"id": 3}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll_candidate/3") - self.assert_model_exists("user/1", {"poll_candidate_ids": []}) - self.assert_model_exists("poll_candidate_list/2", {"poll_candidate_ids": []}) - self.assert_model_exists("meeting/1", {"poll_candidate_ids": []}) diff --git a/tests/system/action/poll_candidate_list/__init__.py b/tests/system/action/poll_candidate_list/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/system/action/poll_candidate_list/test_create.py b/tests/system/action/poll_candidate_list/test_create.py deleted file mode 100644 index 52baae0bb3..0000000000 --- a/tests/system/action/poll_candidate_list/test_create.py +++ /dev/null @@ -1,51 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class PollCandidateList(BaseActionTestCase): - def test_create_correct(self) -> None: - self.set_models( - { - "meeting/1": { - "name": "meeting_1", - "is_active_in_organization_id": 1, - }, - "user/2": { - "username": "test1", - }, - "user/3": { - "username": "test2", - }, - "option/4": { - "meeting_id": 1, - }, - }, - ) - response = self.request( - "poll_candidate_list.create", - { - "option_id": 4, - "meeting_id": 1, - "entries": [{"user_id": 2, "weight": 1}, {"user_id": 3, "weight": 2}], - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "poll_candidate_list/1", - {"option_id": 4, "meeting_id": 1, "poll_candidate_ids": [1, 2]}, - ) - self.assert_model_exists( - "poll_candidate/1", - {"user_id": 2, "meeting_id": 1, "poll_candidate_list_id": 1, "weight": 1}, - ) - self.assert_model_exists( - "poll_candidate/2", - {"user_id": 3, "meeting_id": 1, "poll_candidate_list_id": 1, "weight": 2}, - ) - self.assert_model_exists("user/2", {"poll_candidate_ids": [1]}) - self.assert_model_exists("user/3", {"poll_candidate_ids": [2]}) - self.assert_model_exists( - "meeting/1", {"poll_candidate_list_ids": [1], "poll_candidate_ids": [1, 2]} - ) - self.assert_model_exists( - "option/4", {"content_object_id": "poll_candidate_list/1"} - ) diff --git a/tests/system/action/poll_candidate_list/test_delete.py b/tests/system/action/poll_candidate_list/test_delete.py deleted file mode 100644 index e2c49364cf..0000000000 --- a/tests/system/action/poll_candidate_list/test_delete.py +++ /dev/null @@ -1,50 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class PollCandidateListDeleteTest(BaseActionTestCase): - def test_delete_correct(self) -> None: - self.set_models( - { - "meeting/1": { - "name": "meeting_1", - "poll_candidate_list_ids": [2], - "poll_candidate_ids": [3, 4, 5], - "is_active_in_organization_id": 1, - }, - "user/1": {"poll_candidate_ids": [3]}, - "user/2": {"username": "test1", "poll_candidate_ids": [4]}, - "user/3": {"username": "test2", "poll_candidate_ids": [5]}, - "poll_candidate_list/2": { - "meeting_id": 1, - "poll_candidate_ids": [3, 4, 5], - }, - "poll_candidate/3": { - "meeting_id": 1, - "poll_candidate_list_id": 2, - "user_id": 1, - "weight": 1, - }, - "poll_candidate/4": { - "meeting_id": 1, - "poll_candidate_list_id": 2, - "user_id": 2, - "weight": 2, - }, - "poll_candidate/5": { - "meeting_id": 1, - "poll_candidate_list_id": 2, - "user_id": 3, - "weight": 3, - }, - } - ) - response = self.request("poll_candidate_list.delete", {"id": 2}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("poll_candidate_list/2") - for poll_candidate_id in range(3, 6): - self.assert_model_not_exists(f"poll_candidate/{poll_candidate_id}") - for user_id in range(1, 4): - self.assert_model_exists(f"user/{user_id}", {"poll_candidate_ids": []}) - self.assert_model_exists( - "meeting/1", {"poll_candidate_list_ids": [], "poll_candidate_ids": []} - ) diff --git a/tests/system/action/projector/test_toggle.py b/tests/system/action/projector/test_toggle.py index bc6ccf8495..55fc86f6ef 100644 --- a/tests/system/action/projector/test_toggle.py +++ b/tests/system/action/projector/test_toggle.py @@ -16,13 +16,15 @@ def create_poll(self, base: int) -> None: f"poll/{base}": { "meeting_id": 1, "title": "A very important change", - "type": Poll.TYPE_PSEUDOANONYMOUS, - "backend": "fast", - "pollmethod": "YN", - "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN, - "sequential_number": base, + "visibility": Poll.VISIBILITY_SECRET, + "config_id": f"poll_config_rating_approval/{base}", + "state": Poll.STATE_STARTED, "content_object_id": "motion/1", - } + }, + f"poll_config_rating_approval/{base}": { + "poll_id": base, + "allow_abstain": False, + }, } ) diff --git a/tests/system/action/test_internal_actions.py b/tests/system/action/test_internal_actions.py index bac520f8a1..2fcd22c20c 100644 --- a/tests/system/action/test_internal_actions.py +++ b/tests/system/action/test_internal_actions.py @@ -210,12 +210,12 @@ def test_internal_execute_stack_internal_action(self) -> None: def test_internal_execute_backend_internal_action(self) -> None: response = self.internal_request( - "option.create", - {"meeting_id": 1, "text": "test"}, + "meeting_user.create", + {"meeting_id": 1, "user_id": 1}, self.internal_auth_password, ) self.assert_status_code(response, 400) self.assertEqual( - response.json.get("message"), "Action option.create does not exist." + response.json.get("message"), "Action meeting_user.create does not exist." ) - self.assert_model_not_exists("option/1") + self.assert_model_not_exists("meeting_user/1") diff --git a/tests/system/action/user/test_delete.py b/tests/system/action/user/test_delete.py index 61128e8a78..bbf2d81ad7 100644 --- a/tests/system/action/user/test_delete.py +++ b/tests/system/action/user/test_delete.py @@ -5,7 +5,7 @@ from openslides_backend.permissions.permissions import Permissions from openslides_backend.shared.util import ONE_ORGANIZATION_FQID from tests.system.action.base import BaseActionTestCase - +from openslides_backend.models.models import Poll from .scope_permissions_mixin import ScopePermissionsTestMixin, UserScope @@ -192,30 +192,38 @@ def test_delete_with_submitter(self) -> None: def test_delete_with_poll_candidate(self) -> None: self.create_meeting() + user_id = self.create_user_for_meeting(1) self.set_models( { - "user/111": { - "username": "username_srtgb123", - "poll_candidate_ids": [34], + "poll/1": { + "title": "Poll 1", + "meeting_id": 1, + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/1", + "state": Poll.STATE_FINISHED, }, - "poll_candidate/34": { - "user_id": 111, - "poll_candidate_list_id": 1, - "weight": 1, + "list_of_speakers/1": { "meeting_id": 1, + "content_object_id": "assignment/1", }, - "poll_candidate_list/1": {"meeting_id": 1}, - "option/1": { + "assignment/1": { + "id": 1, + "title": "Duckburg town council", "meeting_id": 1, - "content_object_id": "poll_candidate_list/1", }, + "poll_config_option/1": { + "poll_config_id": "poll_config_approval/1", + "meeting_user_id": 1, + }, + "poll_config_approval/1": {"poll_id": 1}, } ) - response = self.request("user.delete", {"id": 111}) - + response = self.request("user.delete", {"id": user_id}) self.assert_status_code(response, 200) - self.assert_model_not_exists("user/111") - self.assert_model_exists("poll_candidate/34", {"user_id": None}) + self.assert_model_not_exists(f"user/{user_id}") + self.assert_model_not_exists("meeting_user/1") + self.assert_model_exists("poll_config_option/1", {"meeting_user_id": None}) def test_delete_with_group_ids_set_null(self) -> None: self.create_meeting() diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py index fce164a79e..7ff4078b16 100644 --- a/tests/system/action/user/test_merge_together.py +++ b/tests/system/action/user/test_merge_together.py @@ -6,10 +6,12 @@ from zoneinfo import ZoneInfo import pytest +from psycopg.types.json import Jsonb from openslides_backend.action.actions.speaker.speech_state import SpeechState from openslides_backend.action.relations.relation_manager import RelationManager from openslides_backend.action.util.actions_map import actions_map +from openslides_backend.models.models import Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from openslides_backend.shared.patterns import ( CollectionField, @@ -17,7 +19,6 @@ ) from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID from tests.system.action.base import BaseActionTestCase -from tests.system.action.poll.test_vote import BaseVoteTestCase from tests.util import Response @@ -230,6 +231,25 @@ def setUp(self) -> None: } self.set_models(models) + def create_assignment( + self, base: int, meeting_id: int, assignment_data: dict[str, Any] = {} + ) -> None: + self.set_models( + { + f"assignment/{base}": { + "title": "just do it", + "sequential_number": base, + "meeting_id": meeting_id, + **assignment_data, + }, + f"list_of_speakers/{base + 100}": { + "content_object_id": f"assignment/{base}", + "sequential_number": base + 100, + "meeting_id": meeting_id, + }, + } + ) + def test_merge_configuration_up_to_date(self) -> None: """ This test checks, if the merge_together function has been properly @@ -780,6 +800,9 @@ def test_with_multiple_delegations(self) -> None: self.assert_model_exists("meeting_user/106", {"vote_delegated_to_id": 73}) def set_up_polls_for_merge(self) -> None: + self.create_assignment(1, 1) + self.create_motion(4, 1) + self.create_topic(7, 7) self.set_models( { "meeting/1": { @@ -816,15 +839,6 @@ def set_up_polls_for_merge(self) -> None: "title": "Assignment 1", "meeting_id": 1, }, - "motion/1": { - "id": 1, - "text": "XDDD", - "title": "Motion 1", - "state_id": 2, - "meeting_id": 4, - "submitter_ids": [1], - }, - "motion_state/2": {"motion_ids": [1]}, "motion_submitter/1": { "id": 1, "weight": 1, @@ -832,131 +846,220 @@ def set_up_polls_for_merge(self) -> None: "meeting_id": 4, "meeting_user_id": 43, }, - "topic/1": { - "id": 1, - "title": "Topic 1", - "meeting_id": 7, - }, } ) - self.request_multi( - "poll.create", - [ - { - "title": "Assignment poll 1", - "content_object_id": "assignment/1", - "type": "named", - "pollmethod": "Y", - "meeting_id": 1, - "options": [ - {"content_object_id": "user/2"}, - {"content_object_id": "user/5"}, - ], - "global_no": True, - "min_votes_amount": 1, - "max_votes_amount": 2, - "max_votes_per_option": 1, - "backend": "long", - "entitled_group_ids": [1, 2, 3], - }, - { - "title": "Assignment poll 2", - "content_object_id": "assignment/1", - "type": "named", - "pollmethod": "YN", - "meeting_id": 1, - "options": [ - {"poll_candidate_user_ids": [5, 4]}, - ], - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "backend": "fast", - "entitled_group_ids": [1, 2, 3], - }, - { - "title": "Assignment poll 3", - "content_object_id": "assignment/1", - "type": "named", - "pollmethod": "YN", - "meeting_id": 1, - "options": [ - {"poll_candidate_user_ids": [2, 5]}, - ], - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "backend": "fast", - "entitled_group_ids": [1, 2, 3], - }, - { - "title": "Assignment poll 4", - "content_object_id": "assignment/1", - "type": "pseudoanonymous", - "pollmethod": "Y", - "meeting_id": 1, - "options": [ - {"content_object_id": "user/4"}, - {"content_object_id": "user/5"}, - ], - "min_votes_amount": 1, - "max_votes_amount": 2, - "max_votes_per_option": 1, - "backend": "long", - "entitled_group_ids": [1, 2, 3], - }, - { - "title": "Motion poll", - "content_object_id": "motion/1", - "type": "named", - "pollmethod": "YNA", - "meeting_id": 4, - "options": [ - {"content_object_id": "motion/1"}, - ], - "min_votes_amount": 1, - "max_votes_amount": 1, - "max_votes_per_option": 1, - "backend": "fast", - "entitled_group_ids": [4, 5, 6], - }, - { - "title": "Topic poll", - "content_object_id": "topic/1", - "type": "pseudoanonymous", - "pollmethod": "Y", - "meeting_id": 7, - "options": [ - {"text": "Option 1"}, - {"text": "Option 2"}, - {"text": "Option 3"}, - ], - "min_votes_amount": 1, - "max_votes_amount": 3, - "max_votes_per_option": 1, - "backend": "fast", - "entitled_group_ids": [7, 8, 9], - }, - ], + polls_data = [ + { + "id": 1, + "title": "Assignment poll 1", + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_selection/1", + "state": Poll.STATE_STARTED, + "meeting_id": 1, + "entitled_group_ids": [1, 2, 3], + }, + { + "id": 2, + "title": "Assignment poll 2", + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/2", + "state": Poll.STATE_STARTED, + "meeting_id": 1, + "entitled_group_ids": [1, 2, 3], + }, + { + "id": 3, + "title": "Assignment poll 3", + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/3", + "state": Poll.STATE_STARTED, + "meeting_id": 1, + "entitled_group_ids": [1, 2, 3], + }, + { + "id": 4, + "title": "Assignment poll 4", + "content_object_id": "assignment/1", + "visibility": Poll.VISIBILITY_SECRET, + "config_id": "poll_config_selection/4", + "state": Poll.STATE_STARTED, + "meeting_id": 1, + "entitled_group_ids": [1, 2, 3], + }, + { + "id": 5, + "title": "Motion poll", + "content_object_id": "motion/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/5", + "state": Poll.STATE_STARTED, + "meeting_id": 4, + "entitled_group_ids": [4, 5, 6], + }, + { + "id": 6, + "title": "Topic poll", + "content_object_id": "topic/7", + "visibility": Poll.VISIBILITY_SECRET, + "config_id": "poll_config_rating_score/6", + "state": Poll.STATE_STARTED, + "meeting_id": 7, + "entitled_group_ids": [7, 8, 9], + }, + ] + # add entitled groups + configs_data = { + **{ + f"group/{group_id}": {"poll_ids": list(range(1, 5))} + for group_id in range(1, 4) + }, + "poll_config_selection/1": { + "poll_id": 1, + "min_options_amount": 1, + "max_options_amount": 2, + }, + "poll_config_option/112": { + "poll_config_id": "poll_config_selection/1", + "meeting_user_id": 12, + }, + "poll_config_option/115": { + "poll_config_id": "poll_config_selection/1", + "meeting_user_id": 15, + }, + "poll_config_approval/2": {"poll_id": 2, "allow_abstain": False}, + "poll_config_option/214": { + "poll_config_id": "poll_config_approval/2", + "meeting_user_id": 14, + }, + "poll_config_option/215": { + "poll_config_id": "poll_config_approval/2", + "meeting_user_id": 15, + }, + "poll_config_approval/3": {"poll_id": 3, "allow_abstain": False}, + "poll_config_option/312": { + "poll_config_id": "poll_config_approval/3", + "meeting_user_id": 12, + }, + "poll_config_option/315": { + "poll_config_id": "poll_config_approval/3", + "meeting_user_id": 15, + }, + "poll_config_selection/4": { + "poll_id": 4, + "min_options_amount": 1, + "max_options_amount": 2, + }, + "poll_config_option/414": { + "poll_config_id": "poll_config_selection/4", + "meeting_user_id": 14, + }, + "poll_config_option/415": { + "poll_config_id": "poll_config_selection/4", + "meeting_user_id": 15, + }, + "poll_config_approval/5": {"poll_id": 5}, + "poll_config_rating_score/6": { + "poll_id": 6, + "min_options_amount": 1, + "max_options_amount": 3, + "max_votes_per_option": 1, + }, + **{ + f"poll_config_option/{poll_6_option_id}": { + "poll_config_id": "poll_config_rating_score/6", + "text": f"Option {poll_6_option_id}", + } + for poll_6_option_id in range(61, 64) + }, + "meeting_user/13": { + "user_id": 3, + "meeting_id": 1, + "group_ids": [1], + "vote_weight": Decimal("1"), + }, + } + self.set_models( + {**{f"poll/{poll['id']}": poll for poll in polls_data}, **configs_data} + ) + + # def vote(self, poll_id: int, payload: dict[str, Any]) -> None: + # self.vote_service.vote(poll_id, payload) + def vote( + self, + base: int, + poll_id: int, + value, + acting_meeting_user_id: int = None, + represented_meeting_user_id: int = None, + is_secret: bool = False, + ) -> None: + mu_data = ( + { + "acting_meeting_user_id": acting_meeting_user_id, + "represented_meeting_user_id": represented_meeting_user_id + or acting_meeting_user_id, + } + if not is_secret + else {} + ) + self.set_models( + { + f"ballot/{base}": { + "poll_id": poll_id, + "value": value, + **mu_data, + } + } ) - def create_polls_with_correct_votes(self) -> None: + def create_polls_with_correct_votes(self, poll_4_is_running: bool = False) -> None: self.set_up_polls_for_merge() - self.request_multi("poll.start", [{"id": i} for i in range(1, 7)]) - self.login(4) - self.request("poll.vote", {"id": 1, "value": "N"}, stop_poll_after_vote=False) # type: ignore - self.request( - "poll.vote", - {"id": 1, "value": "N", "user_id": 5}, - start_poll_before_vote=False, # type: ignore - ) - self.login(2) - self.request("poll.vote", {"id": 2, "value": {"4": "Y"}}) - self.login(3) - self.request("poll.vote", {"id": 5, "value": {"11": "A"}}) - self.request("poll.vote", {"id": 6, "value": {"13": 1, "14": 1, "15": 0}}) + self.vote(1, 1, [112, 115], 14) + self.vote(2, 1, [112], 14, 15) + self.vote(3, 2, "yes", 12) + # secret + self.vote(4, 5, "abstain", 43, is_secret=True) + self.vote(5, 6, Jsonb({"1": 1, "2": 1, "3": 0}), 73, is_secret=True) + self.login(1) - self.request_multi("poll.stop", [{"id": i} for i in [3, 4]]) + self.set_models( + { + "meeting_user/12": {"poll_voted_ids": [2]}, + "meeting_user/43": {"poll_voted_ids": [5]}, + "meeting_user/73": {"poll_voted_ids": [6]}, + "meeting_user/14": {"poll_voted_ids": [1]}, + "meeting_user/15": {"poll_voted_ids": [1]}, + } + ) + self.set_models( + { + f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True} + for poll_id in range(1, 7) + if not (poll_id == 4 and poll_4_is_running) + } + ) + # self.login(4) + # self.vote(1, {"value": [112, 115]}) + # self.vote(1, {"value": [112], "meeting_user_id": 15}) + + # self.login(2) + # self.vote(2, {"value": "yes"}) + + # self.login(3) + # self.vote(5, {"value": "abstain"}) + # self.vote(6, {"value": {"1": 1, "2": 1, "3": 0}}) + + # self.login(1) + # self.set_models( + # { + # f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True} + # for poll_id in range(1, 7) + # if not (poll_id == 4 and poll_4_is_running) + # } + # ) def assert_merge_with_polls_correct( self, password: str, add_to_creatable_ids: int = 0 @@ -972,46 +1075,69 @@ def assert_merge_with_polls_correct( "default_password": "user2", "meeting_user_ids": [12, 42, 106 + add_to_creatable_ids], "password": password, - "poll_candidate_ids": [2, 3], - "option_ids": [1, 8], - "poll_voted_ids": [1, 2, 5, 6], - "vote_ids": [1, 3, 4], - "delegated_vote_ids": [1, 2, 3, 4], }, ) self.assert_model_exists("committee/60", {"user_ids": [2, 5]}) self.assert_model_exists("committee/66", {"user_ids": [2]}) - for id_ in range(3, 5): - self.assert_model_not_exists(f"user/{id_}") - for id_ in [43, 73, 14, 44, 74, *range(106, 106 + add_to_creatable_ids)]: - self.assert_model_not_exists(f"meeting_user/{id_}") - for meeting_id, id_ in {1: 12, 2: 42, 3: 106 + add_to_creatable_ids}.items(): - self.assert_model_exists( - f"meeting_user/{id_}", {"user_id": 2, "meeting_id": meeting_id} - ) + self.assert_model_exists( + "meeting_user/12", + { + "poll_option_ids": [112, 214, 312, 414], + "poll_voted_ids": [1, 2], + "acting_ballot_ids": [1, 2, 3], + "represented_ballot_ids": [1, 3], + "vote_delegations_from_ids": [15], + }, + ) + self.assert_model_exists( "meeting_user/42", - {"user_id": 2, "meeting_id": 4, "motion_submitter_ids": [2]}, + { + "user_id": 2, + "meeting_id": 4, + "motion_submitter_ids": [2], + "poll_option_ids": None, + "poll_voted_ids": [5], + "acting_ballot_ids": None, + "represented_ballot_ids": None, + }, + ) + self.assert_model_exists( + "meeting_user/106", + { + "user_id": 2, + "meeting_id": 7, + "motion_submitter_ids": None, + "poll_option_ids": None, + "poll_voted_ids": [6], + "acting_ballot_ids": None, + "represented_ballot_ids": None, + }, ) self.assert_model_not_exists("motion_submitter/1") self.assert_model_exists( "motion_submitter/2", {"motion_id": 1, "meeting_user_id": 42, "meeting_id": 4, "weight": 1}, ) - self.assert_model_exists("poll_candidate/2", {"user_id": 2}) - self.assert_model_exists("poll_candidate/3", {"user_id": 2}) - self.assert_model_exists("vote/2", {"user_id": 5, "delegated_user_id": 2}) - for id_ in [1, 3, 4]: + for id_ in [1, 3]: self.assert_model_exists( - f"vote/{id_}", {"user_id": 2, "delegated_user_id": 2} + f"ballot/{id_}", + {"acting_meeting_user_id": 12, "represented_meeting_user_id": 12}, ) - for id_ in [5, 6]: # pseudoanonymous options + self.assert_model_exists( + "ballot/2", + {"acting_meeting_user_id": 12, "represented_meeting_user_id": 15}, + ) + for id_ in [4, 5]: # votes from secret voting + self.assert_model_exists( + f"ballot/{id_}", + {"acting_meeting_user_id": None, "represented_meeting_user_id": None}, + ) + + for id_ in [112, 214, 312, 414]: self.assert_model_exists( - f"vote/{id_}", - {"option_id": id_ + 8, "user_id": None, "delegated_user_id": None}, + f"poll_config_option/{id_}", {"meeting_user_id": 12} ) - self.assert_model_exists("option/1", {"content_object_id": "user/2"}) - self.assert_model_exists("option/8", {"content_object_id": "user/2"}) def build_expected_user_dates( voted_present_user_delegated_merged: list[ @@ -1030,73 +1156,15 @@ def build_expected_user_dates( for date in voted_present_user_delegated_merged ] - self.assert_model_exists( - "poll/1", - { - "voted_ids": [5, 2], - "entitled_users_at_stop": build_expected_user_dates( - [ - (False, True, 2, None, None, None), - (True, True, 4, None, 2, None), - (True, False, 5, 4, None, 2), - ] - ), - }, - ) - self.assert_model_exists( - "poll/2", - { - "voted_ids": [2], - "entitled_users_at_stop": build_expected_user_dates( - [ - (True, True, 2, None, None, None), - (False, True, 4, None, 2, None), - (False, False, 5, 4, None, 2), - ] - ), - }, - ) - for id_ in [3, 4]: + for poll_id, meeting_user_id in {2: 12, 5: 42, 6: 106}.items(): self.assert_model_exists( - f"poll/{id_}", - { - "voted_ids": None, - "entitled_users_at_stop": build_expected_user_dates( - [ - (False, True, 2, None, None, None), - (False, True, 4, None, 2, None), - (False, False, 5, 4, None, 2), - ] - ), - }, + f"poll/{poll_id}", {"voted_ids": [meeting_user_id]} ) - self.assert_model_exists( - "poll/5", - { - "voted_ids": [2], - "entitled_users_at_stop": build_expected_user_dates( - [ - (False, True, 4, None, 2, None), - (False, False, 2, None, None, None), - (True, True, 3, None, 2, None), - ] - ), - }, - ) - self.assert_model_exists( - "poll/6", - { - "voted_ids": [2], - "entitled_users_at_stop": build_expected_user_dates( - [(True, True, 3, None, 2, None), (False, True, 4, None, 2, None)] - ), - }, - ) + self.assert_model_exists("poll/1", {"voted_ids": [12, 15]}) + for id_ in [3, 4]: + self.assert_model_exists(f"poll/{id_}", {"voted_ids": None}) - @pytest.mark.skipif( - not isinstance("UserMergeTogether", BaseVoteTestCase), - reason="set base class to BaseVoteTestCase, if auth isn't mocked for polls anymore. Subsequently remove the skipifs.", - ) + # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready") def test_merge_with_polls_correct(self) -> None: password = self.assert_model_exists("user/2")["password"] self.create_polls_with_correct_votes() @@ -1104,40 +1172,22 @@ def test_merge_with_polls_correct(self) -> None: self.assert_status_code(response, 200) self.assert_merge_with_polls_correct(password) - @pytest.mark.skipif( - not isinstance("UserMergeTogether", BaseVoteTestCase), - reason="set base class to BaseVoteTestCase, if auth isn't mocked for polls anymore. Subsequently remove the skipifs.", - ) + # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready") def test_merge_with_polls_and_subsequent_merges(self) -> None: password = self.assert_model_exists("user/2")["password"] self.create_polls_with_correct_votes() + e = self.datastore.get_everything() response = self.request("user.merge_together", {"id": 3, "user_ids": [4]}) + e = self.datastore.get_everything() self.assert_status_code(response, 200) response = self.request("user.merge_together", {"id": 2, "user_ids": [3]}) + e = self.datastore.get_everything() self.assert_status_code(response, 200) self.assert_merge_with_polls_correct(password, 1) - @pytest.mark.skipif( - not isinstance("UserMergeTogether", BaseVoteTestCase), - reason="set base class to BaseVoteTestCase, if auth isn't mocked for polls anymore. Subsequently remove the skipifs.", - ) + # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready") def test_merge_with_polls_all_errors(self) -> None: - self.set_up_polls_for_merge() - self.request_multi("poll.start", [{"id": i} for i in range(1, 7)]) - self.login(4) - self.request("poll.vote", {"id": 1, "value": "N"}, stop_poll_after_vote=False) # type: ignore - self.request( - "poll.vote", - {"id": 1, "value": "N", "user_id": 5}, - start_poll_before_vote=False, # type: ignore - ) - self.login(2) - self.request("poll.vote", {"id": 2, "value": {"4": "Y"}}) - self.login(3) - self.request("poll.vote", {"id": 5, "value": {"11": "A"}}) - self.request("poll.vote", {"id": 6, "value": {"13": 1, "14": 1, "15": 0}}) - self.login(1) - self.request("poll.stop", {"id": 3}) + self.create_polls_with_correct_votes(True) response = self.request("user.merge_together", {"id": 2, "user_ids": [3, 4, 5]}) self.assert_status_code(response, 400) assert ( @@ -2425,10 +2475,7 @@ def test_merge_archived_normal(self) -> None: self.archive_all_meetings() self.test_merge_normal() - @pytest.mark.skipif( - not isinstance("UserMergeTogether", BaseVoteTestCase), - reason="set base class to BaseVoteTestCase, if auth isn't mocked for polls anymore. Subsequently remove the skipifs.", - ) + # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready") def test_merge_archived_polls(self) -> None: password = self.assert_model_exists("user/2")["password"] self.create_polls_with_correct_votes() diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py index 8ab160a327..bda5b912cc 100644 --- a/tests/system/action/user/test_update.py +++ b/tests/system/action/user/test_update.py @@ -3129,137 +3129,35 @@ def test_partial_group_removal_with_speaker(self) -> None: def test_update_with_internal_fields(self) -> None: self.create_meeting() - self.create_user("dummy2", [1]) - self.create_user("dummy3", [1]) - self.set_models( - { - "user/1": { - "username": "boady", - "poll_candidate_ids": [1], - "option_ids": [1], - "vote_ids": [1, 2], - }, - "user/2": {"username": "john", "delegated_vote_ids": [2]}, - "meeting/1": { - "poll_ids": [1], - "option_ids": [1, 2], - "poll_candidate_list_ids": [1], - "poll_candidate_ids": [1], - "vote_ids": [1, 2], - }, - "topic/1": {"title": "tropic", "sequential_number": 1, "meeting_id": 1}, - "poll/1": { - "title": "pull", - "type": "analog", - "pollmethod": "YNA", - "meeting_id": 1, - "option_ids": [1, 2], - "sequential_number": 1, - "content_object_id": "topic/1", - }, - "option/1": { - "meeting_id": 1, - "vote_ids": [1], - "content_object_id": "user/1", - }, - "option/2": { - "meeting_id": 1, - "vote_ids": [2], - "content_object_id": "poll_candidate_list/1", - }, - "poll_candidate_list/1": { - "meeting_id": 1, - "option_id": 2, - "poll_candidate_ids": [1], - }, - "poll_candidate/1": { - "poll_candidate_list_id": 1, - "meeting_id": 1, - "user_id": 1, - "weight": 3, - }, - "vote/1": { - "meeting_id": 1, - "option_id": 1, - "user_id": 1, - "user_token": "dfjdskjfksdjf", - }, - "vote/2": { - "meeting_id": 1, - "option_id": 2, - "user_id": 1, - "delegated_user_id": 2, - "user_token": "dfjdskjfksdjf", - }, - } - ) + self.create_user_for_meeting(1) response = self.request( "user.update", { - "id": 3, + "id": 2, "is_present_in_meeting_ids": [1], - "option_ids": [1], - "poll_candidate_ids": [1], - "poll_voted_ids": [1], - "vote_ids": [1], - "delegated_vote_ids": [2], }, internal=True, ) self.assert_status_code(response, 200) - expected: dict[str, dict[str, Any]] = { - "user/3": { - "is_present_in_meeting_ids": [1], - "option_ids": [1], - "poll_candidate_ids": [1], - "poll_voted_ids": [1], - "vote_ids": [1], - "delegated_vote_ids": [2], - }, - "meeting/1": { - "present_user_ids": [3], - }, - "poll/1": {"voted_ids": [3]}, - "option/1": {"content_object_id": "user/3"}, - "poll_candidate/1": { - "user_id": 3, - }, - "vote/1": {"user_id": 3}, - "vote/2": {"delegated_user_id": 3}, - } - for fqid, model in expected.items(): - self.assert_model_exists(fqid, model) + self.assert_model_exists("user/2", {"is_present_in_meeting_ids": [1]}) + self.assert_model_exists("meeting/1", {"present_user_ids": [2]}) def test_update_with_internal_fields_error(self) -> None: self.create_meeting() - self.create_user("dummy2", [1]) - self.create_user("dummy3", [1]) + self.create_user_for_meeting(1) response = self.request( "user.update", { - "id": 3, + "id": 2, "is_present_in_meeting_ids": [1], - "option_ids": [1], - "poll_candidate_ids": [1], - "poll_voted_ids": [1], - "vote_ids": [1], - "delegated_vote_ids": [2], }, internal=False, ) self.assert_status_code(response, 400) - message: str = response.json["message"] - assert message.startswith("data must not contain {") - assert message.endswith("} properties") - for field in [ - "'is_present_in_meeting_ids'", - "'option_ids'", - "'poll_candidate_ids'", - "'poll_voted_ids'", - "'vote_ids'", - "'delegated_vote_ids'", - ]: - self.assertIn(field, message) + self.assertEqual( + "data must not contain {'is_present_in_meeting_ids'} properties", + response.json["message"], + ) def test_update_groups_on_last_meeting_admin(self) -> None: self.create_meeting() diff --git a/tests/system/action/vote/__init__.py b/tests/system/action/vote/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/system/action/vote/test_create.py b/tests/system/action/vote/test_create.py deleted file mode 100644 index 8ae1dc7ae3..0000000000 --- a/tests/system/action/vote/test_create.py +++ /dev/null @@ -1,30 +0,0 @@ -from decimal import Decimal - -from tests.system.action.base import BaseActionTestCase - - -class VoteCreateActionTest(BaseActionTestCase): - def test_create(self) -> None: - self.create_meeting(111) - self.set_models({"option/12": {"text": "blabalbal", "meeting_id": 111}}) - response = self.request( - "vote.create", - { - "value": "Y", - "weight": "1.000000", - "option_id": 12, - "user_token": "aaaabbbbccccdddd", - }, - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "vote/1", - { - "value": "Y", - "meeting_id": 111, - "weight": Decimal("1.000000"), - "option_id": 12, - "user_token": "aaaabbbbccccdddd", - }, - ) - self.assert_model_exists("option/12", {"vote_ids": [1]}) diff --git a/tests/system/action/vote/test_delete.py b/tests/system/action/vote/test_delete.py deleted file mode 100644 index 48f7d86869..0000000000 --- a/tests/system/action/vote/test_delete.py +++ /dev/null @@ -1,27 +0,0 @@ -from tests.system.action.base import BaseActionTestCase - - -class VoteDeleteTest(BaseActionTestCase): - def setUp(self) -> None: - super().setUp() - self.create_meeting() - self.set_models( - { - "option/12": {"text": "blabalbal", "meeting_id": 1}, - "vote/111": { - "meeting_id": 1, - "option_id": 12, - "user_token": "aaaabbbbccccdddd", - }, - } - ) - - def test_delete_correct(self) -> None: - response = self.request("vote.delete", {"id": 111}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("vote/111") - - def test_delete_wrong_id(self) -> None: - response = self.request("vote.delete", {"id": 112}) - self.assert_status_code(response, 400) - self.assert_model_exists("vote/111") diff --git a/tests/system/action/vote/test_update.py b/tests/system/action/vote/test_update.py deleted file mode 100644 index 5f757c2cb4..0000000000 --- a/tests/system/action/vote/test_update.py +++ /dev/null @@ -1,26 +0,0 @@ -from decimal import Decimal - -from tests.system.action.base import BaseActionTestCase - - -class VoteUpdateActionTest(BaseActionTestCase): - def test_update(self) -> None: - self.create_meeting(111) - self.set_models( - { - "option/12": {"text": "blabalbal", "meeting_id": 111}, - "vote/1": { - "value": "Y", - "meeting_id": 111, - "weight": "1.000000", - "option_id": 12, - "user_token": "aaaabbbbccccdddd", - }, - } - ) - - response = self.request("vote.update", {"id": 1, "weight": "1.500000"}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "vote/1", {"value": "Y", "meeting_id": 111, "weight": Decimal("1.500000")} - ) diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py index 88c1d90002..95629e6aca 100644 --- a/tests/system/presenter/test_check_database.py +++ b/tests/system/presenter/test_check_database.py @@ -1,6 +1,6 @@ from typing import Any -from openslides_backend.models.models import Meeting +from openslides_backend.models.models import Meeting, Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from .base import BasePresenterTestCase @@ -138,8 +138,8 @@ def get_meeting_defaults(self) -> dict[str, Any]: "assignment_poll_default_backend": "fast", "poll_default_type": "analog", "poll_default_onehundred_percent_base": "YNA", - "poll_default_backend": "fast", "poll_default_live_voting_enabled": False, + "poll_default_allow_invalid": False, "poll_couple_countdown": True, } @@ -312,8 +312,6 @@ def test_correct_relations(self) -> None: "motion_ids": [1], "motion_submitter_ids": [5], "list_of_speakers_ids": [6, 11], - "vote_ids": [7], - "option_ids": [8], "assignment_candidate_ids": [9], "assignment_ids": [10], # relation fields. @@ -368,17 +366,11 @@ def test_correct_relations(self) -> None: ), "user/4": self.get_new_user( "vote_user", - { - "meeting_user_ids": [14], - "vote_ids": [7], - }, + {"meeting_user_ids": [14]}, ), "user/5": self.get_new_user( "delegated_user", - { - "meeting_user_ids": [15], - "delegated_vote_ids": [7], - }, + {"meeting_user_ids": [15]}, ), "user/6": self.get_new_user( "candidate_user", @@ -406,11 +398,13 @@ def test_correct_relations(self) -> None: "user_id": 4, "meeting_id": 1, "group_ids": [1], + "acting_ballot_ids": [7], }, "meeting_user/15": { "user_id": 5, "meeting_id": 1, "group_ids": [1], + "represented_ballot_ids": [7], }, "meeting_user/16": { "user_id": 6, @@ -504,17 +498,19 @@ def test_correct_relations(self) -> None: "content_object_id": "motion/1", "meeting_id": 1, }, - "vote/7": { - "user_token": "test", - "option_id": 8, - "user_id": 4, - "delegated_user_id": 5, + "poll/7": { + "title": "Poll 7", "meeting_id": 1, - }, - "option/8": { - "vote_ids": [7], - "meeting_id": 1, - "weight": 10000, + "content_object_id": "motion/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_rating_approval/1", + "state": Poll.STATE_STARTED, + }, + "poll_config_rating_approval/7": {"poll_id": 7}, + "ballot/8": { + "poll_id": 7, + "acting_meeting_user_id": 14, + "represented_meeting_user_id": 15, }, "assignment_candidate/9": { "weight": 10000, diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py index 4c4ba91089..6f65d0a800 100644 --- a/tests/system/presenter/test_check_database_all.py +++ b/tests/system/presenter/test_check_database_all.py @@ -4,7 +4,7 @@ import pytest from openslides_backend.action.action_worker import ActionWorkerState -from openslides_backend.models.models import Meeting +from openslides_backend.models.models import Meeting, Poll from openslides_backend.permissions.management_levels import OrganizationManagementLevel from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID @@ -135,8 +135,8 @@ def get_meeting_defaults(self) -> dict[str, Any]: "assignment_poll_default_backend": "fast", "poll_default_type": "analog", "poll_default_onehundred_percent_base": "YNA", - "poll_default_backend": "fast", "poll_default_live_voting_enabled": False, + "poll_default_allow_invalid": False, "poll_couple_countdown": True, } @@ -393,8 +393,6 @@ def test_correct_relations(self) -> None: "motion_ids": [1], "motion_submitter_ids": [5], "list_of_speakers_ids": [6, 11], - "vote_ids": [7], - "option_ids": [8], "assignment_candidate_ids": [9], "assignment_ids": [10], # relation fields. @@ -450,14 +448,14 @@ def test_correct_relations(self) -> None: ), "user/4": self.get_new_user( "vote_user", - {"meeting_user_ids": [14], "vote_ids": [7], "is_active": False}, + { + "meeting_user_ids": [14], + "is_active": False, + }, ), "user/5": self.get_new_user( "delegated_user", - { - "meeting_user_ids": [15], - "delegated_vote_ids": [7], - }, + {"meeting_user_ids": [15]}, ), "user/6": self.get_new_user( "candidate_user", @@ -485,11 +483,13 @@ def test_correct_relations(self) -> None: "meeting_id": 1, "user_id": 4, "group_ids": [1], + "acting_ballot_ids": [7], }, "meeting_user/15": { "meeting_id": 1, "user_id": 5, "group_ids": [1], + "represented_ballot_ids": [7], }, "meeting_user/16": { "meeting_id": 1, @@ -588,17 +588,19 @@ def test_correct_relations(self) -> None: "content_object_id": "motion/1", "meeting_id": 1, }, - "vote/7": { - "user_token": "test", - "option_id": 8, - "user_id": 4, - "delegated_user_id": 5, - "meeting_id": 1, - }, - "option/8": { - "vote_ids": [7], + "poll/7": { + "title": "Poll 1", "meeting_id": 1, - "weight": 10000, + "content_object_id": "motion/1", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_rating_approval/1", + "state": Poll.STATE_STARTED, + }, + "poll_config_rating_approval/7": {"poll_id": 7}, + "ballot/8": { + "poll_id": 7, + "acting_meeting_user_id": 14, + "represented_meeting_user_id": 15, }, "assignment_candidate/9": { "weight": 10000, diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py index 440c57b4ba..39eb753365 100644 --- a/tests/system/presenter/test_export_meeting.py +++ b/tests/system/presenter/test_export_meeting.py @@ -1,12 +1,91 @@ from time import time +from openslides_backend.shared.typing import PartialModel +from openslides_backend.shared.util import ONE_ORGANIZATION_FQID from openslides_backend.action.action_worker import ActionWorkerState from openslides_backend.permissions.management_levels import OrganizationManagementLevel - +from openslides_backend.models.models import Poll, Meeting from .base import BasePresenterTestCase class TestExportMeeting(BasePresenterTestCase): + # 2 temporary test methods. Will be in base class after fixing presenter tests + def create_meeting(self, base: int = 1, meeting_data: PartialModel = {}) -> None: + """ + Creates meeting with id 1, committee 60 and groups with ids 1(Default), 2(Admin), 3 by default. + With base you can setup other meetings, but be cautious because of group-ids + The groups have no permissions and no users by default. + """ + committee_id = base + 59 + self.set_models( + { + f"meeting/{base}": { + "default_group_id": base, + "admin_group_id": base + 1, + "motions_default_workflow_id": base, + "motions_default_amendment_workflow_id": base, + "reference_projector_id": base, + "committee_id": committee_id, + "is_active_in_organization_id": 1, + "language": "en", + **meeting_data, + }, + f"projector/{base}": { + "sequential_number": base, + "meeting_id": base, + **{field: base for field in Meeting.reverse_default_projectors()}, + }, + f"group/{base}": {"meeting_id": base, "name": f"group{base}"}, + f"group/{base+1}": {"meeting_id": base, "name": f"group{base+1}"}, + f"group/{base+2}": {"meeting_id": base, "name": f"group{base+2}"}, + f"motion_workflow/{base}": { + "name": "flo", + "sequential_number": base, + "meeting_id": base, + "first_state_id": base, + }, + f"motion_state/{base}": { + "name": "stasis", + "weight": 36, + "meeting_id": base, + "workflow_id": base, + "first_state_of_workflow_id": base, + }, + f"committee/{committee_id}": {"name": f"Committee{committee_id}"}, + ONE_ORGANIZATION_FQID: {"enable_electronic_voting": True}, + } + ) + + def create_motion( + self, + meeting_id: int, + base: int = 1, + state_id: int = 0, + motion_data: PartialModel = {}, + ) -> None: + """ + The meeting and motion_state must already exist. + Creates a motion with id 1 by default. + You can specify another id by setting base. + If no state_id is passed, meeting must have `state_id` equal to `id`. + """ + self.set_models( + { + f"motion/{base}": { + "title": f"motion{base}", + "sequential_number": base, + "state_id": state_id or meeting_id, + "meeting_id": meeting_id, + **motion_data, + }, + f"list_of_speakers/{base}": { + "content_object_id": f"motion/{base}", + "sequential_number": base, + "meeting_id": meeting_id, + }, + } + ) + def test_correct(self) -> None: self.set_models({"meeting/1": {"name": "test_foo"}}) status_code, data = self.request("export_meeting", {"meeting_id": 1}) @@ -30,7 +109,6 @@ def test_correct(self) -> None: "motion_state", "motion_workflow", "poll", - "option", "vote", "assignment", "assignment_candidate", @@ -270,66 +348,92 @@ def test_export_meeting_with_ex_user(self) -> None: def test_export_meeting_find_special_users(self) -> None: """Find users in: - Collection | Field - meeting | present_user_ids - motion | supporter_meeting_user_ids - poll | voted_ids - vote | delegated_meeting_user_id - """ + Collection | Field + meeting | present_user_ids + Find meeting_users in: + Collection | Field + poll | voted_ids + motion | supporter_meeting_user_ids + ballot | acting_meeting_user_id + ballot | represented_meeting_user_id + poll_config_option | meeting_user_id + """ + self.create_meeting() + self.create_motion(1, 30) self.set_models( { - "meeting/1": { - "name": "exported_meeting", - "present_user_ids": [11], - "motion_ids": [30], - "poll_ids": [80], - "vote_ids": [120], - "meeting_user_ids": [112, 114], - }, - "user/11": { - "username": "exuser11", - "is_present_in_meeting_ids": [1], - }, - "user/12": { - "username": "exuser12", - "meeting_user_ids": [112], - }, - "user/13": { - "username": "exuser13", - "poll_voted_ids": [80], - }, - "user/14": { - "username": "exuser14", - "meeting_user_ids": [114], - "delegated_vote_ids": [120], + "meeting/1": {"present_user_ids": [11]}, + "user/11": {"username": "exuser11"}, + "user/12": {"username": "exuser12"}, + "user/13": {"username": "exuser13"}, + "user/14": {"username": "exuser14"}, + "assignment/10": {"title": "test", "meeting_id": 1}, + "list_of_speakers/11": { + "content_object_id": "assignment/10", + "meeting_id": 1, }, "motion/30": { "meeting_id": 1, "supporter_meeting_user_ids": [112], }, "poll/80": { + "title": "Poll 80", "meeting_id": 1, - "voted_ids": [13], - }, - "vote/120": { - "meeting_id": 1, - "delegated_user_id": 14, - "user_id": 14, + "content_object_id": "assignment/10", + "visibility": Poll.VISIBILITY_NAMED, + "config_id": "poll_config_approval/90", + "state": Poll.STATE_STARTED, + "voted_ids": [114], + }, + "poll_config_approval/90": {"poll_id": 80}, + "poll_config_option/100": { + "poll_config_id": "poll_config_approval/90", + "meeting_user_id": 113, + }, + "ballot/120": { + "poll_id": 80, + "value": "yes", + "represented_meeting_user_id": 114, + "acting_meeting_user_id": 114, }, "meeting_user/112": { "meeting_id": 1, "user_id": 12, - "supported_motion_ids": [30], + "group_ids": [1], + }, + "meeting_user/113": { + "meeting_id": 1, + "user_id": 13, + "group_ids": [1], }, "meeting_user/114": { "meeting_id": 1, "user_id": 14, + "group_ids": [1], }, } ) status_code, data = self.request("export_meeting", {"meeting_id": 1}) assert status_code == 200 assert data["meeting"]["1"].get("user_ids") is None - for id_ in ("11", "12", "13", "14"): - assert data["user"][id_] + for id_ in range(11, 15): + assert data["user"][str(id_)] + for id_ in range(112, 115): + assert data["meeting_user"][str(id_)] + + assert data["meeting"]["1"]["present_user_ids"] == [11] + assert data["user"]["11"]["is_present_in_meeting_ids"] == [1] + + assert data["motion"]["30"]["supporter_meeting_user_ids"] == [112] + assert data["meeting_user"]["112"]["supported_motion_ids"] == [30] + + assert data["poll_config_option"]["100"]["meeting_user_id"] == 113 + assert data["meeting_user"]["113"]["poll_option_ids"] == [100] + + assert data["poll"]["80"]["voted_ids"] == [114] + assert data["meeting_user"]["114"]["poll_voted_ids"] == [80] + assert data["ballot"]["120"]["c"] == 114 + assert data["ballot"]["120"]["represented_meeting_user_id"] == 114 + assert data["meeting_user"]["114"]["acting_ballot_ids"] == [120] + assert data["meeting_user"]["114"]["represented_ballot_ids"] == [120] diff --git a/tests/system/util.py b/tests/system/util.py index 896666a6bc..6fed3c97b8 100644 --- a/tests/system/util.py +++ b/tests/system/util.py @@ -1,4 +1,3 @@ -import copy import cProfile import os from abc import abstractmethod @@ -39,18 +38,13 @@ class TestVoteService(VoteService): url: str @abstractmethod - def vote(self, data: dict[str, Any]) -> Response: ... + def vote(self, poll_id: int, payload: dict[str, Any]) -> Response: ... class TestVoteAdapter(VoteAdapter, TestVoteService): - def vote(self, data: dict[str, Any]) -> Response: - data_copy = copy.deepcopy(data) - del data_copy["id"] - response = self.make_request( - self.url.replace("internal", "system") + f"?id={data['id']}", - data_copy, - ) - return convert_to_test_response(response) + def vote(self, poll_id: int, payload: dict[str, Any]) -> Response: + endpoint = self.url + f"?id={poll_id}" + return self.retrieve(endpoint, payload) def create_action_test_application() -> OpenSlidesBackendWSGIApplication: