From 7238b8159ab9182433d7442339f9fe6ad14f82e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 24 Jan 2015 16:35:50 +0100 Subject: [PATCH] Added REST api for motion, mediafile and config app. Refactor REST api in other apps. --- openslides/agenda/models.py | 2 +- openslides/agenda/serializers.py | 41 ++++-- openslides/agenda/views.py | 13 +- openslides/assignment/models.py | 8 +- openslides/assignment/serializers.py | 27 ++-- openslides/assignment/views.py | 10 +- openslides/config/api.py | 8 ++ openslides/config/apps.py | 7 ++ openslides/config/views.py | 20 +++ openslides/core/apps.py | 12 +- openslides/core/models.py | 2 +- openslides/core/serializers.py | 8 +- openslides/core/views.py | 14 +-- openslides/mediafile/apps.py | 5 + openslides/mediafile/models.py | 3 +- openslides/mediafile/serializers.py | 16 +++ openslides/mediafile/views.py | 25 ++++ openslides/motion/apps.py | 7 ++ openslides/motion/models.py | 75 +++++++++-- openslides/motion/serializers.py | 180 +++++++++++++++++++++++++++ openslides/motion/views.py | 67 +++++++++- openslides/users/serializers.py | 8 +- openslides/users/views.py | 11 +- openslides/utils/rest_api.py | 2 +- 24 files changed, 493 insertions(+), 78 deletions(-) create mode 100644 openslides/mediafile/serializers.py create mode 100644 openslides/motion/serializers.py diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 9641fa314..2984ad05e 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 f12809c7d..d27a7bd83 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -1,10 +1,11 @@ -from openslides.utils import rest_api from rest_framework.reverse import reverse +from openslides.utils.rest_api import serializers + from .models import Item, Speaker -class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class SpeakerSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for agenda.models.Speaker objects. """ @@ -18,7 +19,7 @@ class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer): 'weight') -class RelatedItemRelatedField(rest_api.serializers.RelatedField): +class RelatedItemRelatedField(serializers.RelatedField): """ A custom field to use for the `content_object` generic relationship. """ @@ -35,17 +36,37 @@ class RelatedItemRelatedField(rest_api.serializers.RelatedField): return reverse(view_name, kwargs={'pk': value.pk}, request=request) -class ItemSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class ItemSerializer(serializers.HyperlinkedModelSerializer): """ 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') + get_title = serializers.CharField(read_only=True) + get_title_supplement = serializers.CharField(read_only=True) content_object = RelatedItemRelatedField(read_only=True) + item_no = serializers.CharField(read_only=True) + speaker_set = SpeakerSerializer(many=True, read_only=True) class Meta: model = Item - exclude = ('content_type', 'object_id') + fields = ( + 'url', + 'item_number', + 'item_no', + 'title', + 'get_title', + 'get_title_supplement', + 'text', + 'comment', + 'closed', + 'type', + 'duration', + 'speaker_set', + 'speaker_list_closed', + 'content_object', + 'weight', + 'lft', + 'rght', + 'tree_id', + 'level', + 'parent', + 'tags',) diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 36d570523..56fa8a43c 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -23,9 +23,9 @@ from openslides.projector.api import ( get_active_slide, get_projector_overlays_js, get_overlays) -from openslides.utils import rest_api from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet +from openslides.utils.rest_api import viewsets from openslides.utils.utils import html_strong from openslides.utils.views import ( AjaxMixin, @@ -773,9 +773,9 @@ class ItemCSVImportView(CSVImportView): template_name = 'agenda/item_form_csv_import.html' -class ItemViewSet(rest_api.viewsets.ModelViewSet): +class ItemViewSet(viewsets.ModelViewSet): """ - API endpoint to retrieve, create, edit and delete agenda items. + API endpoint to list, retrieve, create, update and destroy agenda items. """ model = Item serializer_class = ItemSerializer @@ -783,8 +783,9 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet): 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. + permission to see the agenda and in case of create, update or destroy + requests the permission to manage the agenda and to see organizational + items. """ if (not request.user.has_perm('agenda.can_see_agenda') or (self.action in ('create', 'update', 'destroy') and not @@ -802,7 +803,7 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet): def get_queryset(self): """ - Filters organizational items if the user has no permission to see it. + Filters organizational items if the user has no permission to see them. """ queryset = Item.objects.all() if not self.request.user.has_perm('agenda.can_see_orga_items'): diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 14f84024f..b9e038b0d 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -36,7 +36,7 @@ class AssignmentCandidate(RESTModelMixin, models.Model): def get_root_rest_element(self): """ - Returns the assignment to this instance which is the root rest element. + Returns the assignment to this instance which is the root REST element. """ return self.assignment @@ -274,7 +274,7 @@ class AssignmentVote(RESTModelMixin, BaseVote): def get_root_rest_element(self): """ - Returns the assignment to this instance which is the root rest element. + Returns the assignment to this instance which is the root REST element. """ return self.option.poll.assignment @@ -289,7 +289,7 @@ class AssignmentOption(RESTModelMixin, BaseOption): def get_root_rest_element(self): """ - Returns the assignment to this instance which is the root rest element. + Returns the assignment to this instance which is the root REST element. """ return self.poll.assignment @@ -348,6 +348,6 @@ class AssignmentPoll(RESTModelMixin, SlideMixin, CollectDefaultVotesMixin, def get_root_rest_element(self): """ - Returns the assignment to this instance which is the root rest element. + 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 index fb688b849..a8fddb847 100644 --- a/openslides/assignment/serializers.py +++ b/openslides/assignment/serializers.py @@ -1,4 +1,4 @@ -from openslides.utils import rest_api +from openslides.utils.rest_api import serializers from .models import ( models, @@ -9,7 +9,7 @@ from .models import ( AssignmentVote) -class AssignmentCandidateSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class AssignmentCandidateSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for assignment.models.AssignmentCandidate objects. """ @@ -22,7 +22,7 @@ class AssignmentCandidateSerializer(rest_api.serializers.HyperlinkedModelSeriali 'blocked') -class AssignmentVoteSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class AssignmentVoteSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for assignment.models.AssignmentVote objects. """ @@ -33,7 +33,7 @@ class AssignmentVoteSerializer(rest_api.serializers.HyperlinkedModelSerializer): 'value') -class AssignmentOptionSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class AssignmentOptionSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for assignment.models.AssignmentOption objects. """ @@ -46,9 +46,9 @@ class AssignmentOptionSerializer(rest_api.serializers.HyperlinkedModelSerializer 'assignmentvote_set') -class FilterPollListSerializer(rest_api.serializers.ListSerializer): +class FilterPollListSerializer(serializers.ListSerializer): """ - Customized serilizer to filter polls and exclude unpublished ones. + Customized serializer to filter polls (exclude unpublished). """ def to_representation(self, data): """ @@ -62,7 +62,7 @@ class FilterPollListSerializer(rest_api.serializers.ListSerializer): return [self.child.to_representation(item) for item in iterable] -class AssignmentAllPollSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class AssignmentAllPollSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for assignment.models.AssignmentPoll objects. @@ -103,25 +103,25 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer): 'votescast') -class AssignmentFullSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class AssignmentFullSerializer(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 = ( + 'url', 'name', 'description', 'posts', - 'poll_description_default', 'status', 'assignmentcandidate_set', + 'poll_description_default', 'poll_set', - 'tags') + 'tags',) class AssignmentShortSerializer(AssignmentFullSerializer): @@ -133,11 +133,12 @@ class AssignmentShortSerializer(AssignmentFullSerializer): class Meta: model = Assignment fields = ( + 'url', 'name', 'description', 'posts', - 'poll_description_default', 'status', 'assignmentcandidate_set', + 'poll_description_default', 'poll_set', - 'tags') + 'tags',) diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index 6b64cc224..8d5070a07 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -14,8 +14,8 @@ 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.rest_api import viewsets from openslides.utils.utils import html_strong from openslides.utils.views import (CreateView, DeleteView, DetailView, ListView, PDFView, PermissionMixin, @@ -190,9 +190,9 @@ class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView): self.is_blocked = self.get_object().is_blocked(self.person) -class AssignmentViewSet(rest_api.viewsets.ModelViewSet): +class AssignmentViewSet(viewsets.ModelViewSet): """ - API endpoint to retrieve, create, edit and delete assignments. + API endpoint to list, retrieve, create, update and destroy assignments. """ model = Assignment queryset = Assignment.objects.all() @@ -200,8 +200,8 @@ class AssignmentViewSet(rest_api.viewsets.ModelViewSet): 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. + permission to see assignments and in case of create, update or destroy + requests the permission to manage assignments. """ if (not request.user.has_perm('assignment.can_see_assignment') or (self.action in ('create', 'update', 'destroy') and not diff --git a/openslides/config/api.py b/openslides/config/api.py index a6d5f3f30..ea5abfbd8 100644 --- a/openslides/config/api.py +++ b/openslides/config/api.py @@ -39,6 +39,14 @@ class ConfigHandler(object): config_variable.on_change() break + def get_data_as_dict(self): + """ + Returns all config variables as dictionary retrieved from the config cache. + """ + if not hasattr(self, '_cache'): + self.setup_cache() + return self._cache + def get_default(self, key): """ Returns the default value for 'key'. diff --git a/openslides/config/apps.py b/openslides/config/apps.py index 5fe9d414a..d1047212c 100644 --- a/openslides/config/apps.py +++ b/openslides/config/apps.py @@ -9,3 +9,10 @@ class ConfigAppConfig(AppConfig): # Load main menu entry. # Do this by just importing all from this file. from . import main_menu # noqa + + # Import all required stuff. + from openslides.utils.rest_api import router + from .views import ConfigViewSet + + # Register viewsets. + router.register('config/config', ConfigViewSet, 'config') diff --git a/openslides/config/views.py b/openslides/config/views.py index 5be725b97..f731e1370 100644 --- a/openslides/config/views.py +++ b/openslides/config/views.py @@ -3,6 +3,7 @@ from django.contrib import messages from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ +from openslides.utils.rest_api import response, viewsets from openslides.utils.views import FormView from .api import config @@ -100,3 +101,22 @@ class ConfigView(FormView): config[key] = form.cleaned_data[key] messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title)) return super(ConfigView, self).form_valid(form) + + +class ConfigViewSet(viewsets.ViewSet): + """ + API endpoint to list and update the config. + """ + def list(self, request): + """ + Lists als config variables. Everybody can see this. + """ + # TODO: Check if we need permission check here. + return response.Response(config.get_data_as_dict()) + + def update(self, request, pk=None): + if not request.user.has_perm('config.can_manage'): + self.permission_denied(request) + else: + # TODO: Implement update method + self.permission_denied(request) diff --git a/openslides/core/apps.py b/openslides/core/apps.py index a332e77cf..a458ba4c7 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -30,7 +30,11 @@ class CoreAppConfig(AppConfig): router.register('core/customslide', CustomSlideViewSet) router.register('core/tag', TagViewSet) - # Update data when any model of any installed app is saved or deleted - signals.post_save.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') - signals.post_delete.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') - # TODO: test if the m2m_changed signal is also needed + # Update data when any model of any installed app is saved or deleted. + # TODO: Test if the m2m_changed signal is also needed. + signals.post_save.connect( + inform_changed_data_receiver, + dispatch_uid='inform_changed_data_receiver') + signals.post_delete.connect( + inform_changed_data_receiver, + dispatch_uid='inform_changed_data_receiver') diff --git a/openslides/core/models.py b/openslides/core/models.py index e48070603..0749f7c6e 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -43,7 +43,7 @@ class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): elif link == 'delete': url = reverse('customslide_delete', args=[str(self.pk)]) else: - url = super(CustomSlide, self).get_absolute_url(link) + url = super().get_absolute_url(link) return url diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index ad8e70f23..a341318ec 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -1,19 +1,21 @@ -from openslides.utils import rest_api +from openslides.utils.rest_api import serializers from .models import CustomSlide, Tag -class CustomSlideSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class CustomSlideSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for core.models.CustomSlide objects. """ class Meta: model = CustomSlide + fields = ('url', 'title', 'text', 'weight',) -class TagSerializer(rest_api.serializers.HyperlinkedModelSerializer): +class TagSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for core.models.Tag objects. """ class Meta: model = Tag + fields = ('url', 'name',) diff --git a/openslides/core/views.py b/openslides/core/views.py index 1fd645bc3..90a6150a1 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -12,9 +12,9 @@ from haystack.views import SearchView as _SearchView from openslides import get_version as get_openslides_version from openslides import get_git_commit_id, RELEASE from openslides.config.api import config -from openslides.utils import rest_api from openslides.utils import views as utils_views from openslides.utils.plugins import get_plugin_description, get_plugin_verbose_name, get_plugin_version +from openslides.utils.rest_api import viewsets from openslides.utils.signals import template_manipulation from openslides.utils.widgets import Widget @@ -218,9 +218,9 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView): pass -class CustomSlideViewSet(rest_api.viewsets.ModelViewSet): +class CustomSlideViewSet(viewsets.ModelViewSet): """ - API endpoint to retrieve, create, update and delete custom slides. + API endpoint to list, retrieve, create, update and destroy custom slides. """ model = CustomSlide queryset = CustomSlide.objects.all() @@ -229,7 +229,7 @@ class CustomSlideViewSet(rest_api.viewsets.ModelViewSet): def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the - permission to manage. + permission to manage projector. """ if not request.user.has_perm('core.can_manage_projector'): self.permission_denied(request) @@ -313,9 +313,9 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView): **context) -class TagViewSet(rest_api.viewsets.ModelViewSet): +class TagViewSet(viewsets.ModelViewSet): """ - API endpoint to retrieve, create, edit and delete tags. + API endpoint to list, retrieve, create, update and destroy tags. """ model = Tag queryset = Tag.objects.all() @@ -324,7 +324,7 @@ class TagViewSet(rest_api.viewsets.ModelViewSet): def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the - permission to manage and it's a create, update or detroy request. + permission to manage tags and it is a create, update or detroy request. """ if (self.action in ('create', 'update', 'destroy') and not request.user.has_perm('core.can_manage_tags')): diff --git a/openslides/mediafile/apps.py b/openslides/mediafile/apps.py index 9dc0b4b92..14b55a610 100644 --- a/openslides/mediafile/apps.py +++ b/openslides/mediafile/apps.py @@ -12,9 +12,11 @@ class MediafileAppConfig(AppConfig): # Import all required stuff. from openslides.projector.api import register_slide + from openslides.utils.rest_api import router from openslides.utils.signals import template_manipulation from .slides import mediafile_presentation_as_slide from .template import add_mediafile_stylesheets + from .views import MediafileViewSet # Connect template signal. template_manipulation.connect(add_mediafile_stylesheets, dispatch_uid='add_mediafile_stylesheets') @@ -22,3 +24,6 @@ class MediafileAppConfig(AppConfig): # Register slides. Mediafile = self.get_model('Mediafile') register_slide('mediafile', mediafile_presentation_as_slide, Mediafile) + + # Register viewsets. + router.register('mediafile/mediafile', MediafileViewSet) diff --git a/openslides/mediafile/models.py b/openslides/mediafile/models.py index 37887a934..ea0b3b882 100644 --- a/openslides/mediafile/models.py +++ b/openslides/mediafile/models.py @@ -7,10 +7,11 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.projector.models import SlideMixin from openslides.utils.models import AbsoluteUrlMixin +from openslides.utils.rest_api import RESTModelMixin from openslides.users.models import User -class Mediafile(SlideMixin, AbsoluteUrlMixin, models.Model): +class Mediafile(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): """ Class for uploaded files which can be delivered under a certain url. """ diff --git a/openslides/mediafile/serializers.py b/openslides/mediafile/serializers.py new file mode 100644 index 000000000..682ba3c63 --- /dev/null +++ b/openslides/mediafile/serializers.py @@ -0,0 +1,16 @@ +from openslides.utils.rest_api import serializers + +from .models import Mediafile + + +class MediafileSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for mediafile.models.Mediafile objects. + """ + filesize = serializers.SerializerMethodField() + + class Meta: + model = Mediafile + + def get_filesize(self, mediafile): + return mediafile.get_filesize() diff --git a/openslides/mediafile/views.py b/openslides/mediafile/views.py index 54f1f60a6..1491180f1 100644 --- a/openslides/mediafile/views.py +++ b/openslides/mediafile/views.py @@ -2,11 +2,13 @@ from django.http import HttpResponse from openslides.config.api import config from openslides.projector.api import get_active_slide +from openslides.utils.rest_api import viewsets from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView, UpdateView) from .forms import MediafileManagerForm, MediafileNormalUserForm from .models import Mediafile +from .serializers import MediafileSerializer class MediafileListView(ListView): @@ -198,3 +200,26 @@ class PdfToggleFullscreenView(RedirectView): def get_ajax_context(self, *args, **kwargs): config['pdf_fullscreen'] = not config['pdf_fullscreen'] return {'fullscreen': config['pdf_fullscreen']} + + +class MediafileViewSet(viewsets.ModelViewSet): + """ + API endpoint to list, retrieve, create, update and destroy mediafile + objects. + """ + model = Mediafile + queryset = Mediafile.objects.all() + serializer_class = MediafileSerializer + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see mediafile objects and in case of create, update or + destroy requests the permission to manage mediafile objects. + """ + # TODO: Use mediafile.can_upload permission to create and update some + # objects but restricted concerning the uploader. + if (not request.user.has_perm('mediafile.can_see') or + (self.action in ('create', 'update', 'destroy') and not + request.user.has_perm('mediafile.can_manage'))): + self.permission_denied(request) diff --git a/openslides/motion/apps.py b/openslides/motion/apps.py index 9a15b4ccb..3413b6128 100644 --- a/openslides/motion/apps.py +++ b/openslides/motion/apps.py @@ -13,8 +13,10 @@ class MotionAppConfig(AppConfig): # Import all required stuff. from openslides.config.signals import config_signal + from openslides.utils.rest_api import router from openslides.projector.api import register_slide_model from .signals import create_builtin_workflows, setup_motion_config + from .views import CategoryViewSet, MotionViewSet, WorkflowViewSet # Connect signals. config_signal.connect(setup_motion_config, dispatch_uid='setup_motion_config') @@ -25,3 +27,8 @@ class MotionAppConfig(AppConfig): MotionPoll = self.get_model('MotionPoll') register_slide_model(Motion, 'motion/slide.html') register_slide_model(MotionPoll, 'motion/motionpoll_slide.html') + + # Register viewsets. + router.register('motion/category', CategoryViewSet) + router.register('motion/motion', MotionViewSet) + router.register('motion/workflow', WorkflowViewSet) diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 4d57bdba3..e73788247 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -12,12 +12,13 @@ from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefau from openslides.projector.models import SlideMixin from jsonfield import JSONField from openslides.utils.models import AbsoluteUrlMixin +from openslides.utils.rest_api import RESTModelMixin from openslides.users.models import User from .exceptions import WorkflowError -class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): +class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): """ The Motion Class. @@ -57,7 +58,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): """ Counts the number of the motion in one category. - Needed to find the next free motion-identifier. + Needed to find the next free motion identifier. """ category = models.ForeignKey('Category', null=True, blank=True) @@ -553,7 +554,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): return config['motion_amendments_enabled'] and self.parent is not None -class MotionVersion(AbsoluteUrlMixin, models.Model): +class MotionVersion(RESTModelMixin, AbsoluteUrlMixin, models.Model): """ A MotionVersion object saves some date of the motion. """ @@ -611,8 +612,14 @@ class MotionVersion(AbsoluteUrlMixin, models.Model): """Return True, if the version is the active version of a motion. Else: False.""" return self.active_version.exists() + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion -class MotionSubmitter(models.Model): + +class MotionSubmitter(RESTModelMixin, models.Model): """Save the submitter of a Motion.""" motion = models.ForeignKey('Motion', related_name="submitter") @@ -625,8 +632,14 @@ class MotionSubmitter(models.Model): """Return the name of the submitter as string.""" return str(self.person) + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion -class MotionSupporter(models.Model): + +class MotionSupporter(RESTModelMixin, models.Model): """Save the submitter of a Motion.""" motion = models.ForeignKey('Motion', related_name="supporter") @@ -639,8 +652,14 @@ class MotionSupporter(models.Model): """Return the name of the supporter as string.""" return str(self.person) + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion -class Category(AbsoluteUrlMixin, models.Model): + +class Category(RESTModelMixin, AbsoluteUrlMixin, models.Model): name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name")) """Name of the category.""" @@ -666,7 +685,7 @@ class Category(AbsoluteUrlMixin, models.Model): ordering = ['prefix'] -class MotionLog(models.Model): +class MotionLog(RESTModelMixin, models.Model): """Save a logmessage for a motion.""" motion = models.ForeignKey(Motion, related_name='log_messages') @@ -674,7 +693,7 @@ class MotionLog(models.Model): message_list = JSONField() """ - The log message. It should be a list of strings in english. + The log message. It should be a list of strings in English. """ person = models.ForeignKey(User, null=True) @@ -697,8 +716,14 @@ class MotionLog(models.Model): 'person': self.person} return time_and_messages + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion -class MotionVote(BaseVote): + +class MotionVote(RESTModelMixin, BaseVote): """Saves the votes for a MotionPoll. There should allways be three MotionVote objects for each poll, @@ -707,8 +732,14 @@ class MotionVote(BaseVote): option = models.ForeignKey('MotionOption') """The option object, to witch the vote belongs.""" + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.option.poll.motion -class MotionOption(BaseOption): + +class MotionOption(RESTModelMixin, BaseOption): """Links between the MotionPollClass and the MotionVoteClass. There should be one MotionOption object for each poll.""" @@ -719,8 +750,14 @@ class MotionOption(BaseOption): vote_class = MotionVote """The VoteClass, to witch this Class links.""" + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.poll.motion -class MotionPoll(SlideMixin, CollectDefaultVotesMixin, + +class MotionPoll(RESTModelMixin, SlideMixin, CollectDefaultVotesMixin, AbsoluteUrlMixin, BasePoll): """The Class to saves the vote result for a motion poll.""" @@ -778,8 +815,14 @@ class MotionPoll(SlideMixin, CollectDefaultVotesMixin, def get_slide_context(self, **context): return super(MotionPoll, self).get_slide_context(poll=self) + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion -class State(models.Model): + +class State(RESTModelMixin, models.Model): """ Defines a state for a motion. @@ -867,8 +910,14 @@ class State(models.Model): if not state.workflow == self.workflow: raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self)) + def get_root_rest_element(self): + """ + Returns the workflow to this instance which is the root REST element. + """ + return self.workflow -class Workflow(models.Model): + +class Workflow(RESTModelMixin, models.Model): """Defines a workflow for a motion.""" name = models.CharField(max_length=255) diff --git a/openslides/motion/serializers.py b/openslides/motion/serializers.py new file mode 100644 index 000000000..3e8332275 --- /dev/null +++ b/openslides/motion/serializers.py @@ -0,0 +1,180 @@ +from rest_framework.reverse import reverse + +from openslides.utils.rest_api import serializers + +from .models import ( + Category, + Motion, + MotionLog, + MotionOption, + MotionPoll, + MotionSubmitter, + MotionSupporter, + MotionVersion, + MotionVote, + State, + Workflow,) + + +class CategorySerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.Category objects. + """ + class Meta: + model = Category + fields = ('url', 'name', 'prefix',) + + +class StateSerializer(serializers.ModelSerializer): + """ + Serializer for motion.models.State objects. + """ + class Meta: + model = State + fields = ( + 'id', + 'name', + 'action_word', + 'icon', + 'required_permission_to_see', + 'allow_support', + 'allow_create_poll', + 'allow_submitter_edit', + 'versioning', + 'leave_old_version_active', + 'dont_set_identifier', + 'next_states',) + + +class WorkflowSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.Workflow objects. + """ + state_set = StateSerializer(many=True, read_only=True) + first_state = serializers.PrimaryKeyRelatedField(read_only=True) + + class Meta: + model = Workflow + fields = ('url', 'name', 'state_set', 'first_state',) + + +class MotionSubmitterSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.MotionSubmitter objects. + """ + class Meta: + model = MotionSubmitter + fields = ('person',) # TODO: Rename this to 'user', see #1348 + + +class MotionSupporterSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.MotionSupporter objects. + """ + class Meta: + model = MotionSupporter + fields = ('person',) # TODO: Rename this to 'user', see #1348 + + +class MotionLogSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.MotionLog objects. + """ + class Meta: + model = MotionLog + fields = ('message_list', 'person', 'time',) + + +class MotionVoteSerializer(serializers.ModelSerializer): + """ + Serializer for motion.models.MotionVote objects. + """ + class Meta: + model = MotionVote + fields = ('value', 'weight',) + + +class MotionOptionSerializer(serializers.ModelSerializer): + """ + Serializer for motion.models.MotionOption objects. + """ + motionvote_set = MotionVoteSerializer(many=True, read_only=True) + + class Meta: + model = MotionOption + fields = ('motionvote_set',) + + +class MotionPollSerializer(serializers.ModelSerializer): + """ + Serializer for motion.models.MotionPoll objects. + """ + motionoption_set = MotionOptionSerializer(many=True, read_only=True) + + class Meta: + model = MotionPoll + fields = ( + 'poll_number', + 'motionoption_set', + 'votesvalid', + 'votesinvalid', + 'votescast',) + + +class MotionVersionSerializer(serializers.ModelSerializer): + """ + Serializer for motion.models.MotionVersion objects. + """ + class Meta: + model = MotionVersion + fields = ( + 'id', + 'version_number', + 'creation_time', + 'title', + 'text', + 'reason',) + + +class MotionSerializer(serializers.HyperlinkedModelSerializer): + """ + Serializer for motion.models.Motion objects. + """ + versions = MotionVersionSerializer(many=True, read_only=True) + active_version = serializers.PrimaryKeyRelatedField(read_only=True) + submitter = MotionSubmitterSerializer(many=True, read_only=True) + supporter = MotionSupporterSerializer(many=True, read_only=True) + state = StateSerializer(read_only=True) + workflow = serializers.SerializerMethodField() + polls = MotionPollSerializer(many=True, read_only=True) + log_messages = MotionLogSerializer(many=True, read_only=True) + + class Meta: + model = Motion + fields = ( + 'url', + 'identifier', + 'identifier_number', + 'parent', + 'category', + 'tags', + 'versions', + 'active_version', + 'submitter', + 'supporter', + 'state', + 'workflow', + 'attachments', + 'polls', + 'log_messages',) + + def get_workflow(self, motion): + """ + Returns the hyperlink to the workflow of the motion. + """ + 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__) + return reverse('workflow-detail', kwargs={'pk': motion.state.workflow.pk}, request=request) diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 967b03bf3..489c56b92 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -10,6 +10,7 @@ from django.shortcuts import get_object_or_404 from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView from openslides.config.api import config from openslides.poll.views import PollFormView +from openslides.utils.rest_api import viewsets from openslides.utils.utils import html_strong, htmldiff from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView, ListView, PDFView, QuestionView, @@ -21,8 +22,9 @@ from .forms import (BaseMotionForm, MotionCategoryMixin, MotionCSVImportForm, MotionSubmitterMixin, MotionSupporterMixin, MotionWorkflowMixin) from .models import (Category, Motion, MotionPoll, MotionSubmitter, - MotionSupporter, MotionVersion, State) + MotionSupporter, MotionVersion, State, Workflow) from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf +from .serializers import CategorySerializer, MotionSerializer, WorkflowSerializer class MotionListView(ListView): @@ -537,6 +539,29 @@ class SupportView(SingleObjectMixin, QuestionView): return _("You have unsupported this motion successfully.") +class MotionViewSet(viewsets.ModelViewSet): + """ + API endpoint to list, retrieve, create, update and destroy motions. + """ + model = Motion + queryset = Motion.objects.all() + serializer_class = MotionSerializer + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see motions and in case of create, update or + destroy requests the permission to manage motions. + """ + # TODO: Use motion.can_create_motion permission and + # motion.can_support_motion permission to create and update some + # objects but restricted concerning the requesting user. + if (not request.user.has_perm('motion.can_see_motion') or + (self.action in ('create', 'update', 'destroy') and not + request.user.has_perm('motion.can_manage_motion'))): + self.permission_denied(request) + + class PollCreateView(SingleObjectMixin, RedirectView): """ View to create a poll for a motion. @@ -815,6 +840,26 @@ class CategoryDeleteView(DeleteView): success_url_name = 'motion_category_list' +class CategoryViewSet(viewsets.ModelViewSet): + """ + API endpoint to list, retrieve, create, update and destroy categories. + """ + model = Category + queryset = Category.objects.all() + serializer_class = CategorySerializer + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see motions and in case of create, update or destroy + requests the permission to manage motions. + """ + if (not request.user.has_perm('motion.can_see_motion') or + (self.action in ('create', 'update', 'destroy') and not + request.user.has_perm('motion.can_manage_motion'))): + self.permission_denied(request) + + class MotionCSVImportView(CSVImportView): """ Imports motions from an uploaded csv file. @@ -839,3 +884,23 @@ class MotionCSVImportView(CSVImportView): messages.error(self.request, error) # Overleap method of CSVImportView return super(CSVImportView, self).form_valid(form) + + +class WorkflowViewSet(viewsets.ModelViewSet): + """ + API endpoint to list, retrieve, create, update and destroy workflows. + """ + model = Workflow + queryset = Workflow.objects.all() + serializer_class = WorkflowSerializer + + def check_permissions(self, request): + """ + Calls self.permission_denied() if the requesting user has not the + permission to see motions and in case of create, update or destroy + requests the permission to manage motions. + """ + if (not request.user.has_perm('motion.can_see_motion') or + (self.action in ('create', 'update', 'destroy') and not + request.user.has_perm('motion.can_manage_motion'))): + self.permission_denied(request) diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index a0e481ec8..e9dcf02b8 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -1,9 +1,9 @@ -from openslides.utils import rest_api +from openslides.utils.rest_api import serializers from .models import User -class UserShortSerializer(rest_api.serializers.ModelSerializer): +class UserShortSerializer(serializers.ModelSerializer): """ Serializer for users.models.User objects. @@ -12,6 +12,7 @@ class UserShortSerializer(rest_api.serializers.ModelSerializer): class Meta: model = User fields = ( + 'url', 'username', 'title', 'first_name', @@ -19,7 +20,7 @@ class UserShortSerializer(rest_api.serializers.ModelSerializer): 'structure_level') -class UserFullSerializer(rest_api.serializers.ModelSerializer): +class UserFullSerializer(serializers.ModelSerializer): """ Serializer for users.models.User objects. @@ -28,6 +29,7 @@ class UserFullSerializer(rest_api.serializers.ModelSerializer): class Meta: model = User fields = ( + 'url', 'is_present', 'username', 'title', diff --git a/openslides/users/views.py b/openslides/users/views.py index 962ae3fb7..4f4aa3187 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy, activate from openslides.config.api import config -from openslides.utils import rest_api +from openslides.utils.rest_api import viewsets from openslides.utils.utils import delete_default_permissions, html_strong from openslides.utils.views import ( CreateView, CSVImportView, DeleteView, DetailView, FormView, ListView, @@ -261,17 +261,18 @@ class ResetPasswordView(SingleObjectMixin, QuestionView): return _('The Password for %s was successfully reset.') % html_strong(self.get_object()) -class UserViewSet(rest_api.viewsets.ModelViewSet): +class UserViewSet(viewsets.ModelViewSet): """ - API endpoint to create, view, edit and delete users. + API endpoint to list, retrive, create, update and delete users. """ model = User queryset = User.objects.all() def check_permissions(self, request): """ - Calls self.permission_denied() if the requesting user has not all - permissions to see users. + Calls self.permission_denied() if the requesting user has not the + permission to see users and in case of create, update or destroy + requests the permission to see extra user data and to manage users. """ if (not request.user.has_perm('users.can_see_name') or (self.action in ('create', 'update', 'destroy') and not diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index c7d01be5c..7fa7efa67 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,5 +1,5 @@ from django.core.urlresolvers import reverse -from rest_framework import permissions, routers, serializers, viewsets # noqa +from rest_framework import response, routers, serializers, viewsets # noqa router = routers.DefaultRouter()