From b034839ac826f37397609799223998b95418228d Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Wed, 22 Aug 2018 07:59:22 +0200 Subject: [PATCH] Remove old projector code --- openslides/agenda/access_permissions.py | 18 -- openslides/agenda/projector.py | 94 ----------- openslides/assignments/access_permissions.py | 14 -- openslides/assignments/projector.py | 30 ---- openslides/core/config.py | 2 +- openslides/core/models.py | 56 ------ openslides/core/projector.py | 18 -- openslides/mediafiles/projector.py | 9 - openslides/motions/access_permissions.py | 12 -- openslides/motions/projector.py | 86 +--------- openslides/routing.py | 3 +- openslides/topics/projector.py | 10 -- openslides/users/access_permissions.py | 21 --- openslides/users/projector.py | 9 - openslides/utils/access_permissions.py | 8 - openslides/utils/autoupdate.py | 44 +---- openslides/utils/collection.py | 125 +------------- openslides/utils/consumers.py | 169 +------------------ openslides/utils/models.py | 16 +- openslides/utils/projector.py | 42 +---- tests/unit/users/test_access_permissions.py | 46 +---- tests/unit/utils/test_collection.py | 19 --- 22 files changed, 29 insertions(+), 822 deletions(-) diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index 28da16c93..9d1738f44 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -92,21 +92,3 @@ class ItemAccessPermissions(BaseAccessPermissions): data = [] return data - - def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Returns the restricted serialized data for the instance prepared - for the projector. Removes field 'comment'. - """ - def filtered_data(full_data, blocked_keys): - """ - Returns a new dict like full_data but with all blocked_keys removed. - """ - whitelist = full_data.keys() - blocked_keys - return {key: full_data[key] for key in whitelist} - - # Parse data. - blocked_keys = ('comment',) - data = [filtered_data(full, blocked_keys) for full in full_data] - - return data diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index 6a7b73c6e..a29af1055 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -1,9 +1,6 @@ from typing import Generator, Type -from ..core.config import config from ..core.exceptions import ProjectorException -from ..core.models import Projector -from ..utils.collection import CollectionElement from ..utils.projector import ProjectorElement from .models import Item @@ -28,9 +25,6 @@ class ItemListSlide(ProjectorElement): if not Item.objects.filter(pk=pk).exists(): raise ProjectorException('Item does not exist.') - def get_requirements(self, config_entry): - yield from Item.objects.all() - class ListOfSpeakersSlide(ProjectorElement): """ @@ -43,35 +37,6 @@ class ListOfSpeakersSlide(ProjectorElement): if not Item.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('Item does not exist.') - def get_requirements(self, config_entry): - pk = config_entry.get('id') - if pk is not None: - # List of speakers slide. - try: - item = Item.objects.get(pk=pk) - except Item.DoesNotExist: - # Item does not exist. Just do nothing. - pass - else: - yield item - yield item.content_object - for speaker in item.speakers.filter(end_time=None): - # Yield current speaker and next speakers - yield speaker.user - query = (item.speakers.exclude(end_time=None) - .order_by('-end_time')[:config['agenda_show_last_speakers']]) - for speaker in query: - # Yield last speakers - yield speaker.user - - def get_collection_elements_required_for_this(self, collection_element, config_entry): - output = super().get_collection_elements_required_for_this(collection_element, config_entry) - # Full update if item changes because then we may have new - # candidates and therefor need new users. - if collection_element.collection_string == Item.get_collection_string() and collection_element.id == config_entry.get('id'): - output.extend(self.get_requirements_as_collection_elements(config_entry)) - return output - def update_data(self): return {'agenda_item_id': self.config_entry.get('id')} @@ -82,65 +47,6 @@ class CurrentListOfSpeakersSlide(ProjectorElement): """ name = 'agenda/current-list-of-speakers' - def get_requirements(self, config_entry): - # The query mechanism on client needs the referenced projector. - try: - reference_projector = Projector.objects.get( - pk=config['projector_currentListOfSpeakers_reference']) - except Projector.DoesNotExist: - # Reference projector was deleted so this projector element is empty. - # Skip yielding more requirements (items and speakers). - pass - else: - yield reference_projector - - items = self.get_agenda_items(reference_projector) - for item in items: - yield item - yield item.content_object - for speaker in item.speakers.filter(end_time=None): - yield speaker.user - query = (item.speakers.exclude(end_time=None) - .order_by('-end_time')[:config['agenda_show_last_speakers']]) - for speaker in query: - # Yield last speakers - yield speaker.user - - def get_agenda_items(self, projector): - for element in projector.elements.values(): - agenda_item_id = element.get('agenda_item_id') - if agenda_item_id is not None: - yield Item.objects.get(pk=agenda_item_id) - - def get_collection_elements_required_for_this(self, collection_element, config_entry): - output = super().get_collection_elements_required_for_this(collection_element, config_entry) - # Full update if agenda_item or referenced projector changes because - # then we may have new candidates and therefor need new users. - try: - reference_projector = Projector.objects.get( - pk=config['projector_currentListOfSpeakers_reference']) - except Projector.DoesNotExist: - # Reference projector was deleted so this projector element is empty. - # Skip appending more stuff to output. - pass - else: - is_reference_projector = collection_element == CollectionElement.from_values( - reference_projector.get_collection_string(), - reference_projector.pk) - is_config = ( - collection_element.collection_string == 'core/config' and - collection_element.information.get('changed_config') == 'projector_currentListOfSpeakers_reference') - - if is_reference_projector or is_config: - output.extend(self.get_requirements_as_collection_elements(config_entry)) - else: - items = self.get_agenda_items(reference_projector) - for item in items: - if collection_element == CollectionElement.from_values(item.get_collection_string(), item.pk): - output.extend(self.get_requirements_as_collection_elements(config_entry)) - break - return output - def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: yield ItemListSlide diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py index 7c0f883e7..f0dd3fe4d 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -55,17 +55,3 @@ class AssignmentAccessPermissions(BaseAccessPermissions): data = [] return data - - def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Returns the restricted serialized data for the instance prepared - for the projector. Removes unpublished polls. - """ - # Parse data. Exclude unpublished polls. - data = [] - for full in full_data: - full_copy = full.copy() - full_copy['polls'] = [poll for poll in full['polls'] if poll['published']] - data.append(full_copy) - - return data diff --git a/openslides/assignments/projector.py b/openslides/assignments/projector.py index f79de375f..e3345d14a 100644 --- a/openslides/assignments/projector.py +++ b/openslides/assignments/projector.py @@ -26,36 +26,6 @@ class AssignmentSlide(ProjectorElement): if poll.assignment_id != self.config_entry.get('id'): raise ProjectorException('Assignment id and poll do not belong together.') - def get_requirements(self, config_entry): - try: - assignment = Assignment.objects.get(pk=config_entry.get('id')) - except Assignment.DoesNotExist: - # Assignment does not exist. Just do nothing. - pass - else: - yield assignment - yield assignment.agenda_item - if not config_entry.get('poll'): - # Assignment detail slide. Yield user instances of current - # candidates (i. e. future poll participants) and elected - # persons (i. e. former poll participants). - for user in assignment.related_users.all(): - yield user - else: - # Assignment poll slide. Yield user instances of the - # participants of all polls. - for poll in assignment.polls.all().prefetch_related('options'): - for option in poll.options.all(): - yield option.candidate - - def get_collection_elements_required_for_this(self, collection_element, config_entry): - output = super().get_collection_elements_required_for_this(collection_element, config_entry) - # Full update if assignment changes because then we may have new - # candidates and therefor need new users. - if collection_element.collection_string == Assignment.get_collection_string() and collection_element.id == config_entry.get('id'): - output.extend(self.get_requirements_as_collection_elements(config_entry)) - return output - def update_data(self): data = None try: diff --git a/openslides/core/config.py b/openslides/core/config.py index dce49330a..9185a5e5f 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -165,7 +165,7 @@ class ConfigHandler: # Save the new value to the database. db_value = ConfigStore.objects.get(key=key) db_value.value = value - db_value.save(information={'changed_config': key}) + db_value.save() # Call on_change callback. if config_variable.on_change: diff --git a/openslides/core/models.py b/openslides/core/models.py index a186962a1..e80ef2f69 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -3,7 +3,6 @@ from django.db import models from django.utils.timezone import now from jsonfield import JSONField -from ..utils.collection import CollectionElement from ..utils.models import RESTModelMixin from ..utils.projector import get_all_projector_elements from .access_permissions import ( @@ -130,61 +129,6 @@ class Projector(RESTModelMixin, models.Model): result[key]['error'] = str(e) return result - def get_all_requirements(self): - """ - Generator which returns all instances that are shown on this projector. - """ - # Get all elements from all apps. - elements = get_all_projector_elements() - - # Generator - for key, value in self.config.items(): - element = elements.get(value['name']) - if element is not None: - yield from element.get_requirements(value) - - def get_collection_elements_required_for_this(self, collection_element): - """ - Returns an iterable of CollectionElements that have to be sent to this - projector according to the given collection_element. - """ - from .config import config - - output = [] - changed_fields = collection_element.information.get('changed_fields', []) - - if (collection_element.collection_string == self.get_collection_string() and - changed_fields and - 'config' not in changed_fields): - # Projector model changed without changeing the projector config. So we just send this data. - output.append(collection_element) - else: - # It is necessary to parse all active projector elements to check whether they require some data. - this_projector = collection_element.collection_string == self.get_collection_string() and collection_element.id == self.pk - collection_element.information['this_projector'] = this_projector - - elements = get_all_projector_elements() - - # Iterate over all active projector elements. - for key, value in self.config.items(): - element = elements.get(value['name']) - if element is not None: - if collection_element.information.get('changed_config') == 'projector_broadcast': - # In case of broadcast we need full update. - output.extend(element.get_requirements_as_collection_elements(value)) - else: - # In normal case we need all collections required by the element. - output.extend(element.get_collection_elements_required_for_this(collection_element, value)) - - # If config changed, send also this config to the projector. - if collection_element.collection_string == config.get_collection_string(): - output.append(collection_element) - if collection_element.information.get('changed_config') == 'projector_broadcast': - # In case of broadcast we also need the projector himself. - output.append(CollectionElement.from_instance(self)) - - return output - @classmethod def remove_any(cls, skip_autoupdate=False, **kwargs): """ diff --git a/openslides/core/projector.py b/openslides/core/projector.py index 35a04311c..a1c39551c 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -22,15 +22,6 @@ class CountdownElement(ProjectorElement): if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('Countdown does not exists.') - def get_requirements(self, config_entry): - try: - countdown = Countdown.objects.get(pk=config_entry.get('id')) - except Countdown.DoesNotExist: - # Just do nothing if message does not exist - pass - else: - yield countdown - class ProjectorMessageElement(ProjectorElement): """ @@ -42,15 +33,6 @@ class ProjectorMessageElement(ProjectorElement): if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('Message does not exists.') - def get_requirements(self, config_entry): - try: - message = ProjectorMessage.objects.get(pk=config_entry.get('id')) - except ProjectorMessage.DoesNotExist: - # Just do nothing if message does not exist - pass - else: - yield message - def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: yield Clock diff --git a/openslides/mediafiles/projector.py b/openslides/mediafiles/projector.py index c4f4e6d1d..f939ce4f4 100644 --- a/openslides/mediafiles/projector.py +++ b/openslides/mediafiles/projector.py @@ -15,15 +15,6 @@ class MediafileSlide(ProjectorElement): if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('File does not exist.') - def get_requirements(self, config_entry): - try: - mediafile = Mediafile.objects.get(pk=config_entry.get('id')) - except Mediafile.DoesNotExist: - # Mediafile does not exist. Just do nothing. - pass - else: - yield mediafile - def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: yield MediafileSlide diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index 1abf1651d..0e5456457 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -69,18 +69,6 @@ class MotionAccessPermissions(BaseAccessPermissions): return data - def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Returns the restricted serialized data for the instance prepared - for the projector. Removes all comments. - """ - data = [] - for full in full_data: - full_copy = deepcopy(full) - full_copy['comments'] = [] - data.append(full_copy) - return data - class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions): """ diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 474a611b7..7b56691b1 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -1,10 +1,8 @@ -import re from typing import Generator, Type -from ..core.config import config from ..core.exceptions import ProjectorException from ..utils.projector import ProjectorElement -from .models import Motion, MotionBlock, MotionChangeRecommendation, Workflow +from .models import Motion, MotionBlock class MotionSlide(ProjectorElement): @@ -17,68 +15,6 @@ class MotionSlide(ProjectorElement): if not Motion.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('Motion does not exist.') - def get_requirements(self, config_entry): - try: - motion = Motion.objects.get(pk=config_entry.get('id')) - except Motion.DoesNotExist: - # Motion does not exist. Just do nothing. - pass - else: - yield motion - yield motion.agenda_item - yield motion.state.workflow - yield from self.required_motions_for_state_and_recommendation(motion) - yield from motion.get_paragraph_based_amendments() - for submitter in motion.submitters.all(): - yield submitter.user - yield from motion.supporters.all() - yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id) - if motion.parent: - yield motion.parent - - def required_motions_for_state_and_recommendation(self, motion): - """ - Returns a list of motions needed for the projector, because they are mentioned - in additional fieds for the state and recommendation. - Keep the motion_syntax syncronized with the MotionStateAndRecommendationParser on the client. - """ - # get the comments field for state and recommendation - motion_syntax = re.compile(r'\[motion:(\d+)\]') - fields = config['motions_comments'] - state_field_id = None - recommendation_field_id = None - - for id, field in fields.items(): - if isinstance(field, dict): - if field.get('forState', False): - state_field_id = id - if field.get('forRecommendation', False): - recommendation_field_id = id - - # extract all mentioned motions from the state and recommendation - motion_ids = set() - if state_field_id is not None: - state_text = motion.comments.get(state_field_id) - motion_ids.update([int(id) for id in motion_syntax.findall(state_text)]) - - if recommendation_field_id is not None: - recommendation_text = motion.comments.get(recommendation_field_id) - motion_ids.update([int(id) for id in motion_syntax.findall(recommendation_text)]) - - # return all motions - return Motion.objects.filter(pk__in=motion_ids) - - def get_collection_elements_required_for_this(self, collection_element, config_entry): - output = super().get_collection_elements_required_for_this(collection_element, config_entry) - # Full update if motion changes because then we may have new - # submitters or supporters and therefor need new users. - # - # Add some logic here if we support live changing of workflows later. - # - if collection_element.collection_string == Motion.get_collection_string() and collection_element.id == config_entry.get('id'): - output.extend(self.get_requirements_as_collection_elements(config_entry)) - return output - def update_data(self): data = None try: @@ -101,26 +37,6 @@ class MotionBlockSlide(ProjectorElement): if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('MotionBlock does not exist.') - def get_requirements(self, config_entry): - try: - motion_block = MotionBlock.objects.get(pk=config_entry.get('id')) - except MotionBlock.DoesNotExist: - # MotionBlock does not exist. Just do nothing. - pass - else: - yield motion_block - yield motion_block.agenda_item - yield from motion_block.motion_set.all() - yield from Workflow.objects.all() - - def get_collection_elements_required_for_this(self, collection_element, config_entry): - output = super().get_collection_elements_required_for_this(collection_element, config_entry) - # Send all changed motions to the projector, because it may be appended - # or removed from the block. - if collection_element.collection_string == Motion.get_collection_string(): - output.append(collection_element) - return output - def update_data(self): data = None try: diff --git a/openslides/routing.py b/openslides/routing.py index 29457c7c3..fb693c89d 100644 --- a/openslides/routing.py +++ b/openslides/routing.py @@ -1,7 +1,7 @@ from channels.routing import ProtocolTypeRouter, URLRouter from django.conf.urls import url -from openslides.utils.consumers import ProjectorConsumer, SiteConsumer +from openslides.utils.consumers import SiteConsumer from openslides.utils.middleware import AuthMiddlewareStack @@ -10,7 +10,6 @@ application = ProtocolTypeRouter({ "websocket": AuthMiddlewareStack( URLRouter([ url(r"^ws/site/$", SiteConsumer), - url(r"^ws/projector/(?P\d+)/$", ProjectorConsumer), ]) ) }) diff --git a/openslides/topics/projector.py b/openslides/topics/projector.py index 793201de2..dfeb43ef6 100644 --- a/openslides/topics/projector.py +++ b/openslides/topics/projector.py @@ -15,16 +15,6 @@ class TopicSlide(ProjectorElement): if not Topic.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('Topic does not exist.') - def get_requirements(self, config_entry): - try: - topic = Topic.objects.get(pk=config_entry.get('id')) - except Topic.DoesNotExist: - # Topic does not exist. Just do nothing. - pass - else: - yield topic - yield topic.agenda_item - def update_data(self): data = None try: diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py index 3648ca991..a7ce3cac4 100644 --- a/openslides/users/access_permissions.py +++ b/openslides/users/access_permissions.py @@ -99,27 +99,6 @@ class UserAccessPermissions(BaseAccessPermissions): return data - def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Returns the restricted serialized data for the instance prepared - for the projector. Removes several fields. - """ - from .serializers import USERCANSEESERIALIZER_FIELDS - - def filtered_data(full_data, whitelist): - """ - Returns a new dict like full_data but only with whitelisted keys. - """ - return {key: full_data[key] for key in whitelist} - - # Parse data. - litte_data_fields = set(USERCANSEESERIALIZER_FIELDS) - litte_data_fields.add('groups_id') - litte_data_fields.discard('groups') - data = [filtered_data(full, litte_data_fields) for full in full_data] - - return data - class GroupAccessPermissions(BaseAccessPermissions): """ diff --git a/openslides/users/projector.py b/openslides/users/projector.py index d4907b13f..dc548a562 100644 --- a/openslides/users/projector.py +++ b/openslides/users/projector.py @@ -15,15 +15,6 @@ class UserSlide(ProjectorElement): if not User.objects.filter(pk=self.config_entry.get('id')).exists(): raise ProjectorException('User does not exist.') - def get_requirements(self, config_entry): - try: - user = User.objects.get(pk=config_entry.get('id')) - except User.DoesNotExist: - # User does not exist. Just do nothing. - pass - else: - yield user - def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: yield UserSlide diff --git a/openslides/utils/access_permissions.py b/openslides/utils/access_permissions.py index c9a83c4d9..309aec9ff 100644 --- a/openslides/utils/access_permissions.py +++ b/openslides/utils/access_permissions.py @@ -56,11 +56,3 @@ class BaseAccessPermissions: retrieve() or list(). """ return full_data if self.check_permissions(user) else [] - - def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Returns the serialized data for the projector. Returns an empty list if - the user has no access to this specific data. Returns reduced data if - the user has limited access. Default: Returns full data. - """ - return full_data diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 044f37a89..762075019 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -1,5 +1,4 @@ import threading -from collections import OrderedDict from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from asgiref.sync import async_to_sync @@ -7,21 +6,10 @@ from channels.layers import get_channel_layer from django.db.models import Model from .cache import element_cache, get_element_id -from .collection import CollectionElement, to_channel_message +from .collection import CollectionElement -def to_ordered_dict(d: Optional[Dict]) -> Optional[OrderedDict]: - """ - Little helper to hash information dict in inform_*_data. - """ - if isinstance(d, dict): - result: Optional[OrderedDict] = OrderedDict([(key, to_ordered_dict(d[key])) for key in sorted(d.keys())]) - else: - result = d - return result - - -def inform_changed_data(instances: Union[Iterable[Model], Model], information: Dict[str, Any] = None) -> None: +def inform_changed_data(instances: Union[Iterable[Model], Model]) -> None: """ Informs the autoupdate system and the caching system about the creation or update of an element. @@ -41,10 +29,8 @@ def inform_changed_data(instances: Union[Iterable[Model], Model], information: D collection_elements = {} for root_instance in root_instances: - collection_element = CollectionElement.from_instance( - root_instance, - information=information) - key = root_instance.get_collection_string() + str(root_instance.get_rest_pk()) + str(to_ordered_dict(information)) + collection_element = CollectionElement.from_instance(root_instance) + key = root_instance.get_collection_string() + str(root_instance.get_rest_pk()) collection_elements[key] = collection_element bundle = autoupdate_bundle.get(threading.get_ident()) @@ -56,21 +42,18 @@ def inform_changed_data(instances: Union[Iterable[Model], Model], information: D async_to_sync(send_autoupdate)(collection_elements.values()) -def inform_deleted_data(elements: Iterable[Tuple[str, int]], information: Dict[str, Any] = None) -> None: +def inform_deleted_data(elements: Iterable[Tuple[str, int]]) -> None: """ Informs the autoupdate system and the caching system about the deletion of elements. - - The argument information is added to each collection element. """ collection_elements: Dict[str, Any] = {} for element in elements: collection_element = CollectionElement.from_values( collection_string=element[0], id=element[1], - deleted=True, - information=information) - key = element[0] + str(element[1]) + str(to_ordered_dict(information)) + deleted=True) + key = element[0] + str(element[1]) collection_elements[key] = collection_element bundle = autoupdate_bundle.get(threading.get_ident()) @@ -82,15 +65,14 @@ def inform_deleted_data(elements: Iterable[Tuple[str, int]], information: Dict[s async_to_sync(send_autoupdate)(collection_elements.values()) -def inform_data_collection_element_list(collection_elements: List[CollectionElement], - information: Dict[str, Any] = None) -> None: +def inform_data_collection_element_list(collection_elements: List[CollectionElement]) -> None: """ Informs the autoupdate system about some collection elements. This is used just to send some data to all users. """ elements = {} for collection_element in collection_elements: - key = collection_element.collection_string + str(collection_element.id) + str(to_ordered_dict(information)) + key = collection_element.collection_string + str(collection_element.id) elements[key] = collection_element bundle = autoupdate_bundle.get(threading.get_ident()) @@ -148,14 +130,6 @@ async def send_autoupdate(collection_elements: Iterable[CollectionElement]) -> N change_id = await element_cache.change_elements(cache_elements) channel_layer = get_channel_layer() - # TODO: don't await. They can be send in parallel - await channel_layer.group_send( - "projector", - { - "type": "send_data", - "message": to_channel_message(collection_elements), - }, - ) await channel_layer.group_send( "autoupdate", { diff --git a/openslides/utils/collection.py b/openslides/utils/collection.py index 362ecbc1d..b7b3f08d9 100644 --- a/openslides/utils/collection.py +++ b/openslides/utils/collection.py @@ -1,14 +1,4 @@ -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generator, - Iterable, - List, - Optional, - Type, - cast, -) +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type from asgiref.sync import async_to_sync from django.apps import apps @@ -34,46 +24,15 @@ AutoupdateFormat = TypedDict( ) -AutoupdateFormatOld = TypedDict( - 'AutoupdateFormatOld', - { - 'collection': str, - 'id': int, - 'action': 'str', - 'data': Dict[str, Any], - }, - total=False, -) - -InnerChannelMessageFormat = TypedDict( - 'InnerChannelMessageFormat', - { - 'collection_string': str, - 'id': int, - 'deleted': bool, - 'information': Dict[str, Any], - 'full_data': Optional[Dict[str, Any]], - } -) - -ChannelMessageFormat = TypedDict( - 'ChannelMessageFormat', - { - 'elements': List[InnerChannelMessageFormat], - } -) - - class CollectionElement: def __init__(self, instance: Model = None, deleted: bool = False, collection_string: str = None, - id: int = None, full_data: Dict[str, Any] = None, information: Dict[str, Any] = None) -> None: + id: int = None, full_data: Dict[str, Any] = None) -> None: """ Do not use this. Use the methods from_instance() or from_values(). """ self.instance = instance self.deleted = deleted self.full_data = full_data - self.information = information or {} if instance is not None: # Collection element is created via instance self.collection_string = instance.get_collection_string() @@ -93,7 +52,7 @@ class CollectionElement: @classmethod def from_instance( - cls, instance: Model, deleted: bool = False, information: Dict[str, Any] = None) -> 'CollectionElement': + cls, instance: Model, deleted: bool = False) -> 'CollectionElement': """ Returns a collection element from a database instance. @@ -101,11 +60,11 @@ class CollectionElement: If deleted is set to True, the element is deleted from the cache. """ - return cls(instance=instance, deleted=deleted, information=information) + return cls(instance=instance, deleted=deleted) @classmethod def from_values(cls, collection_string: str, id: int, deleted: bool = False, - full_data: Dict[str, Any] = None, information: Dict[str, Any] = None) -> 'CollectionElement': + full_data: Dict[str, Any] = None) -> 'CollectionElement': """ Returns a collection element from a collection_string and an id. @@ -114,8 +73,7 @@ class CollectionElement: With the argument full_data, the content of the CollectionElement can be set. It has to be a dict in the format that is used be access_permission.get_full_data(). """ - return cls(collection_string=collection_string, id=id, deleted=deleted, - full_data=full_data, information=information) + return cls(collection_string=collection_string, id=id, deleted=deleted, full_data=full_data) def __eq__(self, collection_element: 'CollectionElement') -> bool: # type: ignore """ @@ -127,23 +85,6 @@ class CollectionElement: return (self.collection_string == collection_element.collection_string and self.id == collection_element.id) - def as_autoupdate_for_projector(self) -> AutoupdateFormatOld: - """ - Returns a dict that can be sent through the autoupdate system for the - projector. - """ - if not self.is_deleted(): - restricted_data = self.get_access_permissions().get_projector_data([self.get_full_data()]) - data = restricted_data[0] if restricted_data else None - else: - data = None - - return format_for_autoupdate_old( - collection_string=self.collection_string, - id=self.id, - action='deleted' if self.is_deleted() else 'changed', - data=data) - def as_dict_for_user(self, user: Optional['CollectionElement']) -> Optional[Dict[str, Any]]: """ Returns a dict with the data for a user. Can be used for the rest api. @@ -329,57 +270,3 @@ def get_model_from_collection_string(collection_string: str) -> Type[Model]: except KeyError: raise ValueError('Invalid message. A valid collection_string is missing.') return model - - -def format_for_autoupdate_old( - collection_string: str, id: int, action: str, data: Dict[str, Any] = None) -> AutoupdateFormatOld: - """ - Returns a dict that can be used for autoupdate. - - This is depricated. Use format_for_autoupdate. - """ - if data is None: - # If the data is None 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 = AutoupdateFormatOld( - collection=collection_string, - id=id, - action=action, - ) - - if action != 'deleted': - data = cast(Dict[str, Any], data) # In this case data can not be None - output['data'] = data - - return output - - -def to_channel_message(elements: Iterable[CollectionElement]) -> ChannelMessageFormat: - """ - Converts a list of collection elements to a dict, that can be send to the - channels system. - """ - output = [] - for element in elements: - output.append(InnerChannelMessageFormat( - collection_string=element.collection_string, - id=element.id, - deleted=element.is_deleted(), - information=element.information, - full_data=element.full_data, - )) - return ChannelMessageFormat(elements=output) - - -def from_channel_message(message: ChannelMessageFormat) -> List[CollectionElement]: - """ - Converts a list of collection elements back from a dict, that was created - via to_channel_message. - """ - elements = [] - for value in message['elements']: - elements.append(CollectionElement.from_values(**value)) - return elements diff --git a/openslides/utils/consumers.py b/openslides/utils/consumers.py index 2a01b6ce0..88c3818d5 100644 --- a/openslides/utils/consumers.py +++ b/openslides/utils/consumers.py @@ -2,20 +2,9 @@ from collections import defaultdict from typing import Any, Dict, List from urllib.parse import parse_qs -from asgiref.sync import sync_to_async -from channels.db import database_sync_to_async - -from ..core.config import config -from ..core.models import Projector -from .auth import async_anonymous_is_enabled, has_perm +from .auth import async_anonymous_is_enabled from .cache import element_cache, split_element_id -from .collection import ( - AutoupdateFormat, - Collection, - CollectionElement, - format_for_autoupdate_old, - from_channel_message, -) +from .collection import AutoupdateFormat from .websocket import ProtocollAsyncJsonWebsocketConsumer, get_element_data @@ -72,13 +61,11 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): for item in event['incomming']: users = item.get('users') reply_channels = item.get('replyChannels') - projectors = item.get('projectors') if ((isinstance(users, list) and user_id in users) or (isinstance(reply_channels, list) and self.channel_name in reply_channels) - or (users is None and reply_channels is None and projectors is None)): + or users is None and reply_channels is None): item['senderReplyChannelName'] = event.get('senderReplyChannelName') item['senderUserId'] = event.get('senderUserId') - item['senderProjectorId'] = event.get('senderProjectorId') out.append(item) if out: @@ -101,153 +88,3 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): from_change_id=change_id, to_change_id=change_id, all_data=False)) - - -class ProjectorConsumer(ProtocollAsyncJsonWebsocketConsumer): - """ - Websocket Consumer for the projector. - """ - - groups = ['projector'] - - async def connect(self) -> None: - """ - Adds the websocket connection to a group specific to the projector with the given id. - Also sends all data that are shown on the projector. - """ - user = self.scope['user'] - projector_id = self.scope["url_route"]["kwargs"]["projector_id"] - await self.accept() - - if not await database_sync_to_async(has_perm)(user, 'core.can_see_projector'): - await self.send_json(type='error', content='No permissions to see this projector.') - # TODO: Shouldend we just close the websocket connection with an error message? - # self.close(code=4403) - else: - out = await sync_to_async(projector_startup_data)(projector_id) - await self.send_json(type='autoupdate', content=out) - - async def receive_content(self, type: str, content: Any, id: str) -> None: - """ - If we recieve something from the client we currently just interpret this - as a notify message. - - The server adds the sender's user id (0 for anonymous) and reply - channel name so that a receiver client may reply to the sender or to all - sender's instances. - """ - projector_id = self.scope["url_route"]["kwargs"]["projector_id"] - await self.channel_layer.group_send( - "projector", - { - "type": "send_notify", - "incomming": content, - "senderReplyChannelName": self.channel_name, - "senderProjectorId": projector_id, - }, - ) - await self.channel_layer.group_send( - "site", - { - "type": "send_notify", - "incomming": content, - "senderReplyChannelName": self.channel_name, - "senderProjectorId": projector_id, - }, - ) - - async def send_notify(self, event: Dict[str, Any]) -> None: - """ - Send a notify message to the projector. - """ - projector_id = self.scope["url_route"]["kwargs"]["projector_id"] - - out = [] - for item in event['incomming']: - users = item.get('users') - reply_channels = item.get('replyChannels') - projectors = item.get('projectors') - if ((isinstance(projectors, list) and projector_id in projectors) - or (isinstance(reply_channels, list) and self.channel_name in reply_channels) - or (users is None and reply_channels is None and projectors is None)): - item['senderReplyChannelName'] = event.get('senderReplyChannelName') - item['senderUserId'] = event.get('senderUserId') - item['senderProjectorId'] = event.get('senderProjectorId') - out.append(item) - - if out: - await self.send_json(type='notify', content=out) - - async def send_data(self, event: Dict[str, Any]) -> None: - """ - Informs all projector clients about changed data. - """ - projector_id = self.scope["url_route"]["kwargs"]["projector_id"] - collection_elements = from_channel_message(event['message']) - - output = await projector_sync_send_data(projector_id, collection_elements) - if output: - await self.send_json(type='autoupdate', content=output) - - -def projector_startup_data(projector_id: int) -> Any: - """ - Generate the startup data for a projector. - """ - try: - projector = Projector.objects.get(pk=projector_id) - except Projector.DoesNotExist: - return {'text': 'The projector {} does not exist.'.format(projector_id)} - else: - # Now check whether broadcast is active at the moment. If yes, - # change the local projector variable. - if config['projector_broadcast'] > 0: - projector = Projector.objects.get(pk=config['projector_broadcast']) - - # Collect all elements that are on the projector. - output = [] - for requirement in projector.get_all_requirements(): - required_collection_element = CollectionElement.from_instance(requirement) - output.append(required_collection_element.as_autoupdate_for_projector()) - - # Collect all config elements. - config_collection = Collection(config.get_collection_string()) - projector_data = (config_collection.get_access_permissions() - .get_projector_data(config_collection.get_full_data())) - for data in projector_data: - output.append(format_for_autoupdate_old( - config_collection.collection_string, - data['id'], - 'changed', - data)) - - # Collect the projector instance. - collection_element = CollectionElement.from_instance(projector) - output.append(collection_element.as_autoupdate_for_projector()) - - # Send all the data that were only collected before. - return output - - -@sync_to_async -def projector_sync_send_data(projector_id: int, collection_elements: List[CollectionElement]) -> List[Any]: - """ - sync function that generates the elements for an projector. - """ - # Load the projector object. If broadcast is on, use the broadcast projector - # instead. - if config['projector_broadcast'] > 0: - projector_id = config['projector_broadcast'] - - projector = Projector.objects.get(pk=projector_id) - - # TODO: This runs once for every open projector tab. Either use - # caching or something else, so this is only called once - output = [] - for collection_element in collection_elements: - if collection_element.is_deleted(): - output.append(collection_element.as_autoupdate_for_projector()) - else: - for element in projector.get_collection_elements_required_for_this(collection_element): - output.append(element.as_autoupdate_for_projector()) - return output diff --git a/openslides/utils/models.py b/openslides/utils/models.py index 8cdb92396..f37b95b74 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -72,7 +72,7 @@ class RESTModelMixin: """ return self.pk # type: ignore - def save(self, skip_autoupdate: bool = False, information: Dict[str, str] = None, *args: Any, **kwargs: Any) -> Any: + def save(self, skip_autoupdate: bool = False, *args: Any, **kwargs: Any) -> Any: """ Calls Django's save() method and afterwards hits the autoupdate system. @@ -82,18 +82,15 @@ class RESTModelMixin: element from the instance: CollectionElement.from_instance(instance) - - The optional argument information can be a dictionary that is given to - the autoupdate system. """ # We don't know how to fix this circular import from .autoupdate import inform_changed_data return_value = super().save(*args, **kwargs) # type: ignore if not skip_autoupdate: - inform_changed_data(self.get_root_rest_element(), information=information) + inform_changed_data(self.get_root_rest_element()) return return_value - def delete(self, skip_autoupdate: bool = False, information: Dict[str, str] = None, *args: Any, **kwargs: Any) -> Any: + def delete(self, skip_autoupdate: bool = False, *args: Any, **kwargs: Any) -> Any: """ Calls Django's delete() method and afterwards hits the autoupdate system. @@ -107,9 +104,6 @@ class RESTModelMixin: or CollectionElement.from_values(collection_string, id, deleted=True) - - The optional argument information can be a dictionary that is given to - the autoupdate system. """ # We don't know how to fix this circular import from .autoupdate import inform_changed_data, inform_deleted_data @@ -118,9 +112,9 @@ class RESTModelMixin: if not skip_autoupdate: if self != self.get_root_rest_element(): # The deletion of a included element is a change of the root element. - inform_changed_data(self.get_root_rest_element(), information=information) + inform_changed_data(self.get_root_rest_element()) else: - inform_deleted_data([(self.get_collection_string(), instance_pk)], information=information) + inform_deleted_data([(self.get_collection_string(), instance_pk)]) return return_value @classmethod diff --git a/openslides/utils/projector.py b/openslides/utils/projector.py index 14db3d7da..8975adecf 100644 --- a/openslides/utils/projector.py +++ b/openslides/utils/projector.py @@ -1,6 +1,4 @@ -from typing import Any, Dict, Generator, Iterable, List, Optional, Type - -from .collection import CollectionElement +from typing import Any, Dict, Generator, Optional, Type class ProjectorElement: @@ -45,44 +43,6 @@ class ProjectorElement: """ pass - def get_requirements(self, config_entry: Any) -> Iterable[Any]: - """ - Returns an iterable of instances that are required for this projector - element. The config_entry has to be given. - """ - return () - - def get_requirements_as_collection_elements(self, config_entry: Any) -> Iterable[CollectionElement]: - """ - Returns an iterable of collection elements that are required for this - projector element. The config_entry has to be given. - """ - return (CollectionElement.from_instance(instance) for instance in self.get_requirements(config_entry)) - - def get_collection_elements_required_for_this( - self, collection_element: CollectionElement, - config_entry: Any) -> List[CollectionElement]: - """ - Returns a list of CollectionElements that have to be sent to every - projector that shows this projector element according to the given - collection_element. - - Default: Returns only the collection_element if it belongs to the - requirements but return all requirements if the projector changes. - """ - requirements_as_collection_elements = list(self.get_requirements_as_collection_elements(config_entry)) - for requirement in requirements_as_collection_elements: - if collection_element == requirement: - output = [collection_element] - break - else: - if collection_element.information.get('this_projector'): - output = [collection_element] - output.extend(requirements_as_collection_elements) - else: - output = [] - return output - projector_elements: Dict[str, ProjectorElement] = {} diff --git a/tests/unit/users/test_access_permissions.py b/tests/unit/users/test_access_permissions.py index c96ed1f7e..1561c7a84 100644 --- a/tests/unit/users/test_access_permissions.py +++ b/tests/unit/users/test_access_permissions.py @@ -1,48 +1,9 @@ from unittest import TestCase -from openslides.users.access_permissions import ( - PersonalNoteAccessPermissions, - UserAccessPermissions, -) +from openslides.users.access_permissions import PersonalNoteAccessPermissions from openslides.utils.collection import CollectionElement -class UserGetProjectorDataTest(TestCase): - def test_get_projector_data_with_collection(self): - """ - This test ensures that comment field is removed. - """ - full_data = { - 'id': 42, - 'username': 'username_ai3Oofu7eit0eeyu1sie', - 'title': '', - 'first_name': 'first_name_iu8toShae0oolie8aevo', - 'last_name': 'last_name_OhZ4beezohY0doNoh2th', - 'structure_level': '', - 'number': '', - 'about_me': '', - 'groups_id': [], - 'is_present': False, - 'is_committee': False, - 'comment': 'comment_gah7aipeJohv9xethoku', - } - - data = UserAccessPermissions().get_projector_data([full_data]) - self.assertEqual(data[0], { - 'id': 42, - 'username': 'username_ai3Oofu7eit0eeyu1sie', - 'title': '', - 'first_name': 'first_name_iu8toShae0oolie8aevo', - 'last_name': 'last_name_OhZ4beezohY0doNoh2th', - 'structure_level': '', - 'number': '', - 'about_me': '', - 'groups_id': [], - 'is_present': False, - 'is_committee': False, - }) - - class TestPersonalNoteAccessPermissions(TestCase): def test_get_restricted_data(self): ap = PersonalNoteAccessPermissions() @@ -54,9 +15,6 @@ class TestPersonalNoteAccessPermissions(TestCase): def test_get_restricted_data_for_anonymous(self): ap = PersonalNoteAccessPermissions() rd = ap.get_restricted_data( - [CollectionElement.from_values( - 'users/personal_note', - 1, - full_data={'user_id': 1}).get_full_data()], + [{'user_id': 1}], None) self.assertEqual(rd, []) diff --git a/tests/unit/utils/test_collection.py b/tests/unit/utils/test_collection.py index b6ea72f7f..0485f170b 100644 --- a/tests/unit/utils/test_collection.py +++ b/tests/unit/utils/test_collection.py @@ -24,25 +24,6 @@ class TestCollectionElement(TestCase): self.assertEqual(collection_element.collection_string, 'testmodule/model') self.assertEqual(collection_element.id, 42) - def test_channel_message(self): - """ - Test that to_channel_message works together with from_channel_message. - """ - collection_element = collection.CollectionElement.from_values( - 'testmodule/model', - 42, - full_data={'data': 'value'}, - information={'some': 'information'}) - - created_collection_element = collection.from_channel_message( - collection.to_channel_message([collection_element]))[0] - - self.assertEqual( - collection_element, - created_collection_element) - self.assertEqual(created_collection_element.full_data, {'data': 'value'}) - self.assertEqual(created_collection_element.information, {'some': 'information'}) - @patch.object(collection.CollectionElement, 'get_full_data') def test_equal(self, mock_get_full_data): self.assertEqual(