From ccaa1eaf27ed98587bd9ee3dac381b30a74212a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sun, 14 Jun 2015 23:26:06 +0200 Subject: [PATCH] Added create, updated and destroy view for assignment polls. --- openslides/assignments/apps.py | 3 +- openslides/assignments/models.py | 2 +- openslides/assignments/serializers.py | 56 ++++++++++++++++++++- openslides/assignments/views.py | 70 ++++++++++++++++++++++----- openslides/utils/rest_api.py | 5 +- 5 files changed, 118 insertions(+), 18 deletions(-) diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index fb0d11ea3..d144d057f 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -17,7 +17,7 @@ class AssignmentAppConfig(AppConfig): from openslides.utils.signals import template_manipulation from .signals import setup_assignment_config from .template import add_assignment_stylesheets - from .views import AssignmentViewSet + from .views import AssignmentViewSet, AssignmentPollViewSet # Connect signals. config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config') @@ -33,3 +33,4 @@ class AssignmentAppConfig(AppConfig): # Register viewsets. router.register('assignments/assignment', AssignmentViewSet) + router.register('assignments/assignmentpoll', AssignmentPollViewSet) diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index 95435f593..b26f8e420 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -247,7 +247,7 @@ class Assignment(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): def create_poll(self): """ - Creates an new poll for the assignment and adds all candidates to all + Creates a new poll for the assignment and adds all candidates to all lists of speakers of related agenda items. """ candidates = self.candidates.all() diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index dcb5ce7fd..2ce6204e4 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -1,4 +1,13 @@ -from openslides.utils.rest_api import ListSerializer, ModelSerializer +from django.db import transaction +from django.utils.translation import ugettext as _ + +from openslides.utils.rest_api import ( + DictField, + IntegerField, + ListField, + ListSerializer, + ModelSerializer, + ValidationError) from .models import ( models, @@ -64,6 +73,10 @@ class AssignmentAllPollSerializer(ModelSerializer): Serializes all polls. """ assignmentoption_set = AssignmentOptionSerializer(many=True, read_only=True) + votes = ListField( + child=DictField( + child=IntegerField(min_value=-2)), + write_only=True) class Meta: model = AssignmentPoll @@ -75,7 +88,46 @@ class AssignmentAllPollSerializer(ModelSerializer): 'assignmentoption_set', 'votesvalid', 'votesinvalid', - 'votescast',) + 'votescast', + 'votes',) + read_only_fields = ('yesnoabstain',) + + @transaction.atomic + def update(self, instance, validated_data): + """ + Customized update method for polls. To update votes use the write + only field 'votes'. + + Example data for a 'yesnoabstain' poll with two candidates: + + "votes": [{"Yes": 10, "No": 4, "Abstain": -2}, + {"Yes": -1, "No": 0, "Abstain": -2}] + """ + # Update votes. + votes = validated_data.get('votes') + if votes: + options = list(instance.get_options()) + if len(votes) != len(options): + raise ValidationError({ + 'detail': _('You have to submit data for %d candidates.') % len(options)}) + for index, option in enumerate(options): + if len(votes[index]) != len(instance.get_vote_values()): + raise ValidationError({ + 'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())}) + for vote_value, vote_weight in votes[index].items(): + if vote_value not in instance.get_vote_values(): + raise ValidationError({ + 'detail': _('Vote value %s is invalid.') % vote_value}) + instance.set_vote_objects_with_values(option, votes[index]) + + # Update remaining writeable fields. + instance.description = validated_data.get('description', instance.description) + instance.published = validated_data.get('published', instance.published) + instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) + instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) + instance.votescast = validated_data.get('votescast', instance.votescast) + instance.save() + return instance class AssignmentShortPollSerializer(AssignmentAllPollSerializer): diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index bb6a1d173..a430a9e2f 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -1,5 +1,6 @@ from cgi import escape +from django.db import transaction from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from reportlab.lib import colors @@ -10,34 +11,46 @@ from reportlab.platypus import (PageBreak, Paragraph, SimpleDocTemplate, Spacer, from openslides.config.api import config from openslides.users.models import Group, User # TODO: remove this from openslides.utils.pdf import stylesheet -from openslides.utils.rest_api import ModelViewSet, Response, ValidationError, detail_route +from openslides.utils.rest_api import ( + DestroyModelMixin, + GenericViewSet, + ModelViewSet, + Response, + UpdateModelMixin, + ValidationError, + detail_route) from openslides.utils.views import PDFView from .models import Assignment, AssignmentPoll -from .serializers import AssignmentFullSerializer, AssignmentShortSerializer +from .serializers import ( + AssignmentAllPollSerializer, + AssignmentFullSerializer, + AssignmentShortSerializer +) class AssignmentViewSet(ModelViewSet): """ - API endpoint to list, retrieve, create, update and destroy assignments and - to manage candidatures. + API endpoint to list, retrieve, create, update and destroy assignments + and to manage candidatures. """ queryset = Assignment.objects.all() def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the - permission to see assignments and in case of create, update or destroy - requests the permission to manage assignments. + permission to see assignments and in case of create, update, + partial_update or destroy requests the permission to manage + assignments. """ if (not request.user.has_perm('assignments.can_see') or - (self.action in ('create', 'update', 'destroy') and not - request.user.has_perm('assignments.can_manage'))): + (self.action in ('create', 'update', 'partial_update', 'destroy') and + not request.user.has_perm('assignments.can_manage'))): self.permission_denied(request) def get_serializer_class(self): """ - Returns different serializer classes with respect to users permissions. + Returns different serializer classes according to users permissions. """ if self.request.user.has_perm('assignments.can_manage'): serializer_class = AssignmentFullSerializer @@ -48,8 +61,8 @@ class AssignmentViewSet(ModelViewSet): @detail_route(methods=['post', 'delete']) def candidature_self(self, request, pk=None): """ - View to nominate self as candidate (POST) or withdraw own candidature - (DELETE). + View to nominate self as candidate (POST) or withdraw own + candidature (DELETE). """ if not request.user.has_perm('assignments.can_nominate_self'): self.permission_denied(request) @@ -157,8 +170,8 @@ class AssignmentViewSet(ModelViewSet): @detail_route(methods=['post', 'delete']) def mark_elected(self, request, pk=None): """ - View to mark other users as elected (POST) undo this (DELETE). The - client has to send {'user': }. + View to mark other users as elected (POST) or undo this (DELETE). + The client has to send {'user': }. """ if not request.user.has_perm('assignments.can_manage'): self.permission_denied(request) @@ -178,6 +191,37 @@ class AssignmentViewSet(ModelViewSet): message = _('User %s was successfully unelected.') % user return Response({'detail': message}) + @detail_route(methods=['post']) + def create_poll(self, request, pk=None): + """ + View to create a poll. It is a POST request without any data. + """ + if not request.user.has_perm('assignments.can_manage'): + self.permission_denied(request) + assignment = self.get_object() + if not assignment.candidates.exists(): + raise ValidationError({'detail': _('Can not create poll because there are no candidates.')}) + with transaction.atomic(): + assignment.create_poll() + return Response({'detail': _(' Poll created successfully.')}) + + +class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): + """ + API endpoint to update and destroy assignment polls. + """ + queryset = AssignmentPoll.objects.all() + serializer_class = AssignmentAllPollSerializer + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see assignments and to manage assignments. + """ + if (not request.user.has_perm('assignments.can_see') or + not request.user.has_perm('assignments.can_manage')): + self.permission_denied(request) + class AssignmentPDF(PDFView): required_permission = 'assignments.can_see' diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 19783bd50..9f9b09ac0 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -6,17 +6,20 @@ from django.core.urlresolvers import reverse from rest_framework.decorators import detail_route # noqa from rest_framework.serializers import ( # noqa CharField, + DictField, Field, IntegerField, + ListField, ListSerializer, ModelSerializer, PrimaryKeyRelatedField, RelatedField, SerializerMethodField, ValidationError) +from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa from rest_framework.response import Response # noqa from rest_framework.routers import DefaultRouter -from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet # noqa +from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet, ViewSet # noqa from rest_framework.decorators import list_route # noqa from .exceptions import OpenSlidesError