"durchstich" for autoupdate optimization

This commit is contained in:
André Böhlke 2016-02-11 11:29:19 +01:00 committed by Norman Jäckel
parent e598b308f0
commit 3db2f2fc16
13 changed files with 134 additions and 105 deletions

View File

@ -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')

View File

@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from openslides.core.config import config from openslides.core.config import config
from openslides.agenda.access_permissions import AccessPermissions
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
@ -36,12 +37,15 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
queryset = Item.objects.all() queryset = Item.objects.all()
serializer_class = ItemSerializer serializer_class = ItemSerializer
access_permissions = AccessPermissions()
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.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.

View File

@ -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')

View File

@ -12,7 +12,6 @@ class AssignmentsAppConfig(AppConfig):
# Load projector elements. # Load projector elements.
# Do this by just importing all from these files. # Do this by just importing all from these files.
from . import projector # noqa from . import projector # noqa
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import config_signal from openslides.core.signals import config_signal
from openslides.utils.rest_api import router 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') 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_name(), AssignmentViewSet)
router.register('assignments/poll', AssignmentPollViewSet) router.register('assignments/poll', AssignmentPollViewSet)

View File

@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.assignments.access_permissions import AccessPermissions
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Tag from openslides.core.models import Tag
from openslides.poll.models import ( from openslides.poll.models import (
@ -49,6 +50,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
class Assignment(RESTModelMixin, models.Model): class Assignment(RESTModelMixin, models.Model):
access_permissions = AccessPermissions()
PHASE_SEARCH = 0 PHASE_SEARCH = 0
PHASE_VOTING = 1 PHASE_VOTING = 1

View File

@ -17,6 +17,7 @@ from reportlab.platypus import (
TableStyle, TableStyle,
) )
from openslides.assignments.access_permissions import AccessPermissions
from openslides.core.config import config from openslides.core.config import config
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
@ -49,12 +50,15 @@ class AssignmentViewSet(ModelViewSet):
mark_elected and create_poll. mark_elected and create_poll.
""" """
queryset = Assignment.objects.all() queryset = Assignment.objects.all()
access_permissions = AccessPermissions()
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.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 +74,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):
""" """

View File

@ -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')

View File

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

View File

@ -13,6 +13,7 @@ from django.http import Http404, HttpResponse
from django.utils.timezone import now from django.utils.timezone import now
from openslides import __version__ as version from openslides import __version__ as version
from openslides.core.access_permissions import AccessPermissions
from openslides.utils import views as utils_views from openslides.utils import views as utils_views
from openslides.utils.plugins import ( from openslides.utils.plugins import (
get_plugin_description, get_plugin_description,
@ -154,12 +155,15 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
""" """
queryset = Projector.objects.all() queryset = Projector.objects.all()
serializer_class = ProjectorSerializer serializer_class = ProjectorSerializer
access_permissions = AccessPermissions()
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.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'):

View File

@ -2,9 +2,10 @@ import json
import os import os
import posixpath import posixpath
from urllib.parse import unquote from urllib.parse import unquote
from django.conf import settings from django.conf import settings
from openslides.users.auth import get_user
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
from django.utils.importlib import import_module
from sockjs.tornado import SockJSConnection, SockJSRouter from sockjs.tornado import SockJSConnection, SockJSRouter
from tornado.httpclient import AsyncHTTPClient, HTTPRequest from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
@ -18,7 +19,6 @@ from tornado.web import (
StaticFileHandler, StaticFileHandler,
) )
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from .rest_api import get_collection_and_id_from_url from .rest_api import get_collection_and_id_from_url
RUNNING_HOST = None RUNNING_HOST = None
@ -59,6 +59,9 @@ class DjangoStaticFileHandler(StaticFileHandler):
return absolute_path return absolute_path
class FakeRequest:
pass
class OpenSlidesSockJSConnection(SockJSConnection): class OpenSlidesSockJSConnection(SockJSConnection):
""" """
SockJS connection for OpenSlides. SockJS connection for OpenSlides.
@ -72,78 +75,41 @@ 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, instance, is_delete):
""" """
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.
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. # Send out internal HTTP request to get data from the REST api.
for waiter in cls.waiters: for waiter in cls.waiters:
# Initiat new headers object.
headers = HTTPHeaders()
# Read waiter's former cookies and parse session cookie to new header object. # Read waiter's former cookies and parse session cookie to new header object.
headers = HTTPHeaders()
try: try:
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME] 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: except KeyError:
# There is no session cookie # There is no session cookie
pass pass
else: else:
headers.add('Cookie', '%s=%s' % (settings.SESSION_COOKIE_NAME, session_cookie.value)) 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): def run_tornado(addr, port, *args, **kwargs):
@ -182,23 +148,23 @@ 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 arguments are Django/OpenSlides models.
""" """
rest_urls = set() root_instances = set()
for instance in args: for instance in args:
try: try:
rest_urls.add(instance.get_root_rest_url()) root_instances.add(instance.get_root_rest_element())
except AttributeError: except AttributeError:
# Instance has no method get_root_rest_url. Just skip it. # Instance has no method get_root_rest_url. Just skip it.
pass pass
if settings.USE_TORNADO_AS_WSGI_SERVER: if settings.USE_TORNADO_AS_WSGI_SERVER:
for url in rest_urls: for root_instance in root_instances:
OpenSlidesSockJSConnection.send_object(url) OpenSlidesSockJSConnection.send_object(root_instance, is_delete)
else: else:
pass pass
# TODO: Implement big varainte with Apache or Nginx as wsgi webserver. # 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. Receiver for the inform_changed_data function to use in a signal.
""" """
inform_changed_data(instance) inform_changed_data(False, instance)
def inform_deleted_data_receiver(sender, instance, **kwargs):
"""
Receiver for the inform_changed_data function to use in a signal.
"""
inform_changed_data(True, instance)

View File

@ -22,6 +22,12 @@ 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): def get_root_rest_element(self):
""" """
Returns the root rest instance. Returns the root rest instance.

View File

@ -95,28 +95,6 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
return IdManyRelatedField(**list_kwargs) 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: class PermissionMixin:
""" """
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet. Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
@ -126,6 +104,13 @@ class PermissionMixin:
Django REST framework's permission system is disabled. 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): def get_permissions(self):
""" """
Overriden method to check view and projector permissions. Returns an Overriden method to check view and projector permissions. Returns an
@ -160,12 +145,34 @@ class PermissionMixin:
return result 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): class GenericViewSet(PermissionMixin, _GenericViewSet):
pass pass
class ModelViewSet(PermissionMixin, _ModelViewSet): class ModelViewSet(PermissionMixin, _ModelViewSet):
pass access_permissions = None
class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet): class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):

View File

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