2019-10-18 14:18:49 +02:00
|
|
|
from decimal import Decimal
|
|
|
|
|
2015-09-07 17:09:29 +02:00
|
|
|
from django.contrib.auth import get_user_model
|
2015-06-14 23:26:06 +02:00
|
|
|
from django.db import transaction
|
2020-04-06 14:14:00 +02:00
|
|
|
from django.db.utils import IntegrityError
|
2012-07-06 18:00:43 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
|
2019-10-18 14:18:49 +02:00
|
|
|
from openslides.utils.auth import has_perm
|
2016-12-06 12:21:29 +01:00
|
|
|
from openslides.utils.autoupdate import inform_changed_data
|
2015-06-14 23:26:06 +02:00
|
|
|
from openslides.utils.rest_api import (
|
|
|
|
ModelViewSet,
|
|
|
|
Response,
|
|
|
|
ValidationError,
|
2015-06-16 10:37:23 +02:00
|
|
|
detail_route,
|
|
|
|
)
|
2019-10-18 14:18:49 +02:00
|
|
|
from openslides.utils.utils import is_int
|
2013-09-25 10:01:01 +02:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
from .access_permissions import AssignmentAccessPermissions
|
2019-11-12 18:30:26 +01:00
|
|
|
from .models import (
|
|
|
|
Assignment,
|
|
|
|
AssignmentOption,
|
|
|
|
AssignmentPoll,
|
|
|
|
AssignmentRelatedUser,
|
|
|
|
AssignmentVote,
|
|
|
|
)
|
2012-07-06 18:00:43 +02:00
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Viewsets for the REST API
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2015-02-12 18:48:14 +01:00
|
|
|
class AssignmentViewSet(ModelViewSet):
|
2015-01-17 14:25:05 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for assignments.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
2020-02-24 16:55:07 +01:00
|
|
|
partial_update, update, destroy, candidature_self, candidature_other and create_poll.
|
2015-01-17 14:25:05 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = AssignmentAccessPermissions()
|
2015-01-17 14:25:05 +01:00
|
|
|
queryset = Assignment.objects.all()
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-01-17 14:25:05 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-01-17 14:25:05 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
2016-09-17 22:26:23 +02:00
|
|
|
# Everybody is allowed to see the metadata.
|
|
|
|
result = True
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in (
|
|
|
|
"create",
|
|
|
|
"partial_update",
|
|
|
|
"update",
|
|
|
|
"destroy",
|
|
|
|
"sort_related_users",
|
|
|
|
):
|
|
|
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
|
|
|
self.request.user, "assignments.can_manage"
|
|
|
|
)
|
|
|
|
elif self.action == "candidature_self":
|
|
|
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
|
|
|
self.request.user, "assignments.can_nominate_self"
|
|
|
|
)
|
|
|
|
elif self.action == "candidature_other":
|
|
|
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
|
|
|
self.request.user, "assignments.can_nominate_other"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2015-01-17 14:25:05 +01:00
|
|
|
|
2019-07-10 08:31:58 +02:00
|
|
|
def perform_create(self, serializer):
|
|
|
|
serializer.save(request_user=self.request.user)
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post", "delete"])
|
2015-03-29 15:49:37 +02:00
|
|
|
def candidature_self(self, request, pk=None):
|
|
|
|
"""
|
2015-06-14 23:26:06 +02:00
|
|
|
View to nominate self as candidate (POST) or withdraw own
|
|
|
|
candidature (DELETE).
|
2015-03-29 15:49:37 +02:00
|
|
|
"""
|
|
|
|
assignment = self.get_object()
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.method == "POST":
|
2015-03-29 15:49:37 +02:00
|
|
|
message = self.nominate_self(request, assignment)
|
|
|
|
else:
|
|
|
|
# request.method == 'DELETE'
|
|
|
|
message = self.withdraw_self(request, assignment)
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2015-03-29 15:49:37 +02:00
|
|
|
|
|
|
|
def nominate_self(self, request, assignment):
|
|
|
|
if assignment.phase == assignment.PHASE_FINISHED:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-01-12 23:01:42 +01:00
|
|
|
"detail": "You can not candidate to this election because it is finished."
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
|
|
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
|
|
|
request.user, "assignments.can_manage"
|
|
|
|
):
|
2015-03-29 15:49:37 +02:00
|
|
|
# To nominate self during voting you have to be a manager.
|
|
|
|
self.permission_denied(request)
|
|
|
|
# If the request.user is already a candidate he can nominate himself nevertheless.
|
2019-10-18 14:18:49 +02:00
|
|
|
assignment.add_candidate(request.user)
|
2017-04-28 22:10:18 +02:00
|
|
|
# Send new candidate via autoupdate because users without permission
|
|
|
|
# to see users may not have it but can get it now.
|
|
|
|
inform_changed_data([request.user])
|
2019-01-12 23:01:42 +01:00
|
|
|
return "You were nominated successfully."
|
2015-03-29 15:49:37 +02:00
|
|
|
|
|
|
|
def withdraw_self(self, request, assignment):
|
2016-01-09 16:26:00 +01:00
|
|
|
# Withdraw candidature.
|
2015-03-29 15:49:37 +02:00
|
|
|
if assignment.phase == assignment.PHASE_FINISHED:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-01-12 23:01:42 +01:00
|
|
|
"detail": "You can not withdraw your candidature to this election because it is finished."
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
|
|
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
|
|
|
request.user, "assignments.can_manage"
|
|
|
|
):
|
2015-03-29 15:49:37 +02:00
|
|
|
# To withdraw self during voting you have to be a manager.
|
|
|
|
self.permission_denied(request)
|
|
|
|
if not assignment.is_candidate(request.user):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "You are not a candidate of this election."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2019-10-18 14:18:49 +02:00
|
|
|
assignment.remove_candidate(request.user)
|
2019-01-12 23:01:42 +01:00
|
|
|
return "You have withdrawn your candidature successfully."
|
2015-03-29 15:49:37 +02:00
|
|
|
|
|
|
|
def get_user_from_request_data(self, request):
|
|
|
|
"""
|
|
|
|
Helper method to get a specific user from request data (not the
|
2020-02-24 16:55:07 +01:00
|
|
|
request.user) so that the view self.candidature_other can play with it.
|
2015-03-29 15:49:37 +02:00
|
|
|
"""
|
|
|
|
if not isinstance(request.data, dict):
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "Invalid data. Expected dictionary, got {0}.",
|
|
|
|
"args": [type(request.data)],
|
|
|
|
}
|
|
|
|
)
|
2019-01-06 16:22:33 +01:00
|
|
|
user_str = request.data.get("user", "")
|
2015-03-29 15:49:37 +02:00
|
|
|
try:
|
|
|
|
user_pk = int(user_str)
|
|
|
|
except ValueError:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": 'Invalid data. Expected something like {"user": <id>}.'}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2015-03-29 15:49:37 +02:00
|
|
|
try:
|
2015-09-07 17:09:29 +02:00
|
|
|
user = get_user_model().objects.get(pk=user_pk)
|
|
|
|
except get_user_model().DoesNotExist:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-09-02 11:09:03 +02:00
|
|
|
{"detail": "Invalid data. User {0} does not exist.", "args": [user_pk]}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2015-03-29 15:49:37 +02:00
|
|
|
return user
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post", "delete"])
|
2015-03-29 15:49:37 +02:00
|
|
|
def candidature_other(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
View to nominate other users (POST) or delete their candidature
|
|
|
|
status (DELETE). The client has to send {'user': <id>}.
|
|
|
|
"""
|
|
|
|
user = self.get_user_from_request_data(request)
|
|
|
|
assignment = self.get_object()
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.method == "POST":
|
2019-09-02 11:09:03 +02:00
|
|
|
return self.nominate_other(request, user, assignment)
|
2015-03-29 15:49:37 +02:00
|
|
|
else:
|
|
|
|
# request.method == 'DELETE'
|
2019-09-02 11:09:03 +02:00
|
|
|
return self.delete_other(request, user, assignment)
|
2015-03-29 15:49:37 +02:00
|
|
|
|
|
|
|
def nominate_other(self, request, user, assignment):
|
|
|
|
if assignment.phase == assignment.PHASE_FINISHED:
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "You can not nominate someone to this election because it is finished."
|
|
|
|
}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
|
|
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
|
|
|
request.user, "assignments.can_manage"
|
|
|
|
):
|
2015-11-28 21:23:21 +01:00
|
|
|
# To nominate another user during voting you have to be a manager.
|
2015-03-29 15:49:37 +02:00
|
|
|
self.permission_denied(request)
|
2015-11-28 21:23:21 +01:00
|
|
|
if assignment.is_candidate(user):
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "User {0} is already nominated.", "args": [str(user)]}
|
|
|
|
)
|
2019-10-18 14:18:49 +02:00
|
|
|
assignment.add_candidate(user)
|
2017-04-28 22:10:18 +02:00
|
|
|
# Send new candidate via autoupdate because users without permission
|
|
|
|
# to see users may not have it but can get it now.
|
|
|
|
inform_changed_data(user)
|
2019-09-02 11:09:03 +02:00
|
|
|
return Response(
|
|
|
|
{"detail": "User {0} was nominated successfully.", "args": [str(user)]}
|
|
|
|
)
|
2015-03-29 15:49:37 +02:00
|
|
|
|
|
|
|
def delete_other(self, request, user, assignment):
|
|
|
|
# To delete candidature status you have to be a manager.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not has_perm(request.user, "assignments.can_manage"):
|
2015-03-29 15:49:37 +02:00
|
|
|
self.permission_denied(request)
|
|
|
|
if assignment.phase == assignment.PHASE_FINISHED:
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "You can not delete someone's candidature to this election because it is finished."
|
|
|
|
}
|
|
|
|
)
|
2020-02-24 16:55:07 +01:00
|
|
|
if not assignment.is_candidate(user):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-09-02 11:09:03 +02:00
|
|
|
{
|
|
|
|
"detail": "User {0} has no status in this election.",
|
|
|
|
"args": [str(user)],
|
|
|
|
}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2019-10-18 14:18:49 +02:00
|
|
|
assignment.remove_candidate(user)
|
2019-09-02 11:09:03 +02:00
|
|
|
return Response(
|
|
|
|
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
|
|
|
|
)
|
2015-03-29 15:49:37 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2016-12-06 12:21:29 +01:00
|
|
|
def sort_related_users(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to sort the assignment related users.
|
|
|
|
|
|
|
|
Expects a list of IDs of the related users (pk of AssignmentRelatedUser model).
|
|
|
|
"""
|
|
|
|
assignment = self.get_object()
|
|
|
|
|
|
|
|
# Check data
|
2019-01-06 16:22:33 +01:00
|
|
|
related_user_ids = request.data.get("related_users")
|
2016-12-06 12:21:29 +01:00
|
|
|
if not isinstance(related_user_ids, list):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "users has to be a list of IDs."})
|
2016-12-06 12:21:29 +01:00
|
|
|
|
|
|
|
# Get all related users from AssignmentRelatedUser.
|
|
|
|
related_users = {}
|
2019-01-06 16:22:33 +01:00
|
|
|
for related_user in AssignmentRelatedUser.objects.filter(
|
|
|
|
assignment__id=assignment.id
|
|
|
|
):
|
2016-12-06 12:21:29 +01:00
|
|
|
related_users[related_user.pk] = related_user
|
|
|
|
|
|
|
|
# Check all given candidates from the request
|
|
|
|
valid_related_users = []
|
|
|
|
for related_user_id in related_user_ids:
|
2019-01-06 16:22:33 +01:00
|
|
|
if (
|
|
|
|
not isinstance(related_user_id, int)
|
|
|
|
or related_users.get(related_user_id) is None
|
|
|
|
):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Invalid data."})
|
2016-12-06 12:21:29 +01:00
|
|
|
valid_related_users.append(related_users[related_user_id])
|
|
|
|
|
|
|
|
# Sort the related users
|
|
|
|
weight = 1
|
|
|
|
with transaction.atomic():
|
|
|
|
for valid_related_user in valid_related_users:
|
|
|
|
valid_related_user.weight = weight
|
|
|
|
valid_related_user.save(skip_autoupdate=True)
|
|
|
|
weight += 1
|
|
|
|
|
|
|
|
# send autoupdate
|
|
|
|
inform_changed_data(assignment)
|
|
|
|
|
|
|
|
# Initiate response.
|
2019-01-12 23:01:42 +01:00
|
|
|
return Response({"detail": "Assignment related users successfully sorted."})
|
2016-12-06 12:21:29 +01:00
|
|
|
|
2015-06-14 23:26:06 +02:00
|
|
|
|
2019-10-18 14:18:49 +02:00
|
|
|
class AssignmentPollViewSet(BasePollViewSet):
|
2015-06-14 23:26:06 +02:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for assignment polls.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: update, partial_update and destroy.
|
2015-06-14 23:26:06 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2015-06-14 23:26:06 +02:00
|
|
|
queryset = AssignmentPoll.objects.all()
|
|
|
|
|
2019-10-18 14:18:49 +02:00
|
|
|
def has_manage_permissions(self):
|
2015-06-14 23:26:06 +02:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-06-14 23:26:06 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
return has_perm(self.request.user, "assignments.can_see") and has_perm(
|
|
|
|
self.request.user, "assignments.can_manage"
|
|
|
|
)
|
2019-10-18 14:18:49 +02:00
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
|
|
|
assignment = serializer.validated_data["assignment"]
|
|
|
|
if not assignment.candidates.exists():
|
|
|
|
raise ValidationError(
|
2019-11-12 18:30:26 +01:00
|
|
|
{"detail": "Cannot create poll because there are no candidates."}
|
2019-10-18 14:18:49 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
super().perform_create(serializer)
|
2020-03-11 10:22:03 +01:00
|
|
|
poll = AssignmentPoll.objects.get(pk=serializer.data["id"])
|
|
|
|
poll.db_amount_global_abstain = Decimal(0)
|
|
|
|
poll.db_amount_global_no = Decimal(0)
|
|
|
|
poll.save()
|
2019-10-18 14:18:49 +02:00
|
|
|
|
|
|
|
def handle_analog_vote(self, data, poll, user):
|
2019-11-12 18:30:26 +01:00
|
|
|
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
|
|
|
setattr(poll, field, data[field])
|
2019-10-18 14:18:49 +02:00
|
|
|
|
|
|
|
global_no_enabled = (
|
|
|
|
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
|
|
|
)
|
2020-03-11 10:22:03 +01:00
|
|
|
if global_no_enabled:
|
|
|
|
poll.amount_global_no = data.get("amount_global_no", Decimal(0))
|
2019-10-18 14:18:49 +02:00
|
|
|
global_abstain_enabled = (
|
|
|
|
poll.global_abstain and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
|
|
|
)
|
2020-03-11 10:22:03 +01:00
|
|
|
if global_abstain_enabled:
|
|
|
|
poll.amount_global_abstain = data.get("amount_global_abstain", Decimal(0))
|
2019-10-18 14:18:49 +02:00
|
|
|
|
|
|
|
options = poll.get_options()
|
2019-11-12 18:30:26 +01:00
|
|
|
options_data = data.get("options")
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
with transaction.atomic():
|
|
|
|
for option_id, vote in options_data.items():
|
|
|
|
option = options.get(pk=int(option_id))
|
|
|
|
vote_obj, _ = AssignmentVote.objects.get_or_create(
|
|
|
|
option=option, value="Y"
|
|
|
|
)
|
|
|
|
vote_obj.weight = vote["Y"]
|
|
|
|
vote_obj.save()
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
if poll.pollmethod in (
|
|
|
|
AssignmentPoll.POLLMETHOD_YN,
|
|
|
|
AssignmentPoll.POLLMETHOD_YNA,
|
|
|
|
):
|
|
|
|
vote_obj, _ = AssignmentVote.objects.get_or_create(
|
|
|
|
option=option, value="N"
|
|
|
|
)
|
|
|
|
vote_obj.weight = vote["N"]
|
|
|
|
vote_obj.save()
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA:
|
|
|
|
vote_obj, _ = AssignmentVote.objects.get_or_create(
|
|
|
|
option=option, value="A"
|
|
|
|
)
|
|
|
|
vote_obj.weight = vote["A"]
|
|
|
|
vote_obj.save()
|
2020-03-11 10:22:03 +01:00
|
|
|
inform_changed_data(option)
|
2019-11-12 18:30:26 +01:00
|
|
|
|
|
|
|
poll.save()
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2020-02-13 18:24:51 +01:00
|
|
|
def validate_vote_data(self, data, poll, user):
|
2019-11-12 18:30:26 +01:00
|
|
|
"""
|
|
|
|
Request data:
|
|
|
|
analog:
|
|
|
|
{
|
|
|
|
"options": {<option_id>: {"Y": <amount>, ["N": <amount>], ["A": <amount>] }},
|
|
|
|
["votesvalid": <amount>], ["votesinvalid": <amount>], ["votescast": <amount>],
|
2020-03-11 10:22:03 +01:00
|
|
|
["amount_global_no": <amount>], ["amount_global_abstain": <amount>]
|
2019-11-12 18:30:26 +01:00
|
|
|
}
|
|
|
|
All amounts are decimals as strings
|
|
|
|
required fields per pollmethod:
|
|
|
|
- votes: Y
|
|
|
|
- YN: YN
|
|
|
|
- YNA: YNA
|
|
|
|
named|pseudoanonymous:
|
|
|
|
votes:
|
|
|
|
{<option_id>: <amount>} | 'N' | 'A'
|
|
|
|
- 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
|
|
|
|
- amounts must be integer numbers >= 0.
|
|
|
|
- ids should be integers of valid option ids for this poll
|
|
|
|
- amounts must be 0 or 1, if poll.allow_multiple_votes_per_candidate is False
|
2020-03-11 10:22:03 +01:00
|
|
|
- The sum of all amounts must be grater than 0 and <= poll.votes_amount
|
2019-11-12 18:30:26 +01:00
|
|
|
|
|
|
|
YN/YNA:
|
|
|
|
{<option_id>: 'Y' | 'N' [|'A']}
|
|
|
|
- 'A' is only allowed in YNA pollmethod
|
|
|
|
"""
|
|
|
|
if poll.type == AssignmentPoll.TYPE_ANALOG:
|
2019-10-18 14:18:49 +02:00
|
|
|
if not isinstance(data, dict):
|
2019-11-12 18:30:26 +01:00
|
|
|
raise ValidationError({"detail": "Data must be a dict"})
|
|
|
|
|
|
|
|
options_data = data.get("options")
|
|
|
|
if not isinstance(options_data, dict):
|
|
|
|
raise ValidationError({"detail": "You must provide options"})
|
|
|
|
|
|
|
|
for key, value in options_data.items():
|
|
|
|
if not is_int(key):
|
2019-10-18 14:18:49 +02:00
|
|
|
raise ValidationError({"detail": "Keys must be int"})
|
2019-11-12 18:30:26 +01:00
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise ValidationError({"detail": "A dict per option is required"})
|
|
|
|
value["Y"] = self.parse_vote_value(value, "Y")
|
|
|
|
if poll.pollmethod in (
|
|
|
|
AssignmentPoll.POLLMETHOD_YN,
|
|
|
|
AssignmentPoll.POLLMETHOD_YNA,
|
2019-10-18 14:18:49 +02:00
|
|
|
):
|
2019-11-12 18:30:26 +01:00
|
|
|
value["N"] = self.parse_vote_value(value, "N")
|
|
|
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA:
|
|
|
|
value["A"] = self.parse_vote_value(value, "A")
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
|
|
|
data[field] = self.parse_vote_value(data, field)
|
|
|
|
|
|
|
|
global_no_enabled = (
|
|
|
|
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
|
|
|
)
|
|
|
|
global_abstain_enabled = (
|
|
|
|
poll.global_abstain
|
|
|
|
and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
|
|
|
)
|
2020-03-11 10:22:03 +01:00
|
|
|
if "amount_global_abstain" in data and global_abstain_enabled:
|
|
|
|
data["amount_global_abstain"] = self.parse_vote_value(
|
|
|
|
data, "amount_global_abstain"
|
|
|
|
)
|
|
|
|
if "amount_global_no" in data and global_no_enabled:
|
|
|
|
data["amount_global_no"] = self.parse_vote_value(
|
|
|
|
data, "amount_global_no"
|
|
|
|
)
|
2019-11-12 18:30:26 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
|
|
|
if isinstance(data, dict):
|
|
|
|
amount_sum = 0
|
|
|
|
for option_id, amount in data.items():
|
|
|
|
if not is_int(option_id):
|
|
|
|
raise ValidationError({"detail": "Each id must be an int."})
|
2020-03-11 10:22:03 +01:00
|
|
|
if not AssignmentOption.objects.filter(id=option_id).exists():
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": f"Option {option_id} does not exist."}
|
|
|
|
)
|
2019-11-12 18:30:26 +01:00
|
|
|
if not is_int(amount):
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Each amounts must be int"}
|
|
|
|
)
|
|
|
|
amount = int(amount)
|
|
|
|
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"}
|
|
|
|
)
|
|
|
|
amount_sum += amount
|
|
|
|
|
2020-01-22 17:31:10 +01:00
|
|
|
if amount_sum > poll.votes_amount:
|
2019-11-12 18:30:26 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
2020-01-22 17:31:10 +01:00
|
|
|
"detail": "You can give a maximum of {0} votes",
|
2019-11-12 18:30:26 +01:00
|
|
|
"args": [poll.votes_amount],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
elif data == "N" and poll.global_no:
|
|
|
|
return # return because we dont have to check option presence
|
|
|
|
elif data == "A" and poll.global_abstain:
|
|
|
|
return # return because we dont have to check option presence
|
|
|
|
else:
|
|
|
|
raise ValidationError({"detail": "invalid data."})
|
|
|
|
|
|
|
|
elif poll.pollmethod in (
|
|
|
|
AssignmentPoll.POLLMETHOD_YN,
|
|
|
|
AssignmentPoll.POLLMETHOD_YNA,
|
|
|
|
):
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
raise ValidationError({"detail": "Data must be a dict."})
|
|
|
|
for option_id, value in data.items():
|
|
|
|
if not is_int(option_id):
|
|
|
|
raise ValidationError({"detail": "Keys must be int"})
|
2020-03-11 10:22:03 +01:00
|
|
|
if not AssignmentOption.objects.filter(id=option_id).exists():
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": f"Option {option_id} does not exist."}
|
|
|
|
)
|
2019-11-12 18:30:26 +01:00
|
|
|
if (
|
|
|
|
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
|
|
|
and value not in ("Y", "N", "A",)
|
|
|
|
):
|
2020-02-12 17:18:01 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Every value must be Y, N or A"}
|
|
|
|
)
|
2019-11-12 18:30:26 +01:00
|
|
|
elif (
|
|
|
|
poll.pollmethod == AssignmentPoll.POLLMETHOD_YN
|
|
|
|
and value not in ("Y", "N",)
|
|
|
|
):
|
2020-02-12 17:18:01 +01:00
|
|
|
raise ValidationError({"detail": "Every value must be Y or N"})
|
2019-11-12 18:30:26 +01:00
|
|
|
|
|
|
|
options_data = data
|
|
|
|
|
2020-02-12 17:18:01 +01:00
|
|
|
def create_votes_type_votes(self, data, poll, user):
|
2019-10-29 12:58:37 +01:00
|
|
|
"""
|
|
|
|
Helper function for handle_(named|pseudoanonymous)_vote
|
|
|
|
Assumes data is already validated
|
|
|
|
"""
|
2019-10-18 14:18:49 +02:00
|
|
|
options = poll.get_options()
|
2020-02-12 17:18:01 +01:00
|
|
|
if isinstance(data, dict):
|
|
|
|
for option_id, amount in data.items():
|
|
|
|
# Add user to the option's voted array
|
|
|
|
option = options.get(pk=option_id)
|
|
|
|
# skip creating votes with empty weights
|
|
|
|
if amount == 0:
|
|
|
|
continue
|
2019-10-18 14:18:49 +02:00
|
|
|
vote = AssignmentVote.objects.create(
|
2020-02-12 17:18:01 +01:00
|
|
|
option=option, user=user, weight=Decimal(amount), value="Y"
|
2019-10-18 14:18:49 +02:00
|
|
|
)
|
|
|
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
2020-02-12 17:18:01 +01:00
|
|
|
else: # global_no or global_abstain
|
2020-03-12 15:42:41 +01:00
|
|
|
option = options[0]
|
|
|
|
vote = AssignmentVote.objects.create(
|
|
|
|
option=option, user=user, weight=Decimal(1), value=data
|
|
|
|
)
|
|
|
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
|
|
|
inform_changed_data(option)
|
|
|
|
inform_changed_data(poll)
|
2020-03-11 10:22:03 +01:00
|
|
|
|
|
|
|
poll.voted.add(user)
|
2020-02-12 17:18:01 +01:00
|
|
|
|
|
|
|
def create_votes_type_named_pseudoanonymous(
|
|
|
|
self, data, poll, check_user, vote_user
|
|
|
|
):
|
2020-04-06 14:14:00 +02:00
|
|
|
"""
|
|
|
|
check_user is used for the voted-array and weight of the vote,
|
|
|
|
vote_user is the one put into the vote
|
|
|
|
"""
|
2020-02-12 17:18:01 +01:00
|
|
|
options = poll.get_options()
|
|
|
|
for option_id, result in data.items():
|
|
|
|
option = options.get(pk=option_id)
|
|
|
|
vote = AssignmentVote.objects.create(
|
2020-04-06 14:14:00 +02:00
|
|
|
option=option,
|
|
|
|
user=vote_user,
|
|
|
|
value=result,
|
|
|
|
weight=check_user.vote_weight,
|
2020-02-12 17:18:01 +01:00
|
|
|
)
|
|
|
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
2020-03-11 10:22:03 +01:00
|
|
|
inform_changed_data(option, no_delete_on_restriction=True)
|
|
|
|
|
|
|
|
poll.voted.add(check_user)
|
2020-02-12 17:18:01 +01:00
|
|
|
|
|
|
|
def handle_named_vote(self, data, poll, user):
|
2020-04-06 14:14:00 +02:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
|
|
|
return self.try_handle_named_vote(data, poll, user)
|
|
|
|
except IntegrityError:
|
|
|
|
raise ValidationError({"detail": "You have already voted"})
|
|
|
|
|
|
|
|
def try_handle_named_vote(self, data, poll, user):
|
2020-03-11 10:22:03 +01:00
|
|
|
if user in poll.voted.all():
|
|
|
|
raise ValidationError({"detail": "You have already voted"})
|
|
|
|
|
2020-02-12 17:18:01 +01:00
|
|
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
|
|
|
self.create_votes_type_votes(data, poll, user)
|
2019-10-18 14:18:49 +02:00
|
|
|
elif poll.pollmethod in (
|
|
|
|
AssignmentPoll.POLLMETHOD_YN,
|
|
|
|
AssignmentPoll.POLLMETHOD_YNA,
|
|
|
|
):
|
2020-02-12 17:18:01 +01:00
|
|
|
self.create_votes_type_named_pseudoanonymous(data, poll, user, user)
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2020-02-12 17:18:01 +01:00
|
|
|
def handle_pseudoanonymous_vote(self, data, poll, user):
|
2020-04-06 14:14:00 +02:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
|
|
|
return self.try_handle_pseudoanonymous_vote(data, poll, user)
|
|
|
|
except IntegrityError:
|
|
|
|
raise ValidationError({"detail": "You have already voted"})
|
|
|
|
|
|
|
|
def try_handle_pseudoanonymous_vote(self, data, poll, user):
|
2020-03-11 10:22:03 +01:00
|
|
|
if user in poll.voted.all():
|
|
|
|
raise ValidationError({"detail": "You have already voted"})
|
|
|
|
|
2020-02-12 17:18:01 +01:00
|
|
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
|
|
|
self.create_votes_type_votes(data, poll, user)
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2020-02-12 17:18:01 +01:00
|
|
|
elif poll.pollmethod in (
|
|
|
|
AssignmentPoll.POLLMETHOD_YN,
|
|
|
|
AssignmentPoll.POLLMETHOD_YNA,
|
|
|
|
):
|
|
|
|
self.create_votes_type_named_pseudoanonymous(data, poll, user, None)
|
2019-10-18 14:18:49 +02:00
|
|
|
|
2019-11-12 18:30:26 +01:00
|
|
|
def convert_option_data(self, poll, data):
|
|
|
|
poll_options = poll.get_options()
|
|
|
|
new_option_data = {}
|
|
|
|
option_data = data.get("options")
|
|
|
|
if option_data is None:
|
|
|
|
raise ValidationError({"detail": "You must provide options"})
|
|
|
|
for id, val in option_data.items():
|
|
|
|
option = poll_options.filter(user_id=id).first()
|
|
|
|
if option is None:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": f"Assignment related user with id {id} not found"}
|
|
|
|
)
|
|
|
|
new_option_data[option.id] = val
|
|
|
|
data["options"] = new_option_data
|
|
|
|
|
|
|
|
|
|
|
|
class AssignmentOptionViewSet(BaseOptionViewSet):
|
|
|
|
queryset = AssignmentOption.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
return has_perm(self.request.user, "assignments.can_see")
|
|
|
|
|
2019-10-18 14:18:49 +02:00
|
|
|
|
|
|
|
class AssignmentVoteViewSet(BaseVoteViewSet):
|
|
|
|
queryset = AssignmentVote.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
return has_perm(self.request.user, "assignments.can_see")
|