From 4821a3b59b1bbc0eca2fbd7003f095616d56a7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 17 Jan 2015 14:25:05 +0100 Subject: [PATCH] Added api for assignments. Also small changes in agenda REST api. --- openslides/agenda/apps.py | 2 +- openslides/agenda/models.py | 2 +- openslides/agenda/serializers.py | 22 ++++- openslides/agenda/views.py | 2 +- openslides/assignment/apps.py | 5 + openslides/assignment/models.py | 35 ++++++- openslides/assignment/serializers.py | 143 +++++++++++++++++++++++++++ openslides/assignment/views.py | 31 ++++++ openslides/core/apps.py | 2 +- openslides/core/views.py | 4 +- openslides/users/apps.py | 2 +- openslides/users/serializers.py | 8 +- 12 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 openslides/assignment/serializers.py diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py index 3a9b98c26..e7a02aefc 100644 --- a/openslides/agenda/apps.py +++ b/openslides/agenda/apps.py @@ -29,5 +29,5 @@ class AgendaAppConfig(AppConfig): Item = self.get_model('Item') register_slide('agenda', agenda_slide, Item) - # Register viewset. + # Register viewsets. router.register('agenda/item', ItemViewSet) diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 1ee63e997..9641fa314 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -437,6 +437,6 @@ class Speaker(RESTModelMixin, AbsoluteUrlMixin, models.Model): def get_root_rest_element(self): """ - Returns the item to this instance, which is the root rest element. + Returns the item to this instance which is the root rest element. """ return self.item diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index d4c7146af..f12809c7d 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -1,4 +1,5 @@ from openslides.utils import rest_api +from rest_framework.reverse import reverse from .models import Item, Speaker @@ -17,16 +18,33 @@ class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer): 'weight') +class RelatedItemRelatedField(rest_api.serializers.RelatedField): + """ + A custom field to use for the `content_object` generic relationship. + """ + def to_representation(self, value): + """ + Returns the url to the related object. + """ + request = self.context.get('request', None) + assert request is not None, ( + "`%s` requires the request in the serializer" + " context. Add `context={'request': request}` when instantiating " + "the serializer." % self.__class__.__name__) + view_name = '%s-detail' % type(value)._meta.object_name.lower() + return reverse(view_name, kwargs={'pk': value.pk}, request=request) + + class ItemSerializer(rest_api.serializers.HyperlinkedModelSerializer): """ - Serializer for a agenda.models.Item objects. + Serializer for agenda.models.Item objects. """ get_title = rest_api.serializers.CharField(read_only=True) get_title_supplement = rest_api.serializers.CharField(read_only=True) item_no = rest_api.serializers.CharField(read_only=True) speaker_set = SpeakerSerializer(many=True, read_only=True) tags = rest_api.serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='tag-detail') - # content_object = serializers.PrimaryKeyRelatedField(read_only=True) + content_object = RelatedItemRelatedField(read_only=True) class Meta: model = Item diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index d2eac3696..552d7aa48 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -774,7 +774,7 @@ class ItemCSVImportView(CSVImportView): class ItemViewSet(rest_api.viewsets.ModelViewSet): """ - API endpoint to view, edit and delete agenda items. + API endpoint to retrieve, create, edit and delete agenda items. """ model = Item serializer_class = ItemSerializer diff --git a/openslides/assignment/apps.py b/openslides/assignment/apps.py index 1331ed1e8..c623017ed 100644 --- a/openslides/assignment/apps.py +++ b/openslides/assignment/apps.py @@ -13,9 +13,11 @@ class AssignmentAppConfig(AppConfig): # Import all required stuff. from openslides.config.signals import config_signal from openslides.projector.api import register_slide_model + from openslides.utils.rest_api import router from openslides.utils.signals import template_manipulation from .signals import setup_assignment_config from .template import add_assignment_stylesheets + from .views import AssignmentViewSet # Connect signals. config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config') @@ -28,3 +30,6 @@ class AssignmentAppConfig(AppConfig): AssignmentPoll = self.get_model('AssignmentPoll') register_slide_model(Assignment, 'assignment/slide.html') register_slide_model(AssignmentPoll, 'assignment/assignmentpoll_slide.html') + + # Register viewsets. + router.register('assignment/assignment', AssignmentViewSet) diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 88002d4ee..14f84024f 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -14,11 +14,12 @@ from openslides.poll.models import (BaseOption, BasePoll, BaseVote, from openslides.projector.models import SlideMixin from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import AbsoluteUrlMixin +from openslides.utils.rest_api import RESTModelMixin from openslides.utils.utils import html_strong from openslides.users.models import User -class AssignmentCandidate(models.Model): +class AssignmentCandidate(RESTModelMixin, models.Model): """ Many2Many table between an assignment and the candidates. """ @@ -33,8 +34,14 @@ class AssignmentCandidate(models.Model): def __str__(self): return str(self.person) + def get_root_rest_element(self): + """ + Returns the assignment to this instance which is the root rest element. + """ + return self.assignment -class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model): + +class Assignment(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): slide_callback_name = 'assignment' STATUS = ( @@ -262,11 +269,17 @@ class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model): return '(%s)' % _('Assignment') -class AssignmentVote(BaseVote): +class AssignmentVote(RESTModelMixin, BaseVote): option = models.ForeignKey('AssignmentOption') + def get_root_rest_element(self): + """ + Returns the assignment to this instance which is the root rest element. + """ + return self.option.poll.assignment -class AssignmentOption(BaseOption): + +class AssignmentOption(RESTModelMixin, BaseOption): poll = models.ForeignKey('AssignmentPoll') candidate = models.ForeignKey(User) vote_class = AssignmentVote @@ -274,8 +287,14 @@ class AssignmentOption(BaseOption): def __str__(self): return str(self.candidate) + def get_root_rest_element(self): + """ + Returns the assignment to this instance which is the root rest element. + """ + return self.poll.assignment -class AssignmentPoll(SlideMixin, CollectDefaultVotesMixin, + +class AssignmentPoll(RESTModelMixin, SlideMixin, CollectDefaultVotesMixin, PublishPollMixin, AbsoluteUrlMixin, BasePoll): slide_callback_name = 'assignmentpoll' @@ -326,3 +345,9 @@ class AssignmentPoll(SlideMixin, CollectDefaultVotesMixin, def get_slide_context(self, **context): return super(AssignmentPoll, self).get_slide_context(poll=self) + + def get_root_rest_element(self): + """ + Returns the assignment to this instance which is the root rest element. + """ + return self.assignment diff --git a/openslides/assignment/serializers.py b/openslides/assignment/serializers.py new file mode 100644 index 000000000..fb688b849 --- /dev/null +++ b/openslides/assignment/serializers.py @@ -0,0 +1,143 @@ +from openslides.utils import rest_api + +from .models import ( + models, + Assignment, + AssignmentCandidate, + AssignmentOption, + AssignmentPoll, + AssignmentVote) + + +class AssignmentCandidateSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for assignment.models.AssignmentCandidate objects. + """ + class Meta: + model = AssignmentCandidate + fields = ( + 'id', + 'person', + 'elected', + 'blocked') + + +class AssignmentVoteSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for assignment.models.AssignmentVote objects. + """ + class Meta: + model = AssignmentVote + fields = ( + 'weight', + 'value') + + +class AssignmentOptionSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for assignment.models.AssignmentOption objects. + """ + assignmentvote_set = AssignmentVoteSerializer(many=True, read_only=True) + + class Meta: + model = AssignmentOption + fields = ( + 'candidate', + 'assignmentvote_set') + + +class FilterPollListSerializer(rest_api.serializers.ListSerializer): + """ + Customized serilizer to filter polls and exclude unpublished ones. + """ + def to_representation(self, data): + """ + List of object instances -> List of dicts of primitive datatypes. + + This method is adapted to filter the data and exclude unpublished polls. + """ + # Dealing with nested relationships, data can be a Manager, + # so, first get a queryset from the Manager if needed + iterable = data.filter(published=True) if isinstance(data, models.Manager) else data + return [self.child.to_representation(item) for item in iterable] + + +class AssignmentAllPollSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for assignment.models.AssignmentPoll objects. + + Serializes all polls. + """ + assignmentoption_set = AssignmentOptionSerializer(many=True, read_only=True) + + class Meta: + model = AssignmentPoll + fields = ( + 'id', + 'yesnoabstain', + 'description', + 'published', + 'assignmentoption_set', + 'votesvalid', + 'votesinvalid', + 'votescast') + + +class AssignmentShortPollSerializer(AssignmentAllPollSerializer): + """ + Serializer for assignment.models.AssignmentPoll objects. + + Serializes only short polls. + """ + class Meta: + list_serializer_class = FilterPollListSerializer + model = AssignmentPoll + fields = ( + 'id', + 'yesnoabstain', + 'description', + 'published', + 'assignmentoption_set', + 'votesvalid', + 'votesinvalid', + 'votescast') + + +class AssignmentFullSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for assignment.models.Assignment objects. With all polls. + """ + assignmentcandidate_set = AssignmentCandidateSerializer(many=True, read_only=True) + poll_set = AssignmentAllPollSerializer(many=True, read_only=True) + tags = rest_api.serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='tag-detail') + + class Meta: + model = Assignment + fields = ( + 'name', + 'description', + 'posts', + 'poll_description_default', + 'status', + 'assignmentcandidate_set', + 'poll_set', + 'tags') + + +class AssignmentShortSerializer(AssignmentFullSerializer): + """ + Serializer for assignment.models.Assignment objects. Without unpublished poll. + """ + poll_set = AssignmentShortPollSerializer(many=True, read_only=True) + + class Meta: + model = Assignment + fields = ( + 'name', + 'description', + 'posts', + 'poll_description_default', + 'status', + 'assignmentcandidate_set', + 'poll_set', + 'tags') diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index 3a7f786fe..69f063687 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -12,6 +12,7 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate from openslides.config.api import config from openslides.users.models import Group, User # TODO: remove this from openslides.poll.views import PollFormView +from openslides.utils import rest_api from openslides.utils.pdf import stylesheet from openslides.utils.utils import html_strong from openslides.utils.views import (CreateView, DeleteView, DetailView, @@ -21,6 +22,7 @@ from openslides.utils.views import (CreateView, DeleteView, DetailView, from .forms import AssignmentForm, AssignmentRunForm from .models import Assignment, AssignmentPoll +from .serializers import AssignmentFullSerializer, AssignmentShortSerializer class AssignmentListView(ListView): @@ -186,6 +188,35 @@ class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView): self.is_blocked = self.get_object().is_blocked(self.person) +class AssignmentViewSet(rest_api.viewsets.ModelViewSet): + """ + API endpoint to retrieve, create, edit and delete assignments. + """ + model = Assignment + queryset = Assignment.objects.all() + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see and in case of create, update or destroy requests + the permission to manage and to see organizational items. + """ + if (not request.user.has_perm('assignment.can_see_assignment') or + (self.action in ('create', 'update', 'destroy') and not + request.user.has_perm('assignment.can_manage_assignment'))): + self.permission_denied(request) + + def get_serializer_class(self): + """ + Returns different serializer classes with respect to users permissions. + """ + if self.request.user.has_perm('assignment.can_manage_assignment'): + serializer_class = AssignmentFullSerializer + else: + serializer_class = AssignmentShortSerializer + return serializer_class + + class PollCreateView(SingleObjectMixin, RedirectView): model = Assignment required_permission = 'assignment.can_manage_assignment' diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 65249733c..a332e77cf 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -26,7 +26,7 @@ class CoreAppConfig(AppConfig): CustomSlide = self.get_model('CustomSlide') register_slide_model(CustomSlide, 'core/customslide_slide.html') - # Register viewset. + # Register viewsets. router.register('core/customslide', CustomSlideViewSet) router.register('core/tag', TagViewSet) diff --git a/openslides/core/views.py b/openslides/core/views.py index f0d9cf934..f86be03a7 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -219,7 +219,7 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView): class CustomSlideViewSet(rest_api.viewsets.ModelViewSet): """ - API endpoint to view, edit and delete custom slides. + API endpoint to retrieve, create, update and delete custom slides. """ model = CustomSlide queryset = CustomSlide.objects.all() @@ -314,7 +314,7 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView): class TagViewSet(rest_api.viewsets.ModelViewSet): """ - API endpoint to view, edit and delete tags. + API endpoint to retrieve, create, edit and delete tags. """ model = Tag queryset = Tag.objects.all() diff --git a/openslides/users/apps.py b/openslides/users/apps.py index a58dfec76..4c4b65539 100644 --- a/openslides/users/apps.py +++ b/openslides/users/apps.py @@ -30,5 +30,5 @@ class UsersAppConfig(AppConfig): # Register slides. register_slide_model(User, 'participant/user_slide.html') - # Register viewset. + # Register viewsets. router.register('users/user', UserViewSet) diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index c5e61e286..a0e481ec8 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -5,7 +5,9 @@ from .models import User class UserShortSerializer(rest_api.serializers.ModelSerializer): """ - Serializer for a users.models.User objects. + Serializer for users.models.User objects. + + Serializes only name fields. """ class Meta: model = User @@ -19,7 +21,9 @@ class UserShortSerializer(rest_api.serializers.ModelSerializer): class UserFullSerializer(rest_api.serializers.ModelSerializer): """ - Serializer for a users.models.User objects. + Serializer for users.models.User objects. + + Serializes all relevant fields. """ class Meta: model = User