Added REST api for motion, mediafile and config app. Refactor REST api in other apps.

This commit is contained in:
Norman Jäckel 2015-01-24 16:35:50 +01:00
parent 19fb7018af
commit 7238b8159a
24 changed files with 493 additions and 78 deletions

View File

@ -437,6 +437,6 @@ class Speaker(RESTModelMixin, AbsoluteUrlMixin, models.Model):
def get_root_rest_element(self): 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 return self.item

View File

@ -1,10 +1,11 @@
from openslides.utils import rest_api
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from openslides.utils.rest_api import serializers
from .models import Item, Speaker from .models import Item, Speaker
class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer): class SpeakerSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for agenda.models.Speaker objects. Serializer for agenda.models.Speaker objects.
""" """
@ -18,7 +19,7 @@ class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer):
'weight') 'weight')
class RelatedItemRelatedField(rest_api.serializers.RelatedField): class RelatedItemRelatedField(serializers.RelatedField):
""" """
A custom field to use for the `content_object` generic relationship. 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) 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. Serializer for agenda.models.Item objects.
""" """
get_title = rest_api.serializers.CharField(read_only=True) get_title = serializers.CharField(read_only=True)
get_title_supplement = rest_api.serializers.CharField(read_only=True) get_title_supplement = 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 = RelatedItemRelatedField(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: class Meta:
model = Item 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',)

View File

@ -23,9 +23,9 @@ from openslides.projector.api import (
get_active_slide, get_active_slide,
get_projector_overlays_js, get_projector_overlays_js,
get_overlays) get_overlays)
from openslides.utils import rest_api
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import viewsets
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import ( from openslides.utils.views import (
AjaxMixin, AjaxMixin,
@ -773,9 +773,9 @@ class ItemCSVImportView(CSVImportView):
template_name = 'agenda/item_form_csv_import.html' 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 model = Item
serializer_class = ItemSerializer serializer_class = ItemSerializer
@ -783,8 +783,9 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet):
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to see and in case of create, update or destroy requests permission to see the agenda and in case of create, update or destroy
the permission to manage and to see organizational items. requests the permission to manage the agenda and to see organizational
items.
""" """
if (not request.user.has_perm('agenda.can_see_agenda') or if (not request.user.has_perm('agenda.can_see_agenda') or
(self.action in ('create', 'update', 'destroy') and not (self.action in ('create', 'update', 'destroy') and not
@ -802,7 +803,7 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet):
def get_queryset(self): 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() queryset = Item.objects.all()
if not self.request.user.has_perm('agenda.can_see_orga_items'): if not self.request.user.has_perm('agenda.can_see_orga_items'):

View File

@ -36,7 +36,7 @@ class AssignmentCandidate(RESTModelMixin, models.Model):
def get_root_rest_element(self): 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 return self.assignment
@ -274,7 +274,7 @@ class AssignmentVote(RESTModelMixin, BaseVote):
def get_root_rest_element(self): 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 return self.option.poll.assignment
@ -289,7 +289,7 @@ class AssignmentOption(RESTModelMixin, BaseOption):
def get_root_rest_element(self): 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 return self.poll.assignment
@ -348,6 +348,6 @@ class AssignmentPoll(RESTModelMixin, SlideMixin, CollectDefaultVotesMixin,
def get_root_rest_element(self): 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 return self.assignment

View File

@ -1,4 +1,4 @@
from openslides.utils import rest_api from openslides.utils.rest_api import serializers
from .models import ( from .models import (
models, models,
@ -9,7 +9,7 @@ from .models import (
AssignmentVote) AssignmentVote)
class AssignmentCandidateSerializer(rest_api.serializers.HyperlinkedModelSerializer): class AssignmentCandidateSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for assignment.models.AssignmentCandidate objects. Serializer for assignment.models.AssignmentCandidate objects.
""" """
@ -22,7 +22,7 @@ class AssignmentCandidateSerializer(rest_api.serializers.HyperlinkedModelSeriali
'blocked') 'blocked')
class AssignmentVoteSerializer(rest_api.serializers.HyperlinkedModelSerializer): class AssignmentVoteSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for assignment.models.AssignmentVote objects. Serializer for assignment.models.AssignmentVote objects.
""" """
@ -33,7 +33,7 @@ class AssignmentVoteSerializer(rest_api.serializers.HyperlinkedModelSerializer):
'value') 'value')
class AssignmentOptionSerializer(rest_api.serializers.HyperlinkedModelSerializer): class AssignmentOptionSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for assignment.models.AssignmentOption objects. Serializer for assignment.models.AssignmentOption objects.
""" """
@ -46,9 +46,9 @@ class AssignmentOptionSerializer(rest_api.serializers.HyperlinkedModelSerializer
'assignmentvote_set') '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): 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] 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. Serializer for assignment.models.AssignmentPoll objects.
@ -103,25 +103,25 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
'votescast') 'votescast')
class AssignmentFullSerializer(rest_api.serializers.HyperlinkedModelSerializer): class AssignmentFullSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for assignment.models.Assignment objects. With all polls. Serializer for assignment.models.Assignment objects. With all polls.
""" """
assignmentcandidate_set = AssignmentCandidateSerializer(many=True, read_only=True) assignmentcandidate_set = AssignmentCandidateSerializer(many=True, read_only=True)
poll_set = AssignmentAllPollSerializer(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: class Meta:
model = Assignment model = Assignment
fields = ( fields = (
'url',
'name', 'name',
'description', 'description',
'posts', 'posts',
'poll_description_default',
'status', 'status',
'assignmentcandidate_set', 'assignmentcandidate_set',
'poll_description_default',
'poll_set', 'poll_set',
'tags') 'tags',)
class AssignmentShortSerializer(AssignmentFullSerializer): class AssignmentShortSerializer(AssignmentFullSerializer):
@ -133,11 +133,12 @@ class AssignmentShortSerializer(AssignmentFullSerializer):
class Meta: class Meta:
model = Assignment model = Assignment
fields = ( fields = (
'url',
'name', 'name',
'description', 'description',
'posts', 'posts',
'poll_description_default',
'status', 'status',
'assignmentcandidate_set', 'assignmentcandidate_set',
'poll_description_default',
'poll_set', 'poll_set',
'tags') 'tags',)

View File

@ -14,8 +14,8 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
from openslides.config.api import config from openslides.config.api import config
from openslides.users.models import Group, User # TODO: remove this from openslides.users.models import Group, User # TODO: remove this
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.utils import rest_api
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import viewsets
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, DeleteView, DetailView,
ListView, PDFView, PermissionMixin, ListView, PDFView, PermissionMixin,
@ -190,9 +190,9 @@ class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView):
self.is_blocked = self.get_object().is_blocked(self.person) 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 model = Assignment
queryset = Assignment.objects.all() queryset = Assignment.objects.all()
@ -200,8 +200,8 @@ class AssignmentViewSet(rest_api.viewsets.ModelViewSet):
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to see and in case of create, update or destroy requests permission to see assignments and in case of create, update or destroy
the permission to manage and to see organizational items. requests the permission to manage assignments.
""" """
if (not request.user.has_perm('assignment.can_see_assignment') or if (not request.user.has_perm('assignment.can_see_assignment') or
(self.action in ('create', 'update', 'destroy') and not (self.action in ('create', 'update', 'destroy') and not

View File

@ -39,6 +39,14 @@ class ConfigHandler(object):
config_variable.on_change() config_variable.on_change()
break 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): def get_default(self, key):
""" """
Returns the default value for 'key'. Returns the default value for 'key'.

View File

@ -9,3 +9,10 @@ class ConfigAppConfig(AppConfig):
# Load main menu entry. # Load main menu entry.
# Do this by just importing all from this file. # Do this by just importing all from this file.
from . import main_menu # noqa 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')

View File

@ -3,6 +3,7 @@ from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.utils.rest_api import response, viewsets
from openslides.utils.views import FormView from openslides.utils.views import FormView
from .api import config from .api import config
@ -100,3 +101,22 @@ class ConfigView(FormView):
config[key] = form.cleaned_data[key] config[key] = form.cleaned_data[key]
messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title)) messages.success(self.request, _('%s settings successfully saved.') % _(self.config_collection.title))
return super(ConfigView, self).form_valid(form) 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)

View File

@ -30,7 +30,11 @@ class CoreAppConfig(AppConfig):
router.register('core/customslide', CustomSlideViewSet) router.register('core/customslide', CustomSlideViewSet)
router.register('core/tag', TagViewSet) router.register('core/tag', TagViewSet)
# Update data when any model of any installed app is saved or deleted # 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') # TODO: Test if the m2m_changed signal is also needed.
signals.post_delete.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') signals.post_save.connect(
# TODO: test if the m2m_changed signal is also needed inform_changed_data_receiver,
dispatch_uid='inform_changed_data_receiver')
signals.post_delete.connect(
inform_changed_data_receiver,
dispatch_uid='inform_changed_data_receiver')

View File

@ -43,7 +43,7 @@ class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
elif link == 'delete': elif link == 'delete':
url = reverse('customslide_delete', args=[str(self.pk)]) url = reverse('customslide_delete', args=[str(self.pk)])
else: else:
url = super(CustomSlide, self).get_absolute_url(link) url = super().get_absolute_url(link)
return url return url

View File

@ -1,19 +1,21 @@
from openslides.utils import rest_api from openslides.utils.rest_api import serializers
from .models import CustomSlide, Tag from .models import CustomSlide, Tag
class CustomSlideSerializer(rest_api.serializers.HyperlinkedModelSerializer): class CustomSlideSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for core.models.CustomSlide objects. Serializer for core.models.CustomSlide objects.
""" """
class Meta: class Meta:
model = CustomSlide model = CustomSlide
fields = ('url', 'title', 'text', 'weight',)
class TagSerializer(rest_api.serializers.HyperlinkedModelSerializer): class TagSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer for core.models.Tag objects. Serializer for core.models.Tag objects.
""" """
class Meta: class Meta:
model = Tag model = Tag
fields = ('url', 'name',)

View File

@ -12,9 +12,9 @@ from haystack.views import SearchView as _SearchView
from openslides import get_version as get_openslides_version from openslides import get_version as get_openslides_version
from openslides import get_git_commit_id, RELEASE from openslides import get_git_commit_id, RELEASE
from openslides.config.api import config from openslides.config.api import config
from openslides.utils import rest_api
from openslides.utils import views as utils_views 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.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.signals import template_manipulation
from openslides.utils.widgets import Widget from openslides.utils.widgets import Widget
@ -218,9 +218,9 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
pass 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 model = CustomSlide
queryset = CustomSlide.objects.all() queryset = CustomSlide.objects.all()
@ -229,7 +229,7 @@ class CustomSlideViewSet(rest_api.viewsets.ModelViewSet):
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the 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'): if not request.user.has_perm('core.can_manage_projector'):
self.permission_denied(request) self.permission_denied(request)
@ -313,9 +313,9 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView):
**context) **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 model = Tag
queryset = Tag.objects.all() queryset = Tag.objects.all()
@ -324,7 +324,7 @@ class TagViewSet(rest_api.viewsets.ModelViewSet):
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the 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') if (self.action in ('create', 'update', 'destroy')
and not request.user.has_perm('core.can_manage_tags')): and not request.user.has_perm('core.can_manage_tags')):

View File

@ -12,9 +12,11 @@ class MediafileAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from openslides.projector.api import register_slide from openslides.projector.api import register_slide
from openslides.utils.rest_api import router
from openslides.utils.signals import template_manipulation from openslides.utils.signals import template_manipulation
from .slides import mediafile_presentation_as_slide from .slides import mediafile_presentation_as_slide
from .template import add_mediafile_stylesheets from .template import add_mediafile_stylesheets
from .views import MediafileViewSet
# Connect template signal. # Connect template signal.
template_manipulation.connect(add_mediafile_stylesheets, dispatch_uid='add_mediafile_stylesheets') template_manipulation.connect(add_mediafile_stylesheets, dispatch_uid='add_mediafile_stylesheets')
@ -22,3 +24,6 @@ class MediafileAppConfig(AppConfig):
# Register slides. # Register slides.
Mediafile = self.get_model('Mediafile') Mediafile = self.get_model('Mediafile')
register_slide('mediafile', mediafile_presentation_as_slide, Mediafile) register_slide('mediafile', mediafile_presentation_as_slide, Mediafile)
# Register viewsets.
router.register('mediafile/mediafile', MediafileViewSet)

View File

@ -7,10 +7,11 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.projector.models import SlideMixin from openslides.projector.models import SlideMixin
from openslides.utils.models import AbsoluteUrlMixin from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin
from openslides.users.models import User 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. Class for uploaded files which can be delivered under a certain url.
""" """

View File

@ -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()

View File

@ -2,11 +2,13 @@ from django.http import HttpResponse
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import get_active_slide 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, from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
UpdateView) UpdateView)
from .forms import MediafileManagerForm, MediafileNormalUserForm from .forms import MediafileManagerForm, MediafileNormalUserForm
from .models import Mediafile from .models import Mediafile
from .serializers import MediafileSerializer
class MediafileListView(ListView): class MediafileListView(ListView):
@ -198,3 +200,26 @@ class PdfToggleFullscreenView(RedirectView):
def get_ajax_context(self, *args, **kwargs): def get_ajax_context(self, *args, **kwargs):
config['pdf_fullscreen'] = not config['pdf_fullscreen'] config['pdf_fullscreen'] = not config['pdf_fullscreen']
return {'fullscreen': 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)

View File

@ -13,8 +13,10 @@ class MotionAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
from openslides.utils.rest_api import router
from openslides.projector.api import register_slide_model from openslides.projector.api import register_slide_model
from .signals import create_builtin_workflows, setup_motion_config from .signals import create_builtin_workflows, setup_motion_config
from .views import CategoryViewSet, MotionViewSet, WorkflowViewSet
# Connect signals. # Connect signals.
config_signal.connect(setup_motion_config, dispatch_uid='setup_motion_config') config_signal.connect(setup_motion_config, dispatch_uid='setup_motion_config')
@ -25,3 +27,8 @@ class MotionAppConfig(AppConfig):
MotionPoll = self.get_model('MotionPoll') MotionPoll = self.get_model('MotionPoll')
register_slide_model(Motion, 'motion/slide.html') register_slide_model(Motion, 'motion/slide.html')
register_slide_model(MotionPoll, 'motion/motionpoll_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)

View File

@ -12,12 +12,13 @@ from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefau
from openslides.projector.models import SlideMixin from openslides.projector.models import SlideMixin
from jsonfield import JSONField from jsonfield import JSONField
from openslides.utils.models import AbsoluteUrlMixin from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin
from openslides.users.models import User from openslides.users.models import User
from .exceptions import WorkflowError from .exceptions import WorkflowError
class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
""" """
The Motion Class. The Motion Class.
@ -57,7 +58,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
""" """
Counts the number of the motion in one category. 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) 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 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. 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 True, if the version is the active version of a motion. Else: False."""
return self.active_version.exists() 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.""" """Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="submitter") motion = models.ForeignKey('Motion', related_name="submitter")
@ -625,8 +632,14 @@ class MotionSubmitter(models.Model):
"""Return the name of the submitter as string.""" """Return the name of the submitter as string."""
return str(self.person) 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.""" """Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="supporter") motion = models.ForeignKey('Motion', related_name="supporter")
@ -639,8 +652,14 @@ class MotionSupporter(models.Model):
"""Return the name of the supporter as string.""" """Return the name of the supporter as string."""
return str(self.person) 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 = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
"""Name of the category.""" """Name of the category."""
@ -666,7 +685,7 @@ class Category(AbsoluteUrlMixin, models.Model):
ordering = ['prefix'] ordering = ['prefix']
class MotionLog(models.Model): class MotionLog(RESTModelMixin, models.Model):
"""Save a logmessage for a motion.""" """Save a logmessage for a motion."""
motion = models.ForeignKey(Motion, related_name='log_messages') motion = models.ForeignKey(Motion, related_name='log_messages')
@ -674,7 +693,7 @@ class MotionLog(models.Model):
message_list = JSONField() 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) person = models.ForeignKey(User, null=True)
@ -697,8 +716,14 @@ class MotionLog(models.Model):
'person': self.person} 'person': self.person}
return time_and_messages 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. """Saves the votes for a MotionPoll.
There should allways be three MotionVote objects for each poll, There should allways be three MotionVote objects for each poll,
@ -707,8 +732,14 @@ class MotionVote(BaseVote):
option = models.ForeignKey('MotionOption') option = models.ForeignKey('MotionOption')
"""The option object, to witch the vote belongs.""" """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. """Links between the MotionPollClass and the MotionVoteClass.
There should be one MotionOption object for each poll.""" There should be one MotionOption object for each poll."""
@ -719,8 +750,14 @@ class MotionOption(BaseOption):
vote_class = MotionVote vote_class = MotionVote
"""The VoteClass, to witch this Class links.""" """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): AbsoluteUrlMixin, BasePoll):
"""The Class to saves the vote result for a motion poll.""" """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): def get_slide_context(self, **context):
return super(MotionPoll, self).get_slide_context(poll=self) 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. Defines a state for a motion.
@ -867,8 +910,14 @@ class State(models.Model):
if not state.workflow == self.workflow: 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)) 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.""" """Defines a workflow for a motion."""
name = models.CharField(max_length=255) name = models.CharField(max_length=255)

View File

@ -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)

View File

@ -10,6 +10,7 @@ from django.shortcuts import get_object_or_404
from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView
from openslides.config.api import config from openslides.config.api import config
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.utils.rest_api import viewsets
from openslides.utils.utils import html_strong, htmldiff from openslides.utils.utils import html_strong, htmldiff
from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView, from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView,
ListView, PDFView, QuestionView, ListView, PDFView, QuestionView,
@ -21,8 +22,9 @@ from .forms import (BaseMotionForm, MotionCategoryMixin,
MotionCSVImportForm, MotionSubmitterMixin, MotionCSVImportForm, MotionSubmitterMixin,
MotionSupporterMixin, MotionWorkflowMixin) MotionSupporterMixin, MotionWorkflowMixin)
from .models import (Category, Motion, MotionPoll, MotionSubmitter, 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 .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
from .serializers import CategorySerializer, MotionSerializer, WorkflowSerializer
class MotionListView(ListView): class MotionListView(ListView):
@ -537,6 +539,29 @@ class SupportView(SingleObjectMixin, QuestionView):
return _("You have unsupported this motion successfully.") 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): class PollCreateView(SingleObjectMixin, RedirectView):
""" """
View to create a poll for a motion. View to create a poll for a motion.
@ -815,6 +840,26 @@ class CategoryDeleteView(DeleteView):
success_url_name = 'motion_category_list' 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): class MotionCSVImportView(CSVImportView):
""" """
Imports motions from an uploaded csv file. Imports motions from an uploaded csv file.
@ -839,3 +884,23 @@ class MotionCSVImportView(CSVImportView):
messages.error(self.request, error) messages.error(self.request, error)
# Overleap method of CSVImportView # Overleap method of CSVImportView
return super(CSVImportView, self).form_valid(form) 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)

View File

@ -1,9 +1,9 @@
from openslides.utils import rest_api from openslides.utils.rest_api import serializers
from .models import User from .models import User
class UserShortSerializer(rest_api.serializers.ModelSerializer): class UserShortSerializer(serializers.ModelSerializer):
""" """
Serializer for users.models.User objects. Serializer for users.models.User objects.
@ -12,6 +12,7 @@ class UserShortSerializer(rest_api.serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ( fields = (
'url',
'username', 'username',
'title', 'title',
'first_name', 'first_name',
@ -19,7 +20,7 @@ class UserShortSerializer(rest_api.serializers.ModelSerializer):
'structure_level') 'structure_level')
class UserFullSerializer(rest_api.serializers.ModelSerializer): class UserFullSerializer(serializers.ModelSerializer):
""" """
Serializer for users.models.User objects. Serializer for users.models.User objects.
@ -28,6 +29,7 @@ class UserFullSerializer(rest_api.serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ( fields = (
'url',
'is_present', 'is_present',
'username', 'username',
'title', 'title',

View File

@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _, ugettext_lazy, activate from django.utils.translation import ugettext as _, ugettext_lazy, activate
from openslides.config.api import config 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.utils import delete_default_permissions, html_strong
from openslides.utils.views import ( from openslides.utils.views import (
CreateView, CSVImportView, DeleteView, DetailView, FormView, ListView, 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()) 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 model = User
queryset = User.objects.all() queryset = User.objects.all()
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not all Calls self.permission_denied() if the requesting user has not the
permissions to see users. 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 if (not request.user.has_perm('users.can_see_name') or
(self.action in ('create', 'update', 'destroy') and not (self.action in ('create', 'update', 'destroy') and not

View File

@ -1,5 +1,5 @@
from django.core.urlresolvers import reverse 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() router = routers.DefaultRouter()