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')
|
dispatch_uid='listen_to_related_object_post_delete')
|
||||||
|
|
||||||
# Register viewsets.
|
# 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.models import RESTModelMixin
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
|
|
||||||
|
from .access_permissions import ItemAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
class ItemManager(models.Manager):
|
class ItemManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
@ -184,6 +186,7 @@ class Item(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
An Agenda Item
|
An Agenda Item
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ItemAccessPermissions()
|
||||||
objects = ItemManager()
|
objects = ItemManager()
|
||||||
|
|
||||||
AGENDA_ITEM = 1
|
AGENDA_ITEM = 1
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
from django.core.urlresolvers import reverse
|
from openslides.utils.rest_api import ModelSerializer, RelatedField
|
||||||
|
|
||||||
from openslides.utils.rest_api import (
|
|
||||||
ModelSerializer,
|
|
||||||
RelatedField,
|
|
||||||
get_collection_and_id_from_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import Item, Speaker
|
from .models import Item, Speaker
|
||||||
|
|
||||||
@ -34,10 +28,7 @@ class RelatedItemRelatedField(RelatedField):
|
|||||||
Returns info concerning the related object extracted from the api URL
|
Returns info concerning the related object extracted from the api URL
|
||||||
of this object.
|
of this object.
|
||||||
"""
|
"""
|
||||||
view_name = '%s-detail' % type(value)._meta.object_name.lower()
|
return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
|
||||||
url = reverse(view_name, kwargs={'pk': value.pk})
|
|
||||||
collection, obj_id = get_collection_and_id_from_url(url)
|
|
||||||
return {'collection': collection, 'id': obj_id}
|
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(ModelSerializer):
|
class ItemSerializer(ModelSerializer):
|
||||||
|
@ -21,8 +21,8 @@ from openslides.utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView
|
from openslides.utils.views import PDFView
|
||||||
|
|
||||||
|
from .access_permissions import ItemAccessPermissions
|
||||||
from .models import Item, Speaker
|
from .models import Item, Speaker
|
||||||
from .serializers import ItemSerializer
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -34,14 +34,16 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update, destroy, manage_speaker, speak and tree.
|
partial_update, update, destroy, manage_speaker, speak and tree.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ItemAccessPermissions()
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
serializer_class = ItemSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('agenda.can_see')
|
||||||
# For manage_speaker and tree requests the rest of the check is
|
# For manage_speaker and tree requests the rest of the check is
|
||||||
# done in the specific method. See below.
|
# 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')
|
config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config')
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register('assignments/assignment', AssignmentViewSet)
|
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)
|
||||||
router.register('assignments/poll', AssignmentPollViewSet)
|
router.register('assignments/poll', AssignmentPollViewSet)
|
||||||
|
@ -20,6 +20,8 @@ from openslides.utils.exceptions import OpenSlidesError
|
|||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.search import user_name_helper
|
from openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
@ -49,6 +51,10 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Assignment(RESTModelMixin, models.Model):
|
class Assignment(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for assignments.
|
||||||
|
"""
|
||||||
|
access_permissions = AssignmentAccessPermissions()
|
||||||
|
|
||||||
PHASE_SEARCH = 0
|
PHASE_SEARCH = 0
|
||||||
PHASE_VOTING = 1
|
PHASE_VOTING = 1
|
||||||
|
@ -30,12 +30,9 @@ from openslides.utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView
|
from openslides.utils.views import PDFView
|
||||||
|
|
||||||
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
from .models import Assignment, AssignmentPoll
|
from .models import Assignment, AssignmentPoll
|
||||||
from .serializers import (
|
from .serializers import AssignmentAllPollSerializer
|
||||||
AssignmentAllPollSerializer,
|
|
||||||
AssignmentFullSerializer,
|
|
||||||
AssignmentShortSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -48,13 +45,16 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
partial_update, update, destroy, candidature_self, candidature_other,
|
partial_update, update, destroy, candidature_self, candidature_other,
|
||||||
mark_elected and create_poll.
|
mark_elected and create_poll.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = AssignmentAccessPermissions()
|
||||||
queryset = Assignment.objects.all()
|
queryset = Assignment.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('assignments.can_see')
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
||||||
'mark_elected', 'create_poll'):
|
'mark_elected', 'create_poll'):
|
||||||
@ -70,16 +70,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
result = False
|
result = False
|
||||||
return result
|
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'])
|
@detail_route(methods=['post', 'delete'])
|
||||||
def candidature_self(self, request, pk=None):
|
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.
|
# Import all required stuff.
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from openslides.core.signals import config_signal, post_permission_creation
|
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.rest_api import router
|
||||||
from openslides.utils.search import index_add_instance, index_del_instance
|
from openslides.utils.search import index_add_instance, index_del_instance
|
||||||
from .signals import delete_django_app_permissions, setup_general_config
|
from .signals import delete_django_app_permissions, setup_general_config
|
||||||
@ -37,11 +37,11 @@ class CoreAppConfig(AppConfig):
|
|||||||
dispatch_uid='delete_django_app_permissions')
|
dispatch_uid='delete_django_app_permissions')
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register('core/projector', ProjectorViewSet)
|
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
||||||
router.register('core/chatmessage', ChatMessageViewSet)
|
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
||||||
router.register('core/customslide', CustomSlideViewSet)
|
router.register(self.get_model('CustomSlide').get_collection_string(), CustomSlideViewSet)
|
||||||
router.register('core/tag', TagViewSet)
|
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
||||||
router.register('core/config', ConfigViewSet, 'config')
|
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
||||||
|
|
||||||
# Update data when any model of any installed app is saved or deleted.
|
# Update data when any model of any installed app is saved or deleted.
|
||||||
# TODO: Test if the m2m_changed signal is also needed.
|
# TODO: Test if the m2m_changed signal is also needed.
|
||||||
@ -49,8 +49,8 @@ class CoreAppConfig(AppConfig):
|
|||||||
inform_changed_data_receiver,
|
inform_changed_data_receiver,
|
||||||
dispatch_uid='inform_changed_data_receiver')
|
dispatch_uid='inform_changed_data_receiver')
|
||||||
signals.post_delete.connect(
|
signals.post_delete.connect(
|
||||||
inform_changed_data_receiver,
|
inform_deleted_data_receiver,
|
||||||
dispatch_uid='inform_changed_data_receiver')
|
dispatch_uid='inform_deleted_data_receiver')
|
||||||
|
|
||||||
# Update the search when a model is saved or deleted
|
# Update the search when a model is saved or deleted
|
||||||
signals.post_save.connect(
|
signals.post_save.connect(
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# Generated by Django 1.9.2 on 2016-03-02 01:22
|
# Generated by Django 1.9.2 on 2016-03-02 01:22
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -12,18 +13,15 @@ from django.db import migrations, models
|
|||||||
import openslides.utils.models
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
def add_default_projector(apps, schema_editor):
|
def add_default_projector_via_sql():
|
||||||
"""
|
"""
|
||||||
Adds default projector and activates clock.
|
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 = {}
|
||||||
projector_config[uuid.uuid4().hex] = {
|
projector_config[uuid.uuid4().hex] = {
|
||||||
'name': 'core/clock',
|
'name': 'core/clock',
|
||||||
'stable': True}
|
'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):
|
class Migration(migrations.Migration):
|
||||||
@ -107,9 +105,5 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunSQL(add_default_projector_via_sql()),
|
||||||
code=add_default_projector,
|
|
||||||
reverse_code=None,
|
|
||||||
atomic=True,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
@ -8,6 +7,13 @@ from openslides.mediafiles.models import Mediafile
|
|||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.projector import ProjectorElement
|
from openslides.utils.projector import ProjectorElement
|
||||||
|
|
||||||
|
from .access_permissions import (
|
||||||
|
ChatMessageAccessPermissions,
|
||||||
|
ConfigAccessPermissions,
|
||||||
|
CustomSlideAccessPermissions,
|
||||||
|
ProjectorAccessPermissions,
|
||||||
|
TagAccessPermissions,
|
||||||
|
)
|
||||||
from .exceptions import ProjectorException
|
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
|
The projector can be controlled using the REST API with POST requests
|
||||||
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ProjectorAccessPermissions()
|
||||||
|
|
||||||
config = JSONField()
|
config = JSONField()
|
||||||
|
|
||||||
scale = models.IntegerField(default=0)
|
scale = models.IntegerField(default=0)
|
||||||
@ -121,6 +129,8 @@ class CustomSlide(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
Model for slides with custom content.
|
Model for slides with custom content.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = CustomSlideAccessPermissions()
|
||||||
|
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
max_length=256)
|
max_length=256)
|
||||||
text = models.TextField(
|
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,
|
Model for tags. This tags can be used for other models like agenda items,
|
||||||
motions or assignments.
|
motions or assignments.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = TagAccessPermissions()
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True)
|
unique=True)
|
||||||
@ -189,10 +201,11 @@ class Tag(RESTModelMixin, models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class ConfigStore(models.Model):
|
class ConfigStore(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
A model class to store all config variables in the database.
|
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)
|
key = models.CharField(max_length=255, unique=True, db_index=True)
|
||||||
"""A string, the key of the config variable."""
|
"""A string, the key of the config variable."""
|
||||||
@ -205,11 +218,15 @@ class ConfigStore(models.Model):
|
|||||||
permissions = (
|
permissions = (
|
||||||
('can_manage_config', 'Can manage configuration'),)
|
('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):
|
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.
|
At the moment we only have one global chat room for managers.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ChatMessageAccessPermissions()
|
||||||
|
|
||||||
message = models.TextField()
|
message = models.TextField()
|
||||||
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
@ -30,15 +30,16 @@ from openslides.utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from openslides.utils.search import search
|
from openslides.utils.search import search
|
||||||
|
|
||||||
|
from .access_permissions import (
|
||||||
|
ChatMessageAccessPermissions,
|
||||||
|
ConfigAccessPermissions,
|
||||||
|
CustomSlideAccessPermissions,
|
||||||
|
ProjectorAccessPermissions,
|
||||||
|
TagAccessPermissions,
|
||||||
|
)
|
||||||
from .config import config
|
from .config import config
|
||||||
from .exceptions import ConfigError, ConfigNotFound
|
from .exceptions import ConfigError, ConfigNotFound
|
||||||
from .models import ChatMessage, CustomSlide, Projector, Tag
|
from .models import ChatMessage, CustomSlide, Projector, Tag
|
||||||
from .serializers import (
|
|
||||||
ChatMessageSerializer,
|
|
||||||
CustomSlideSerializer,
|
|
||||||
ProjectorSerializer,
|
|
||||||
TagSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Special Django views
|
# Special Django views
|
||||||
@ -152,14 +153,16 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
|||||||
activate_elements, prune_elements, update_elements,
|
activate_elements, prune_elements, update_elements,
|
||||||
deactivate_elements, clear_elements and control_view.
|
deactivate_elements, clear_elements and control_view.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ProjectorAccessPermissions()
|
||||||
queryset = Projector.objects.all()
|
queryset = Projector.objects.all()
|
||||||
serializer_class = ProjectorSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('core.can_see_projector')
|
||||||
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
|
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
|
||||||
'deactivate_elements', 'clear_elements', 'control_view'):
|
'deactivate_elements', 'clear_elements', 'control_view'):
|
||||||
@ -366,14 +369,18 @@ class CustomSlideViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = CustomSlideAccessPermissions()
|
||||||
queryset = CustomSlide.objects.all()
|
queryset = CustomSlide.objects.all()
|
||||||
serializer_class = CustomSlideSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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):
|
class TagViewSet(ModelViewSet):
|
||||||
@ -383,16 +390,18 @@ class TagViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = TagAccessPermissions()
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all()
|
||||||
serializer_class = TagSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('metadata', 'list', 'retrieve'):
|
if self.action == 'retrieve':
|
||||||
# Every authenticated user can see the metadata and list or
|
result = self.get_access_permissions().can_retrieve(self.request.user)
|
||||||
# retrieve tags. Anonymous users can do so if they are enabled.
|
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']
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
elif self.action in ('create', 'update', 'destroy'):
|
elif self.action in ('create', 'update', 'destroy'):
|
||||||
result = self.request.user.has_perm('core.can_manage_tags')
|
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.
|
There are the following views: metadata, list, retrieve and update.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ConfigAccessPermissions()
|
||||||
metadata_class = ConfigMetadata
|
metadata_class = ConfigMetadata
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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
|
# Every authenticated user can see the metadata and list or
|
||||||
# retrieve the config. Anonymous users can do so if they are
|
# retrieve the config. Anonymous users can do so if they are
|
||||||
# enabled.
|
# enabled.
|
||||||
@ -472,6 +484,8 @@ class ConfigViewSet(ViewSet):
|
|||||||
value = config[key]
|
value = config[key]
|
||||||
except ConfigNotFound:
|
except ConfigNotFound:
|
||||||
raise Http404
|
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})
|
return Response({'key': key, 'value': value})
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
@ -504,18 +518,23 @@ class ChatMessageViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve and create.
|
There are the following views: metadata, list, retrieve and create.
|
||||||
The views partial_update, update and destroy are disabled.
|
The views partial_update, update and destroy are disabled.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = ChatMessageAccessPermissions()
|
||||||
queryset = ChatMessage.objects.all()
|
queryset = ChatMessage.objects.all()
|
||||||
serializer_class = ChatMessageSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
# We do not want anonymous users to use the chat even the anonymous
|
if self.action == 'retrieve':
|
||||||
# group has the permission core.can_use_chat.
|
result = self.get_access_permissions().can_retrieve(self.request.user)
|
||||||
return (self.action in ('metadata', 'list', 'retrieve', 'create') and
|
else:
|
||||||
|
# We do not want anonymous users to use the chat even the anonymous
|
||||||
|
# group has the permission core.can_use_chat.
|
||||||
|
result = (
|
||||||
|
self.action in ('metadata', 'list', 'create') and
|
||||||
self.request.user.is_authenticated() and
|
self.request.user.is_authenticated() and
|
||||||
self.request.user.has_perm('core.can_use_chat'))
|
self.request.user.has_perm('core.can_use_chat'))
|
||||||
|
return result
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
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
|
from .views import MediafileViewSet
|
||||||
|
|
||||||
# Register viewsets.
|
# 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 openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
|
from .access_permissions import MediafileAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
class Mediafile(RESTModelMixin, models.Model):
|
class Mediafile(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Class for uploaded files which can be delivered under a certain url.
|
Class for uploaded files which can be delivered under a certain url.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = MediafileAccessPermissions()
|
||||||
|
|
||||||
mediafile = models.FileField(upload_to='file')
|
mediafile = models.FileField(upload_to='file')
|
||||||
"""
|
"""
|
||||||
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from ..utils.rest_api import ModelViewSet, ValidationError
|
from ..utils.rest_api import ModelViewSet, ValidationError
|
||||||
|
from .access_permissions import MediafileAccessPermissions
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
from .serializers import MediafileSerializer
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -12,14 +12,16 @@ class MediafileViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = MediafileAccessPermissions()
|
||||||
queryset = Mediafile.objects.all()
|
queryset = Mediafile.objects.all()
|
||||||
serializer_class = MediafileSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('mediafiles.can_see')
|
||||||
elif self.action == 'create':
|
elif self.action == 'create':
|
||||||
result = (self.request.user.has_perm('mediafiles.can_see') and
|
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')
|
post_migrate.connect(create_builtin_workflows, dispatch_uid='motion_create_builtin_workflows')
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register('motions/category', CategoryViewSet)
|
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
|
||||||
router.register('motions/motion', MotionViewSet)
|
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/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.models import RESTModelMixin
|
||||||
from openslides.utils.search import user_name_helper
|
from openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
|
from .access_permissions import (
|
||||||
|
CategoryAccessPermissions,
|
||||||
|
MotionAccessPermissions,
|
||||||
|
WorkflowAccessPermissions,
|
||||||
|
)
|
||||||
from .exceptions import WorkflowError
|
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.
|
This class is the main entry point to all other classes related to a motion.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = MotionAccessPermissions()
|
||||||
|
|
||||||
active_version = models.ForeignKey(
|
active_version = models.ForeignKey(
|
||||||
'MotionVersion',
|
'MotionVersion',
|
||||||
@ -624,6 +630,11 @@ class MotionVersion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Category(RESTModelMixin, models.Model):
|
class Category(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for categories of motions.
|
||||||
|
"""
|
||||||
|
access_permissions = CategoryAccessPermissions()
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
"""Name of the category."""
|
"""Name of the category."""
|
||||||
|
|
||||||
@ -879,7 +890,10 @@ class State(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Workflow(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)
|
name = models.CharField(max_length=255)
|
||||||
"""A string representing the workflow."""
|
"""A string representing the workflow."""
|
||||||
|
@ -18,15 +18,15 @@ from openslides.utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView, SingleObjectMixin
|
from openslides.utils.views import PDFView, SingleObjectMixin
|
||||||
|
|
||||||
|
from .access_permissions import (
|
||||||
|
CategoryAccessPermissions,
|
||||||
|
MotionAccessPermissions,
|
||||||
|
WorkflowAccessPermissions,
|
||||||
|
)
|
||||||
from .exceptions import WorkflowError
|
from .exceptions import WorkflowError
|
||||||
from .models import Category, Motion, MotionPoll, MotionVersion, Workflow
|
from .models import Category, Motion, MotionPoll, MotionVersion, Workflow
|
||||||
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
|
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
|
||||||
from .serializers import (
|
from .serializers import MotionPollSerializer
|
||||||
CategorySerializer,
|
|
||||||
MotionPollSerializer,
|
|
||||||
MotionSerializer,
|
|
||||||
WorkflowSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -39,14 +39,16 @@ class MotionViewSet(ModelViewSet):
|
|||||||
partial_update, update, destroy, manage_version, support, set_state and
|
partial_update, update, destroy, manage_version, support, set_state and
|
||||||
create_poll.
|
create_poll.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = MotionAccessPermissions()
|
||||||
queryset = Motion.objects.all()
|
queryset = Motion.objects.all()
|
||||||
serializer_class = MotionSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
# For partial_update and update requests the rest of the check is
|
# For partial_update and update requests the rest of the check is
|
||||||
# done in the update method. See below.
|
# done in the update method. See below.
|
||||||
@ -281,14 +283,16 @@ class CategoryViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = CategoryAccessPermissions()
|
||||||
queryset = Category.objects.all()
|
queryset = Category.objects.all()
|
||||||
serializer_class = CategorySerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
result = (self.request.user.has_perm('motions.can_see') and
|
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,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = WorkflowAccessPermissions()
|
||||||
queryset = Workflow.objects.all()
|
queryset = Workflow.objects.all()
|
||||||
serializer_class = WorkflowSerializer
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
result = (self.request.user.has_perm('motions.can_see') and
|
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')
|
dispatch_uid='create_builtin_groups_and_admin')
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register('users/user', UserViewSet)
|
router.register(self.get_model('User').get_collection_string(), UserViewSet)
|
||||||
router.register('users/group', GroupViewSet)
|
router.register('users/group', GroupViewSet)
|
||||||
|
@ -13,6 +13,7 @@ from openslides.utils.search import user_name_helper
|
|||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
|
from .access_permissions import UserAccessPermissions
|
||||||
from .exceptions import UsersError
|
from .exceptions import UsersError
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +95,8 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
in other OpenSlides apps like motion submitter or (assignment) election
|
in other OpenSlides apps like motion submitter or (assignment) election
|
||||||
candidates.
|
candidates.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = UserAccessPermissions()
|
||||||
|
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
|
|
||||||
username = models.CharField(
|
username = models.CharField(
|
||||||
|
@ -11,16 +11,7 @@ from ..utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from .models import Group, User
|
from .models import Group, User
|
||||||
|
|
||||||
|
USERSHORTSERIALIZER_FIELDS = (
|
||||||
class UserShortSerializer(ModelSerializer):
|
|
||||||
"""
|
|
||||||
Serializer for users.models.User objects.
|
|
||||||
|
|
||||||
Serializes only name fields and about me field.
|
|
||||||
"""
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = (
|
|
||||||
'id',
|
'id',
|
||||||
'username',
|
'username',
|
||||||
'title',
|
'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):
|
class UserFullSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for users.models.User objects.
|
Serializer for users.models.User objects.
|
||||||
|
@ -14,13 +14,10 @@ from ..utils.rest_api import (
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView, PDFView
|
from ..utils.views import APIView, PDFView
|
||||||
|
from .access_permissions import UserAccessPermissions
|
||||||
from .models import Group, User
|
from .models import Group, User
|
||||||
from .pdf import users_passwords_to_pdf, users_to_pdf
|
from .pdf import users_passwords_to_pdf, users_to_pdf
|
||||||
from .serializers import (
|
from .serializers import GroupSerializer, UserFullSerializer
|
||||||
GroupSerializer,
|
|
||||||
UserFullSerializer,
|
|
||||||
UserShortSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
@ -32,13 +29,16 @@ class UserViewSet(ModelViewSet):
|
|||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update, destroy and reset_password.
|
partial_update, update, destroy and reset_password.
|
||||||
"""
|
"""
|
||||||
|
access_permissions = UserAccessPermissions()
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
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')
|
result = self.request.user.has_perm('users.can_see_name')
|
||||||
elif self.action in ('create', 'destroy', 'reset_password'):
|
elif self.action in ('create', 'destroy', 'reset_password'):
|
||||||
result = (self.request.user.has_perm('users.can_see_name') and
|
result = (self.request.user.has_perm('users.can_see_name') and
|
||||||
@ -50,16 +50,13 @@ class UserViewSet(ModelViewSet):
|
|||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""
|
"""
|
||||||
Returns different serializer classes with respect to action and user's
|
Returns different serializer classes with respect to action.
|
||||||
permissions.
|
|
||||||
"""
|
"""
|
||||||
if (self.action in ('create', 'partial_update', 'update') or
|
if self.action in ('create', 'partial_update', 'update'):
|
||||||
self.request.user.has_perm('users.can_see_extra_data')):
|
# Return the UserFullSerializer for edit requests.
|
||||||
# Return the UserFullSerializer for edit requests or for
|
|
||||||
# list/retrieve requests of users with more permissions.
|
|
||||||
serializer_class = UserFullSerializer
|
serializer_class = UserFullSerializer
|
||||||
else:
|
else:
|
||||||
serializer_class = UserShortSerializer
|
serializer_class = super().get_serializer_class()
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
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 json
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
|
from importlib import import_module
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
from sockjs.tornado import SockJSConnection, SockJSRouter
|
from sockjs.tornado import SockJSConnection, SockJSRouter
|
||||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
|
||||||
from tornado.httpserver import HTTPServer
|
from tornado.httpserver import HTTPServer
|
||||||
from tornado.httputil import HTTPHeaders
|
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
from tornado.options import parse_command_line
|
from tornado.options import parse_command_line
|
||||||
from tornado.web import (
|
from tornado.web import (
|
||||||
@ -19,7 +18,8 @@ from tornado.web import (
|
|||||||
)
|
)
|
||||||
from tornado.wsgi import WSGIContainer
|
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_HOST = None
|
||||||
RUNNING_PORT = None
|
RUNNING_PORT = None
|
||||||
@ -72,78 +72,58 @@ class OpenSlidesSockJSConnection(SockJSConnection):
|
|||||||
def on_close(self):
|
def on_close(self):
|
||||||
OpenSlidesSockJSConnection.waiters.remove(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
|
@classmethod
|
||||||
def send_object(cls, object_url):
|
def send_object(cls, json_container):
|
||||||
"""
|
"""
|
||||||
Sends an OpenSlides object to all connected clients (waiters).
|
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.
|
# Load JSON
|
||||||
if settings.OPENSLIDES_WSGI_NETWORK_LOCATION:
|
container = json.loads(json_container)
|
||||||
wsgi_network_location = settings.OPENSLIDES_WSGI_NETWORK_LOCATION
|
|
||||||
|
# Search our AccessPermission class.
|
||||||
|
for access_permissions in BaseAccessPermissions.get_all():
|
||||||
|
if access_permissions.get_dispatch_uid() == container.get('dispatch_uid'):
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
if RUNNING_HOST == '0.0.0.0':
|
raise ValueError('Invalid container. A valid dispatch_uid is missing.')
|
||||||
# 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))
|
|
||||||
|
|
||||||
# Send out internal HTTP request to get data from the REST api.
|
# Loop over all waiters
|
||||||
for waiter in cls.waiters:
|
for waiter in cls.waiters:
|
||||||
# Initiat new headers object.
|
# Read waiter's former cookies and parse session cookie to get user instance.
|
||||||
headers = HTTPHeaders()
|
|
||||||
|
|
||||||
# Read waiter's former cookies and parse session cookie to new header object.
|
|
||||||
try:
|
try:
|
||||||
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME]
|
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# There is no session cookie
|
# There is no session cookie so use anonymous user here.
|
||||||
pass
|
user = AnonymousUser()
|
||||||
else:
|
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.
|
# Two cases: models instance was changed or deleted
|
||||||
try:
|
if container.get('action') == 'changed':
|
||||||
languages = waiter.connection_info.headers['Accept-Language']
|
data = access_permissions.get_restricted_data(container.get('full_data'), user)
|
||||||
except KeyError:
|
if data is None:
|
||||||
# There is no language header
|
# There are no data for the user so he can't see the object. Skip him.
|
||||||
pass
|
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:
|
else:
|
||||||
headers.parse_line('Accept-Language: ' + languages)
|
raise ValueError('Invalid container. A valid action is missing.')
|
||||||
|
|
||||||
# Setup uncompressed request.
|
# Send output to the waiter (client).
|
||||||
request = HTTPRequest(
|
waiter.send(output)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def run_tornado(addr, port, *args, **kwargs):
|
def run_tornado(addr, port, *args, **kwargs):
|
||||||
@ -182,30 +162,50 @@ def run_tornado(addr, port, *args, **kwargs):
|
|||||||
RUNNING_PORT = None
|
RUNNING_PORT = None
|
||||||
|
|
||||||
|
|
||||||
def inform_changed_data(*args):
|
def inform_changed_data(is_delete, *args):
|
||||||
"""
|
"""
|
||||||
Informs all users about changed data.
|
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()
|
|
||||||
for instance in args:
|
|
||||||
try:
|
|
||||||
rest_urls.add(instance.get_root_rest_url())
|
|
||||||
except AttributeError:
|
|
||||||
# Instance has no method get_root_rest_url. Just skip it.
|
|
||||||
pass
|
|
||||||
|
|
||||||
if settings.USE_TORNADO_AS_WSGI_SERVER:
|
if settings.USE_TORNADO_AS_WSGI_SERVER:
|
||||||
for url in rest_urls:
|
for instance in args:
|
||||||
OpenSlidesSockJSConnection.send_object(url)
|
try:
|
||||||
|
root_instance = instance.get_root_rest_element()
|
||||||
|
except AttributeError:
|
||||||
|
# Instance has no method get_root_rest_element. Just skip it.
|
||||||
|
pass
|
||||||
|
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:
|
else:
|
||||||
pass
|
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):
|
def inform_changed_data_receiver(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Receiver for the inform_changed_data function to use in a signal.
|
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
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
@ -19,9 +18,11 @@ class MinMaxIntegerField(models.IntegerField):
|
|||||||
|
|
||||||
class RESTModelMixin:
|
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):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
Returns the root rest instance.
|
Returns the root rest instance.
|
||||||
@ -30,20 +31,25 @@ class RESTModelMixin:
|
|||||||
"""
|
"""
|
||||||
return self
|
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
|
return self.access_permissions
|
||||||
# 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)])
|
|
||||||
|
|
||||||
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
|
# TODO Check if this is a root rest element class and return None if not.
|
||||||
from .rest_api import get_collection_and_id_from_url
|
return '/'.join((cls._meta.app_label.lower(), cls._meta.object_name.lower()))
|
||||||
return get_collection_and_id_from_url(self.get_root_rest_url())[0]
|
|
||||||
|
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 collections import OrderedDict
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from rest_framework import status # noqa
|
from rest_framework import status # noqa
|
||||||
from rest_framework.decorators import detail_route, list_route # noqa
|
from rest_framework.decorators import detail_route, list_route # noqa
|
||||||
@ -35,8 +33,6 @@ from rest_framework.viewsets import \
|
|||||||
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
|
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
|
||||||
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
||||||
|
|
||||||
from .exceptions import OpenSlidesError
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
|
||||||
|
|
||||||
@ -95,6 +91,73 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|||||||
return IdManyRelatedField(**list_kwargs)
|
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):
|
class ModelSerializer(_ModelSerializer):
|
||||||
"""
|
"""
|
||||||
ModelSerializer that changes the field names of related fields to
|
ModelSerializer that changes the field names of related fields to
|
||||||
@ -117,49 +180,6 @@ class ModelSerializer(_ModelSerializer):
|
|||||||
return fields
|
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):
|
class GenericViewSet(PermissionMixin, _GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -174,20 +194,3 @@ class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
class ViewSet(PermissionMixin, _ViewSet):
|
class ViewSet(PermissionMixin, _ViewSet):
|
||||||
pass
|
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.agenda.models import Item, Speaker
|
||||||
|
from openslides.core.models import CustomSlide
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
@ -6,8 +7,8 @@ from openslides.utils.test import TestCase
|
|||||||
|
|
||||||
class ListOfSpeakerModelTests(TestCase):
|
class ListOfSpeakerModelTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.item1 = Item.objects.create(title='item1')
|
self.item1 = CustomSlide.objects.create(title='item1').agenda_item
|
||||||
self.item2 = Item.objects.create(title='item2')
|
self.item2 = CustomSlide.objects.create(title='item2').agenda_item
|
||||||
self.speaker1 = User.objects.create(username='user1')
|
self.speaker1 = User.objects.create(username='user1')
|
||||||
self.speaker2 = User.objects.create(username='user2')
|
self.speaker2 = User.objects.create(username='user2')
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class MediafileTest(TestCase):
|
|||||||
os.close(tmpfile_no)
|
os.close(tmpfile_no)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.object.mediafile.delete()
|
self.object.mediafile.delete(save=False)
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
self.assertEqual(str(self.object), 'Title File 1')
|
self.assertEqual(str(self.object), 'Title File 1')
|
||||||
|
Loading…
Reference in New Issue
Block a user