Massive refactoring for autoupdate optimization.

This commit is contained in:
Norman Jäckel 2016-02-11 22:58:32 +01:00
parent 3db2f2fc16
commit 4daa61888f
29 changed files with 501 additions and 180 deletions

View File

@ -1,9 +1,20 @@
class AccessPermissions:
def get_serializer_class(self, user):
return None
from ..utils.access_permissions import BaseAccessPermissions
class ItemAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Item and ItemViewSet.
"""
def can_retrieve(self, user):
"""
TODO
Returns True if the user has read access model instances.
"""
return user.has_perm('assignments.can_see')
return user.has_perm('agenda.can_see')
def get_serializer_class(self, user):
"""
Returns serializer class.
"""
from .serializers import ItemSerializer
return ItemSerializer

View File

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

View File

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

View File

@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy
from reportlab.platypus import Paragraph
from openslides.core.config import config
from openslides.agenda.access_permissions import AccessPermissions
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import (
@ -22,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
@ -35,16 +34,15 @@ 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
access_permissions = AccessPermissions()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.access_permissions.can_retrieve(self.request.user)
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
@ -65,6 +63,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
Checks if the requesting user has permission to see also an
organizational item if it is one.
"""
#TODO
if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'):
self.permission_denied(request)

View File

@ -1,18 +1,24 @@
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')
class AccessPermissions:
def get_serializer_class(self, user):
"""
Returns different serializer classes according to users permissions.
"""
from openslides.assignments.serializers import AssignmentFullSerializer, AssignmentShortSerializer
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
if user.has_perm('assignments.can_manage'):
serializer_class = AssignmentFullSerializer
else:
serializer_class = AssignmentShortSerializer
return serializer_class
def can_retrieve(self, user):
"""
TODO
"""
return user.has_perm('agenda.can_see')

View File

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

View File

@ -7,7 +7,6 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.agenda.models import Item, Speaker
from openslides.assignments.access_permissions import AccessPermissions
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.poll.models import (
@ -21,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):
"""
@ -50,7 +51,10 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
class Assignment(RESTModelMixin, models.Model):
access_permissions = AccessPermissions()
"""
Model for assignments.
"""
access_permissions = AssignmentAccessPermissions()
PHASE_SEARCH = 0
PHASE_VOTING = 1

View File

@ -17,7 +17,6 @@ from reportlab.platypus import (
TableStyle,
)
from openslides.assignments.access_permissions import AccessPermissions
from openslides.core.config import config
from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import (
@ -31,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
@ -49,15 +45,15 @@ class AssignmentViewSet(ModelViewSet):
partial_update, update, destroy, candidature_self, candidature_other,
mark_elected and create_poll.
"""
access_permissions = AssignmentAccessPermissions()
queryset = Assignment.objects.all()
access_permissions = AccessPermissions()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.access_permissions.can_retrieve(self.request.user)
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',

View File

@ -1,9 +1,109 @@
class AccessPermissions:
def get_serializer_class(self, user):
return None
from ..utils.access_permissions import BaseAccessPermissions
class ProjectorAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Projector and ProjectorViewSet.
"""
def can_retrieve(self, user):
"""
TODO
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user):
"""
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):
"""
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):
"""
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):
"""
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_serialized_data(self, instance, user):
"""
Returns the serlialized config data or None if the user is not
allowed to see it.
"""
from .config import config
if self.can_retrieve(user) is not None:
return {'key': instance.key, 'value': config[instance.key]}

View File

@ -12,11 +12,11 @@ class CoreAppConfig(AppConfig):
# Load projector elements.
# Do this by just importing all from these files.
from . import projector # noqa
# 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_deleted_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.

View File

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

View File

@ -13,7 +13,6 @@ from django.http import Http404, HttpResponse
from django.utils.timezone import now
from openslides import __version__ as version
from openslides.core.access_permissions import AccessPermissions
from openslides.utils import views as utils_views
from openslides.utils.plugins import (
get_plugin_description,
@ -31,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
@ -153,16 +153,15 @@ 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
access_permissions = AccessPermissions()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.access_permissions.can_retrieve(self.request.user)
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',
@ -370,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):
@ -387,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')
@ -444,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.
@ -476,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_serialized_data method of ConfigAccessPermissions.
return Response({'key': key, 'value': value})
def update(self, request, *args, **kwargs):
@ -508,18 +518,23 @@ class ChatMessageViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve and create.
The views partial_update, update and destroy are disabled.
"""
access_permissions = ChatMessageAccessPermissions()
queryset = ChatMessage.objects.all()
serializer_class = ChatMessageSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
else:
# We do not want anonymous users to use the chat even the anonymous
# group has the permission core.can_use_chat.
return (self.action in ('metadata', 'list', 'retrieve', 'create') and
result = (
self.action in ('metadata', 'list', 'create') and
self.request.user.is_authenticated() and
self.request.user.has_perm('core.can_use_chat'))
return result
def perform_create(self, serializer):
"""

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,58 @@
from ..utils.access_permissions import BaseAccessPermissions
class MotionAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for Motion and MotionViewSet.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('motions.can_see')
def get_serializer_class(self, user):
"""
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):
"""
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):
"""
Returns serializer class.
"""
from .serializers import WorkflowSerializer
return WorkflowSerializer

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
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):
"""
Returns different serializer classes with respect user's permissions.
"""
from .serializers import UserFullSerializer, UserShortSerializer
if 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

View File

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

View File

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

View File

@ -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):
@ -78,6 +75,7 @@ class UserViewSet(ModelViewSet):
Hides the default_password for non admins.
"""
#TODO: Hide default_password also in case of autoupdate.
response = super().retrieve(request, *args, **kwargs)
self.extract_default_password(response)
return response

View File

@ -0,0 +1,30 @@
class BaseAccessPermissions:
"""
Base access permissions container.
"""
def can_retrieve(self, user):
"""
Returns True if the user has read access model instances.
"""
return False
def get_serializer_class(self, user):
"""
Returns different serializer classes according to users permissions.
"""
raise NotImplementedError(
"You have to add the classmethod 'get_serializer_class' to your "
"access permissions class.".format(self))
def get_serialized_data(self, instance, user):
"""
Returns the serialized data for the instance prepared for the user.
Returns None if the user has no read access.
"""
if self.can_retrieve(user):
serializer_class = self.get_serializer_class(user)
data = serializer_class(instance).data
else:
data = None
return data

View File

@ -1,15 +1,12 @@
import json
import os
import posixpath
from importlib import import_module
from urllib.parse import unquote
from django.conf import settings
from openslides.users.auth import get_user
from django.core.wsgi import get_wsgi_application
from django.utils.importlib import import_module
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 +16,8 @@ from tornado.web import (
StaticFileHandler,
)
from tornado.wsgi import WSGIContainer
from .rest_api import get_collection_and_id_from_url
from openslides.users.auth import AnonymousUser, get_user
RUNNING_HOST = None
RUNNING_PORT = None
@ -59,9 +57,6 @@ class DjangoStaticFileHandler(StaticFileHandler):
return absolute_path
class FakeRequest:
pass
class OpenSlidesSockJSConnection(SockJSConnection):
"""
SockJS connection for OpenSlides.
@ -80,36 +75,29 @@ class OpenSlidesSockJSConnection(SockJSConnection):
"""
Sends an OpenSlides object to all connected clients (waiters).
"""
# Send out internal HTTP request to get data from the REST api.
for waiter in cls.waiters:
# Read waiter's former cookies and parse session cookie to new header object.
headers = HTTPHeaders()
# 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 so use anonymous user here.
user = AnonymousUser()
else:
# Get session from session store and use it to retrieve the user.
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore(session_cookie)
request = FakeRequest()
request.session = session
user = get_user(request)
serializer_class = instance.access_permissions.get_serializer_class(user)
serialized_instance_data = serializer_class(instance).data
session = engine.SessionStore(session_cookie.value)
fake_request = type('FakeRequest', (), {})()
fake_request.session = session
user = get_user(fake_request)
# Fetch serialized data and send them out to the waiter (client).
serialized_instance_data = instance.get_access_permissions().get_serialized_data(instance, user)
if serialized_instance_data is not None:
data = {
'url': "foobar",
'status_code': 404 if is_delete else 200,
'collection': instance.get_collection_name(),
'id': instance.id,
'status_code': 404 if is_delete else 200, # TODO: Refactor this. Use strings like 'change' or 'delete'.
'collection': instance.get_collection_string(),
'id': instance.get_rest_pk(),
'data': serialized_instance_data}
waiter.send(data)
except KeyError:
# There is no session cookie
pass
else:
headers.add('Cookie', '%s=%s' % (settings.SESSION_COOKIE_NAME, session_cookie.value))
def run_tornado(addr, port, *args, **kwargs):
@ -152,22 +140,28 @@ 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.
"""
root_instances = set()
if settings.USE_TORNADO_AS_WSGI_SERVER:
for instance in args:
try:
root_instances.add(instance.get_root_rest_element())
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 root_instance in root_instances:
else:
if is_delete and instance == root_instance:
# A root instance is deleted.
OpenSlidesSockJSConnection.send_object(root_instance, is_delete)
else:
# A non root instance is deleted or any instance is just changed.
root_instance.refresh_from_db()
OpenSlidesSockJSConnection.send_object(root_instance, False)
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):

View File

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

View File

@ -101,20 +101,17 @@ class PermissionMixin:
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.
"""
Django REST Framework's permission system is disabled.
def get_serializer_class(self):
Also connects container to handle access permissions for model and
viewset.
"""
TODO
"""
serializer_class = self.access_permissions.get_serializer_class(self.request.user) if self.access_permissions is not None else None
return super().get_serializer_class() if serializer_class is None else serializer_class
access_permissions = None
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
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.
"""
@ -126,6 +123,8 @@ class PermissionMixin:
"""
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
@ -144,6 +143,24 @@ class PermissionMixin:
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):
"""
@ -172,7 +189,7 @@ class GenericViewSet(PermissionMixin, _GenericViewSet):
class ModelViewSet(PermissionMixin, _ModelViewSet):
access_permissions = None
pass
class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
@ -183,12 +200,13 @@ class ViewSet(PermissionMixin, _ViewSet):
pass
#TODO: Remove this method
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/')
For example get_collection_and_id_from_url('http://localhost/rest/users/user/3/')
returns ('users/user', '3').
Raises OpenSlidesError if the URL is invalid.
@ -196,5 +214,5 @@ def get_collection_and_id_from_url(url):
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)
raise OpenSlidesError('Invalid REST API URL: %s' % url)
return match.group('collection'), match.group('id')

View File

@ -10,7 +10,3 @@ def to_roman(number):
return roman.toRoman(number)
except (roman.NotIntegerError, roman.OutOfRangeError):
return None
def collection_name(model_class):
return "{1}/{2}".format(model_class.Meta.app_label.lower(), model_class.Meta.object_name)