diff --git a/server/openslides/assignments/migrations/0015_assignmentvote_delegated_user.py b/server/openslides/assignments/migrations/0015_assignmentvote_delegated_user.py new file mode 100644 index 000000000..9a591b9e6 --- /dev/null +++ b/server/openslides/assignments/migrations/0015_assignmentvote_delegated_user.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.16 on 2020-09-10 11:02 + +from django.conf import settings +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("assignments", "0014_remove_deprecated_slides"), + ] + + operations = [ + migrations.AddField( + model_name="assignmentvote", + name="delegated_user", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE, + related_name="assignmentvote_delegated_votes", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/openslides/assignments/views.py b/server/openslides/assignments/views.py index 3cab3b452..9f00f055a 100644 --- a/server/openslides/assignments/views.py +++ b/server/openslides/assignments/views.py @@ -289,7 +289,7 @@ class AssignmentPollViewSet(BasePollViewSet): poll.db_amount_global_no = Decimal(0) poll.save() - def handle_analog_vote(self, data, poll, user): + def handle_analog_vote(self, data, poll): for field in ["votesvalid", "votesinvalid", "votescast"]: setattr(poll, field, data[field]) @@ -336,7 +336,7 @@ class AssignmentPollViewSet(BasePollViewSet): poll.save() - def validate_vote_data(self, data, poll, user): + def validate_vote_data(self, data, poll): """ Request data: analog: @@ -478,10 +478,12 @@ class AssignmentPollViewSet(BasePollViewSet): options_data = data - def create_votes_type_votes(self, data, poll, vote_weight, vote_user): + def create_votes_type_votes(self, data, poll, vote_weight, vote_user, request_user): """ Helper function for handle_(named|pseudoanonymous)_vote Assumes data is already validated + vote_user is the user whose vote is given + request_user is the user who gives the vote, may be a delegate """ options = poll.get_options() if isinstance(data, dict): @@ -495,30 +497,46 @@ class AssignmentPollViewSet(BasePollViewSet): if config["users_activate_vote_weight"]: weight *= vote_weight vote = AssignmentVote.objects.create( - option=option, user=vote_user, weight=weight, value="Y" + option=option, + user=vote_user, + delegated_user=request_user, + weight=weight, + value="Y", ) inform_changed_data(vote, no_delete_on_restriction=True) else: # global_no or global_abstain option = options[0] weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1) vote = AssignmentVote.objects.create( - option=option, user=vote_user, weight=weight, value=data + option=option, + user=vote_user, + delegated_user=request_user, + weight=weight, + value=data, ) inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(option) inform_changed_data(poll) - def create_votes_types_yn_yna(self, data, poll, vote_weight, vote_user): + def create_votes_types_yn_yna( + self, data, poll, vote_weight, vote_user, request_user + ): """ Helper function for handle_(named|pseudoanonymous)_vote Assumes data is already validated + vote_user is the user whose vote is given + request_user is the user who gives the vote, may be a delegate """ options = poll.get_options() weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1) for option_id, result in data.items(): option = options.get(pk=option_id) vote = AssignmentVote.objects.create( - option=option, user=vote_user, value=result, weight=weight + option=option, + user=vote_user, + delegated_user=request_user, + value=result, + weight=weight, ) inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(option, no_delete_on_restriction=True) @@ -527,24 +545,28 @@ class AssignmentPollViewSet(BasePollViewSet): VotedModel = AssignmentPoll.voted.through VotedModel.objects.create(assignmentpoll=poll, user=user) - def handle_named_vote(self, data, poll, user): + def handle_named_vote(self, data, poll, vote_user, request_user): if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: - self.create_votes_type_votes(data, poll, user.vote_weight, user) + self.create_votes_type_votes( + data, poll, vote_user.vote_weight, vote_user, request_user + ) elif poll.pollmethod in ( AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YNA, ): - self.create_votes_types_yn_yna(data, poll, user.vote_weight, user) + self.create_votes_types_yn_yna( + data, poll, vote_user.vote_weight, vote_user, request_user + ) def handle_pseudoanonymous_vote(self, data, poll, user): if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: - self.create_votes_type_votes(data, poll, user.vote_weight, None) + self.create_votes_type_votes(data, poll, user.vote_weight, None, None) elif poll.pollmethod in ( AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YNA, ): - self.create_votes_types_yn_yna(data, poll, user.vote_weight, None) + self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None) def convert_option_data(self, poll, data): poll_options = poll.get_options() diff --git a/server/openslides/motions/migrations/0037_motionvote_delegated_user.py b/server/openslides/motions/migrations/0037_motionvote_delegated_user.py new file mode 100644 index 000000000..04e13cd00 --- /dev/null +++ b/server/openslides/motions/migrations/0037_motionvote_delegated_user.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.16 on 2020-09-10 11:02 + +from django.conf import settings +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("motions", "0036_rename_verbose_poll_types"), + ] + + operations = [ + migrations.AddField( + model_name="motionvote", + name="delegated_user", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE, + related_name="motionvote_delegated_votes", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/openslides/motions/views.py b/server/openslides/motions/views.py index a9d7253dc..e1ba80b9a 100644 --- a/server/openslides/motions/views.py +++ b/server/openslides/motions/views.py @@ -1190,7 +1190,7 @@ class MotionPollViewSet(BasePollViewSet): return result - def handle_analog_vote(self, data, poll, user): + def handle_analog_vote(self, data, poll): option = poll.options.get() vote, _ = MotionVote.objects.get_or_create(option=option, value="Y") vote.weight = data["Y"] @@ -1209,7 +1209,7 @@ class MotionPollViewSet(BasePollViewSet): poll.save() - def validate_vote_data(self, data, poll, user): + def validate_vote_data(self, data, poll): """ Request data for analog: { "Y": , "N": , ["A": ], @@ -1240,15 +1240,25 @@ class MotionPollViewSet(BasePollViewSet): VotedModel = MotionPoll.voted.through VotedModel.objects.create(motionpoll=poll, user=user) - def handle_named_vote(self, data, poll, user): - self.handle_named_and_pseudoanonymous_vote(data, user, user, poll) + def handle_named_vote(self, data, poll, vote_user, request_user): + self.handle_named_and_pseudoanonymous_vote( + data, + poll, + weight_user=vote_user, + vote_user=vote_user, + request_user=request_user, + ) def handle_pseudoanonymous_vote(self, data, poll, user): - self.handle_named_and_pseudoanonymous_vote(data, user, None, poll) + self.handle_named_and_pseudoanonymous_vote(data, poll, user, None, None) - def handle_named_and_pseudoanonymous_vote(self, data, weight_user, vote_user, poll): + def handle_named_and_pseudoanonymous_vote( + self, data, poll, weight_user, vote_user, request_user + ): option = poll.options.get() - vote = MotionVote.objects.create(user=vote_user, option=option) + vote = MotionVote.objects.create( + user=vote_user, delegated_user=request_user, option=option + ) vote.value = data vote.weight = ( weight_user.vote_weight diff --git a/server/openslides/poll/access_permissions.py b/server/openslides/poll/access_permissions.py index 576ee0ace..f9b1f124e 100644 --- a/server/openslides/poll/access_permissions.py +++ b/server/openslides/poll/access_permissions.py @@ -2,8 +2,13 @@ import json from typing import Any, Dict, List from ..poll.views import BasePoll +from ..utils import logging from ..utils.access_permissions import BaseAccessPermissions -from ..utils.auth import async_has_perm +from ..utils.auth import async_has_perm, user_collection_string +from ..utils.cache import element_cache + + +logger = logging.getLogger(__name__) class BaseVoteAccessPermissions(BaseAccessPermissions): @@ -26,6 +31,7 @@ class BaseVoteAccessPermissions(BaseAccessPermissions): for vote in full_data if vote["pollstate"] == BasePoll.STATE_PUBLISHED or vote["user_id"] == user_id + or vote["delegated_user_id"] == user_id ] return data @@ -71,8 +77,24 @@ class BasePollAccessPermissions(BaseAccessPermissions): """ # add has_voted for all users to check whether op has voted + # also fill user_has_voted_for_delegations with all users for which he has + # already voted + user_data = await element_cache.get_element_data( + user_collection_string, user_id + ) + if user_data is None: + logger.error(f"Could not find userdata for {user_id}") + vote_delegated_from_ids = set() + else: + vote_delegated_from_ids = set(user_data["vote_delegated_from_users_id"]) + for poll in full_data: poll["user_has_voted"] = user_id in poll["voted_id"] + voted_ids = set(poll["voted_id"]) + voted_for_delegations = list( + vote_delegated_from_ids.intersection(voted_ids) + ) + poll["user_has_voted_for_delegations"] = voted_for_delegations if await async_has_perm(user_id, self.manage_permission): data = full_data diff --git a/server/openslides/poll/models.py b/server/openslides/poll/models.py index b3d47d4d8..e362efbe4 100644 --- a/server/openslides/poll/models.py +++ b/server/openslides/poll/models.py @@ -29,6 +29,14 @@ class BaseVote(models.Model): blank=True, on_delete=SET_NULL_AND_AUTOUPDATE, ) + delegated_user = models.ForeignKey( + settings.AUTH_USER_MODEL, + default=None, + null=True, + blank=True, + on_delete=SET_NULL_AND_AUTOUPDATE, + related_name="%(class)s_delegated_votes", + ) class Meta: abstract = True diff --git a/server/openslides/poll/serializers.py b/server/openslides/poll/serializers.py index a2b5d8311..03b682851 100644 --- a/server/openslides/poll/serializers.py +++ b/server/openslides/poll/serializers.py @@ -12,7 +12,15 @@ from ..utils.rest_api import ( from .models import BasePoll -BASE_VOTE_FIELDS = ("id", "weight", "value", "user", "option", "pollstate") +BASE_VOTE_FIELDS = ( + "id", + "weight", + "value", + "user", + "delegated_user", + "option", + "pollstate", +) class BaseVoteSerializer(ModelSerializer): diff --git a/server/openslides/poll/views.py b/server/openslides/poll/views.py index b89cdda02..9a11416b6 100644 --- a/server/openslides/poll/views.py +++ b/server/openslides/poll/views.py @@ -1,5 +1,6 @@ from textwrap import dedent +from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.db import transaction from django.db.utils import IntegrityError @@ -104,8 +105,8 @@ class BasePollViewSet(ModelViewSet): # convert user ids to option ids self.convert_option_data(poll, vote_data) - self.validate_vote_data(vote_data, poll, request.user) - self.handle_analog_vote(vote_data, poll, request.user) + self.validate_vote_data(vote_data, poll) + self.handle_analog_vote(vote_data, poll) if request.data.get("publish_immediately"): poll.state = BasePoll.STATE_PUBLISHED @@ -198,25 +199,41 @@ class BasePollViewSet(ModelViewSet): if isinstance(request.user, AnonymousUser): self.permission_denied(request) - # check permissions based on poll type and handle requests - self.assert_can_vote(poll, request) - + # data format is: + # { data: , [user_id: int] } + # if user_id is given, the operator votes for this user instead of himself + # user_id is ignored for analog polls data = request.data - self.validate_vote_data(data, poll, request.user) + if "data" not in data: + raise ValidationError({"detail": "No data provided."}) + vote_data = data["data"] + if "user_id" in data and poll.type != BasePoll.TYPE_ANALOG: + try: + vote_user = get_user_model().objects.get(pk=data["user_id"]) + except get_user_model().DoesNotExist: + raise ValidationError({"detail": "The given user does not exist."}) + else: + vote_user = request.user + + # check permissions based on poll type and user + self.assert_can_vote(poll, request, vote_user) + + # validate the vote data + self.validate_vote_data(vote_data, poll) if poll.type == BasePoll.TYPE_ANALOG: - self.handle_analog_vote(data, poll, request.user) - if request.data.get("publish_immediately") == "1": + self.handle_analog_vote(vote_data, poll) + if vote_data.get("publish_immediately") == "1": poll.state = BasePoll.STATE_PUBLISHED else: poll.state = BasePoll.STATE_FINISHED poll.save() elif poll.type == BasePoll.TYPE_NAMED: - self.handle_named_vote(data, poll, request.user) + self.handle_named_vote(vote_data, poll, vote_user, request.user) elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS: - self.handle_pseudoanonymous_vote(data, poll, request.user) + self.handle_pseudoanonymous_vote(vote_data, poll, vote_user) inform_changed_data(poll) @@ -231,13 +248,16 @@ class BasePollViewSet(ModelViewSet): inform_changed_data(poll.get_votes(), final_data=True) return Response() - def assert_can_vote(self, poll, request): + def assert_can_vote(self, poll, request, vote_user): """ Raises a permission denied, if the user is not allowed to vote (or has already voted). - Adds the user to the voted array, so this needs to be reverted on error! + Adds the user to the voted array, so this needs to be reverted if a later error happens! Analog: has to have manage permissions Named & Pseudoanonymous: has to be in a poll group and present """ + if request.user != vote_user and request.user != vote_user.vote_delegated_to: + self.permission_denied(request) + if poll.type == BasePoll.TYPE_ANALOG: if not self.has_manage_permissions(): self.permission_denied(request) @@ -246,14 +266,14 @@ class BasePollViewSet(ModelViewSet): raise ValidationError("You can only vote on a started poll.") if not request.user.is_present or not in_some_groups( - request.user.id, + vote_user.id, list(poll.groups.values_list("pk", flat=True)), exact=True, ): self.permission_denied(request) try: - self.add_user_to_voted_array(request.user, poll) + self.add_user_to_voted_array(vote_user, poll) inform_changed_data(poll) except IntegrityError: raise ValidationError({"detail": "You have already voted"}) @@ -292,20 +312,20 @@ class BasePollViewSet(ModelViewSet): """ raise NotImplementedError() - def validate_vote_data(self, data, poll, user): + def validate_vote_data(self, data, poll): """ To be implemented by subclass. Validates the data according to poll type and method and fields by validated versions. Raises ValidationError on failure """ raise NotImplementedError() - def handle_analog_vote(self, data, poll, user): + def handle_analog_vote(self, data, poll): """ To be implemented by subclass. Handles the analog vote. Assumes data is validated """ raise NotImplementedError() - def handle_named_vote(self, data, poll, user): + def handle_named_vote(self, data, poll, vote_user, request_user): """ To be implemented by subclass. Handles the named vote. Assumes data is validated. Needs to manage the voted-array per option. diff --git a/server/openslides/users/access_permissions.py b/server/openslides/users/access_permissions.py index 4e4084f01..a299a2ab2 100644 --- a/server/openslides/users/access_permissions.py +++ b/server/openslides/users/access_permissions.py @@ -58,6 +58,8 @@ class UserAccessPermissions(BaseAccessPermissions): own_data_fields = set(little_data_fields) own_data_fields.add("email") own_data_fields.add("gender") + own_data_fields.add("vote_delegated_to_id") + own_data_fields.add("vote_delegated_from_users_id") # Check user permissions. if await async_has_perm(user_id, "users.can_see_name"): diff --git a/server/openslides/users/migrations/0015_user_vote_delegated_to.py b/server/openslides/users/migrations/0015_user_vote_delegated_to.py new file mode 100644 index 000000000..7d9c0499e --- /dev/null +++ b/server/openslides/users/migrations/0015_user_vote_delegated_to.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.16 on 2020-09-03 11:13 + +from django.conf import settings +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0014_user_rename_permission"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="vote_delegated_to", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE, + related_name="vote_delegated_from_users", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/openslides/users/models.py b/server/openslides/users/models.py index 44794fe61..ad3c45fe5 100644 --- a/server/openslides/users/models.py +++ b/server/openslides/users/models.py @@ -22,7 +22,11 @@ from openslides.utils.manager import BaseManager from ..core.config import config from ..utils.auth import GROUP_ADMIN_PK -from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin +from ..utils.models import ( + CASCADE_AND_AUTOUPDATE, + SET_NULL_AND_AUTOUPDATE, + RESTModelMixin, +) from .access_permissions import ( GroupAccessPermissions, PersonalNoteAccessPermissions, @@ -54,7 +58,8 @@ class UserManager(BaseUserManager): queryset=Permission.objects.select_related("content_type"), ) ), - ) + ), + "vote_delegated_from_users", ) def create_user(self, username, password, skip_autoupdate=False, **kwargs): @@ -164,6 +169,14 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): default=Decimal("1"), max_digits=15, decimal_places=6, null=False, blank=True ) + vote_delegated_to = models.ForeignKey( + "self", + on_delete=SET_NULL_AND_AUTOUPDATE, + null=True, + blank=True, + related_name="vote_delegated_from_users", + ) + objects = UserManager() class Meta: diff --git a/server/openslides/users/serializers.py b/server/openslides/users/serializers.py index 6c2f44949..6397e8a14 100644 --- a/server/openslides/users/serializers.py +++ b/server/openslides/users/serializers.py @@ -7,6 +7,7 @@ from ..utils.rest_api import ( JSONField, ModelSerializer, RelatedField, + SerializerMethodField, ValidationError, ) from ..utils.validate import validate_html_strict @@ -36,6 +37,8 @@ USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + ( "comment", "is_active", "auth_type", + "vote_delegated_to_id", + "vote_delegated_from_users_id", ) @@ -57,11 +60,14 @@ class UserSerializer(ModelSerializer): ), ) + vote_delegated_from_users_id = SerializerMethodField() + class Meta: model = User fields = USERCANSEEEXTRASERIALIZER_FIELDS + ( "default_password", "session_auth_hash", + "vote_delegated_to", ) read_only_fields = ("last_email_send", "auth_type") @@ -119,6 +125,13 @@ class UserSerializer(ModelSerializer): inform_changed_data(user) return user + def get_vote_delegated_from_users_id(self, user): + # check needed to prevent errors on import since we only give an OrderedDict there + if hasattr(user, "vote_delegated_from_users"): + return [delegator.id for delegator in user.vote_delegated_from_users.all()] + else: + return [] + class PermissionRelatedField(RelatedField): """ diff --git a/server/openslides/users/views.py b/server/openslides/users/views.py index 1361f8da5..5d795f05c 100644 --- a/server/openslides/users/views.py +++ b/server/openslides/users/views.py @@ -174,9 +174,77 @@ class UserViewSet(ModelViewSet): ): request.data["username"] = user.username + # check that no chains are created with vote delegation + delegate_id = request.data.get("vote_delegated_to_id") + if delegate_id: + try: + delegate = User.objects.get(id=delegate_id) + except User.DoesNotExist: + raise ValidationError( + { + "detail": f"Vote delegation: The user with id {delegate_id} does not exist" + } + ) + + self.assert_no_self_delegation(user, [delegate_id]) + self.assert_vote_not_delegated(delegate) + self.assert_has_no_delegated_votes(user) + + inform_changed_data(delegate) + if user.vote_delegated_to: + inform_changed_data(user.vote_delegated_to) + + # handle delegated_from field seperately since its a SerializerMethodField + new_delegation_ids = request.data.get("vote_delegated_from_users_id") + if "vote_delegated_from_users_id" in request.data: + del request.data["vote_delegated_from_users_id"] + response = super().update(request, *args, **kwargs) + + # after rest of the request succeeded, handle delegation changes + if new_delegation_ids: + self.assert_no_self_delegation(user, new_delegation_ids) + self.assert_vote_not_delegated(user) + + for id in new_delegation_ids: + delegation_user = User.objects.get(id=id) + self.assert_has_no_delegated_votes(delegation_user) + delegation_user.vote_delegated_to = user + delegation_user.save() + + delegations_to_remove = user.vote_delegated_from_users.exclude( + id__in=(new_delegation_ids or []) + ) + for old_delegation_user in delegations_to_remove: + old_delegation_user.vote_delegated_to = None + old_delegation_user.save() + + # if only delegated_from was changed, we need an autoupdate for the operator + if new_delegation_ids or delegations_to_remove: + inform_changed_data(user) + return response + def assert_vote_not_delegated(self, user): + if user.vote_delegated_to: + raise ValidationError( + { + "detail": "You cannot delegate a vote to a user who has already delegated his vote." + } + ) + + def assert_has_no_delegated_votes(self, user): + if user.vote_delegated_from_users and len(user.vote_delegated_from_users.all()): + raise ValidationError( + { + "detail": "You cannot delegate a vote of a user who is already a delegate of another user." + } + ) + + def assert_no_self_delegation(self, user, delegate_ids): + if user.id in delegate_ids: + raise ValidationError({"detail": "You cannot delegate a vote to yourself."}) + def destroy(self, request, *args, **kwargs): """ Customized view endpoint to delete an user. @@ -391,6 +459,7 @@ class UserViewSet(ModelViewSet): data = serializer.prepare_password(serializer.data) groups = data["groups_id"] del data["groups_id"] + del data["vote_delegated_from_users_id"] db_user = User(**data) try: diff --git a/server/tests/integration/assignments/test_polls.py b/server/tests/integration/assignments/test_polls.py index 275cbaf71..090f4ff2f 100644 --- a/server/tests/integration/assignments/test_polls.py +++ b/server/tests/integration/assignments/test_polls.py @@ -770,13 +770,15 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { - "options": { - "1": {"Y": "1", "N": "2.35", "A": "-1"}, - "2": {"Y": "30", "N": "-2", "A": "8.93"}, + "data": { + "options": { + "1": {"Y": "1", "N": "2.35", "A": "-1"}, + "2": {"Y": "30", "N": "-2", "A": "8.93"}, + }, + "votesvalid": "4.64", + "votesinvalid": "-2", + "votescast": "-2", }, - "votesvalid": "4.64", - "votesinvalid": "-2", - "votescast": "-2", }, ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -800,10 +802,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { - "options": {"1": {"Y": "1", "N": "1", "A": "1"}}, - "votesvalid": "-1.5", - "votesinvalid": "-2", - "votescast": "-2", + "data": { + "options": {"1": {"Y": "1", "N": "1", "A": "1"}}, + "votesvalid": "-1.5", + "votesinvalid": "-2", + "votescast": "-2", + }, }, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -814,10 +818,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): 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"}, - } + "data": { + "options": { + "1": {"Y": "1", "N": "2.35", "A": "-1"}, + "2": {"Y": "1", "N": "2.35", "A": "-1"}, + } + }, }, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -828,7 +834,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}}, + {"data": {"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -839,9 +845,11 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { - "options": { - "1": {"Y": "1", "N": "2.35", "A": "-1"}, - "3": {"Y": "1", "N": "2.35", "A": "-1"}, + "data": { + "options": { + "1": {"Y": "1", "N": "2.35", "A": "-1"}, + "3": {"Y": "1", "N": "2.35", "A": "-1"}, + } } }, ) @@ -851,25 +859,31 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): def test_no_permissions(self): self.start_poll() self.make_admin_delegate() - response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(AssignmentVote.objects.exists()) def test_wrong_state(self): - response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, 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] + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentVote.objects.exists()) @@ -878,7 +892,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"options": [1, "string"]}, + {"data": {"options": [1, "string"]}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -887,7 +901,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"options": {"string": "some_other_string"}}, + {"data": {"options": {"string": "some_other_string"}}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentVote.objects.exists()) @@ -896,7 +910,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"options": {"1": [None]}}, + {"data": {"options": {"1": [None]}}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentVote.objects.exists()) @@ -907,7 +921,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): data = {"options": {"1": {"Y": "1", "N": "3", "A": "-1"}}} del data["options"]["1"][value] response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), data + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": data} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentVote.objects.exists()) @@ -917,10 +931,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { - "options": {"1": {"Y": 5, "N": 0, "A": 1}}, - "votesvalid": "-2", - "votesinvalid": "1", - "votescast": "-1", + "data": { + "options": {"1": {"Y": 5, "N": 0, "A": 1}}, + "votesvalid": "-2", + "votesinvalid": "1", + "votescast": "-1", + } }, ) self.poll.state = 3 @@ -928,10 +944,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass): response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), { - "options": {"1": {"Y": 2, "N": 2, "A": 2}}, - "votesvalid": "4.64", - "votesinvalid": "-2", - "votescast": "3", + "data": { + "options": {"1": {"Y": 2, "N": 2, "A": 2}}, + "votesvalid": "4.64", + "votesinvalid": "-2", + "votescast": "3", + } }, ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -973,7 +991,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "2": "N", "3": "A"}, + {"data": {"1": "Y", "2": "N", "3": "A"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1007,7 +1025,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "2": "N", "3": "A"}, + {"data": {"1": "Y", "2": "N", "3": "A"}}, ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertEqual(AssignmentVote.objects.count(), 3) @@ -1039,12 +1057,12 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "N"}, + {"data": {"1": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1064,7 +1082,8 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): option2 = self.poll2.options.get() # Do request to poll with option2 (which is wrong...) response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {str(option2.id): "Y"} + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {str(option2.id): "Y"}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertEqual(AssignmentVote.objects.count(), 0) @@ -1081,7 +1100,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "2": "N"}, + {"data": {"1": "Y", "2": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1092,7 +1111,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1103,7 +1122,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "3": "N"}, + {"data": {"1": "Y", "3": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1114,7 +1133,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.make_admin_delegate() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) @@ -1125,7 +1144,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): gclient = self.create_guest_client() response = gclient.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) @@ -1137,20 +1156,24 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.admin.save() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose( response, status.HTTP_200_OK ) # new "feature" because of partial requests: empty requests work! @@ -1160,7 +1183,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - [1, 2, 5], + {"data": [1, 2, 5]}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1170,7 +1193,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "string"}, + {"data": {"1": "string"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1180,7 +1203,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"id": "Y"}, + {"data": {"id": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1190,7 +1213,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": [None]}, + {"data": {"1": [None]}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1229,7 +1252,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 0}, + {"data": {"1": 1, "2": 0}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1254,12 +1277,12 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 0}, + {"data": {"1": 1, "2": 0}}, format="json", ) response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 0, "2": 1}, + {"data": {"1": 0, "2": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1278,7 +1301,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.poll.save() self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), "N" + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = AssignmentPoll.objects.get() @@ -1294,7 +1317,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.poll.save() self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), "N" + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -1305,7 +1328,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.poll.save() self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), "A" + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = AssignmentPoll.objects.get() @@ -1321,7 +1344,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.poll.save() self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), "A" + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -1331,7 +1354,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": -1}, + {"data": {"1": -1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1342,7 +1365,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 2, "2": 1}, + {"data": {"1": 2, "2": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1361,7 +1384,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 2, "2": 2}, + {"data": {"1": 2, "2": 2}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1372,7 +1395,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 1, "3": 1}, + {"data": {"1": 1, "2": 1, "3": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1381,7 +1404,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): def test_wrong_options(self): self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"2": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -1390,7 +1415,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() self.make_admin_delegate() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(AssignmentVote.objects.exists()) @@ -1399,7 +1426,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() gclient = self.create_guest_client() response = gclient.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(AssignmentVote.objects.exists()) @@ -1409,21 +1438,27 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.admin.is_present = False self.admin.save() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, 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" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertFalse(AssignmentVote.objects.exists()) @@ -1431,7 +1466,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - [1, 2, 5], + {"data": [1, 2, 5]}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1441,7 +1476,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "string"}, + {"data": {"1": "string"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1451,7 +1486,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"id": 1}, + {"data": {"id": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1461,7 +1496,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": [None]}, + {"data": {"1": [None]}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1495,7 +1530,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "2": "N", "3": "A"}, + {"data": {"1": "Y", "2": "N", "3": "A"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1522,12 +1557,12 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "N"}, + {"data": {"1": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1541,7 +1576,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "2": "N"}, + {"data": {"1": "Y", "2": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1552,7 +1587,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1563,7 +1598,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y", "3": "N"}, + {"data": {"1": "Y", "3": "N"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1574,7 +1609,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.make_admin_delegate() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) @@ -1585,7 +1620,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): gclient = self.create_guest_client() response = gclient.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) @@ -1597,20 +1632,24 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.admin.save() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "Y"}, + {"data": {"1": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertFalse(AssignmentVote.objects.exists()) @@ -1618,7 +1657,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - [1, 2, 5], + {"data": [1, 2, 5]}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1628,7 +1667,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "string"}, + {"data": {"1": "string"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1638,7 +1677,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"id": "Y"}, + {"data": {"id": "Y"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1648,7 +1687,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": [None]}, + {"data": {"1": [None]}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1687,7 +1726,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 0}, + {"data": {"1": 1, "2": 0}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1714,12 +1753,12 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 0}, + {"data": {"1": 1, "2": 0}}, format="json", ) response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 0, "2": 1}, + {"data": {"1": 0, "2": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1737,7 +1776,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": -1}, + {"data": {"1": -1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1748,7 +1787,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 2, "2": 1}, + {"data": {"1": 2, "2": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -1769,7 +1808,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 2, "2": 2}, + {"data": {"1": 2, "2": 2}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1780,7 +1819,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": 1, "2": 1, "3": 1}, + {"data": {"1": 1, "2": 1, "3": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1789,7 +1828,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): def test_wrong_options(self): self.start_poll() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"2": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) @@ -1798,7 +1839,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() self.make_admin_delegate() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(AssignmentVote.objects.exists()) @@ -1807,7 +1850,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() gclient = self.create_guest_client() response = gclient.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(AssignmentVote.objects.exists()) @@ -1817,21 +1862,27 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.admin.is_present = False self.admin.save() response = self.client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, 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" + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", ) self.assertHttpStatusVerbose(response, 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])) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertFalse(AssignmentVote.objects.exists()) @@ -1839,7 +1890,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - [1, 2, 5], + {"data": {"data": [1, 2, 5]}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1849,7 +1900,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": "string"}, + {"data": {"1": "string"}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1859,7 +1910,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"id": 1}, + {"data": {"id": 1}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1869,7 +1920,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.start_poll() response = self.client.post( reverse("assignmentpoll-vote", args=[self.poll.pk]), - {"1": [None]}, + {"data": {"1": [None]}}, format="json", ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) @@ -1923,7 +1974,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) def test_vote(self): response = self.user_client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"} + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = AssignmentPoll.objects.get() @@ -1956,6 +2007,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "votesinvalid": "0.000000", "votesvalid": "1.000000", "user_has_voted": False, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], }, "assignments/assignment-option:1": { @@ -1973,6 +2025,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "option_id": 1, "pollstate": AssignmentPoll.STATE_STARTED, "user_id": self.user.id, + "delegated_user_id": self.user.id, "value": "A", "weight": "1.000000", }, @@ -1989,6 +2042,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "option_id": 1, "pollstate": AssignmentPoll.STATE_STARTED, "user_id": self.user.id, + "delegated_user_id": self.user.id, "value": "A", "weight": "1.000000", }, @@ -2018,6 +2072,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "id": 1, "votes_amount": 1, "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], }, ) @@ -2073,6 +2128,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "votesinvalid": "0.000000", "votesvalid": "1.000000", "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], }, ) @@ -2084,6 +2140,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "weight": "1.000000", "value": "A", "user_id": 3, + "delegated_user_id": None, "option_id": 1, }, ) @@ -2108,9 +2165,9 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( ): poll_type = AssignmentPoll.TYPE_PSEUDOANONYMOUS - def test_vote(self): + def test_votex(self): response = self.user_client.post( - reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"} + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = AssignmentPoll.objects.get() @@ -2136,6 +2193,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "description": self.description, "type": AssignmentPoll.TYPE_PSEUDOANONYMOUS, "user_has_voted": False, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST, "majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS, @@ -2159,6 +2217,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "option_id": 1, "pollstate": AssignmentPoll.STATE_STARTED, "user_id": None, + "delegated_user_id": None, "value": "A", "weight": "1.000000", }, @@ -2190,6 +2249,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "id": 1, "votes_amount": 1, "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], }, ) @@ -2245,6 +2305,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "votesinvalid": "0.000000", "votesvalid": "1.000000", "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], }, "assignments/assignment-vote:1": { @@ -2253,6 +2314,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "weight": "1.000000", "value": "A", "user_id": None, + "delegated_user_id": None, "option_id": 1, }, "assignments/assignment-option:1": { diff --git a/server/tests/integration/motions/test_polls.py b/server/tests/integration/motions/test_polls.py index 2b93759bd..e4bc1ed10 100644 --- a/server/tests/integration/motions/test_polls.py +++ b/server/tests/integration/motions/test_polls.py @@ -167,6 +167,7 @@ class CreateMotionPoll(TestCase): "id": 1, "voted_id": [], "user_has_voted": False, + "user_has_voted_for_delegations": [], }, ) self.assertEqual(autoupdate[1], []) @@ -610,12 +611,14 @@ class VoteMotionPollAnalog(TestCase): response = self.client.post( reverse("motionpoll-vote", args=[self.poll.pk]), { - "Y": "1", - "N": "2.35", - "A": "-1", - "votesvalid": "4.64", - "votesinvalid": "-2", - "votescast": "-2", + "data": { + "Y": "1", + "N": "2.35", + "A": "-1", + "votesvalid": "4.64", + "votesinvalid": "-2", + "votescast": "-2", + }, }, ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -634,14 +637,23 @@ class VoteMotionPollAnalog(TestCase): def test_vote_no_permissions(self): self.start_poll() self.make_admin_delegate() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + def test_vote_no_data(self): + self.start_poll() + response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]), {}) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + def test_vote_missing_data(self): self.start_poll() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), {"Y": "4", "N": "22.6"} + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": {"Y": "4", "N": "22.6"}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -649,7 +661,7 @@ class VoteMotionPollAnalog(TestCase): def test_vote_wrong_data_format(self): self.start_poll() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5] + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -658,7 +670,7 @@ class VoteMotionPollAnalog(TestCase): self.start_poll() response = self.client.post( reverse("motionpoll-vote", args=[self.poll.pk]), - {"Y": "some string", "N": "-2", "A": "3"}, + {"data": {"Y": "some string", "N": "-2", "A": "3"}}, ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -668,12 +680,14 @@ class VoteMotionPollAnalog(TestCase): self.client.post( reverse("motionpoll-vote", args=[self.poll.pk]), { - "Y": "3", - "N": "1", - "A": "5", - "votesvalid": "-2", - "votesinvalid": "1", - "votescast": "-1", + "data": { + "Y": "3", + "N": "1", + "A": "5", + "votesvalid": "-2", + "votesinvalid": "1", + "votescast": "-1", + }, }, ) self.poll.state = 3 @@ -681,12 +695,14 @@ class VoteMotionPollAnalog(TestCase): response = self.client.post( reverse("motionpoll-vote", args=[self.poll.pk]), { - "Y": "1", - "N": "2.35", - "A": "-1", - "votesvalid": "4.64", - "votesinvalid": "-2", - "votescast": "3", + "data": { + "Y": "1", + "N": "2.35", + "A": "-1", + "votesvalid": "4.64", + "votesinvalid": "-2", + "votescast": "3", + }, }, ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) @@ -749,7 +765,7 @@ class VoteMotionPollNamed(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "N" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = MotionPoll.objects.get() @@ -773,7 +789,7 @@ class VoteMotionPollNamed(TestCase): self.admin.vote_weight = weight = Decimal("3.5") self.admin.save() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "A" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = MotionPoll.objects.get() @@ -799,11 +815,11 @@ class VoteMotionPollNamed(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "N" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "A" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) poll = MotionPoll.objects.get() @@ -824,38 +840,35 @@ class VoteMotionPollNamed(TestCase): config["general_system_enable_anonymous"] = True guest_client = APIClient() response = guest_client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "Y" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"} ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) - # TODO: Move to unit tests - def test_not_set_vote_values(self): - with self.assertRaises(ValueError): - self.poll.votesvalid = Decimal("1") - with self.assertRaises(ValueError): - self.poll.votesinvalid = Decimal("1") - with self.assertRaises(ValueError): - self.poll.votescast = Decimal("1") - def test_vote_wrong_state(self): self.make_admin_present() self.make_admin_delegate() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) def test_vote_wrong_group(self): self.start_poll() self.make_admin_present() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) def test_vote_not_present(self): self.start_poll() self.make_admin_delegate() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -863,7 +876,9 @@ class VoteMotionPollNamed(TestCase): self.start_poll() self.make_admin_delegate() self.make_admin_present() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -872,11 +887,118 @@ class VoteMotionPollNamed(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5] + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + def setup_vote_delegation(self, with_delegation=True): + """ user -> admin """ + self.start_poll() + self.make_admin_delegate() + self.make_admin_present() + user, _ = self.create_user() + user.groups.add(GROUP_DELEGATE_PK) + if with_delegation: + user.vote_delegated_to = self.admin + user.save() + inform_changed_data(self.admin) # put the admin into the cache to update + # its vote_delegated_to_id field + self.user = user + + def test_vote_delegation(self): + self.setup_vote_delegation() + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": "N", "user_id": self.user.pk}, # user not present + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = MotionPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("1")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("1")) + self.assertEqual(poll.get_votes().count(), 1) + option = poll.options.get() + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("1")) + self.assertEqual(option.abstain, Decimal("0")) + vote = option.votes.get() + self.assertEqual(vote.user, self.user) + self.assertEqual(vote.delegated_user, self.admin) + + autoupdate = self.get_last_autoupdate(user=self.admin) + self.assertIn("motions/motion-poll:1", autoupdate[0]) + self.assertEqual( + autoupdate[0]["motions/motion-poll:1"]["user_has_voted_for_delegations"], + [self.user.pk], + ) + + def test_vote_delegation_and_self_vote(self): + self.test_vote_delegation() + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = MotionPoll.objects.get() + self.assertEqual(poll.votesvalid, Decimal("2")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("2")) + self.assertEqual(poll.get_votes().count(), 2) + option = poll.options.get() + self.assertEqual(option.yes, Decimal("1")) + self.assertEqual(option.no, Decimal("1")) + self.assertEqual(option.abstain, Decimal("0")) + vote = option.votes.get(user_id=self.admin.pk) + self.assertEqual(vote.user, self.admin) + self.assertEqual(vote.delegated_user, self.admin) + + def test_vote_delegation_forbidden(self): + self.setup_vote_delegation(False) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": "N", "user_id": self.user.pk}, + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + + def test_vote_delegation_not_present(self): + self.setup_vote_delegation() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": "N", "user_id": self.user.pk}, + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + + def test_vote_delegation_delegatee_not_in_group(self): + self.setup_vote_delegation() + self.admin.groups.remove(GROUP_DELEGATE_PK) + self.admin.save() + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": "N", "user_id": self.user.pk}, + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = MotionPoll.objects.get() + self.assertEqual(poll.get_votes().count(), 1) + vote = poll.get_votes()[0] + self.assertEqual(vote.value, "N") + self.assertEqual(vote.user, self.user) + self.assertEqual(vote.delegated_user, self.admin) + + def test_vote_delegation_delegator_not_in_group(self): + self.setup_vote_delegation() + self.user.groups.remove(GROUP_DELEGATE_PK) + self.user.save() + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), + {"data": "N", "user_id": self.user.pk}, + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(MotionPoll.objects.get().get_votes().exists()) + class VoteMotionPollNamedAutoupdates(TestCase): """3 important users: @@ -916,7 +1038,7 @@ class VoteMotionPollNamedAutoupdates(TestCase): def test_vote(self): response = self.user_client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "A" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = MotionPoll.objects.get() @@ -942,6 +1064,7 @@ class VoteMotionPollNamedAutoupdates(TestCase): "options_id": [1], "id": 1, "user_has_voted": False, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], }, "motions/motion-vote:1": { @@ -950,6 +1073,7 @@ class VoteMotionPollNamedAutoupdates(TestCase): "weight": "1.000000", "value": "A", "user_id": self.user.id, + "delegated_user_id": self.user.id, "option_id": 1, }, "motions/motion-option:1": { @@ -975,6 +1099,7 @@ class VoteMotionPollNamedAutoupdates(TestCase): "weight": "1.000000", "value": "A", "user_id": self.user.id, + "delegated_user_id": self.user.id, }, ) self.assertEqual( @@ -1001,6 +1126,7 @@ class VoteMotionPollNamedAutoupdates(TestCase): "options_id": [1], "id": 1, "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], }, ) self.assertEqual( @@ -1055,7 +1181,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase): def test_vote(self): response = self.user_client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "A" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = MotionPoll.objects.get() @@ -1081,6 +1207,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase): "options_id": [1], "id": 1, "user_has_voted": False, + "user_has_voted_for_delegations": [], "voted_id": [self.user.id], }, "motions/motion-vote:1": { @@ -1090,6 +1217,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase): "weight": "1.000000", "value": "A", "user_id": None, + "delegated_user_id": None, }, "motions/motion-option:1": { "abstain": "1.000000", @@ -1122,6 +1250,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase): "options_id": [1], "id": 1, "user_has_voted": user == self.user, + "user_has_voted_for_delegations": [], }, ) @@ -1177,7 +1306,7 @@ class VoteMotionPollPseudoanonymous(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "N" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = MotionPoll.objects.get() @@ -1199,11 +1328,11 @@ class VoteMotionPollPseudoanonymous(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "N" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"} ) self.assertHttpStatusVerbose(response, status.HTTP_200_OK) response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "A" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) option = MotionPoll.objects.get().options.get() @@ -1219,7 +1348,7 @@ class VoteMotionPollPseudoanonymous(TestCase): config["general_system_enable_anonymous"] = True guest_client = APIClient() response = guest_client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), "Y" + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"} ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -1227,21 +1356,27 @@ class VoteMotionPollPseudoanonymous(TestCase): def test_vote_wrong_state(self): self.make_admin_present() self.make_admin_delegate() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) def test_vote_wrong_group(self): self.start_poll() self.make_admin_present() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) def test_vote_not_present(self): self.start_poll() self.make_admin_delegate() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -1249,7 +1384,9 @@ class VoteMotionPollPseudoanonymous(TestCase): self.start_poll() self.make_admin_delegate() self.make_admin_present() - response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk])) + response = self.client.post( + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}} + ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -1258,7 +1395,7 @@ class VoteMotionPollPseudoanonymous(TestCase): self.make_admin_delegate() self.make_admin_present() response = self.client.post( - reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5] + reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]} ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertFalse(MotionPoll.objects.get().get_votes().exists()) @@ -1344,6 +1481,7 @@ class PublishMotionPoll(TestCase): "options_id": [1], "id": 1, "user_has_voted": False, + "user_has_voted_for_delegations": [], "voted_id": [], }, "motions/motion-vote:1": { @@ -1353,6 +1491,7 @@ class PublishMotionPoll(TestCase): "weight": "2.000000", "value": "N", "user_id": None, + "delegated_user_id": None, }, "motions/motion-option:1": { "abstain": "0.000000", @@ -1495,3 +1634,45 @@ class ResetMotionPoll(TestCase): for user in (self.admin, self.user1, self.user2): self.assertDeletedAutoupdate(self.vote1, user=user) self.assertDeletedAutoupdate(self.vote2, user=user) + + +class TestMotionPollWithVoteDelegationAutoupdate(TestCase): + def advancedSetUp(self): + """ Set up user -> other_user delegation. """ + self.motion = Motion( + title="test_title_dL91JqhMTiQuQLSDRItZ", + text="test_text_R7nURdXKVEfEnnJBXJYa", + ) + self.motion.save() + + self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK) + + self.other_user, _ = self.create_user() + self.user, user_password = self.create_user() + self.user.groups.add(self.delegate_group) + self.user.is_present = True + self.user.vote_delegated_to = self.other_user + self.user.save() + + self.user_client = APIClient() + self.user_client.login(username=self.user.username, password=user_password) + + self.poll = MotionPoll.objects.create( + motion=self.motion, + title="test_title_Q3EuRaALSCCPJuQ2tMqj", + pollmethod="YNA", + type=BasePoll.TYPE_NAMED, + onehundred_percent_base="YN", + majority_method="simple", + ) + self.poll.create_options() + self.poll.groups.add(self.delegate_group) + self.poll.save() + + def test_start_poll(self): + response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk])) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + + # other_user has to receive an autoupdate because he was delegated + autoupdate = self.get_last_autoupdate(user=self.other_user) + assert "motions/motion-poll:1" in autoupdate[0] diff --git a/server/tests/integration/users/test_viewset.py b/server/tests/integration/users/test_viewset.py index 9f7dad1e7..45ff81983 100644 --- a/server/tests/integration/users/test_viewset.py +++ b/server/tests/integration/users/test_viewset.py @@ -24,12 +24,13 @@ def test_user_db_queries(): """ Tests that only the following db queries are done: * 2 requests to get the list of all users and - * 1 requests to get the list of all groups. + * 1 request to get all vote delegations + * 1 request to get the list of all groups. """ for index in range(10): User.objects.create(username=f"user{index}") - assert count_queries(User.get_elements)() == 3 + assert count_queries(User.get_elements)() == 4 @pytest.mark.django_db(transaction=False) @@ -232,6 +233,188 @@ class UserUpdate(TestCase): # The user is not allowed to change some other fields (like last_name). self.assertNotEqual(user.last_name, "New name fae1Bu1Eyeis9eRox4xu") + def test_update_vote_delegation(self): + user = User.objects.create_user( + username="non-admin Yd4ejrJXZi4Wn16ugHgY", + password="non-admin AQ4Dw2tN9byKpGD4f1gs", + ) + + response = self.client.patch( + reverse("user-detail", args=[user.pk]), + {"vote_delegated_to_id": self.admin.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + user = User.objects.get(pk=user.pk) + self.assertEqual(user.vote_delegated_to_id, self.admin.pk) + admin = User.objects.get(username="admin") + self.assertEqual( + list(admin.vote_delegated_from_users.values_list("id", flat=True)), + [user.pk], + ) + + def test_update_vote_delegation_non_admin(self): + user = User.objects.create_user( + username="non-admin WpBQRSsCg6qNWNtN6bLP", + password="non-admin IzsDBt1uoqc2wo5BSUF1", + ) + client = APIClient() + client.login( + username="non-admin WpBQRSsCg6qNWNtN6bLP", + password="non-admin IzsDBt1uoqc2wo5BSUF1", + ) + + response = client.patch( + reverse("user-detail", args=[user.pk]), + {"vote_delegated_to_id": self.admin.pk}, + ) + + # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + user = User.objects.get(pk=user.pk) + self.assertIsNone(user.vote_delegated_to_id) + + def test_update_vote_delegated_to_self(self): + response = self.client.patch( + reverse("user-detail", args=[self.admin.pk]), + {"vote_delegated_to_id": self.admin.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + admin = User.objects.get(pk=self.admin.pk) + self.assertIsNone(admin.vote_delegated_to_id) + + def test_update_vote_delegation_invalid_id(self): + response = self.client.patch( + reverse("user-detail", args=[self.admin.pk]), + {"vote_delegated_to_id": 42}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + admin = User.objects.get(pk=self.admin.pk) + self.assertIsNone(admin.vote_delegated_to_id) + + def test_update_vote_delegated_from_self(self): + response = self.client.patch( + reverse("user-detail", args=[self.admin.pk]), + {"vote_delegated_from_users_id": [self.admin.pk]}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + admin = User.objects.get(pk=self.admin.pk) + self.assertIsNone(admin.vote_delegated_to_id) + + def setup_vote_delegation(self): + """ login and setup user -> user2 delegation """ + self.user, _ = self.create_user() + self.user2, _ = self.create_user() + self.user.vote_delegated_to = self.user2 + self.user.save() + self.assertEqual(self.user.vote_delegated_to_id, self.user2.pk) + + def test_update_reset_vote_delegated_to(self): + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user.pk]), + {"vote_delegated_to_id": None}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + user = User.objects.get(pk=self.user.pk) + self.assertEqual(user.vote_delegated_to_id, None) + + def test_update_reset_vote_delegated_from(self): + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user2.pk]), + {"vote_delegated_from_users_id": None}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + user = User.objects.get(pk=self.user.pk) + self.assertEqual(user.vote_delegated_to_id, None) + + def test_update_nested_vote_delegation_1(self): + """ user -> user2 -> admin """ + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user2.pk]), + {"vote_delegated_to_id": self.admin.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + user2 = User.objects.get(pk=self.user2.pk) + self.assertIsNone(user2.vote_delegated_to_id) + + def test_update_nested_vote_delegation_2(self): + """ admin -> user -> user2 """ + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.admin.pk]), + {"vote_delegated_to_id": self.user.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + admin = User.objects.get(pk=self.admin.pk) + self.assertIsNone(admin.vote_delegated_to_id) + + def test_update_vote_delegation_autoupdate(self): + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user.pk]), + {"vote_delegated_to_id": self.admin.pk}, + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + + autoupdate = self.get_last_autoupdate(user=self.admin) + user_au = autoupdate[0].get(f"users/user:{self.user.pk}") + self.assertIsNotNone(user_au) + self.assertEqual(user_au["vote_delegated_to_id"], self.admin.pk) + user2_au = autoupdate[0].get(f"users/user:{self.user2.pk}") + self.assertIsNotNone(user2_au) + self.assertEqual(user2_au["vote_delegated_from_users_id"], []) + admin_au = autoupdate[0].get(f"users/user:{self.admin.pk}") + self.assertIsNotNone(admin_au) + self.assertEqual(admin_au["vote_delegated_from_users_id"], [self.user.pk]) + self.assertEqual(autoupdate[1], []) + + def test_update_vote_delegated_from(self): + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user2.pk]), + {"vote_delegated_from_users_id": [self.admin.pk]}, + ) + + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + admin = User.objects.get(pk=self.admin.pk) + self.assertEqual(admin.vote_delegated_to_id, self.user2.id) + user = User.objects.get(pk=self.user.pk) + self.assertIsNone(user.vote_delegated_to_id) + + def test_update_vote_delegated_from_nested_1(self): + """ admin -> user -> user2 """ + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.user.pk]), + {"vote_delegated_from_users_id": [self.admin.pk]}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + admin = User.objects.get(pk=self.admin.pk) + self.assertIsNone(admin.vote_delegated_to_id) + + def test_update_vote_delegated_from_nested_2(self): + """ user -> user2 -> admin """ + self.setup_vote_delegation() + response = self.client.patch( + reverse("user-detail", args=[self.admin.pk]), + {"vote_delegated_from_users_id": [self.user2.pk]}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + user2 = User.objects.get(pk=self.user2.pk) + self.assertIsNone(user2.vote_delegated_to_id) + class UserDelete(TestCase): """ diff --git a/server/tests/unit/motions/test_models.py b/server/tests/unit/motions/test_models.py index dc3af8163..ede566292 100644 --- a/server/tests/unit/motions/test_models.py +++ b/server/tests/unit/motions/test_models.py @@ -1,6 +1,7 @@ +from decimal import Decimal from unittest import TestCase -from openslides.motions.models import Motion, MotionChangeRecommendation +from openslides.motions.models import Motion, MotionChangeRecommendation, MotionPoll # TODO: test for MotionPoll.set_options() @@ -50,3 +51,25 @@ class MotionChangeRecommendationTest(TestCase): other_recommendations ) self.assertFalse(collides) + + +class MotionPollAnalogFieldsTest(TestCase): + def setUp(self): + self.motion = Motion( + title="test_title_OoK9IeChe2Jeib9Deeji", + text="test_text_eichui1oobiSeit9aifo", + ) + self.poll = MotionPoll( + motion=self.motion, + title="test_title_tho8PhiePh8upaex6phi", + pollmethod="YNA", + type=MotionPoll.TYPE_NAMED, + ) + + def test_not_set_vote_values(self): + with self.assertRaises(ValueError): + self.poll.votesvalid = Decimal("1") + with self.assertRaises(ValueError): + self.poll.votesinvalid = Decimal("1") + with self.assertRaises(ValueError): + self.poll.votescast = Decimal("1")