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:
parent
4daa61888f
commit
132c6e81ec
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]}
|
||||
|
@ -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()),
|
||||
]
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user