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')
def get_serializer_class(self, user):
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import 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,
get_collection_and_id_from_url,
)
from openslides.utils.rest_api import ModelSerializer, RelatedField
from .models import Item, Speaker
@ -34,10 +28,7 @@ class RelatedItemRelatedField(RelatedField):
Returns info concerning the related object extracted from the api URL
of this object.
"""
view_name = '%s-detail' % type(value)._meta.object_name.lower()
url = reverse(view_name, kwargs={'pk': value.pk})
collection, obj_id = get_collection_and_id_from_url(url)
return {'collection': collection, 'id': obj_id}
return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
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
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

@ -11,14 +11,27 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
"""
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.
"""
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
else:
serializer_class = AssignmentShortSerializer
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.
# 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

View File

@ -11,7 +11,7 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
"""
return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user):
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
@ -30,7 +30,7 @@ class CustomSlideAccessPermissions(BaseAccessPermissions):
"""
return user.has_perm('core.can_manage_projector')
def get_serializer_class(self, user):
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
@ -53,7 +53,7 @@ class TagAccessPermissions(BaseAccessPermissions):
# so if they are enabled.
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.
"""
@ -74,7 +74,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
# 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):
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
@ -98,12 +98,12 @@ class ConfigAccessPermissions(BaseAccessPermissions):
# 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):
def get_full_data(self, instance):
"""
Returns the serlialized config data or None if the user is not
allowed to see it.
Returns the serlialized config data.
"""
from .config import config
if self.can_retrieve(user) is not None:
return {'key': instance.key, 'value': config[instance.key]}
# 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]}

View File

@ -2,6 +2,7 @@
# Generated by Django 1.9.2 on 2016-03-02 01:22
from __future__ import unicode_literals
import json
import uuid
import django.db.models.deletion
@ -12,18 +13,15 @@ from django.db import migrations, models
import openslides.utils.models
def add_default_projector(apps, schema_editor):
def add_default_projector_via_sql():
"""
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[uuid.uuid4().hex] = {
'name': 'core/clock',
'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):
@ -107,9 +105,5 @@ class Migration(migrations.Migration):
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.RunPython(
code=add_default_projector,
reverse_code=None,
atomic=True,
),
migrations.RunSQL(add_default_projector_via_sql()),
]

View File

@ -485,7 +485,7 @@ class ConfigViewSet(ViewSet):
except ConfigNotFound:
raise Http404
# 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})
def update(self, request, *args, **kwargs):

View File

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

View File

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

View File

@ -11,16 +11,39 @@ class UserAccessPermissions(BaseAccessPermissions):
"""
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.
"""
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
# permissions.
serializer_class = UserFullSerializer
else:
serializer_class = UserShortSerializer
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
class UserShortSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes only name fields and about me field.
"""
class Meta:
model = User
fields = (
USERSHORTSERIALIZER_FIELDS = (
'id',
'username',
'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):
"""
Serializer for users.models.User objects.

View File

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

@ -1,30 +1,77 @@
class BaseAccessPermissions:
from django.dispatch import Signal
from .dispatch import SignalConnectMetaClass
class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
"""
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):
"""
Returns True if the user has read access model instances.
Returns True if the user has read access to model instances.
"""
return False
def get_serializer_class(self, user):
def get_serializer_class(self, user=None):
"""
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(
"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))
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):
serializer_class = self.get_serializer_class(user)
data = serializer_class(instance).data
data = full_data
else:
data = None
return data

View File

@ -1,3 +1,4 @@
import json
import os
import posixpath
from importlib import import_module
@ -17,7 +18,8 @@ from tornado.web import (
)
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_PORT = None
@ -71,10 +73,21 @@ class OpenSlidesSockJSConnection(SockJSConnection):
OpenSlidesSockJSConnection.waiters.remove(self)
@classmethod
def send_object(cls, instance, is_delete):
def send_object(cls, json_container):
"""
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:
# Read waiter's former cookies and parse session cookie to get user instance.
try:
@ -89,15 +102,28 @@ class OpenSlidesSockJSConnection(SockJSConnection):
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 = {
'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)
# Two cases: models instance was changed or deleted
if container.get('action') == 'changed':
data = access_permissions.get_restricted_data(container.get('full_data'), user)
if data is None:
# There are no data for the user so he can't see the object. Skip him.
break
output = {
'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):
@ -152,13 +178,20 @@ def inform_changed_data(is_delete, *args):
# Instance has no method get_root_rest_element. Just skip it.
pass
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:
# A root instance is deleted.
OpenSlidesSockJSConnection.send_object(root_instance, is_delete)
container['action'] = 'deleted'
else:
# A non root instance is deleted or any instance is just changed.
container['action'] = 'changed'
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:
pass
# TODO: Implement big variant with Apache or Nginx as WSGI webserver.

View File

@ -1,6 +1,4 @@
import re
from collections import OrderedDict
from urllib.parse import urlparse
from rest_framework import status # noqa
from rest_framework.decorators import detail_route, list_route # noqa
@ -35,8 +33,6 @@ from rest_framework.viewsets import \
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
from .exceptions import OpenSlidesError
router = DefaultRouter()
@ -198,21 +194,3 @@ class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
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/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.core.models import CustomSlide
from openslides.users.models import User
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.test import TestCase
@ -6,8 +7,8 @@ from openslides.utils.test import TestCase
class ListOfSpeakerModelTests(TestCase):
def setUp(self):
self.item1 = Item.objects.create(title='item1')
self.item2 = Item.objects.create(title='item2')
self.item1 = CustomSlide.objects.create(title='item1').agenda_item
self.item2 = CustomSlide.objects.create(title='item2').agenda_item
self.speaker1 = User.objects.create(username='user1')
self.speaker2 = User.objects.create(username='user2')

View File

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