From 3db2f2fc1629ac965dd16d83c06a676243e431a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=B6hlke?= Date: Thu, 11 Feb 2016 11:29:19 +0100 Subject: [PATCH] "durchstich" for autoupdate optimization --- openslides/agenda/access_permissions.py | 9 ++ openslides/agenda/views.py | 6 +- openslides/assignments/access_permissions.py | 18 ++++ openslides/assignments/apps.py | 3 +- openslides/assignments/models.py | 2 + openslides/assignments/views.py | 16 +-- openslides/core/access_permissions.py | 9 ++ openslides/core/apps.py | 6 +- openslides/core/views.py | 6 +- openslides/utils/autoupdate.py | 101 +++++++------------ openslides/utils/models.py | 6 ++ openslides/utils/rest_api.py | 53 +++++----- openslides/utils/utils.py | 4 + 13 files changed, 134 insertions(+), 105 deletions(-) create mode 100644 openslides/agenda/access_permissions.py create mode 100644 openslides/assignments/access_permissions.py create mode 100644 openslides/core/access_permissions.py diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py new file mode 100644 index 000000000..173dfbf58 --- /dev/null +++ b/openslides/agenda/access_permissions.py @@ -0,0 +1,9 @@ +class AccessPermissions: + def get_serializer_class(self, user): + return None + + def can_retrieve(self, user): + """ + TODO + """ + return user.has_perm('assignments.can_see') diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 3881170dd..d680d5f92 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -7,6 +7,7 @@ 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 ( @@ -36,12 +37,15 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV """ 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 in ('metadata', 'list', 'retrieve', 'manage_speaker', 'tree'): + if self.action == 'retrieve': + result = self.access_permissions.can_retrieve(self.request.user) + elif self.action in ('metadata', 'list', 'manage_speaker', 'tree'): result = self.request.user.has_perm('agenda.can_see') # For manage_speaker and tree requests the rest of the check is # done in the specific method. See below. diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py new file mode 100644 index 000000000..f31b0c528 --- /dev/null +++ b/openslides/assignments/access_permissions.py @@ -0,0 +1,18 @@ + +class AccessPermissions: + def get_serializer_class(self, user): + """ + Returns different serializer classes according to users permissions. + """ + from openslides.assignments.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') diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index d439ca00d..7fa797c72 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -12,7 +12,6 @@ class AssignmentsAppConfig(AppConfig): # Load projector elements. # Do this by just importing all from these files. from . import projector # noqa - # Import all required stuff. from openslides.core.signals import config_signal from openslides.utils.rest_api import router @@ -23,5 +22,5 @@ class AssignmentsAppConfig(AppConfig): config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config') # Register viewsets. - router.register('assignments/assignment', AssignmentViewSet) + router.register(self.get_model('Assignment').get_collection_name(), AssignmentViewSet) router.register('assignments/poll', AssignmentPollViewSet) diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index 2791343f0..f32ef95f3 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -7,6 +7,7 @@ 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 ( @@ -49,6 +50,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model): class Assignment(RESTModelMixin, models.Model): + access_permissions = AccessPermissions() PHASE_SEARCH = 0 PHASE_VOTING = 1 diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index f6463dea0..7071b1db6 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -17,6 +17,7 @@ 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 ( @@ -49,12 +50,15 @@ class AssignmentViewSet(ModelViewSet): mark_elected and create_poll. """ queryset = Assignment.objects.all() + access_permissions = AccessPermissions() 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.access_permissions.can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('assignments.can_see') elif self.action in ('create', 'partial_update', 'update', 'destroy', 'mark_elected', 'create_poll'): @@ -70,16 +74,6 @@ class AssignmentViewSet(ModelViewSet): result = False return result - def get_serializer_class(self): - """ - Returns different serializer classes according to users permissions. - """ - if self.request.user.has_perm('assignments.can_manage'): - serializer_class = AssignmentFullSerializer - else: - serializer_class = AssignmentShortSerializer - return serializer_class - @detail_route(methods=['post', 'delete']) def candidature_self(self, request, pk=None): """ diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py new file mode 100644 index 000000000..38985369b --- /dev/null +++ b/openslides/core/access_permissions.py @@ -0,0 +1,9 @@ +class AccessPermissions: + def get_serializer_class(self, user): + return None + + def can_retrieve(self, user): + """ + TODO + """ + return user.has_perm('core.can_see_projector') diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 3889274b4..ef99c6d88 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -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.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 @@ -49,8 +49,8 @@ class CoreAppConfig(AppConfig): inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') signals.post_delete.connect( - inform_changed_data_receiver, - dispatch_uid='inform_changed_data_receiver') + inform_deleted_data_receiver, + dispatch_uid='inform_deleted_data_receiver') # Update the search when a model is saved or deleted signals.post_save.connect( diff --git a/openslides/core/views.py b/openslides/core/views.py index bbb98fc4c..52f3e0575 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -13,6 +13,7 @@ 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, @@ -154,12 +155,15 @@ class ProjectorViewSet(ReadOnlyModelViewSet): """ 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 in ('metadata', 'list', 'retrieve'): + if self.action == 'retrieve': + result = self.access_permissions.can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('core.can_see_projector') elif self.action in ('activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements', 'control_view'): diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 556199a53..d530bf590 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -2,9 +2,10 @@ import json import os import posixpath 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 @@ -18,7 +19,6 @@ from tornado.web import ( StaticFileHandler, ) from tornado.wsgi import WSGIContainer - from .rest_api import get_collection_and_id_from_url RUNNING_HOST = None @@ -59,6 +59,9 @@ class DjangoStaticFileHandler(StaticFileHandler): return absolute_path +class FakeRequest: + pass + class OpenSlidesSockJSConnection(SockJSConnection): """ SockJS connection for OpenSlides. @@ -72,78 +75,41 @@ class OpenSlidesSockJSConnection(SockJSConnection): def on_close(self): OpenSlidesSockJSConnection.waiters.remove(self) - def forward_rest_response(self, response): - """ - Sends data to the client of the connection instance. - - This method is called after succesful response of AsyncHTTPClient(). - See send_object(). - """ - if response.code in (200, 404): - # Only send something to the client in case of one of these status - # codes. You have to change the client code (autoupdate.onMessage) - # if you want to handle some more codes. - collection, obj_id = get_collection_and_id_from_url(response.request.url) - data = { - 'url': response.request.url, - 'status_code': response.code, - 'collection': collection, - 'id': obj_id, - 'data': json.loads(response.body.decode())} - self.send(data) - @classmethod - def send_object(cls, object_url): + def send_object(cls, instance, is_delete): """ Sends an OpenSlides object to all connected clients (waiters). - - First, retrieve the object from the OpenSlides REST api using the given - object_url. """ - # Join network location with object URL. - if settings.OPENSLIDES_WSGI_NETWORK_LOCATION: - wsgi_network_location = settings.OPENSLIDES_WSGI_NETWORK_LOCATION - else: - if RUNNING_HOST == '0.0.0.0': - # Windows can not connect to 0.0.0.0, so connect to localhost instead. - wsgi_network_location = 'http://localhost:{}'.format(RUNNING_PORT) - else: - wsgi_network_location = 'http://{}:{}'.format(RUNNING_HOST, RUNNING_PORT) - url = ''.join((wsgi_network_location, object_url)) - # Send out internal HTTP request to get data from the REST api. for waiter in cls.waiters: - # Initiat new headers object. - headers = HTTPHeaders() - # Read waiter's former cookies and parse session cookie to new header object. + headers = HTTPHeaders() try: session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME] + 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 + + data = { + 'url': "foobar", + 'status_code': 404 if is_delete else 200, + 'collection': instance.get_collection_name(), + 'id': instance.id, + '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)) - # Read waiter's language header. - try: - languages = waiter.connection_info.headers['Accept-Language'] - except KeyError: - # There is no language header - pass - else: - headers.parse_line('Accept-Language: ' + languages) - # Setup uncompressed request. - request = HTTPRequest( - url=url, - headers=headers, - decompress_response=False) - # Setup non-blocking HTTP client - http_client = AsyncHTTPClient() - # Executes the request, asynchronously returning an HTTPResponse - # and calling waiter's forward_rest_response() method. - http_client.fetch(request, waiter.forward_rest_response) def run_tornado(addr, port, *args, **kwargs): @@ -182,23 +148,23 @@ def run_tornado(addr, port, *args, **kwargs): RUNNING_PORT = None -def inform_changed_data(*args): +def inform_changed_data(is_delete, *args): """ Informs all users about changed data. The arguments are Django/OpenSlides models. """ - rest_urls = set() + root_instances = set() for instance in args: try: - rest_urls.add(instance.get_root_rest_url()) + root_instances.add(instance.get_root_rest_element()) except AttributeError: # Instance has no method get_root_rest_url. Just skip it. pass if settings.USE_TORNADO_AS_WSGI_SERVER: - for url in rest_urls: - OpenSlidesSockJSConnection.send_object(url) + for root_instance in root_instances: + OpenSlidesSockJSConnection.send_object(root_instance, is_delete) else: pass # TODO: Implement big varainte with Apache or Nginx as wsgi webserver. @@ -208,4 +174,11 @@ def inform_changed_data_receiver(sender, instance, **kwargs): """ Receiver for the inform_changed_data function to use in a signal. """ - inform_changed_data(instance) + inform_changed_data(False, instance) + + +def inform_deleted_data_receiver(sender, instance, **kwargs): + """ + Receiver for the inform_changed_data function to use in a signal. + """ + inform_changed_data(True, instance) diff --git a/openslides/utils/models.py b/openslides/utils/models.py index 43c6643fc..bc7d0f0ea 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -22,6 +22,12 @@ class RESTModelMixin: 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. diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index c2cdb2fb1..58d587bfd 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -95,28 +95,6 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField): return IdManyRelatedField(**list_kwargs) -class ModelSerializer(_ModelSerializer): - """ - ModelSerializer that changes the field names of related fields to - FIELD_NAME_id. - """ - serializer_related_field = IdPrimaryKeyRelatedField - - def get_fields(self): - """ - Returns all fields of the serializer. - """ - fields = OrderedDict() - - for field_name, field in super().get_fields().items(): - try: - field_name += field.field_name_suffix - except AttributeError: - pass - fields[field_name] = field - return fields - - class PermissionMixin: """ Mixin for subclasses of APIView like GenericViewSet and ModelViewSet. @@ -126,6 +104,13 @@ class PermissionMixin: Django REST framework's permission system is disabled. """ + def get_serializer_class(self): + """ + 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 + def get_permissions(self): """ Overriden method to check view and projector permissions. Returns an @@ -160,12 +145,34 @@ class PermissionMixin: return result +class ModelSerializer(_ModelSerializer): + """ + ModelSerializer that changes the field names of related fields to + FIELD_NAME_id. + """ + serializer_related_field = IdPrimaryKeyRelatedField + + def get_fields(self): + """ + Returns all fields of the serializer. + """ + fields = OrderedDict() + + for field_name, field in super().get_fields().items(): + try: + field_name += field.field_name_suffix + except AttributeError: + pass + fields[field_name] = field + return fields + + class GenericViewSet(PermissionMixin, _GenericViewSet): pass class ModelViewSet(PermissionMixin, _ModelViewSet): - pass + access_permissions = None class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet): diff --git a/openslides/utils/utils.py b/openslides/utils/utils.py index b2d01f01e..620083636 100644 --- a/openslides/utils/utils.py +++ b/openslides/utils/utils.py @@ -10,3 +10,7 @@ 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)