Refactoring of data parsing for startup and autoupdate.

This commit is contained in:
Norman Jäckel 2017-05-01 23:12:42 +02:00
parent 4963bfa7bf
commit ebabc291c5
16 changed files with 327 additions and 200 deletions

View File

@ -42,6 +42,8 @@ Core:
pdf [#3184, #3207, #3208]. pdf [#3184, #3207, #3208].
- Fixing error when clearing empty chat [#3199]. - Fixing error when clearing empty chat [#3199].
- Added notify system [#3212]. - Added notify system [#3212].
- Enhanced performance esp. for server restart and first connection of all
clients by refactorting autoupdate, Collection and AccessPermission [#3223].
General: General:
- Switched from npm to Yarn [#3188]. - Switched from npm to Yarn [#3188].

View File

@ -1,5 +1,6 @@
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import has_perm
from ..utils.collection import Collection
class ItemAccessPermissions(BaseAccessPermissions): class ItemAccessPermissions(BaseAccessPermissions):
@ -22,10 +23,13 @@ class ItemAccessPermissions(BaseAccessPermissions):
# TODO: In the following method we use full_data['is_hidden'] but this can be out of date. # TODO: In the following method we use full_data['is_hidden'] but this can be out of date.
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user.
We remove comments for non admins/managers and a lot of fields of
hidden items for users without permission to see hidden items.
""" """
def filtered_data(full_data, blocked_keys): def filtered_data(full_data, blocked_keys):
""" """
@ -34,35 +38,56 @@ class ItemAccessPermissions(BaseAccessPermissions):
whitelist = full_data.keys() - blocked_keys whitelist = full_data.keys() - blocked_keys
return {key: full_data[key] for key in whitelist} return {key: full_data[key] for key in whitelist}
# many_items is True, when there are more then one items in full_data. # Expand full_data to a list if it is not one.
many_items = not isinstance(full_data, dict) full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
full_data = full_data if many_items else [full_data]
# Parse data.
if has_perm(user, 'agenda.can_see'): if has_perm(user, 'agenda.can_see'):
if has_perm(user, 'agenda.can_manage'): if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_hidden_items'):
# Managers with special permission can see everything.
data = full_data data = full_data
elif has_perm(user, 'agenda.can_see_hidden_items'): elif has_perm(user, 'agenda.can_see_hidden_items'):
# Non managers with special permission can see everything but comments.
blocked_keys = ('comment',) blocked_keys = ('comment',)
data = [filtered_data(full, blocked_keys) for full in full_data] data = [filtered_data(full, blocked_keys) for full in full_data]
else: else:
data = [] # Users without special permissin for hidden items.
filtered_blocked_keys = full_data[0].keys() - (
# In hidden case managers and non managers see only some fields
# so that list of speakers is provided regardless.
blocked_keys_hidden_case = full_data[0].keys() - (
'id', 'id',
'title', 'title',
'speakers', 'speakers',
'speaker_list_closed', 'speaker_list_closed',
'content_object') 'content_object')
not_filtered_blocked_keys = ('comment',)
# In non hidden case managers see everything and non managers see
# everything but comments.
if has_perm(user, 'agenda.can_manage'):
blocked_keys_non_hidden_case = []
else:
blocked_keys_non_hidden_case = ('comment',)
data = []
for full in full_data: for full in full_data:
if full['is_hidden']: if full['is_hidden']:
blocked_keys = filtered_blocked_keys data.append(filtered_data(full, blocked_keys_hidden_case))
else: else:
blocked_keys = not_filtered_blocked_keys data.append(filtered_data(full, blocked_keys_non_hidden_case))
data.append(filtered_data(full, blocked_keys))
else: else:
data = None data = []
return data if many_items else data[0] # Reduce result to a single item or None if it was not a collection at
# the beginning of the method.
if isinstance(container, Collection):
restricted_data = data
elif data:
restricted_data = data[0]
else:
restricted_data = None
return restricted_data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):
""" """

View File

@ -54,10 +54,11 @@ def get_permission_change_data(sender, permissions, **kwargs):
break break
def is_user_data_required(sender, request_user, user_data, **kwargs): def is_user_data_required(sender, request_user, **kwargs):
""" """
If request_user can see the agenda, then returns all user ids that are Returns all user ids that are displayed as speakers in any agenda item
speakers in some agenda items. Else, it returns an empty set. if request_user can see the agenda. This function may return an empty
set.
""" """
speakers = set() speakers = set()
if has_perm(request_user, 'agenda.can_see'): if has_perm(request_user, 'agenda.can_see'):

View File

@ -1,5 +1,6 @@
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import has_perm
from ..utils.collection import Collection
class AssignmentAccessPermissions(BaseAccessPermissions): class AssignmentAccessPermissions(BaseAccessPermissions):
@ -24,26 +25,38 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
serializer_class = AssignmentShortSerializer serializer_class = AssignmentShortSerializer
return serializer_class return serializer_class
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. Removes unpublished polls for non admins so that they for the user. Removes unpublished polls for non admins so that they
only get a result like the AssignmentShortSerializer would give them. only get a result like the AssignmentShortSerializer would give them.
""" """
# Expand full_data to a list if it is not one.
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
# Parse data.
if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'): if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'):
data = full_data data = full_data
elif has_perm(user, 'assignments.can_see'): elif has_perm(user, 'assignments.can_see'):
many_items = not isinstance(full_data, dict) # Exclude unpublished polls.
full_data_list = full_data if many_items else [full_data] data = []
out = [] for full in full_data:
for full_data in full_data_list: full_copy = full.copy()
data = full_data.copy() full_copy['polls'] = [poll for poll in full['polls'] if poll['published']]
data['polls'] = [poll for poll in data['polls'] if data['published']] data.append(full_copy)
out.append(data)
data = out if many_items else out[0]
else: else:
data = None data = []
return data
# Reduce result to a single item or None if it was not a collection at
# the beginning of the method.
if isinstance(container, Collection):
restricted_data = data
elif data:
restricted_data = data[0]
else:
restricted_data = None
return restricted_data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):
""" """

View File

@ -16,16 +16,17 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
yield from assignments_app.get_startup_elements() yield from assignments_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs): def is_user_data_required(sender, request_user, **kwargs):
""" """
If request_user can see assignments, then returns all user ids that are Returns all user ids that are displayed as candidates (including poll
displayed as candidates (including poll options). Else, it returns an empty set. options) in any assignment if request_user can see assignments. This
function may return an empty set.
""" """
user_ids = set() candidates = set()
if has_perm(request_user, 'assignments.can_see'): if has_perm(request_user, 'assignments.can_see'):
for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator(): for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator():
full_data = assignment_collection_element.get_full_data() full_data = assignment_collection_element.get_full_data()
user_ids.update(related_user['user_id'] for related_user in full_data['assignment_related_users']) candidates.update(related_user['user_id'] for related_user in full_data['assignment_related_users'])
for poll in full_data['polls']: for poll in full_data['polls']:
user_ids.update(option['candidate_id'] for option in poll['options']) candidates.update(option['candidate_id'] for option in poll['options'])
return user_ids return candidates

View File

@ -52,14 +52,14 @@ def get_permission_change_data(sender, permissions, **kwargs):
yield Collection(core_app.get_model('ChatMessage').get_collection_string()) yield Collection(core_app.get_model('ChatMessage').get_collection_string())
def is_user_data_required(sender, request_user, user_data, **kwargs): def is_user_data_required(sender, request_user, **kwargs):
""" """
If request_user can use chat, then returns all user ids that are Returns all user ids that are displayed as chatters if request_user can
displayed as chatter. Else, it returns an empty set. use the chat. This function may return an empty set.
""" """
user_ids = set() chatters = set()
if has_perm(request_user, 'core.can_use_chat'): if has_perm(request_user, 'core.can_use_chat'):
for chat_message_collection_element in Collection(ChatMessage.get_collection_string()).element_generator(): for chat_message_collection_element in Collection(ChatMessage.get_collection_string()).element_generator():
full_data = chat_message_collection_element.get_full_data() full_data = chat_message_collection_element.get_full_data()
user_ids.add(full_data['user_id']) chatters.add(full_data['user_id'])
return user_ids return chatters

View File

@ -1,5 +1,6 @@
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import has_perm
from ..utils.collection import Collection
class MediafileAccessPermissions(BaseAccessPermissions): class MediafileAccessPermissions(BaseAccessPermissions):
@ -20,19 +21,30 @@ class MediafileAccessPermissions(BaseAccessPermissions):
return MediafileSerializer return MediafileSerializer
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user. Removes hidden mediafiles for some users.
""" """
data = None # Expand full_data to a list if it is not one.
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
# Parse data.
if has_perm(user, 'mediafiles.can_see') and has_perm(user, 'mediafiles.can_see_hidden'): if has_perm(user, 'mediafiles.can_see') and has_perm(user, 'mediafiles.can_see_hidden'):
data = full_data data = full_data
elif has_perm(user, 'mediafiles.can_see'): elif has_perm(user, 'mediafiles.can_see'):
many_items = not isinstance(full_data, dict) # Exclude hidden mediafiles.
full_data_list = full_data if many_items else [full_data] data = [full for full in full_data if not full['hidden']]
data = [full_data for full_data in full_data_list if not full_data['hidden']]
data = data if many_items else data[0]
else: else:
data = None data = []
return data
# Reduce result to a single item or None if it was not a collection at
# the beginning of the method.
if isinstance(container, Collection):
restricted_data = data
elif data:
restricted_data = data[0]
else:
restricted_data = None
return restricted_data

View File

@ -16,17 +16,15 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
yield from mediafiles_app.get_startup_elements() yield from mediafiles_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs): def is_user_data_required(sender, request_user, **kwargs):
""" """
Returns True if request user can see mediafiles and user_data is required Returns all user ids that are displayed as uploaders in any mediafile
to be displayed as uploader. if request_user can see mediafiles. This function may return an empty
set.
If request_user can see mediafiles, then returns all user ids that are
displayed as uploader. Else, it returns an empty set.
""" """
user_ids = set() uploaders = set()
if has_perm(request_user, 'mediafiles.can_see'): if has_perm(request_user, 'mediafiles.can_see'):
for mediafile_collection_element in Collection(Mediafile.get_collection_string()).element_generator(): for mediafile_collection_element in Collection(Mediafile.get_collection_string()).element_generator():
full_data = mediafile_collection_element.get_full_data() full_data = mediafile_collection_element.get_full_data()
user_ids.add(full_data['uploader_id']) uploaders.add(full_data['uploader_id'])
return user_ids return uploaders

View File

@ -3,7 +3,7 @@ from copy import deepcopy
from ..core.config import config from ..core.config import config
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import has_perm
from ..utils.collection import CollectionElement from ..utils.collection import Collection, CollectionElement
class MotionAccessPermissions(BaseAccessPermissions): class MotionAccessPermissions(BaseAccessPermissions):
@ -24,51 +24,77 @@ class MotionAccessPermissions(BaseAccessPermissions):
return MotionSerializer return MotionSerializer
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared for Returns the restricted serialized data for the instance prepared for
the user. Removes motion if the user has not the permission to see the user. Removes motion if the user has not the permission to see
the motion in this state. Removes non public comment fields for the motion in this state. Removes non public comment fields for
some unauthorized users. some unauthorized users. Ensures that a user can only see his own
personal notes.
""" """
many_items = not isinstance(full_data, dict) # Expand full_data to a list if it is not one.
full_data_list = full_data if many_items else [full_data] full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
out = []
for full_data in full_data_list:
if isinstance(user, CollectionElement):
is_submitter = user.get_full_data()['id'] in full_data.get('submitters_id', [])
else:
# Anonymous users can not be submitters
is_submitter = False
required_permission_to_see = full_data['state_required_permission_to_see'] # Parse data.
data = None if has_perm(user, 'motions.can_see'):
if has_perm(user, 'motions.can_see'): # TODO: Refactor this after personal_notes system is refactored.
if (not required_permission_to_see or data = []
has_perm(user, required_permission_to_see) or for full in full_data:
has_perm(user, 'motions.can_manage') or # Check if user is submitter of this motion.
is_submitter): if isinstance(user, CollectionElement):
if has_perm(user, 'motions.can_see_and_manage_comments') or not full_data.get('comments'): is_submitter = user.get_full_data()['id'] in full.get('submitters_id', [])
data = full_data else:
# Anonymous users can not be submitters.
is_submitter = False
# Check see permission for this motion.
required_permission_to_see = full['state_required_permission_to_see']
permission = (
not required_permission_to_see or
has_perm(user, required_permission_to_see) or
has_perm(user, 'motions.can_manage') or
is_submitter)
# Parse single motion.
if permission:
if has_perm(user, 'motions.can_see_and_manage_comments') or not full.get('comments'):
# Provide access to all fields.
motion = full
else: else:
data = deepcopy(full_data) # Set private comment fields to None.
full_copy = deepcopy(full)
for i, field in enumerate(config['motions_comments']): for i, field in enumerate(config['motions_comments']):
if not field.get('public'): if not field.get('public'):
try: try:
data['comments'][i] = None full_copy['comments'][i] = None
except IndexError: except IndexError:
# No data in range. Just do nothing. # No data in range. Just do nothing.
pass pass
motion = full_copy
# Now filter personal notes. # Now filter personal notes.
data = data.copy() motion = motion.copy()
data['personal_notes'] = [] motion['personal_notes'] = []
if user is not None: if user is not None:
for personal_note in full_data.get('personal_notes', []): for personal_note in full.get('personal_notes', []):
if personal_note.get('user_id') == user.id: if personal_note.get('user_id') == user.id:
data['personal_notes'].append(personal_note) motion['personal_notes'].append(personal_note)
break break
out.append(data)
return out if many_items else out[0] data.append(motion)
else:
data = []
# Reduce result to a single item or None if it was not a collection at
# the beginning of the method.
if isinstance(container, Collection):
restricted_data = data
elif data:
restricted_data = data[0]
else:
restricted_data = None
return restricted_data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):
""" """

View File

@ -118,15 +118,16 @@ def get_permission_change_data(sender, permissions, **kwargs):
yield from motions_app.get_startup_elements() yield from motions_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs): def is_user_data_required(sender, request_user, **kwargs):
""" """
If request_user can see motions, then returns all user ids that are Returns all user ids that are displayed as as submitter or supporter in
displayed as submitter or supporter. Else, it returns an empty set. any motion if request_user can see motions. This function may return an
empty set.
""" """
user_ids = set() submitters_supporters = set()
if has_perm(request_user, 'motions.can_see'): if has_perm(request_user, 'motions.can_see'):
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator(): for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
full_data = motion_collection_element.get_full_data() full_data = motion_collection_element.get_full_data()
user_ids.update(full_data['submitters_id']) submitters_supporters.update(full_data['submitters_id'])
user_ids.update(full_data['supporters_id']) submitters_supporters.update(full_data['supporters_id'])
return user_ids return submitters_supporters

View File

@ -3,6 +3,7 @@ from django.contrib.auth.models import AnonymousUser
from ..core.signals import user_data_required from ..core.signals import user_data_required
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import anonymous_is_enabled, has_perm from ..utils.auth import anonymous_is_enabled, has_perm
from ..utils.collection import Collection
class UserAccessPermissions(BaseAccessPermissions): class UserAccessPermissions(BaseAccessPermissions):
@ -23,7 +24,7 @@ class UserAccessPermissions(BaseAccessPermissions):
return UserFullSerializer return UserFullSerializer
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. Removes several fields for non admins so that they do for the user. Removes several fields for non admins so that they do
@ -31,22 +32,28 @@ class UserAccessPermissions(BaseAccessPermissions):
""" """
from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS
def filtered_data(full_data, only_keys): def filtered_data(full_data, whitelist):
""" """
Returns a new dict like full_data but with all blocked_keys removed. Returns a new dict like full_data but only with whitelisted keys.
""" """
return {key: full_data[key] for key in only_keys} return {key: full_data[key] for key in whitelist}
# many_items is True, when there are more then one items in full_data. # Expand full_data to a list if it is not one.
many_items = not isinstance(full_data, dict) full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
full_data = full_data if many_items else [full_data]
many_fields = set(USERCANSEEEXTRASERIALIZER_FIELDS) # We have four sets of data to be sent:
little_fields = set(USERCANSEESERIALIZER_FIELDS) # * full data i. e. all fields,
many_fields.add('groups_id') # * many data i. e. all fields but not the default password,
many_fields.discard('groups') # * little data i. e. all fields but not the default password, comments and active status,
little_fields.add('groups_id') # * no data.
little_fields.discard('groups')
# Prepare field set for users with "many" data and with "little" data.
many_data_fields = set(USERCANSEEEXTRASERIALIZER_FIELDS)
many_data_fields.add('groups_id')
many_data_fields.discard('groups')
litte_data_fields = set(USERCANSEESERIALIZER_FIELDS)
litte_data_fields.add('groups_id')
litte_data_fields.discard('groups')
# Check user permissions. # Check user permissions.
if has_perm(user, 'users.can_see_name'): if has_perm(user, 'users.can_see_name'):
@ -54,33 +61,46 @@ class UserAccessPermissions(BaseAccessPermissions):
if has_perm(user, 'users.can_manage'): if has_perm(user, 'users.can_manage'):
data = full_data data = full_data
else: else:
data = [filtered_data(full, many_fields) for full in full_data] data = [filtered_data(full, many_data_fields) for full in full_data]
else: else:
data = [filtered_data(full, little_fields) for full in full_data] data = [filtered_data(full, litte_data_fields) for full in full_data]
else: else:
# Build a list of users, that can be seen without permissions. # Build a list of users, that can be seen without any permissions (with little fields).
no_perm_users = set()
if user is not None:
no_perm_users.add(user.id)
# Get a list of all users, that are needed by another app user_ids = set()
# Everybody can see himself. Also everybody can see every user
# that is required e. g. as speaker, motion submitter or
# assignment candidate.
# Add oneself.
if user is not None:
user_ids.add(user.id)
# Get a list of all users, that are required by another app.
receiver_responses = user_data_required.send( receiver_responses = user_data_required.send(
sender=self.__class__, sender=self.__class__,
request_user=user, request_user=user)
user_data=full_data)
for receiver, response in receiver_responses: for receiver, response in receiver_responses:
no_perm_users.update(response) user_ids.update(response)
# Parse data.
data = [ data = [
filtered_data(full, little_fields) filtered_data(full, litte_data_fields)
for full for full
in full_data in full_data
if full['id'] in no_perm_users] if full['id'] in user_ids]
# Set data to [None] if data is empty # Reduce result to a single item or None if it was not a collection at
data = data or [None] # the beginning of the method.
if isinstance(container, Collection):
restricted_data = data
elif data:
restricted_data = data[0]
else:
restricted_data = None
return data if many_items else data[0] return restricted_data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):
""" """

View File

@ -1,5 +1,6 @@
from django.dispatch import Signal from django.dispatch import Signal
from .collection import Collection
from .dispatch import SignalConnectMetaClass from .dispatch import SignalConnectMetaClass
@ -56,31 +57,29 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
""" """
return self.get_serializer_class(user=None)(instance).data return self.get_serializer_class(user=None)(instance).data
def get_restricted_data(self, full_data, user): def get_restricted_data(self, container, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user.
Returns None or an empty list if the user has no read access. Returns The argument container should be a CollectionElement or a
reduced data if the user has limited access. Default: Returns full data Collection. The type of the return value is a dictionary or a list
if the user has read access to model instances. according to the given type (or None). Returns None or an empty
list 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() Hint: You should override this method if your get_serializer_class()
method returns different serializers for different users or if you method returns different serializers for different users or if you
have access restrictions in your view or viewset in methods like have access restrictions in your view or viewset in methods like
retrieve() or list(). retrieve() or list().
full_data can be a list or one single item. If it is a list, then the
retun value is also a list of restricted data.
When the user can not access
""" """
if self.check_permissions(user): if self.check_permissions(user):
data = full_data data = container.get_full_data()
elif isinstance(full_data, dict): elif isinstance(container, Collection):
data = None
else:
data = [] data = []
else:
data = None
return data return data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):

View File

@ -12,14 +12,8 @@ from django.db import transaction
from ..core.config import config from ..core.config import config
from ..core.models import Projector from ..core.models import Projector
from .auth import has_perm, user_to_collection_user from .auth import has_perm, user_to_collection_user
from .cache import start_up_cache, websocket_user_cache from .cache import startup_cache, websocket_user_cache
from .collection import ( from .collection import Collection, CollectionElement, CollectionElementList
Collection,
CollectionElement,
CollectionElementList,
format_for_autoupdate,
get_model_from_collection_string,
)
def send_or_wait(send_func, *args, **kwargs): def send_or_wait(send_func, *args, **kwargs):
@ -47,6 +41,28 @@ def send_or_wait(send_func, *args, **kwargs):
) )
def format_for_autoupdate(collection_string, id, action, data=None):
"""
Returns a dict that can be used for autoupdate.
"""
if not data:
# If the data is None or is empty, then the action has to be deleted,
# even when it says diffrently. This can happen when the object is not
# deleted, but the user has no permission to see it.
action = 'deleted'
output = {
'collection': collection_string,
'id': id,
'action': action,
}
if action != 'deleted':
output['data'] = data
return output
@channel_session_user_from_http @channel_session_user_from_http
def ws_add_site(message): def ws_add_site(message):
""" """
@ -65,32 +81,31 @@ def ws_add_site(message):
send_or_wait(message.reply_channel.send, {'accept': True}) send_or_wait(message.reply_channel.send, {'accept': True})
# Collect all elements that shoud be send to the client when the websocket # Collect all elements that shoud be send to the client when the websocket
# connection is established # connection is established.
user = user_to_collection_user(message.user.id) user = user_to_collection_user(message.user.id)
output = [] output = []
for collection_string, full_data in start_up_cache.get_collection_elements().items(): for collection in startup_cache.get_collections():
access_permission = get_model_from_collection_string(collection_string).get_access_permissions() access_permissions = collection.get_access_permissions()
if collection_string == 'core/config': restricted_data = access_permissions.get_restricted_data(collection, user)
if collection.collection_string == 'core/config':
id_key = 'key' id_key = 'key'
else: else:
id_key = 'id' id_key = 'id'
restricted_data = access_permission.get_restricted_data(full_data, user)
if restricted_data is None:
continue
for data in restricted_data: for data in restricted_data:
if data is None: if data is None:
# We do not want to send 'deleted' objects on startup.
# That's why we skip such data.
continue continue
output.append( output.append(
format_for_autoupdate( format_for_autoupdate(
collection_string=collection_string, collection_string=collection.collection_string,
id=data[id_key], id=data[id_key],
action='changed', action='changed',
data=data)) data=data))
# Send all data. If there is no data, then only accept the connection # Send all data.
if output: if output:
send_or_wait(message.reply_channel.send, {'text': json.dumps(output)}) send_or_wait(message.reply_channel.send, {'text': json.dumps(output)})
@ -354,10 +369,13 @@ def send_autoupdate(collection_elements):
Helper function, that sends collection_elements through a channel to the Helper function, that sends collection_elements through a channel to the
autoupdate system. autoupdate system.
Before sending the startup_cache is cleared because it is possibly out of
date.
Does nothing if collection_elements is empty. Does nothing if collection_elements is empty.
""" """
if collection_elements: if collection_elements:
start_up_cache.clear() startup_cache.clear()
send_or_wait( send_or_wait(
Channel('autoupdate.send_data').send, Channel('autoupdate.send_data').send,
collection_elements.as_channels_message()) collection_elements.as_channels_message())

View File

@ -187,16 +187,15 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
class StartupCache: class StartupCache:
""" """
Cache of all data that are needed when a clients connects via websocket. Cache of all data that are required when a client connects via websocket.
""" """
cache_key = "full_data_startup_cache"
cache_key = "full_data_cache"
def build(self): def build(self):
""" """
Generate the cache by going though all apps. Generate the cache by going through all apps. Returns a dict where the
Returns a dict where the key is the collection string and the value a key is the collection string and the value a list of the full_data from
list of the full_data from the collection_elements. the collection elements.
""" """
cache_data = {} cache_data = {}
for app in apps.get_app_configs(): for app in apps.get_app_configs():
@ -205,7 +204,7 @@ class StartupCache:
# This method has to return an iterable of Collection objects. # This method has to return an iterable of Collection objects.
get_startup_elements = app.get_startup_elements get_startup_elements = app.get_startup_elements
except AttributeError: except AttributeError:
# Skip apps that do not implement get_startup_elements # Skip apps that do not implement get_startup_elements.
continue continue
for collection in get_startup_elements(): for collection in get_startup_elements():
@ -223,25 +222,23 @@ class StartupCache:
""" """
cache.delete(self.cache_key) cache.delete(self.cache_key)
def get_collection_elements(self): def get_collections(self):
""" """
Returns a dict of all collection_elements, that are needed at startup. Generator that returns all cached Collections.
The key is the collection_string and the value a list of CollectionElement The data is read from the cache if it exists. It builds the cache if it
objects. Each list has at least one value.
The data is read from the cache if it exists. It builds the cache, if it
does not exists. does not exists.
""" """
from .collection import Collection
data = cache.get(self.cache_key) data = cache.get(self.cache_key)
if data is None: if data is None:
# The cache does not exist. # The cache does not exist.
data = self.build() data = self.build()
for collection_string, full_data in data.items():
return data yield Collection(collection_string, full_data)
start_up_cache = StartupCache() startup_cache = StartupCache()
def use_redis_cache(): def use_redis_cache():

View File

@ -100,9 +100,18 @@ class CollectionElement:
Only for internal use. Do not use it directly. Use as_autoupdate_for_user() Only for internal use. Do not use it directly. Use as_autoupdate_for_user()
or as_autoupdate_for_projector(). or as_autoupdate_for_projector().
""" """
from .autoupdate import format_for_autoupdate
# TODO: Revert this after get_projector_data is also enhanced like get_restricted_data.
if method == 'get_restricted_data':
container = self
else:
container = self.get_full_data()
# End of hack
if not self.is_deleted(): if not self.is_deleted():
data = getattr(self.get_access_permissions(), method)( data = getattr(self.get_access_permissions(), method)(
self.get_full_data(), container,
*args) *args)
else: else:
data = None data = None
@ -140,9 +149,7 @@ class CollectionElement:
The argument `user` can be anything, that is allowd as argument for The argument `user` can be anything, that is allowd as argument for
utils.auth.has_perm(). utils.auth.has_perm().
""" """
return self.get_access_permissions().get_restricted_data( return self.get_access_permissions().get_restricted_data(self, user)
self.get_full_data(),
user)
def get_model(self): def get_model(self):
""" """
@ -283,8 +290,15 @@ class Collection:
Represents all elements of one collection. Represents all elements of one collection.
""" """
def __init__(self, collection_string): def __init__(self, collection_string, full_data=None):
"""
Initiates a Collection. A collection_string has to be given. If
full_data (a list of dictionaries) is not given the method
get_full_data() exposes all data by iterating over all
CollectionElements.
"""
self.collection_string = collection_string self.collection_string = collection_string
self.full_data = full_data
def get_cache_key(self, raw=False): def get_cache_key(self, raw=False):
""" """
@ -304,10 +318,18 @@ class Collection:
""" """
return get_model_from_collection_string(self.collection_string) return get_model_from_collection_string(self.collection_string)
def get_access_permissions(self):
"""
Returns the get_access_permissions object for the this collection.
"""
return self.get_model().get_access_permissions()
def element_generator(self): def element_generator(self):
""" """
Generator that yields all collection_elements of this collection. Generator that yields all collection_elements of this collection.
""" """
# TODO: This method should use self.full_data if it already exists.
# Get all cache keys. # Get all cache keys.
ids = self.get_all_ids() ids = self.get_all_ids()
cache_keys = [ cache_keys = [
@ -352,10 +374,23 @@ class Collection:
for instance in query.filter(pk__in=missing_ids): for instance in query.filter(pk__in=missing_ids):
yield CollectionElement.from_instance(instance) yield CollectionElement.from_instance(instance)
def get_full_data(self):
"""
Returns a list of dictionaries with full_data of all collection
elements.
"""
if self.full_data is None:
self.full_data = [
collection_element.get_full_data()
for collection_element
in self.element_generator()]
return self.full_data
def as_autoupdate_for_projector(self): def as_autoupdate_for_projector(self):
""" """
Returns a list of dictonaries to send them to the projector. Returns a list of dictonaries to send them to the projector.
""" """
# TODO: This method is only used in one case. Remove it.
output = [] output = []
for collection_element in self.element_generator(): for collection_element in self.element_generator():
content = collection_element.as_autoupdate_for_projector() content = collection_element.as_autoupdate_for_projector()
@ -371,6 +406,7 @@ class Collection:
The argument `user` can be anything, that is allowd as argument for The argument `user` can be anything, that is allowd as argument for
utils.auth.has_perm(). utils.auth.has_perm().
""" """
# TODO: This method is not used. Remove it.
output = [] output = []
for collection_element in self.element_generator(): for collection_element in self.element_generator():
content = collection_element.as_autoupdate_for_user(user) content = collection_element.as_autoupdate_for_user(user)
@ -548,25 +584,3 @@ def get_collection_id_from_cache_key(cache_key):
# The id is no integer. This can happen on config elements # The id is no integer. This can happen on config elements
pass pass
return (collection_string, id) return (collection_string, id)
def format_for_autoupdate(collection_string, id, action, data=None):
"""
Returns a dict that can be used for autoupdate.
"""
if not data:
# If the data is None or is empty, then the action has to be deleted,
# even when it says diffrently. This can happen when the object is not
# deleted, but the user has no permission to see it.
action = 'deleted'
output = {
'collection': collection_string,
'id': id,
'action': action,
}
if action != 'deleted':
output['data'] = data
return output

View File

@ -128,7 +128,7 @@ class TestCollectionElement(TestCase):
'id': 42, 'id': 42,
'action': 'changed', 'action': 'changed',
'data': 'restricted_data'}) 'data': 'restricted_data'})
collection_element.get_full_data.assert_called_once_with() collection_element.get_full_data.assert_not_called()
def test_as_autoupdate_for_user_no_permission(self): def test_as_autoupdate_for_user_no_permission(self):
with patch.object(collection.CollectionElement, 'get_full_data'): with patch.object(collection.CollectionElement, 'get_full_data'):
@ -143,7 +143,7 @@ class TestCollectionElement(TestCase):
{'collection': 'testmodule/model', {'collection': 'testmodule/model',
'id': 42, 'id': 42,
'action': 'deleted'}) 'action': 'deleted'})
collection_element.get_full_data.assert_called_once_with() collection_element.get_full_data.assert_not_called()
def test_as_autoupdate_for_user_deleted(self): def test_as_autoupdate_for_user_deleted(self):
collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True) collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True)