Forwarding JSON instead of Django model instances to autoupdate loop.

- Used raw SQL for createing default projector during inital migration.
- Removed default_password and hidden agenda items from autoupdate data for some users.
- Removed old get_collection_and_id_from_url() function.
This commit is contained in:
Norman Jäckel 2016-03-02 00:46:19 +01:00
parent 4daa61888f
commit 132c6e81ec
18 changed files with 193 additions and 99 deletions

View File

@ -11,10 +11,23 @@ class ItemAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('agenda.can_see') return user.has_perm('agenda.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
from .serializers import ItemSerializer from .serializers import ItemSerializer
return ItemSerializer return ItemSerializer
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user.
"""
if (self.can_retrieve(user) and
(not full_data['is_hidden'] or
user.has_perm('agenda.can_see_hidden_items'))):
data = full_data
else:
data = None
return data

View File

@ -1,10 +1,4 @@
from django.core.urlresolvers import reverse from openslides.utils.rest_api import ModelSerializer, RelatedField
from openslides.utils.rest_api import (
ModelSerializer,
RelatedField,
get_collection_and_id_from_url,
)
from .models import Item, Speaker from .models import Item, Speaker
@ -34,10 +28,7 @@ class RelatedItemRelatedField(RelatedField):
Returns info concerning the related object extracted from the api URL Returns info concerning the related object extracted from the api URL
of this object. of this object.
""" """
view_name = '%s-detail' % type(value)._meta.object_name.lower() return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
url = reverse(view_name, kwargs={'pk': value.pk})
collection, obj_id = get_collection_and_id_from_url(url)
return {'collection': collection, 'id': obj_id}
class ItemSerializer(ModelSerializer): class ItemSerializer(ModelSerializer):

View File

@ -63,7 +63,6 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
Checks if the requesting user has permission to see also an Checks if the requesting user has permission to see also an
organizational item if it is one. organizational item if it is one.
""" """
#TODO
if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'): if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'):
self.permission_denied(request) self.permission_denied(request)

View File

@ -11,14 +11,27 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('assignments.can_see') return user.has_perm('assignments.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns different serializer classes according to users permissions. Returns different serializer classes according to users permissions.
""" """
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
if user.has_perm('assignments.can_manage'): if user is None or user.has_perm('assignments.can_manage'):
serializer_class = AssignmentFullSerializer serializer_class = AssignmentFullSerializer
else: else:
serializer_class = AssignmentShortSerializer serializer_class = AssignmentShortSerializer
return serializer_class return serializer_class
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user. Removes unpublushed polls for non admins so that they
only get a result like the AssignmentShortSerializer would give them.
"""
if user.has_perm('assignments.can_manage'):
data = full_data
else:
data = full_data.copy()
data['polls'] = [poll for poll in data['polls'] if poll['published']]
return data

View File

@ -12,6 +12,7 @@ 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

View File

@ -11,7 +11,7 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('core.can_see_projector') return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -30,7 +30,7 @@ class CustomSlideAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('core.can_manage_projector') return user.has_perm('core.can_manage_projector')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -53,7 +53,7 @@ class TagAccessPermissions(BaseAccessPermissions):
# so if they are enabled. # so if they are enabled.
return user.is_authenticated() or config['general_system_enable_anonymous'] return user.is_authenticated() or config['general_system_enable_anonymous']
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -74,7 +74,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
# permission core.can_use_chat. But they can not use it. See views.py. # permission core.can_use_chat. But they can not use it. See views.py.
return user.has_perm('core.can_use_chat') return user.has_perm('core.can_use_chat')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -98,12 +98,12 @@ class ConfigAccessPermissions(BaseAccessPermissions):
# the config. Anonymous users can do so if they are enabled. # the config. Anonymous users can do so if they are enabled.
return user.is_authenticated() or config['general_system_enable_anonymous'] return user.is_authenticated() or config['general_system_enable_anonymous']
def get_serialized_data(self, instance, user): def get_full_data(self, instance):
""" """
Returns the serlialized config data or None if the user is not Returns the serlialized config data.
allowed to see it.
""" """
from .config import config from .config import config
if self.can_retrieve(user) is not None: # Attention: The format of this response has to be the same as in
# the retrieve method of ConfigViewSet.
return {'key': instance.key, 'value': config[instance.key]} return {'key': instance.key, 'value': config[instance.key]}

View File

@ -2,6 +2,7 @@
# Generated by Django 1.9.2 on 2016-03-02 01:22 # Generated by Django 1.9.2 on 2016-03-02 01:22
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import uuid import uuid
import django.db.models.deletion import django.db.models.deletion
@ -12,18 +13,15 @@ from django.db import migrations, models
import openslides.utils.models import openslides.utils.models
def add_default_projector(apps, schema_editor): def add_default_projector_via_sql():
""" """
Adds default projector and activates clock. Adds default projector and activates clock.
""" """
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
Projector = apps.get_model('core', 'Projector')
projector_config = {} projector_config = {}
projector_config[uuid.uuid4().hex] = { projector_config[uuid.uuid4().hex] = {
'name': 'core/clock', 'name': 'core/clock',
'stable': True} 'stable': True}
Projector.objects.create(config=projector_config) return ["INSERT INTO core_projector (config, scale, scroll) VALUES ('{}', 0, 0);".format(json.dumps(projector_config))]
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -107,9 +105,5 @@ class Migration(migrations.Migration):
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.RunPython( migrations.RunSQL(add_default_projector_via_sql()),
code=add_default_projector,
reverse_code=None,
atomic=True,
),
] ]

View File

@ -485,7 +485,7 @@ class ConfigViewSet(ViewSet):
except ConfigNotFound: except ConfigNotFound:
raise Http404 raise Http404
# Attention: The format of this response has to be the same as in # Attention: The format of this response has to be the same as in
# the get_serialized_data method of ConfigAccessPermissions. # the get_full_data method of ConfigAccessPermissions.
return Response({'key': key, 'value': value}) return Response({'key': key, 'value': value})
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):

View File

@ -11,7 +11,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('mediafiles.can_see') return user.has_perm('mediafiles.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """

View File

@ -11,7 +11,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('motions.can_see') return user.has_perm('motions.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -30,7 +30,7 @@ class CategoryAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('motions.can_see') return user.has_perm('motions.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """
@ -49,7 +49,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('motions.can_see') return user.has_perm('motions.can_see')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns serializer class. Returns serializer class.
""" """

View File

@ -11,16 +11,39 @@ class UserAccessPermissions(BaseAccessPermissions):
""" """
return user.has_perm('users.can_see_name') return user.has_perm('users.can_see_name')
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns different serializer classes with respect user's permissions. Returns different serializer classes with respect user's permissions.
""" """
from .serializers import UserFullSerializer, UserShortSerializer from .serializers import UserFullSerializer, UserShortSerializer
if user.has_perm('users.can_see_extra_data'): if user is None or user.has_perm('users.can_see_extra_data'):
# Return the UserFullSerializer for requests of users with more # Return the UserFullSerializer for requests of users with more
# permissions. # permissions.
serializer_class = UserFullSerializer serializer_class = UserFullSerializer
else: else:
serializer_class = UserShortSerializer serializer_class = UserShortSerializer
return serializer_class return serializer_class
def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user. Removes several fields for non admins so that they do
not get the default_password or even get only the fields as the
UserShortSerializer would give them.
"""
from .serializers import USERSHORTSERIALIZER_FIELDS
if user.has_perm('users.can_manage'):
data = full_data
elif user.has_perm('users.can_see_extra_data'):
# Only remove default password from full data.
data = full_data.copy()
del data['default_password']
else:
# Let only fields as in the UserShortSerializer pass this method.
data = {}
for key in full_data.keys():
if key in USERSHORTSERIALIZER_FIELDS:
data[key] = full_data[key]
return data

View File

@ -11,16 +11,7 @@ from ..utils.rest_api import (
) )
from .models import Group, User from .models import Group, User
USERSHORTSERIALIZER_FIELDS = (
class UserShortSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes only name fields and about me field.
"""
class Meta:
model = User
fields = (
'id', 'id',
'username', 'username',
'title', 'title',
@ -32,6 +23,17 @@ class UserShortSerializer(ModelSerializer):
) )
class UserShortSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes only name fields and about me field.
"""
class Meta:
model = User
fields = USERSHORTSERIALIZER_FIELDS
class UserFullSerializer(ModelSerializer): class UserFullSerializer(ModelSerializer):
""" """
Serializer for users.models.User objects. Serializer for users.models.User objects.

View File

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

View File

@ -1,30 +1,77 @@
class BaseAccessPermissions: from django.dispatch import Signal
from .dispatch import SignalConnectMetaClass
class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
""" """
Base access permissions container. Base access permissions container.
Every app which has autoupdate models has to create classes subclassing
from this base class for every autoupdate root model. Each subclass has
to have a globally unique name. The metaclass (SignalConnectMetaClass)
does the rest of the magic.
""" """
signal = Signal()
def __init__(self, **kwargs):
"""
Initializes the access permission instance. This is done when the
signal is sent.
Because of Django's signal API, we have to take wildcard keyword
arguments. But they are not used here.
"""
pass
@classmethod
def get_dispatch_uid(cls):
"""
Returns the classname as a unique string for each class. Returns None
for the base class so it will not be connected to the signal.
"""
if not cls.__name__ == 'BaseAccessPermissions':
return cls.__name__
def can_retrieve(self, user): def can_retrieve(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access to model instances.
""" """
return False return False
def get_serializer_class(self, user): def get_serializer_class(self, user=None):
""" """
Returns different serializer classes according to users permissions. Returns different serializer classes according to users permissions.
This should return the serializer for full data access if user is
None. See get_full_data().
""" """
raise NotImplementedError( raise NotImplementedError(
"You have to add the classmethod 'get_serializer_class' to your " "You have to add the method 'get_serializer_class' to your "
"access permissions class.".format(self)) "access permissions class.".format(self))
def get_serialized_data(self, instance, user): def get_full_data(self, instance):
""" """
Returns the serialized data for the instance prepared for the user. Returns all possible serialized data for the given instance.
"""
return self.get_serializer_class(user=None)(instance).data
Returns None if the user has no read access. def get_restricted_data(self, full_data, user):
"""
Returns the restricted serialized data for the instance prepared
for the user.
Returns None if the user has no read access. Returns reduced data
if the user has limited access. Default: Returns full data if the
user has read access to model instances.
Hint: You should override this method if your
get_serializer_class() method may return different serializer for
different users or if you have access restrictions in your view or
viewset in methods like retrieve() or check_object_permissions().
""" """
if self.can_retrieve(user): if self.can_retrieve(user):
serializer_class = self.get_serializer_class(user) data = full_data
data = serializer_class(instance).data
else: else:
data = None data = None
return data return data

View File

@ -1,3 +1,4 @@
import json
import os import os
import posixpath import posixpath
from importlib import import_module from importlib import import_module
@ -17,7 +18,8 @@ from tornado.web import (
) )
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from openslides.users.auth import AnonymousUser, get_user from ..users.auth import AnonymousUser, get_user
from .access_permissions import BaseAccessPermissions
RUNNING_HOST = None RUNNING_HOST = None
RUNNING_PORT = None RUNNING_PORT = None
@ -71,10 +73,21 @@ class OpenSlidesSockJSConnection(SockJSConnection):
OpenSlidesSockJSConnection.waiters.remove(self) OpenSlidesSockJSConnection.waiters.remove(self)
@classmethod @classmethod
def send_object(cls, instance, is_delete): def send_object(cls, json_container):
""" """
Sends an OpenSlides object to all connected clients (waiters). Sends an OpenSlides object to all connected clients (waiters).
""" """
# Load JSON
container = json.loads(json_container)
# Search our AccessPermission class.
for access_permissions in BaseAccessPermissions.get_all():
if access_permissions.get_dispatch_uid() == container.get('dispatch_uid'):
break
else:
raise ValueError('Invalid container. A valid dispatch_uid is missing.')
# Loop over all waiters
for waiter in cls.waiters: for waiter in cls.waiters:
# Read waiter's former cookies and parse session cookie to get user instance. # Read waiter's former cookies and parse session cookie to get user instance.
try: try:
@ -89,15 +102,28 @@ class OpenSlidesSockJSConnection(SockJSConnection):
fake_request = type('FakeRequest', (), {})() fake_request = type('FakeRequest', (), {})()
fake_request.session = session fake_request.session = session
user = get_user(fake_request) 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) # Two cases: models instance was changed or deleted
if serialized_instance_data is not None: if container.get('action') == 'changed':
data = { data = access_permissions.get_restricted_data(container.get('full_data'), user)
'status_code': 404 if is_delete else 200, # TODO: Refactor this. Use strings like 'change' or 'delete'. if data is None:
'collection': instance.get_collection_string(), # There are no data for the user so he can't see the object. Skip him.
'id': instance.get_rest_pk(), break
'data': serialized_instance_data} output = {
waiter.send(data) 'status_code': 200, # TODO: Refactor this. Use strings like 'change' or 'delete'.
'collection': container['collection_string'],
'id': container['rest_pk'],
'data': data}
elif container.get('action') == 'deleted':
output = {
'status_code': 404, # TODO: Refactor this. Use strings like 'change' or 'delete'.
'collection': container['collection_string'],
'id': container['rest_pk']}
else:
raise ValueError('Invalid container. A valid action is missing.')
# Send output to the waiter (client).
waiter.send(output)
def run_tornado(addr, port, *args, **kwargs): def run_tornado(addr, port, *args, **kwargs):
@ -152,13 +178,20 @@ def inform_changed_data(is_delete, *args):
# Instance has no method get_root_rest_element. Just skip it. # Instance has no method get_root_rest_element. Just skip it.
pass pass
else: else:
access_permissions = root_instance.get_access_permissions()
container = {
'dispatch_uid': access_permissions.get_dispatch_uid(),
'collection_string': root_instance.get_collection_string(),
'rest_pk': root_instance.get_rest_pk()}
if is_delete and instance == root_instance: if is_delete and instance == root_instance:
# A root instance is deleted. # A root instance is deleted.
OpenSlidesSockJSConnection.send_object(root_instance, is_delete) container['action'] = 'deleted'
else: else:
# A non root instance is deleted or any instance is just changed. # A non root instance is deleted or any instance is just changed.
container['action'] = 'changed'
root_instance.refresh_from_db() root_instance.refresh_from_db()
OpenSlidesSockJSConnection.send_object(root_instance, False) container['full_data'] = access_permissions.get_full_data(root_instance)
OpenSlidesSockJSConnection.send_object(json.dumps(container))
else: else:
pass pass
# TODO: Implement big variant with Apache or Nginx as WSGI webserver. # TODO: Implement big variant with Apache or Nginx as WSGI webserver.

View File

@ -1,6 +1,4 @@
import re
from collections import OrderedDict from collections import OrderedDict
from urllib.parse import urlparse
from rest_framework import status # noqa from rest_framework import status # noqa
from rest_framework.decorators import detail_route, list_route # noqa from rest_framework.decorators import detail_route, list_route # noqa
@ -35,8 +33,6 @@ from rest_framework.viewsets import \
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
from rest_framework.viewsets import ViewSet as _ViewSet # noqa from rest_framework.viewsets import ViewSet as _ViewSet # noqa
from .exceptions import OpenSlidesError
router = DefaultRouter() router = DefaultRouter()
@ -198,21 +194,3 @@ class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
class ViewSet(PermissionMixin, _ViewSet): class ViewSet(PermissionMixin, _ViewSet):
pass 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/rest/users/user/3/')
returns ('users/user', '3').
Raises OpenSlidesError if the URL is invalid.
"""
path = urlparse(url).path
match = re.match(r'^/rest/(?P<collection>[-\w]+/[-\w]+)/(?P<id>[-\w]+)/$', path)
if not match:
raise OpenSlidesError('Invalid REST API URL: %s' % url)
return match.group('collection'), match.group('id')

View File

@ -1,4 +1,5 @@
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.core.models import CustomSlide
from openslides.users.models import User from openslides.users.models import User
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -6,8 +7,8 @@ from openslides.utils.test import TestCase
class ListOfSpeakerModelTests(TestCase): class ListOfSpeakerModelTests(TestCase):
def setUp(self): def setUp(self):
self.item1 = Item.objects.create(title='item1') self.item1 = CustomSlide.objects.create(title='item1').agenda_item
self.item2 = Item.objects.create(title='item2') self.item2 = CustomSlide.objects.create(title='item2').agenda_item
self.speaker1 = User.objects.create(username='user1') self.speaker1 = User.objects.create(username='user1')
self.speaker2 = User.objects.create(username='user2') self.speaker2 = User.objects.create(username='user2')

View File

@ -36,7 +36,7 @@ class MediafileTest(TestCase):
os.close(tmpfile_no) os.close(tmpfile_no)
def tearDown(self): def tearDown(self):
self.object.mediafile.delete() self.object.mediafile.delete(save=False)
def test_str(self): def test_str(self):
self.assertEqual(str(self.object), 'Title File 1') self.assertEqual(str(self.object), 'Title File 1')