Merge pull request #2018 from normanjaeckel/optimization

Optimization of autoupdate behavior.
This commit is contained in:
Norman Jäckel 2016-03-06 14:29:43 +01:00
commit 9440903dfd
33 changed files with 718 additions and 274 deletions

View File

@ -0,0 +1,33 @@
from ..utils.access_permissions import BaseAccessPermissions
class ItemAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Item and ItemViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('agenda.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import ItemSerializer
return ItemSerializer
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user.
"""
if (self.can_retrieve(user) and
(not full_data['is_hidden'] or
user.has_perm('agenda.can_see_hidden_items'))):
data = full_data
else:
data = None
return data

View File

@ -33,4 +33,4 @@ class AgendaAppConfig(AppConfig):
dispatch_uid='listen_to_related_object_post_delete')
# Register viewsets.
router.register('agenda/item', ItemViewSet)
router.register(self.get_model('Item').get_collection_string(), ItemViewSet)

View File

@ -15,6 +15,8 @@ from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin
from openslides.utils.utils import to_roman
from .access_permissions import ItemAccessPermissions
class ItemManager(models.Manager):
"""
@ -184,6 +186,7 @@ class Item(RESTModelMixin, models.Model):
"""
An Agenda Item
"""
access_permissions = ItemAccessPermissions()
objects = ItemManager()
AGENDA_ITEM = 1

View File

@ -1,10 +1,4 @@
from django.core.urlresolvers import reverse
from openslides.utils.rest_api import (
ModelSerializer,
RelatedField,
get_collection_and_id_from_url,
)
from openslides.utils.rest_api import ModelSerializer, RelatedField
from .models import Item, Speaker
@ -34,10 +28,7 @@ class RelatedItemRelatedField(RelatedField):
Returns info concerning the related object extracted from the api URL
of this object.
"""
view_name = '%s-detail' % type(value)._meta.object_name.lower()
url = reverse(view_name, kwargs={'pk': value.pk})
collection, obj_id = get_collection_and_id_from_url(url)
return {'collection': collection, 'id': obj_id}
return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
class ItemSerializer(ModelSerializer):

View File

@ -21,8 +21,8 @@ from openslides.utils.rest_api import (
)
from openslides.utils.views import PDFView
from .access_permissions import ItemAccessPermissions
from .models import Item, Speaker
from .serializers import ItemSerializer
# Viewsets for the REST API
@ -34,14 +34,16 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
There are the following views: metadata, list, retrieve, create,
partial_update, update, destroy, manage_speaker, speak and tree.
"""
access_permissions = ItemAccessPermissions()
queryset = Item.objects.all()
serializer_class = ItemSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve', 'manage_speaker', 'tree'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'manage_speaker', 'tree'):
result = self.request.user.has_perm('agenda.can_see')
# For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below.

View File

@ -0,0 +1,37 @@
from ..utils.access_permissions import BaseAccessPermissions
class AssignmentAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Assignment and AssignmentViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('assignments.can_see')
def get_serializer_class(self, user=None):
"""
Returns different serializer classes according to users permissions.
"""
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
if user is None or user.has_perm('assignments.can_manage'):
serializer_class = AssignmentFullSerializer
else:
serializer_class = AssignmentShortSerializer
return serializer_class
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user. Removes unpublushed polls for non admins so that they
only get a result like the AssignmentShortSerializer would give them.
"""
if user.has_perm('assignments.can_manage'):
data = full_data
else:
data = full_data.copy()
data['polls'] = [poll for poll in data['polls'] if poll['published']]
return data

View File

@ -23,5 +23,5 @@ class AssignmentsAppConfig(AppConfig):
config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config')
# Register viewsets.
router.register('assignments/assignment', AssignmentViewSet)
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)
router.register('assignments/poll', AssignmentPollViewSet)

View File

@ -20,6 +20,8 @@ from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin
from openslides.utils.search import user_name_helper
from .access_permissions import AssignmentAccessPermissions
class AssignmentRelatedUser(RESTModelMixin, models.Model):
"""
@ -49,6 +51,10 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
class Assignment(RESTModelMixin, models.Model):
"""
Model for assignments.
"""
access_permissions = AssignmentAccessPermissions()
PHASE_SEARCH = 0
PHASE_VOTING = 1

View File

@ -30,12 +30,9 @@ from openslides.utils.rest_api import (
)
from openslides.utils.views import PDFView
from .access_permissions import AssignmentAccessPermissions
from .models import Assignment, AssignmentPoll
from .serializers import (
AssignmentAllPollSerializer,
AssignmentFullSerializer,
AssignmentShortSerializer,
)
from .serializers import AssignmentAllPollSerializer
# Viewsets for the REST API
@ -48,13 +45,16 @@ class AssignmentViewSet(ModelViewSet):
partial_update, update, destroy, candidature_self, candidature_other,
mark_elected and create_poll.
"""
access_permissions = AssignmentAccessPermissions()
queryset = Assignment.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('assignments.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy',
'mark_elected', 'create_poll'):
@ -70,16 +70,6 @@ class AssignmentViewSet(ModelViewSet):
result = False
return result
def get_serializer_class(self):
"""
Returns different serializer classes according to users permissions.
"""
if self.request.user.has_perm('assignments.can_manage'):
serializer_class = AssignmentFullSerializer
else:
serializer_class = AssignmentShortSerializer
return serializer_class
@detail_route(methods=['post', 'delete'])
def candidature_self(self, request, pk=None):
"""

View File

@ -0,0 +1,109 @@
from ..utils.access_permissions import BaseAccessPermissions
class ProjectorAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Projector and ProjectorViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import ProjectorSerializer
return ProjectorSerializer
class CustomSlideAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for CustomSlide and CustomSlideViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_manage_projector')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import CustomSlideSerializer
return CustomSlideSerializer
class TagAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Tag and TagViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
from .config import config
# Every authenticated user can retrieve tags. Anonymous users can do
# so if they are enabled.
return user.is_authenticated() or config['general_system_enable_anonymous']
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import TagSerializer
return TagSerializer
class ChatMessageAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for ChatMessage and ChatMessageViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
# Anonymous users can see the chat if the anonymous group has the
# permission core.can_use_chat. But they can not use it. See views.py.
return user.has_perm('core.can_use_chat')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import ChatMessageSerializer
return ChatMessageSerializer
class ConfigAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for the config (ConfigStore and
ConfigViewSet).
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
from .config import config
# Every authenticated user can see the metadata and list or retrieve
# the config. Anonymous users can do so if they are enabled.
return user.is_authenticated() or config['general_system_enable_anonymous']
def get_full_data(self, instance):
"""
Returns the serlialized config data.
"""
from .config import config
# Attention: The format of this response has to be the same as in
# the retrieve method of ConfigViewSet.
return {'key': instance.key, 'value': config[instance.key]}

View File

@ -16,7 +16,7 @@ class CoreAppConfig(AppConfig):
# Import all required stuff.
from django.db.models import signals
from openslides.core.signals import config_signal, post_permission_creation
from openslides.utils.autoupdate import inform_changed_data_receiver
from openslides.utils.autoupdate import inform_changed_data_receiver, inform_deleted_data_receiver
from openslides.utils.rest_api import router
from openslides.utils.search import index_add_instance, index_del_instance
from .signals import delete_django_app_permissions, setup_general_config
@ -37,11 +37,11 @@ class CoreAppConfig(AppConfig):
dispatch_uid='delete_django_app_permissions')
# Register viewsets.
router.register('core/projector', ProjectorViewSet)
router.register('core/chatmessage', ChatMessageViewSet)
router.register('core/customslide', CustomSlideViewSet)
router.register('core/tag', TagViewSet)
router.register('core/config', ConfigViewSet, 'config')
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
router.register(self.get_model('CustomSlide').get_collection_string(), CustomSlideViewSet)
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
# Update data when any model of any installed app is saved or deleted.
# TODO: Test if the m2m_changed signal is also needed.
@ -49,8 +49,8 @@ class CoreAppConfig(AppConfig):
inform_changed_data_receiver,
dispatch_uid='inform_changed_data_receiver')
signals.post_delete.connect(
inform_changed_data_receiver,
dispatch_uid='inform_changed_data_receiver')
inform_deleted_data_receiver,
dispatch_uid='inform_deleted_data_receiver')
# Update the search when a model is saved or deleted
signals.post_save.connect(

View File

@ -2,6 +2,7 @@
# Generated by Django 1.9.2 on 2016-03-02 01:22
from __future__ import unicode_literals
import json
import uuid
import django.db.models.deletion
@ -12,18 +13,15 @@ from django.db import migrations, models
import openslides.utils.models
def add_default_projector(apps, schema_editor):
def add_default_projector_via_sql():
"""
Adds default projector and activates clock.
"""
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
Projector = apps.get_model('core', 'Projector')
projector_config = {}
projector_config[uuid.uuid4().hex] = {
'name': 'core/clock',
'stable': True}
Projector.objects.create(config=projector_config)
return ["INSERT INTO core_projector (config, scale, scroll) VALUES ('{}', 0, 0);".format(json.dumps(projector_config))]
class Migration(migrations.Migration):
@ -107,9 +105,5 @@ class Migration(migrations.Migration):
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.RunPython(
code=add_default_projector,
reverse_code=None,
atomic=True,
),
migrations.RunSQL(add_default_projector_via_sql()),
]

View File

@ -1,6 +1,5 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db import models
from jsonfield import JSONField
@ -8,6 +7,13 @@ from openslides.mediafiles.models import Mediafile
from openslides.utils.models import RESTModelMixin
from openslides.utils.projector import ProjectorElement
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
CustomSlideAccessPermissions,
ProjectorAccessPermissions,
TagAccessPermissions,
)
from .exceptions import ProjectorException
@ -51,6 +57,8 @@ class Projector(RESTModelMixin, models.Model):
The projector can be controlled using the REST API with POST requests
on e. g. the URL /rest/core/projector/1/activate_elements/.
"""
access_permissions = ProjectorAccessPermissions()
config = JSONField()
scale = models.IntegerField(default=0)
@ -121,6 +129,8 @@ class CustomSlide(RESTModelMixin, models.Model):
"""
Model for slides with custom content.
"""
access_permissions = CustomSlideAccessPermissions()
title = models.CharField(
max_length=256)
text = models.TextField(
@ -175,6 +185,8 @@ class Tag(RESTModelMixin, models.Model):
Model for tags. This tags can be used for other models like agenda items,
motions or assignments.
"""
access_permissions = TagAccessPermissions()
name = models.CharField(
max_length=255,
unique=True)
@ -189,10 +201,11 @@ class Tag(RESTModelMixin, models.Model):
return self.name
class ConfigStore(models.Model):
class ConfigStore(RESTModelMixin, models.Model):
"""
A model class to store all config variables in the database.
"""
access_permissions = ConfigAccessPermissions()
key = models.CharField(max_length=255, unique=True, db_index=True)
"""A string, the key of the config variable."""
@ -205,11 +218,15 @@ class ConfigStore(models.Model):
permissions = (
('can_manage_config', 'Can manage configuration'),)
def get_root_rest_url(self):
@classmethod
def get_collection_string(cls):
return 'core/config'
def get_rest_pk(self):
"""
Returns the detail url of config value.
Returns the primary key used in the REST API.
"""
return reverse('config-detail', args=[str(self.key)])
return self.key
class ChatMessage(RESTModelMixin, models.Model):
@ -218,6 +235,8 @@ class ChatMessage(RESTModelMixin, models.Model):
At the moment we only have one global chat room for managers.
"""
access_permissions = ChatMessageAccessPermissions()
message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)

View File

@ -30,15 +30,16 @@ from openslides.utils.rest_api import (
)
from openslides.utils.search import search
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
CustomSlideAccessPermissions,
ProjectorAccessPermissions,
TagAccessPermissions,
)
from .config import config
from .exceptions import ConfigError, ConfigNotFound
from .models import ChatMessage, CustomSlide, Projector, Tag
from .serializers import (
ChatMessageSerializer,
CustomSlideSerializer,
ProjectorSerializer,
TagSerializer,
)
# Special Django views
@ -152,14 +153,16 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
activate_elements, prune_elements, update_elements,
deactivate_elements, clear_elements and control_view.
"""
access_permissions = ProjectorAccessPermissions()
queryset = Projector.objects.all()
serializer_class = ProjectorSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
'deactivate_elements', 'clear_elements', 'control_view'):
@ -366,14 +369,18 @@ class CustomSlideViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = CustomSlideAccessPermissions()
queryset = CustomSlide.objects.all()
serializer_class = CustomSlideSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
return self.request.user.has_perm('core.can_manage_projector')
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
else:
result = self.request.user.has_perm('core.can_manage_projector')
return result
class TagViewSet(ModelViewSet):
@ -383,16 +390,18 @@ class TagViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = TagAccessPermissions()
queryset = Tag.objects.all()
serializer_class = TagSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
# Every authenticated user can see the metadata and list or
# retrieve tags. Anonymous users can do so if they are enabled.
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
# Every authenticated user can see the metadata and list tags.
# Anonymous users can do so if they are enabled.
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
elif self.action in ('create', 'update', 'destroy'):
result = self.request.user.has_perm('core.can_manage_tags')
@ -440,13 +449,16 @@ class ConfigViewSet(ViewSet):
There are the following views: metadata, list, retrieve and update.
"""
access_permissions = ConfigAccessPermissions()
metadata_class = ConfigMetadata
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
# Every authenticated user can see the metadata and list or
# retrieve the config. Anonymous users can do so if they are
# enabled.
@ -472,6 +484,8 @@ class ConfigViewSet(ViewSet):
value = config[key]
except ConfigNotFound:
raise Http404
# Attention: The format of this response has to be the same as in
# the get_full_data method of ConfigAccessPermissions.
return Response({'key': key, 'value': value})
def update(self, request, *args, **kwargs):
@ -504,18 +518,23 @@ class ChatMessageViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve and create.
The views partial_update, update and destroy are disabled.
"""
access_permissions = ChatMessageAccessPermissions()
queryset = ChatMessage.objects.all()
serializer_class = ChatMessageSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
else:
# We do not want anonymous users to use the chat even the anonymous
# group has the permission core.can_use_chat.
return (self.action in ('metadata', 'list', 'retrieve', 'create') and
result = (
self.action in ('metadata', 'list', 'create') and
self.request.user.is_authenticated() and
self.request.user.has_perm('core.can_use_chat'))
return result
def perform_create(self, serializer):
"""

View File

@ -0,0 +1,20 @@
from ..utils.access_permissions import BaseAccessPermissions
class MediafileAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Mediafile and MediafileViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('mediafiles.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import MediafileSerializer
return MediafileSerializer

View File

@ -18,4 +18,4 @@ class MediafilesAppConfig(AppConfig):
from .views import MediafileViewSet
# Register viewsets.
router.register('mediafiles/mediafile', MediafileViewSet)
router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet)

View File

@ -5,12 +5,15 @@ from django.utils.translation import ugettext as _
from openslides.utils.search import user_name_helper
from ..utils.models import RESTModelMixin
from .access_permissions import MediafileAccessPermissions
class Mediafile(RESTModelMixin, models.Model):
"""
Class for uploaded files which can be delivered under a certain url.
"""
access_permissions = MediafileAccessPermissions()
mediafile = models.FileField(upload_to='file')
"""
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield

View File

@ -1,6 +1,6 @@
from ..utils.rest_api import ModelViewSet, ValidationError
from .access_permissions import MediafileAccessPermissions
from .models import Mediafile
from .serializers import MediafileSerializer
# Viewsets for the REST API
@ -12,14 +12,16 @@ class MediafileViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = MediafileAccessPermissions()
queryset = Mediafile.objects.all()
serializer_class = MediafileSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('mediafiles.can_see')
elif self.action == 'create':
result = (self.request.user.has_perm('mediafiles.can_see') and

View File

@ -0,0 +1,58 @@
from ..utils.access_permissions import BaseAccessPermissions
class MotionAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Motion and MotionViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('motions.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import MotionSerializer
return MotionSerializer
class CategoryAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Category and CategoryViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('motions.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import CategorySerializer
return CategorySerializer
class WorkflowAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Workflow and WorkflowViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('motions.can_see')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import WorkflowSerializer
return WorkflowSerializer

View File

@ -25,7 +25,7 @@ class MotionsAppConfig(AppConfig):
post_migrate.connect(create_builtin_workflows, dispatch_uid='motion_create_builtin_workflows')
# Register viewsets.
router.register('motions/category', CategoryViewSet)
router.register('motions/motion', MotionViewSet)
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet)
router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet)
router.register('motions/motionpoll', MotionPollViewSet)
router.register('motions/workflow', WorkflowViewSet)

View File

@ -20,6 +20,11 @@ from openslides.poll.models import (
from openslides.utils.models import RESTModelMixin
from openslides.utils.search import user_name_helper
from .access_permissions import (
CategoryAccessPermissions,
MotionAccessPermissions,
WorkflowAccessPermissions,
)
from .exceptions import WorkflowError
@ -29,6 +34,7 @@ class Motion(RESTModelMixin, models.Model):
This class is the main entry point to all other classes related to a motion.
"""
access_permissions = MotionAccessPermissions()
active_version = models.ForeignKey(
'MotionVersion',
@ -624,6 +630,11 @@ class MotionVersion(RESTModelMixin, models.Model):
class Category(RESTModelMixin, models.Model):
"""
Model for categories of motions.
"""
access_permissions = CategoryAccessPermissions()
name = models.CharField(max_length=255)
"""Name of the category."""
@ -879,7 +890,10 @@ class State(RESTModelMixin, models.Model):
class Workflow(RESTModelMixin, models.Model):
"""Defines a workflow for a motion."""
"""
Defines a workflow for a motion.
"""
access_permissions = WorkflowAccessPermissions()
name = models.CharField(max_length=255)
"""A string representing the workflow."""

View File

@ -18,15 +18,15 @@ from openslides.utils.rest_api import (
)
from openslides.utils.views import PDFView, SingleObjectMixin
from .access_permissions import (
CategoryAccessPermissions,
MotionAccessPermissions,
WorkflowAccessPermissions,
)
from .exceptions import WorkflowError
from .models import Category, Motion, MotionPoll, MotionVersion, Workflow
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
from .serializers import (
CategorySerializer,
MotionPollSerializer,
MotionSerializer,
WorkflowSerializer,
)
from .serializers import MotionPollSerializer
# Viewsets for the REST API
@ -39,14 +39,16 @@ class MotionViewSet(ModelViewSet):
partial_update, update, destroy, manage_version, support, set_state and
create_poll.
"""
access_permissions = MotionAccessPermissions()
queryset = Motion.objects.all()
serializer_class = MotionSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve', 'partial_update', 'update'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'partial_update', 'update'):
result = self.request.user.has_perm('motions.can_see')
# For partial_update and update requests the rest of the check is
# done in the update method. See below.
@ -281,14 +283,16 @@ class CategoryViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = CategoryAccessPermissions()
queryset = Category.objects.all()
serializer_class = CategorySerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
result = (self.request.user.has_perm('motions.can_see') and
@ -305,14 +309,16 @@ class WorkflowViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
access_permissions = WorkflowAccessPermissions()
queryset = Workflow.objects.all()
serializer_class = WorkflowSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
result = (self.request.user.has_perm('motions.can_see') and

View File

@ -0,0 +1,49 @@
from ..utils.access_permissions import BaseAccessPermissions
class UserAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for User and UserViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('users.can_see_name')
def get_serializer_class(self, user=None):
"""
Returns different serializer classes with respect user's permissions.
"""
from .serializers import UserFullSerializer, UserShortSerializer
if user is None or user.has_perm('users.can_see_extra_data'):
# Return the UserFullSerializer for requests of users with more
# permissions.
serializer_class = UserFullSerializer
else:
serializer_class = UserShortSerializer
return serializer_class
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user. Removes several fields for non admins so that they do
not get the default_password or even get only the fields as the
UserShortSerializer would give them.
"""
from .serializers import USERSHORTSERIALIZER_FIELDS
if user.has_perm('users.can_manage'):
data = full_data
elif user.has_perm('users.can_see_extra_data'):
# Only remove default password from full data.
data = full_data.copy()
del data['default_password']
else:
# Let only fields as in the UserShortSerializer pass this method.
data = {}
for key in full_data.keys():
if key in USERSHORTSERIALIZER_FIELDS:
data[key] = full_data[key]
return data

View File

@ -28,5 +28,5 @@ class UsersAppConfig(AppConfig):
dispatch_uid='create_builtin_groups_and_admin')
# Register viewsets.
router.register('users/user', UserViewSet)
router.register(self.get_model('User').get_collection_string(), UserViewSet)
router.register('users/group', GroupViewSet)

View File

@ -13,6 +13,7 @@ from openslides.utils.search import user_name_helper
from ..core.config import config
from ..utils.models import RESTModelMixin
from .access_permissions import UserAccessPermissions
from .exceptions import UsersError
@ -94,6 +95,8 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
in other OpenSlides apps like motion submitter or (assignment) election
candidates.
"""
access_permissions = UserAccessPermissions()
USERNAME_FIELD = 'username'
username = models.CharField(

View File

@ -11,16 +11,7 @@ from ..utils.rest_api import (
)
from .models import Group, User
class UserShortSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes only name fields and about me field.
"""
class Meta:
model = User
fields = (
USERSHORTSERIALIZER_FIELDS = (
'id',
'username',
'title',
@ -32,6 +23,17 @@ class UserShortSerializer(ModelSerializer):
)
class UserShortSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes only name fields and about me field.
"""
class Meta:
model = User
fields = USERSHORTSERIALIZER_FIELDS
class UserFullSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.

View File

@ -14,13 +14,10 @@ from ..utils.rest_api import (
status,
)
from ..utils.views import APIView, PDFView
from .access_permissions import UserAccessPermissions
from .models import Group, User
from .pdf import users_passwords_to_pdf, users_to_pdf
from .serializers import (
GroupSerializer,
UserFullSerializer,
UserShortSerializer,
)
from .serializers import GroupSerializer, UserFullSerializer
# Viewsets for the REST API
@ -32,13 +29,16 @@ class UserViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create,
partial_update, update, destroy and reset_password.
"""
access_permissions = UserAccessPermissions()
queryset = User.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve', 'update', 'partial_update'):
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'update', 'partial_update'):
result = self.request.user.has_perm('users.can_see_name')
elif self.action in ('create', 'destroy', 'reset_password'):
result = (self.request.user.has_perm('users.can_see_name') and
@ -50,16 +50,13 @@ class UserViewSet(ModelViewSet):
def get_serializer_class(self):
"""
Returns different serializer classes with respect to action and user's
permissions.
Returns different serializer classes with respect to action.
"""
if (self.action in ('create', 'partial_update', 'update') or
self.request.user.has_perm('users.can_see_extra_data')):
# Return the UserFullSerializer for edit requests or for
# list/retrieve requests of users with more permissions.
if self.action in ('create', 'partial_update', 'update'):
# Return the UserFullSerializer for edit requests.
serializer_class = UserFullSerializer
else:
serializer_class = UserShortSerializer
serializer_class = super().get_serializer_class()
return serializer_class
def list(self, request, *args, **kwargs):

View File

@ -0,0 +1,77 @@
from django.dispatch import Signal
from .dispatch import SignalConnectMetaClass
class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
"""
Base access permissions container.
Every app which has autoupdate models has to create classes subclassing
from this base class for every autoupdate root model. Each subclass has
to have a globally unique name. The metaclass (SignalConnectMetaClass)
does the rest of the magic.
"""
signal = Signal()
def __init__(self, **kwargs):
"""
Initializes the access permission instance. This is done when the
signal is sent.
Because of Django's signal API, we have to take wildcard keyword
arguments. But they are not used here.
"""
pass
@classmethod
def get_dispatch_uid(cls):
"""
Returns the classname as a unique string for each class. Returns None
for the base class so it will not be connected to the signal.
"""
if not cls.__name__ == 'BaseAccessPermissions':
return cls.__name__
def can_retrieve(self, user):
"""
Returns True if the user has read access to model instances.
"""
return False
def get_serializer_class(self, user=None):
"""
Returns different serializer classes according to users permissions.
This should return the serializer for full data access if user is
None. See get_full_data().
"""
raise NotImplementedError(
"You have to add the method 'get_serializer_class' to your "
"access permissions class.".format(self))
def get_full_data(self, instance):
"""
Returns all possible serialized data for the given instance.
"""
return self.get_serializer_class(user=None)(instance).data
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user.
Returns None if the user has no read access. Returns reduced data
if the user has limited access. Default: Returns full data if the
user has read access to model instances.
Hint: You should override this method if your
get_serializer_class() method may return different serializer for
different users or if you have access restrictions in your view or
viewset in methods like retrieve() or check_object_permissions().
"""
if self.can_retrieve(user):
data = full_data
else:
data = None
return data

View File

@ -1,14 +1,13 @@
import json
import os
import posixpath
from importlib import import_module
from urllib.parse import unquote
from django.conf import settings
from django.core.wsgi import get_wsgi_application
from sockjs.tornado import SockJSConnection, SockJSRouter
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.httpserver import HTTPServer
from tornado.httputil import HTTPHeaders
from tornado.ioloop import IOLoop
from tornado.options import parse_command_line
from tornado.web import (
@ -19,7 +18,8 @@ from tornado.web import (
)
from tornado.wsgi import WSGIContainer
from .rest_api import get_collection_and_id_from_url
from ..users.auth import AnonymousUser, get_user
from .access_permissions import BaseAccessPermissions
RUNNING_HOST = None
RUNNING_PORT = None
@ -72,78 +72,58 @@ class OpenSlidesSockJSConnection(SockJSConnection):
def on_close(self):
OpenSlidesSockJSConnection.waiters.remove(self)
def forward_rest_response(self, response):
"""
Sends data to the client of the connection instance.
This method is called after succesful response of AsyncHTTPClient().
See send_object().
"""
if response.code in (200, 404):
# Only send something to the client in case of one of these status
# codes. You have to change the client code (autoupdate.onMessage)
# if you want to handle some more codes.
collection, obj_id = get_collection_and_id_from_url(response.request.url)
data = {
'url': response.request.url,
'status_code': response.code,
'collection': collection,
'id': obj_id,
'data': json.loads(response.body.decode())}
self.send(data)
@classmethod
def send_object(cls, object_url):
def send_object(cls, json_container):
"""
Sends an OpenSlides object to all connected clients (waiters).
First, retrieve the object from the OpenSlides REST api using the given
object_url.
"""
# Join network location with object URL.
if settings.OPENSLIDES_WSGI_NETWORK_LOCATION:
wsgi_network_location = settings.OPENSLIDES_WSGI_NETWORK_LOCATION
else:
if RUNNING_HOST == '0.0.0.0':
# Windows can not connect to 0.0.0.0, so connect to localhost instead.
wsgi_network_location = 'http://localhost:{}'.format(RUNNING_PORT)
else:
wsgi_network_location = 'http://{}:{}'.format(RUNNING_HOST, RUNNING_PORT)
url = ''.join((wsgi_network_location, object_url))
# Load JSON
container = json.loads(json_container)
# Send out internal HTTP request to get data from the REST api.
# Search our AccessPermission class.
for access_permissions in BaseAccessPermissions.get_all():
if access_permissions.get_dispatch_uid() == container.get('dispatch_uid'):
break
else:
raise ValueError('Invalid container. A valid dispatch_uid is missing.')
# Loop over all waiters
for waiter in cls.waiters:
# Initiat new headers object.
headers = HTTPHeaders()
# Read waiter's former cookies and parse session cookie to new header object.
# Read waiter's former cookies and parse session cookie to get user instance.
try:
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME]
except KeyError:
# There is no session cookie
pass
# There is no session cookie so use anonymous user here.
user = AnonymousUser()
else:
headers.add('Cookie', '%s=%s' % (settings.SESSION_COOKIE_NAME, session_cookie.value))
# Get session from session store and use it to retrieve the user.
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_cookie.value)
fake_request = type('FakeRequest', (), {})()
fake_request.session = session
user = get_user(fake_request)
# Read waiter's language header.
try:
languages = waiter.connection_info.headers['Accept-Language']
except KeyError:
# There is no language header
pass
# Two cases: models instance was changed or deleted
if container.get('action') == 'changed':
data = access_permissions.get_restricted_data(container.get('full_data'), user)
if data is None:
# There are no data for the user so he can't see the object. Skip him.
break
output = {
'status_code': 200, # TODO: Refactor this. Use strings like 'change' or 'delete'.
'collection': container['collection_string'],
'id': container['rest_pk'],
'data': data}
elif container.get('action') == 'deleted':
output = {
'status_code': 404, # TODO: Refactor this. Use strings like 'change' or 'delete'.
'collection': container['collection_string'],
'id': container['rest_pk']}
else:
headers.parse_line('Accept-Language: ' + languages)
raise ValueError('Invalid container. A valid action is missing.')
# Setup uncompressed request.
request = HTTPRequest(
url=url,
headers=headers,
decompress_response=False)
# Setup non-blocking HTTP client
http_client = AsyncHTTPClient()
# Executes the request, asynchronously returning an HTTPResponse
# and calling waiter's forward_rest_response() method.
http_client.fetch(request, waiter.forward_rest_response)
# Send output to the waiter (client).
waiter.send(output)
def run_tornado(addr, port, *args, **kwargs):
@ -182,30 +162,50 @@ def run_tornado(addr, port, *args, **kwargs):
RUNNING_PORT = None
def inform_changed_data(*args):
def inform_changed_data(is_delete, *args):
"""
Informs all users about changed data.
The arguments are Django/OpenSlides models.
The first argument is whether the object or the objects are deleted.
The other arguments are the changed or deleted Django/OpenSlides model
instances.
"""
rest_urls = set()
if settings.USE_TORNADO_AS_WSGI_SERVER:
for instance in args:
try:
rest_urls.add(instance.get_root_rest_url())
root_instance = instance.get_root_rest_element()
except AttributeError:
# Instance has no method get_root_rest_url. Just skip it.
# Instance has no method get_root_rest_element. Just skip it.
pass
if settings.USE_TORNADO_AS_WSGI_SERVER:
for url in rest_urls:
OpenSlidesSockJSConnection.send_object(url)
else:
access_permissions = root_instance.get_access_permissions()
container = {
'dispatch_uid': access_permissions.get_dispatch_uid(),
'collection_string': root_instance.get_collection_string(),
'rest_pk': root_instance.get_rest_pk()}
if is_delete and instance == root_instance:
# A root instance is deleted.
container['action'] = 'deleted'
else:
# A non root instance is deleted or any instance is just changed.
container['action'] = 'changed'
root_instance.refresh_from_db()
container['full_data'] = access_permissions.get_full_data(root_instance)
OpenSlidesSockJSConnection.send_object(json.dumps(container))
else:
pass
# TODO: Implement big varainte with Apache or Nginx as wsgi webserver.
# TODO: Implement big variant with Apache or Nginx as WSGI webserver.
def inform_changed_data_receiver(sender, instance, **kwargs):
"""
Receiver for the inform_changed_data function to use in a signal.
"""
inform_changed_data(instance)
inform_changed_data(False, instance)
def inform_deleted_data_receiver(sender, instance, **kwargs):
"""
Receiver for the inform_changed_data function to use in a signal.
"""
inform_changed_data(True, instance)

View File

@ -1,4 +1,3 @@
from django.core.urlresolvers import reverse
from django.db import models
@ -19,9 +18,11 @@ class MinMaxIntegerField(models.IntegerField):
class RESTModelMixin:
"""
Mixin for django models which are used in our rest api.
Mixin for Django models which are used in our REST API.
"""
access_permissions = None
def get_root_rest_element(self):
"""
Returns the root rest instance.
@ -30,20 +31,25 @@ class RESTModelMixin:
"""
return self
def get_root_rest_url(self):
def get_access_permissions(self):
"""
Returns the detail url of the root model of this object.
Returns a container to handle access permissions for this model and
its corresponding viewset.
"""
# Gets the default url-name in the same way as django rest framework
# does in relations.HyperlinkedModelSerializer
root_instance = self.get_root_rest_element()
rest_url = '%s-detail' % type(root_instance)._meta.object_name.lower()
return reverse(rest_url, args=[str(root_instance.pk)])
return self.access_permissions
def get_collection_string(self):
@classmethod
def get_collection_string(cls):
"""
Returns the string representing the name of the collection.
Returns the string representing the name of the collection. Returns
None if this is not a so called root rest instance.
"""
# TODO: find a way not to use the url. See #1791
from .rest_api import get_collection_and_id_from_url
return get_collection_and_id_from_url(self.get_root_rest_url())[0]
# TODO Check if this is a root rest element class and return None if not.
return '/'.join((cls._meta.app_label.lower(), cls._meta.object_name.lower()))
def get_rest_pk(self):
"""
Returns the primary key used in the REST API. By default this is
the database pk.
"""
return self.pk

View File

@ -1,6 +1,4 @@
import re
from collections import OrderedDict
from urllib.parse import urlparse
from rest_framework import status # noqa
from rest_framework.decorators import detail_route, list_route # noqa
@ -35,8 +33,6 @@ from rest_framework.viewsets import \
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
from .exceptions import OpenSlidesError
router = DefaultRouter()
@ -95,6 +91,73 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
return IdManyRelatedField(**list_kwargs)
class PermissionMixin:
"""
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
The methods check_view_permissions or check_projector_requirements are
evaluated. If both return False self.permission_denied() is called.
Django REST Framework's permission system is disabled.
Also connects container to handle access permissions for model and
viewset.
"""
access_permissions = None
def get_permissions(self):
"""
Overridden method to check view and projector permissions. Returns an
empty iterable so Django REST framework won't do any other
permission checks by evaluating Django REST framework style permission
classes and the request passes.
"""
if not self.check_view_permissions() and not self.check_projector_requirements():
self.permission_denied(self.request)
return ()
def check_view_permissions(self):
"""
Override this and return True if the requesting user should be able to
get access to your view.
Use access permissions container for retrieve requests.
"""
return False
def check_projector_requirements(self):
"""
Helper method which returns True if the current request (on this
view instance) is required for at least one active projector element.
"""
from openslides.core.models import Projector
result = False
if self.request.user.has_perm('core.can_see_projector'):
for requirement in Projector.get_all_requirements():
if requirement.is_currently_required(view_instance=self):
result = True
break
return result
def get_access_permissions(self):
"""
Returns a container to handle access permissions for this viewset and
its corresponding model.
"""
return self.access_permissions
def get_serializer_class(self):
"""
Overridden method to return the serializer class given by the
access permissions container.
"""
if self.get_access_permissions() is not None:
serializer_class = self.get_access_permissions().get_serializer_class(self.request.user)
else:
serializer_class = super().get_serializer_class()
return serializer_class
class ModelSerializer(_ModelSerializer):
"""
ModelSerializer that changes the field names of related fields to
@ -117,49 +180,6 @@ class ModelSerializer(_ModelSerializer):
return fields
class PermissionMixin:
"""
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
The methods check_view_permissions or check_projector_requirements are
evaluated. If both return False self.permission_denied() is called.
Django REST framework's permission system is disabled.
"""
def get_permissions(self):
"""
Overriden method to check view and projector permissions. Returns an
empty interable so Django REST framework won't do any other
permission checks by evaluating Django REST framework style permission
classes and the request passes.
"""
if not self.check_view_permissions() and not self.check_projector_requirements():
self.permission_denied(self.request)
return ()
def check_view_permissions(self):
"""
Override this and return True if the requesting user should be able to
get access to your view.
"""
return False
def check_projector_requirements(self):
"""
Helper method which returns True if the current request (on this
view instance) is required for at least one active projector element.
"""
from openslides.core.models import Projector
result = False
if self.request.user.has_perm('core.can_see_projector'):
for requirement in Projector.get_all_requirements():
if requirement.is_currently_required(view_instance=self):
result = True
break
return result
class GenericViewSet(PermissionMixin, _GenericViewSet):
pass
@ -174,20 +194,3 @@ class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
class ViewSet(PermissionMixin, _ViewSet):
pass
def get_collection_and_id_from_url(url):
"""
Helper function. Returns a tuple containing the collection name and the id
extracted out of the given REST api URL.
For example get_collection_and_id_from_url('http://localhost/api/users/user/3/')
returns ('users/user', '3').
Raises OpenSlidesError if the URL is invalid.
"""
path = urlparse(url).path
match = re.match(r'^/rest/(?P<collection>[-\w]+/[-\w]+)/(?P<id>[-\w]+)/$', path)
if not match:
raise OpenSlidesError('Invalid REST api URL: %s' % url)
return match.group('collection'), match.group('id')

View File

@ -1,4 +1,5 @@
from openslides.agenda.models import Item, Speaker
from openslides.core.models import CustomSlide
from openslides.users.models import User
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase
@ -6,8 +7,8 @@ from openslides.utils.test import TestCase
class ListOfSpeakerModelTests(TestCase):
def setUp(self):
self.item1 = Item.objects.create(title='item1')
self.item2 = Item.objects.create(title='item2')
self.item1 = CustomSlide.objects.create(title='item1').agenda_item
self.item2 = CustomSlide.objects.create(title='item2').agenda_item
self.speaker1 = User.objects.create(username='user1')
self.speaker2 = User.objects.create(username='user2')

View File

@ -36,7 +36,7 @@ class MediafileTest(TestCase):
os.close(tmpfile_no)
def tearDown(self):
self.object.mediafile.delete()
self.object.mediafile.delete(save=False)
def test_str(self):
self.assertEqual(str(self.object), 'Title File 1')