diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py index 3fb5ed90a..96a43f647 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -1,5 +1,8 @@ +import json from typing import Any, Dict, List +from ..poll.access_permissions import BaseVoteAccessPermissions +from ..poll.views import BasePoll from ..utils.access_permissions import BaseAccessPermissions from ..utils.auth import async_has_perm @@ -50,13 +53,35 @@ class AssignmentPollAccessPermissions(BaseAccessPermissions): async def get_restricted_data( self, full_data: List[Dict[str, Any]], user_id: int ) -> List[Dict[str, Any]]: - return full_data + """ + Poll-managers have full access, even during an active poll. + Non-published polls will be restricted: + - Remove votes* values from the poll + - Remove yes/no/abstain fields from options + - Remove voted_id field from the poll + """ + + if await async_has_perm(user_id, "assignments.can_manage_polls"): + data = full_data + else: + data = [] + for poll in full_data: + if poll["state"] != BasePoll.STATE_PUBLISHED: + poll = json.loads( + json.dumps(poll) + ) # copy, so we can remove some fields. + del poll["votesvalid"] + del poll["votesinvalid"] + del poll["votescast"] + del poll["voted_id"] + for option in poll["options"]: + del option["yes"] + del option["no"] + del option["abstain"] + data.append(poll) + return data -class AssignmentVoteAccessPermissions(BaseAccessPermissions): +class AssignmentVoteAccessPermissions(BaseVoteAccessPermissions): base_permission = "assignments.can_see" - - async def get_restricted_data( - self, full_data: List[Dict[str, Any]], user_id: int - ) -> List[Dict[str, Any]]: - return full_data + manage_permission = "assignments.can_manage" diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index 8211abc32..008ecf2ca 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -268,8 +268,23 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model return {"title": self.title} +class AssignmentVoteManager(models.Manager): + """ + Customized model manager to support our get_full_queryset method. + """ + + def get_full_queryset(self): + """ + Returns the normal queryset with all assignment votes. In the background we + join and prefetch all related models. + """ + return self.get_queryset().select_related("user", "option", "option__poll") + + class AssignmentVote(RESTModelMixin, BaseVote): access_permissions = AssignmentVoteAccessPermissions() + objects = AssignmentVoteManager() + option = models.ForeignKey( "AssignmentOption", on_delete=models.CASCADE, related_name="votes" ) @@ -296,11 +311,32 @@ class AssignmentOption(RESTModelMixin, BaseOption): return self.poll +class AssignmentPollManager(models.Manager): + """ + Customized model manager to support our get_full_queryset method. + """ + + def get_full_queryset(self): + """ + Returns the normal queryset with all assignment polls. In the background we + join and prefetch all related models. + """ + return ( + self.get_queryset() + .select_related("assignment") + .prefetch_related( + "options", "options__user", "options__votes", "groups", "voted" + ) + ) + + # Meta-TODO: Is this todo resolved? # TODO: remove the type-ignoring in the next line, after this is solved: # https://github.com/python/mypy/issues/3855 class AssignmentPoll(RESTModelMixin, BasePoll): access_permissions = AssignmentPollAccessPermissions() + objects = AssignmentPollManager() + option_class = AssignmentOption assignment = models.ForeignKey( diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index 51069d693..5d60a5f59 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -330,8 +330,8 @@ class AssignmentPollViewSet(BasePollViewSet): required fields per pollmethod: - votes: Y - - YN: YN - - YNA: YNA + - YN: YN + - YNA: YNA """ if not isinstance(data, dict): raise ValidationError({"detail": "Data must be a dict"}) @@ -428,8 +428,13 @@ class AssignmentPollViewSet(BasePollViewSet): if not is_int(amount): raise ValidationError({"detail": "Each amounts must be int"}) amount = int(amount) - if amount < 1: - raise ValidationError({"detail": "At least 1 vote per option"}) + if amount < 0: + raise ValidationError( + {"detail": "Negative votes are not allowed"} + ) + # skip empty votes + if amount == 0: + continue if not poll.allow_multiple_votes_per_candidate and amount != 1: raise ValidationError( {"detail": "Multiple votes are not allowed"} @@ -485,27 +490,41 @@ class AssignmentPollViewSet(BasePollViewSet): ) def create_votes(self, data, poll, user=None): + """ + Helper function for handle_(named|pseudoanonymous)_vote + Assumes data is already validated + """ options = poll.get_options() if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: if isinstance(data, dict): for option_id, amount in data.items(): + # skip empty votes + if amount == 0: + continue option = options.get(pk=option_id) vote = AssignmentVote.objects.create( option=option, user=user, weight=Decimal(amount), value="Y" ) inform_changed_data(vote, no_delete_on_restriction=True) - else: + else: # global_no or global_abstain option = options.first() vote = AssignmentVote.objects.create( - option=option, user=user, weight=Decimal(1), value=data + option=option, + user=user, + weight=Decimal(poll.votes_amount), + value=data, ) inform_changed_data(vote, no_delete_on_restriction=True) elif poll.pollmethod in ( AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YNA, ): - pass - # TODO + for option_id, result in data.items(): + option = options.get(pk=option_id) + vote = AssignmentVote.objects.create( + option=option, user=user, value=result + ) + inform_changed_data(vote, no_delete_on_restriction=True) def handle_named_vote(self, data, poll, user): """ @@ -514,9 +533,9 @@ class AssignmentPollViewSet(BasePollViewSet): - Exactly one of the three options must be given - 'N' is only valid if poll.global_no==True - 'A' is only valid if poll.global_abstain==True - - amonts must be integer numbers >= 1. + - amounts must be integer numbers >= 1. - ids should be integers of valid option ids for this poll - - amounts must be one ("1"), if poll.allow_multiple_votes_per_candidate if False + - amounts must be 0 or 1, if poll.allow_multiple_votes_per_candidate is False - The sum of all amounts must be poll.votes_amount votes Request data for YN/YNA pollmethod: diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index b27e7dccc..3f1a07903 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -1,6 +1,8 @@ import json from typing import Any, Dict, List +from ..poll.access_permissions import BaseVoteAccessPermissions +from ..poll.views import BasePoll from ..utils.access_permissions import BaseAccessPermissions from ..utils.auth import async_has_perm, async_in_some_groups @@ -183,7 +185,6 @@ class StateAccessPermissions(BaseAccessPermissions): class MotionPollAccessPermissions(BaseAccessPermissions): base_permission = "motions.can_see" - STATE_PUBLISHED = 4 async def get_restricted_data( self, full_data: List[Dict[str, Any]], user_id: int @@ -201,7 +202,7 @@ class MotionPollAccessPermissions(BaseAccessPermissions): else: data = [] for poll in full_data: - if poll["state"] != self.STATE_PUBLISHED: + if poll["state"] != BasePoll.STATE_PUBLISHED: poll = json.loads( json.dumps(poll) ) # copy, so we can remove some fields. @@ -217,26 +218,6 @@ class MotionPollAccessPermissions(BaseAccessPermissions): return data -class MotionVoteAccessPermissions(BaseAccessPermissions): +class MotionVoteAccessPermissions(BaseVoteAccessPermissions): base_permission = "motions.can_see" - STATE_PUBLISHED = 4 - - async def get_restricted_data( - self, full_data: List[Dict[str, Any]], user_id: int - ) -> List[Dict[str, Any]]: - """ - Poll-managers have full access, even during an active poll. - Every user can see it's own votes. - If the pollstate is published, everyone can see the votes. - """ - - if await async_has_perm(user_id, "motions.can_manage_polls"): - data = full_data - else: - data = [ - vote - for vote in full_data - if vote["pollstate"] == self.STATE_PUBLISHED - or vote["user_id"] == user_id - ] - return data + manage_permission = "motions.can_manage_polls" diff --git a/openslides/motions/models.py b/openslides/motions/models.py index e3f22fee7..46dd0b9f7 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -867,12 +867,27 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode return {"title": self.title} +class MotionVoteManager(models.Manager): + """ + Customized model manager to support our get_full_queryset method. + """ + + def get_full_queryset(self): + """ + Returns the normal queryset with all motion votes. In the background we + join and prefetch all related models. + """ + return self.get_queryset().select_related("user", "option", "option__poll") + + class MotionVote(RESTModelMixin, BaseVote): access_permissions = MotionVoteAccessPermissions() option = models.ForeignKey( "MotionOption", on_delete=models.CASCADE, related_name="votes" ) + objects = MotionVoteManager() + class Meta: default_permissions = () @@ -891,6 +906,23 @@ class MotionOption(RESTModelMixin, BaseOption): return self.poll +class MotionPollManager(models.Manager): + """ + Customized model manager to support our get_full_queryset method. + """ + + def get_full_queryset(self): + """ + Returns the normal queryset with all motion polls. In the background we + join and prefetch all related models. + """ + return ( + self.get_queryset() + .select_related("motion") + .prefetch_related("options", "options__votes", "groups", "voted") + ) + + # Meta-TODO: Is this todo resolved? # TODO: remove the type-ignoring in the next line, after this is solved: # https://github.com/python/mypy/issues/3855 @@ -898,6 +930,8 @@ class MotionPoll(RESTModelMixin, BasePoll): access_permissions = MotionPollAccessPermissions() option_class = MotionOption + objects = MotionPollManager() + motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls") POLLMETHOD_YN = "YN" diff --git a/openslides/poll/access_permissions.py b/openslides/poll/access_permissions.py new file mode 100644 index 000000000..ba3d3aa1b --- /dev/null +++ b/openslides/poll/access_permissions.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from ..poll.views import BasePoll +from ..utils.access_permissions import BaseAccessPermissions +from ..utils.auth import async_has_perm + + +class BaseVoteAccessPermissions(BaseAccessPermissions): + manage_permission = "" # set by subclass + + async def get_restricted_data( + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: + """ + Poll-managers have full access, even during an active poll. + Every user can see it's own votes. + If the pollstate is published, everyone can see the votes. + """ + + if await async_has_perm(user_id, self.manage_permission): + data = full_data + else: + data = [ + vote + for vote in full_data + if vote["pollstate"] == BasePoll.STATE_PUBLISHED + or vote["user_id"] == user_id + ] + return data diff --git a/openslides/poll/views.py b/openslides/poll/views.py index 4772f9cdb..b8ab9f942 100644 --- a/openslides/poll/views.py +++ b/openslides/poll/views.py @@ -136,9 +136,7 @@ class BasePollViewSet(ModelViewSet): self.assert_can_vote(poll, request) if request.user in poll.voted.all(): - raise ValidationError( - {"detail": "You have already voted for this poll."} - ) + self.permission_denied(request) self.handle_pseudoanonymous_vote(request.data, poll) poll.voted.add(request.user) diff --git a/tests/integration/assignments/test_polls.py b/tests/integration/assignments/test_polls.py index d2faefa9b..ef05cb594 100644 --- a/tests/integration/assignments/test_polls.py +++ b/tests/integration/assignments/test_polls.py @@ -1,7 +1,12 @@ +import random from decimal import Decimal +from typing import Any +import pytest +from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework import status +from rest_framework.test import APIClient from openslides.assignments.models import ( Assignment, @@ -11,8 +16,77 @@ from openslides.assignments.models import ( ) from openslides.poll.models import BasePoll from openslides.utils.auth import get_group_model +from openslides.utils.autoupdate import inform_changed_data +from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK from tests.test_case import TestCase +from ..helpers import count_queries + + +@pytest.mark.django_db(transaction=False) +def test_assignment_poll_db_queries(): + """ + Tests that only the following db queries are done: + * 1 request to get the polls, + * 1 request to get all options for all polls, + * 1 request to get all users for all options (candidates), + * 1 request to get all votes for all options, + * 1 request to get all users for all votes, + * 1 request to get all poll groups, + = 6 queries + """ + create_assignment_polls() + assert count_queries(AssignmentPoll.get_elements) == 6 + + +@pytest.mark.django_db(transaction=False) +def test_assignment_vote_db_queries(): + """ + Tests that only 1 query is done when fetching AssignmentVotes + """ + create_assignment_polls() + assert count_queries(AssignmentVote.get_elements) == 1 + + +def create_assignment_polls(): + """ + Creates 1 assignment with 3 candidates which has 5 polls in which each candidate got a random amount of votes between 0 and 10 from 3 users + """ + assignment = Assignment.objects.create( + title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1 + ) + group1 = get_group_model().objects.get(pk=1) + group2 = get_group_model().objects.get(pk=2) + for i in range(3): + user = get_user_model().objects.create_user( + username=f"test_username_{i}", password="test_password_UOrnlCZMD0lmxFGwEj54" + ) + assignment.add_candidate(user) + + for i in range(5): + poll = AssignmentPoll.objects.create( + assignment=assignment, + title="test_title_UnMiGzEHmwqplmVBPNEZ", + pollmethod=AssignmentPoll.POLLMETHOD_YN, + type=AssignmentPoll.TYPE_NAMED, + ) + poll.create_options() + poll.groups.add(group1) + poll.groups.add(group2) + + for j in range(3): + user = get_user_model().objects.create_user( + username=f"test_username_{i}{j}", + password="test_password_kbzj5L8ZtVxBllZzoW6D", + ) + for option in poll.options.all(): + weight = random.randint(0, 10) + if weight > 0: + AssignmentVote.objects.create( + user=user, option=option, value="Y", weight=Decimal(weight) + ) + poll.voted.add(user) + class CreateAssignmentPoll(TestCase): def advancedSetUp(self): @@ -329,28 +403,40 @@ class UpdateAssignmentPoll(TestCase): self.assertEqual(poll.votes_amount, 42) -class VoteAssignmentPollAnalogYNA(TestCase): +class VoteAssignmentPollBaseTestClass(TestCase): def advancedSetUp(self): self.assignment = Assignment.objects.create( - title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1 + title="test_assignment_tcLT59bmXrXif424Qw7K", open_posts=1 ) self.assignment.add_candidate(self.admin) - self.poll = AssignmentPoll.objects.create( - assignment=self.assignment, - title="test_title_beeFaihuNae1vej2ai8m", - pollmethod="YNA", - type=BasePoll.TYPE_ANALOG, - ) + self.poll = self.create_poll() + self.admin.is_present = True + self.admin.save() + self.poll.groups.add(GROUP_ADMIN_PK) self.poll.create_options() + def create_poll(self): + # has to be implemented by subclasses + raise NotImplementedError() + def start_poll(self): self.poll.state = AssignmentPoll.STATE_STARTED self.poll.save() - def add_second_candidate(self): + def add_candidate(self): user, _ = self.create_user() AssignmentOption.objects.create(user=user, poll=self.poll) + +class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_04k0y4TwPLpJKaSvIGm1", + pollmethod=AssignmentPoll.POLLMETHOD_YNA, + type=BasePoll.TYPE_ANALOG, + ) + def test_start_poll(self): response = self.client.post( reverse("assignmentpoll-start", args=[self.poll.pk]) @@ -364,7 +450,7 @@ class VoteAssignmentPollAnalogYNA(TestCase): self.assertFalse(poll.get_votes().exists()) def test_vote(self): - self.add_second_candidate() + self.add_candidate() self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), @@ -410,7 +496,7 @@ class VoteAssignmentPollAnalogYNA(TestCase): self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) def test_too_few_options(self): - self.add_second_candidate() + self.add_candidate() self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), @@ -421,15 +507,14 @@ class VoteAssignmentPollAnalogYNA(TestCase): self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) def test_wrong_options(self): - user, _ = self.create_user() - self.assignment.add_candidate(user) + self.add_candidate() self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { "options": { "1": {"Y": "1", "N": "2.35", "A": "-1"}, - "2": {"Y": "1", "N": "2.35", "A": "-1"}, + "3": {"Y": "1", "N": "2.35", "A": "-1"}, } }, format="json", @@ -505,3 +590,1065 @@ class VoteAssignmentPollAnalogYNA(TestCase): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentVote.objects.exists()) + + +class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_OkHAIvOSIcpFnCxbaL6v", + pollmethod=AssignmentPoll.POLLMETHOD_YNA, + type=BasePoll.TYPE_NAMED, + ) + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "2": "N", "3": "A"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 3) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("1")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("1")) + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + option3 = poll.options.get(pk=3) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("1")) + self.assertEqual(option2.abstain, Decimal("0")) + self.assertEqual(option3.yes, Decimal("0")) + self.assertEqual(option3.no, Decimal("0")) + self.assertEqual(option3.abstain, Decimal("1")) + + def test_change_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 1) + vote = AssignmentVote.objects.get() + self.assertEqual(vote.value, "N") + + def test_too_many_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "2": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_few_options(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "3": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + [1, 2, 5], + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "string"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"id": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": [None]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + +class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_Zrvh146QAdq7t6iSDwZk", + pollmethod=AssignmentPoll.POLLMETHOD_VOTES, + type=BasePoll.TYPE_NAMED, + ) + + def setup_for_multiple_votes(self): + self.poll.allow_multiple_votes_per_candidate = True + self.poll.votes_amount = 3 + self.poll.save() + self.add_candidate() + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 1, "2": 0}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 1) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("1")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("1")) + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_change_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("1")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_global_no(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), "N" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("2")) + self.assertEqual(option.abstain, Decimal("0")) + + def test_global_no_forbidden(self): + self.poll.global_no = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), "N" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_global_abstain(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), "A" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("0")) + self.assertEqual(option.abstain, Decimal("2")) + + def test_global_abstain_forbidden(self): + self.poll.global_abstain = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), "A" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_negative_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": -1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_multiple_votes(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 2, "2": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("2")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("1")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_multiple_votes_wrong_amount(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 2, "2": 2}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_many_options(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 1, "2": 1, "3": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + [1, 2, 5], + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "string"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"id": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": [None]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + +class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_OkHAIvOSIcpFnCxbaL6v", + pollmethod=AssignmentPoll.POLLMETHOD_YNA, + type=BasePoll.TYPE_PSEUDOANONYMOUS, + ) + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "2": "N", "3": "A"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 3) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("1")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("1")) + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + option3 = poll.options.get(pk=3) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("1")) + self.assertEqual(option2.abstain, Decimal("0")) + self.assertEqual(option3.yes, Decimal("0")) + self.assertEqual(option3.no, Decimal("0")) + self.assertEqual(option3.abstain, Decimal("1")) + + def test_change_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + + def test_too_many_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "2": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_few_options(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y", "3": "N"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + [1, 2, 5], + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "string"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"id": "Y"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": [None]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + +class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_Zrvh146QAdq7t6iSDwZk", + pollmethod=AssignmentPoll.POLLMETHOD_VOTES, + type=BasePoll.TYPE_PSEUDOANONYMOUS, + ) + + def setup_for_multiple_votes(self): + self.poll.allow_multiple_votes_per_candidate = True + self.poll.votes_amount = 3 + self.poll.save() + self.add_candidate() + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 1, "2": 0}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 1) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("1")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("1")) + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_change_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("1")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_negative_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": -1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_multiple_votes(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 2, "2": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("2")) + self.assertEqual(option1.no, Decimal("0")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("1")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_multiple_votes_wrong_amount(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 2, "2": 2}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_many_options(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": 1, "2": 1, "3": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + [1, 2, 5], + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": "string"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"id": 1}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"1": [None]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + +# test autoupdates +class VoteAssignmentPollAutoupdatesBaseClass(TestCase): + poll_type = "" # set by subclass, defines which poll type we use + + """ + 3 important users: + self.admin: manager, has can_see, can_manage, can_manage_polls (in admin group) + self.user: votes, has can_see perms and in in delegate group + self.other_user: Just has can_see perms and is NOT in the delegate group. + """ + + def advancedSetUp(self): + self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK) + self.other_user, _ = self.create_user() + inform_changed_data(self.other_user) + + self.user, user_password = self.create_user() + self.user.groups.add(self.delegate_group) + self.user.is_present = True + self.user.save() + self.user_client = APIClient() + self.user_client.login(username=self.user.username, password=user_password) + + self.assignment = Assignment.objects.create( + title="test_assignment_" + self._get_random_string(), open_posts=1 + ) + self.assignment.add_candidate(self.admin) + self.poll = AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_" + self._get_random_string(), + pollmethod=AssignmentPoll.POLLMETHOD_YNA, + type=self.poll_type, + state=AssignmentPoll.STATE_STARTED, + ) + self.poll.create_options() + self.poll.groups.add(self.delegate_group) + + +class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass): + poll_type = AssignmentPoll.TYPE_NAMED + + def test_vote(self): + response = self.user_client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + vote = AssignmentVote.objects.get() + + # Expect the admin to see the full data in the autoupdate + autoupdate = self.get_last_autoupdate(user=self.admin) + self.assertEqual( + autoupdate[0], + { + "assignments/assignment-poll:1": { + "allow_multiple_votes_per_candidate": False, + "assignment_id": 1, + "global_abstain": True, + "global_no": True, + "groups_id": [GROUP_DELEGATE_PK], + "id": 1, + "options": [ + { + "abstain": "1.000000", + "id": 1, + "no": "0.000000", + "user_id": self.admin.id, + "weight": 1, + "yes": "0.000000", + } + ], + "pollmethod": AssignmentPoll.POLLMETHOD_YNA, + "state": AssignmentPoll.STATE_STARTED, + "title": self.poll.title, + "type": AssignmentPoll.TYPE_NAMED, + "voted_id": [self.user.id], + "votes_amount": 1, + "votescast": "1.000000", + "votesinvalid": "0.000000", + "votesvalid": "1.000000", + }, + "assignments/assignment-vote:1": { + "id": 1, + "option_id": 1, + "pollstate": AssignmentPoll.STATE_STARTED, + "user_id": self.user.id, + "value": "A", + "weight": "1.000000", + }, + }, + ) + self.assertEqual(autoupdate[1], []) + + # Expect user to receive his vote + autoupdate = self.get_last_autoupdate(user=self.user) + self.assertEqual( + autoupdate[0]["assignments/assignment-vote:1"], + { + "id": 1, + "option_id": 1, + "pollstate": AssignmentPoll.STATE_STARTED, + "user_id": self.user.id, + "value": "A", + "weight": "1.000000", + }, + ) + self.assertEqual(autoupdate[1], []) + + # Expect non-admins to get a restricted poll update + for user in (self.user, self.other_user): + self.assertAutoupdate(poll, user=user) + autoupdate = self.get_last_autoupdate(user=user) + self.assertEqual( + autoupdate[0]["assignments/assignment-poll:1"], + { + "allow_multiple_votes_per_candidate": False, + "assignment_id": 1, + "global_abstain": True, + "global_no": True, + "pollmethod": AssignmentPoll.POLLMETHOD_YNA, + "state": AssignmentPoll.STATE_STARTED, + "type": AssignmentPoll.TYPE_NAMED, + "title": self.poll.title, + "groups_id": [GROUP_DELEGATE_PK], + "options": [{"id": 1, "user_id": self.admin.id, "weight": 1}], + "id": 1, + "votes_amount": 1, + }, + ) + + # Other users should not get a vote autoupdate + self.assertNoAutoupdate(vote, user=self.other_user) + self.assertNoDeletedAutoupdate(vote, user=self.other_user) + + +class VoteAssignmentPollPseudoanonymousAutoupdates( + VoteAssignmentPollAutoupdatesBaseClass +): + poll_type = AssignmentPoll.TYPE_PSEUDOANONYMOUS + + def test_vote(self): + response = self.user_client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + vote = AssignmentVote.objects.get() + + # Expect the admin to see the full data in the autoupdate + autoupdate = self.get_last_autoupdate(user=self.admin) + # TODO: mypy complains without the Any type; check why and fix it + should_be: Any = { + "assignments/assignment-poll:1": { + "allow_multiple_votes_per_candidate": False, + "assignment_id": 1, + "global_abstain": True, + "global_no": True, + "groups_id": [GROUP_DELEGATE_PK], + "id": 1, + "options": [ + { + "abstain": "1.000000", + "id": 1, + "no": "0.000000", + "user_id": self.admin.id, + "weight": 1, + "yes": "0.000000", + } + ], + "pollmethod": AssignmentPoll.POLLMETHOD_YNA, + "state": AssignmentPoll.STATE_STARTED, + "title": self.poll.title, + "type": AssignmentPoll.TYPE_PSEUDOANONYMOUS, + "voted_id": [self.user.id], + "votes_amount": 1, + "votescast": "1.000000", + "votesinvalid": "0.000000", + "votesvalid": "1.000000", + }, + "assignments/assignment-vote:1": { + "id": 1, + "option_id": 1, + "pollstate": AssignmentPoll.STATE_STARTED, + "user_id": None, + "value": "A", + "weight": "1.000000", + }, + } + self.assertEqual(autoupdate[0], should_be) + self.assertEqual(autoupdate[1], []) + + # Expect non-admins to get a restricted poll update and no autoupdate + # for a changed vote nor a deleted one + for user in (self.user, self.other_user): + self.assertAutoupdate(poll, user=user) + autoupdate = self.get_last_autoupdate(user=user) + self.assertEqual( + autoupdate[0]["assignments/assignment-poll:1"], + { + "allow_multiple_votes_per_candidate": False, + "assignment_id": 1, + "global_abstain": True, + "global_no": True, + "pollmethod": AssignmentPoll.POLLMETHOD_YNA, + "state": AssignmentPoll.STATE_STARTED, + "type": AssignmentPoll.TYPE_PSEUDOANONYMOUS, + "title": self.poll.title, + "groups_id": [GROUP_DELEGATE_PK], + "options": [{"id": 1, "user_id": self.admin.id, "weight": 1}], + "id": 1, + "votes_amount": 1, + }, + ) + + self.assertNoAutoupdate(vote, user=user) + self.assertNoDeletedAutoupdate(vote, user=user) diff --git a/tests/integration/motions/test_polls.py b/tests/integration/motions/test_polls.py index 1aa528090..ae4b3f977 100644 --- a/tests/integration/motions/test_polls.py +++ b/tests/integration/motions/test_polls.py @@ -1,18 +1,77 @@ from decimal import Decimal +import pytest from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from openslides.core.config import config -from openslides.motions.models import Motion, MotionPoll, MotionVote +from openslides.motions.models import Motion, MotionOption, MotionPoll, MotionVote from openslides.poll.models import BasePoll from openslides.utils.auth import get_group_model from openslides.utils.autoupdate import inform_changed_data from tests.common_groups import GROUP_ADMIN_PK, GROUP_DEFAULT_PK, GROUP_DELEGATE_PK from tests.test_case import TestCase +from ..helpers import count_queries + + +@pytest.mark.django_db(transaction=False) +def test_motion_poll_db_queries(): + """ + Tests that only the following db queries are done: + * 1 request to get the polls, + * 1 request to get all options for all polls, + * 1 request to get all votes for all options, + * 1 request to get all users for all votes, + * 1 request to get all poll groups, + = 5 queries + """ + create_motion_polls() + assert count_queries(MotionPoll.get_elements) == 5 + + +@pytest.mark.django_db(transaction=False) +def test_motion_vote_db_queries(): + """ + Tests that only 1 query is done when fetching MotionVotes + """ + create_motion_polls() + assert count_queries(MotionVote.get_elements) == 1 + + +def create_motion_polls(): + """ + Creates 1 Motion with 5 polls with 5 options each which have 2 votes each + """ + motion = Motion.objects.create(title="test_motion_wfLrsjEHXBmPplbvQ65N") + group1 = get_group_model().objects.get(pk=1) + group2 = get_group_model().objects.get(pk=2) + + for index in range(5): + poll = MotionPoll.objects.create( + motion=motion, title=f"test_title_{index}", pollmethod="YN", type="named" + ) + poll.groups.add(group1) + poll.groups.add(group2) + + for j in range(5): + option = MotionOption.objects.create(poll=poll) + + for k in range(2): + user = get_user_model().objects.create_user( + username=f"test_username_{index}{j}{k}", + password="test_password_kbzj5L8ZtVxBllZzoW6D", + ) + MotionVote.objects.create( + user=user, + option=option, + value=("Y" if k == 0 else "N"), + weight=Decimal(1), + ) + poll.voted.add(user) + class CreateMotionPoll(TestCase): """ @@ -816,7 +875,7 @@ class VoteMotionPollPseudoanonymous(TestCase): response = self.client.post( reverse("motionpoll-vote", args=[self.poll.pk]), "A", format="json" ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) option = MotionPoll.objects.get().options.get() self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.no, Decimal("1"))