Merge pull request #2018 from normanjaeckel/optimization
Optimization of autoupdate behavior.
This commit is contained in:
commit
9440903dfd
33
openslides/agenda/access_permissions.py
Normal file
33
openslides/agenda/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
37
openslides/assignments/access_permissions.py
Normal file
37
openslides/assignments/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
109
openslides/core/access_permissions.py
Normal file
109
openslides/core/access_permissions.py
Normal 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]}
|
@ -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(
|
||||
|
@ -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()),
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
"""
|
||||
|
20
openslides/mediafiles/access_permissions.py
Normal file
20
openslides/mediafiles/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
58
openslides/motions/access_permissions.py
Normal file
58
openslides/motions/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
49
openslides/users/access_permissions.py
Normal file
49
openslides/users/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
77
openslides/utils/access_permissions.py
Normal file
77
openslides/utils/access_permissions.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user