"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 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.

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.
# 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)

View File

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

View File

@ -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):
"""

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.
# 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(

View File

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

View File

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

View File

@ -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.

View File

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

View File

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