Remove old projector code

This commit is contained in:
Oskar Hahn 2018-08-22 07:59:22 +02:00
parent f0d60a6a96
commit b034839ac8
22 changed files with 29 additions and 822 deletions

View File

@ -92,21 +92,3 @@ class ItemAccessPermissions(BaseAccessPermissions):
data = [] data = []
return 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

View File

@ -1,9 +1,6 @@
from typing import Generator, Type from typing import Generator, Type
from ..core.config import config
from ..core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from ..core.models import Projector
from ..utils.collection import CollectionElement
from ..utils.projector import ProjectorElement from ..utils.projector import ProjectorElement
from .models import Item from .models import Item
@ -28,9 +25,6 @@ class ItemListSlide(ProjectorElement):
if not Item.objects.filter(pk=pk).exists(): if not Item.objects.filter(pk=pk).exists():
raise ProjectorException('Item does not exist.') raise ProjectorException('Item does not exist.')
def get_requirements(self, config_entry):
yield from Item.objects.all()
class ListOfSpeakersSlide(ProjectorElement): class ListOfSpeakersSlide(ProjectorElement):
""" """
@ -43,35 +37,6 @@ class ListOfSpeakersSlide(ProjectorElement):
if not Item.objects.filter(pk=self.config_entry.get('id')).exists(): if not Item.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Item does not exist.') 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): def update_data(self):
return {'agenda_item_id': self.config_entry.get('id')} return {'agenda_item_id': self.config_entry.get('id')}
@ -82,65 +47,6 @@ class CurrentListOfSpeakersSlide(ProjectorElement):
""" """
name = 'agenda/current-list-of-speakers' 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]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
yield ItemListSlide yield ItemListSlide

View File

@ -55,17 +55,3 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
data = [] data = []
return 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

View File

@ -26,36 +26,6 @@ class AssignmentSlide(ProjectorElement):
if poll.assignment_id != self.config_entry.get('id'): if poll.assignment_id != self.config_entry.get('id'):
raise ProjectorException('Assignment id and poll do not belong together.') 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): def update_data(self):
data = None data = None
try: try:

View File

@ -165,7 +165,7 @@ class ConfigHandler:
# Save the new value to the database. # Save the new value to the database.
db_value = ConfigStore.objects.get(key=key) db_value = ConfigStore.objects.get(key=key)
db_value.value = value db_value.value = value
db_value.save(information={'changed_config': key}) db_value.save()
# Call on_change callback. # Call on_change callback.
if config_variable.on_change: if config_variable.on_change:

View File

@ -3,7 +3,6 @@ from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from jsonfield import JSONField from jsonfield import JSONField
from ..utils.collection import CollectionElement
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from ..utils.projector import get_all_projector_elements from ..utils.projector import get_all_projector_elements
from .access_permissions import ( from .access_permissions import (
@ -130,61 +129,6 @@ class Projector(RESTModelMixin, models.Model):
result[key]['error'] = str(e) result[key]['error'] = str(e)
return result 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 @classmethod
def remove_any(cls, skip_autoupdate=False, **kwargs): def remove_any(cls, skip_autoupdate=False, **kwargs):
""" """

View File

@ -22,15 +22,6 @@ class CountdownElement(ProjectorElement):
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists(): if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Countdown does not 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): class ProjectorMessageElement(ProjectorElement):
""" """
@ -42,15 +33,6 @@ class ProjectorMessageElement(ProjectorElement):
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists(): if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Message does not 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]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
yield Clock yield Clock

View File

@ -15,15 +15,6 @@ class MediafileSlide(ProjectorElement):
if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists(): if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('File does not exist.') 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]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
yield MediafileSlide yield MediafileSlide

View File

@ -69,18 +69,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
return 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 all comments.
"""
data = []
for full in full_data:
full_copy = deepcopy(full)
full_copy['comments'] = []
data.append(full_copy)
return data
class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions): class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
""" """

View File

@ -1,10 +1,8 @@
import re
from typing import Generator, Type from typing import Generator, Type
from ..core.config import config
from ..core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement from ..utils.projector import ProjectorElement
from .models import Motion, MotionBlock, MotionChangeRecommendation, Workflow from .models import Motion, MotionBlock
class MotionSlide(ProjectorElement): class MotionSlide(ProjectorElement):
@ -17,68 +15,6 @@ class MotionSlide(ProjectorElement):
if not Motion.objects.filter(pk=self.config_entry.get('id')).exists(): if not Motion.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Motion does not exist.') 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): def update_data(self):
data = None data = None
try: try:
@ -101,26 +37,6 @@ class MotionBlockSlide(ProjectorElement):
if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists(): if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('MotionBlock does not exist.') 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): def update_data(self):
data = None data = None
try: try:

View File

@ -1,7 +1,7 @@
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url 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 from openslides.utils.middleware import AuthMiddlewareStack
@ -10,7 +10,6 @@ application = ProtocolTypeRouter({
"websocket": AuthMiddlewareStack( "websocket": AuthMiddlewareStack(
URLRouter([ URLRouter([
url(r"^ws/site/$", SiteConsumer), url(r"^ws/site/$", SiteConsumer),
url(r"^ws/projector/(?P<projector_id>\d+)/$", ProjectorConsumer),
]) ])
) )
}) })

View File

@ -15,16 +15,6 @@ class TopicSlide(ProjectorElement):
if not Topic.objects.filter(pk=self.config_entry.get('id')).exists(): if not Topic.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Topic does not exist.') 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): def update_data(self):
data = None data = None
try: try:

View File

@ -99,27 +99,6 @@ class UserAccessPermissions(BaseAccessPermissions):
return 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 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): class GroupAccessPermissions(BaseAccessPermissions):
""" """

View File

@ -15,15 +15,6 @@ class UserSlide(ProjectorElement):
if not User.objects.filter(pk=self.config_entry.get('id')).exists(): if not User.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('User does not exist.') 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]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
yield UserSlide yield UserSlide

View File

@ -56,11 +56,3 @@ class BaseAccessPermissions:
retrieve() or list(). retrieve() or list().
""" """
return full_data if self.check_permissions(user) else [] 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

View File

@ -1,5 +1,4 @@
import threading import threading
from collections import OrderedDict
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from asgiref.sync import async_to_sync 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 django.db.models import Model
from .cache import element_cache, get_element_id 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]: def inform_changed_data(instances: Union[Iterable[Model], Model]) -> None:
"""
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:
""" """
Informs the autoupdate system and the caching system about the creation or Informs the autoupdate system and the caching system about the creation or
update of an element. update of an element.
@ -41,10 +29,8 @@ def inform_changed_data(instances: Union[Iterable[Model], Model], information: D
collection_elements = {} collection_elements = {}
for root_instance in root_instances: for root_instance in root_instances:
collection_element = CollectionElement.from_instance( collection_element = CollectionElement.from_instance(root_instance)
root_instance, key = root_instance.get_collection_string() + str(root_instance.get_rest_pk())
information=information)
key = root_instance.get_collection_string() + str(root_instance.get_rest_pk()) + str(to_ordered_dict(information))
collection_elements[key] = collection_element collection_elements[key] = collection_element
bundle = autoupdate_bundle.get(threading.get_ident()) 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()) 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 Informs the autoupdate system and the caching system about the deletion of
elements. elements.
The argument information is added to each collection element.
""" """
collection_elements: Dict[str, Any] = {} collection_elements: Dict[str, Any] = {}
for element in elements: for element in elements:
collection_element = CollectionElement.from_values( collection_element = CollectionElement.from_values(
collection_string=element[0], collection_string=element[0],
id=element[1], id=element[1],
deleted=True, deleted=True)
information=information) key = element[0] + str(element[1])
key = element[0] + str(element[1]) + str(to_ordered_dict(information))
collection_elements[key] = collection_element collection_elements[key] = collection_element
bundle = autoupdate_bundle.get(threading.get_ident()) 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()) async_to_sync(send_autoupdate)(collection_elements.values())
def inform_data_collection_element_list(collection_elements: List[CollectionElement], def inform_data_collection_element_list(collection_elements: List[CollectionElement]) -> None:
information: Dict[str, Any] = None) -> None:
""" """
Informs the autoupdate system about some collection elements. This is Informs the autoupdate system about some collection elements. This is
used just to send some data to all users. used just to send some data to all users.
""" """
elements = {} elements = {}
for collection_element in collection_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 elements[key] = collection_element
bundle = autoupdate_bundle.get(threading.get_ident()) 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) change_id = await element_cache.change_elements(cache_elements)
channel_layer = get_channel_layer() 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( await channel_layer.group_send(
"autoupdate", "autoupdate",
{ {

View File

@ -1,14 +1,4 @@
from typing import ( from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type
TYPE_CHECKING,
Any,
Dict,
Generator,
Iterable,
List,
Optional,
Type,
cast,
)
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from django.apps import apps 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: class CollectionElement:
def __init__(self, instance: Model = None, deleted: bool = False, collection_string: str = None, 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(). Do not use this. Use the methods from_instance() or from_values().
""" """
self.instance = instance self.instance = instance
self.deleted = deleted self.deleted = deleted
self.full_data = full_data self.full_data = full_data
self.information = information or {}
if instance is not None: if instance is not None:
# Collection element is created via instance # Collection element is created via instance
self.collection_string = instance.get_collection_string() self.collection_string = instance.get_collection_string()
@ -93,7 +52,7 @@ class CollectionElement:
@classmethod @classmethod
def from_instance( 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. 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. 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 @classmethod
def from_values(cls, collection_string: str, id: int, deleted: bool = False, 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. 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. 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(). 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, return cls(collection_string=collection_string, id=id, deleted=deleted, full_data=full_data)
full_data=full_data, information=information)
def __eq__(self, collection_element: 'CollectionElement') -> bool: # type: ignore def __eq__(self, collection_element: 'CollectionElement') -> bool: # type: ignore
""" """
@ -127,23 +85,6 @@ class CollectionElement:
return (self.collection_string == collection_element.collection_string and return (self.collection_string == collection_element.collection_string and
self.id == collection_element.id) 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]]: 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. 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: except KeyError:
raise ValueError('Invalid message. A valid collection_string is missing.') raise ValueError('Invalid message. A valid collection_string is missing.')
return model 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

View File

@ -2,20 +2,9 @@ from collections import defaultdict
from typing import Any, Dict, List from typing import Any, Dict, List
from urllib.parse import parse_qs from urllib.parse import parse_qs
from asgiref.sync import sync_to_async from .auth import async_anonymous_is_enabled
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 .cache import element_cache, split_element_id from .cache import element_cache, split_element_id
from .collection import ( from .collection import AutoupdateFormat
AutoupdateFormat,
Collection,
CollectionElement,
format_for_autoupdate_old,
from_channel_message,
)
from .websocket import ProtocollAsyncJsonWebsocketConsumer, get_element_data from .websocket import ProtocollAsyncJsonWebsocketConsumer, get_element_data
@ -72,13 +61,11 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
for item in event['incomming']: for item in event['incomming']:
users = item.get('users') users = item.get('users')
reply_channels = item.get('replyChannels') reply_channels = item.get('replyChannels')
projectors = item.get('projectors')
if ((isinstance(users, list) and user_id in users) if ((isinstance(users, list) and user_id in users)
or (isinstance(reply_channels, list) and self.channel_name in reply_channels) 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['senderReplyChannelName'] = event.get('senderReplyChannelName')
item['senderUserId'] = event.get('senderUserId') item['senderUserId'] = event.get('senderUserId')
item['senderProjectorId'] = event.get('senderProjectorId')
out.append(item) out.append(item)
if out: if out:
@ -101,153 +88,3 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
from_change_id=change_id, from_change_id=change_id,
to_change_id=change_id, to_change_id=change_id,
all_data=False)) 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

View File

@ -72,7 +72,7 @@ class RESTModelMixin:
""" """
return self.pk # type: ignore 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. Calls Django's save() method and afterwards hits the autoupdate system.
@ -82,18 +82,15 @@ class RESTModelMixin:
element from the instance: element from the instance:
CollectionElement.from_instance(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 # We don't know how to fix this circular import
from .autoupdate import inform_changed_data from .autoupdate import inform_changed_data
return_value = super().save(*args, **kwargs) # type: ignore return_value = super().save(*args, **kwargs) # type: ignore
if not skip_autoupdate: 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 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. Calls Django's delete() method and afterwards hits the autoupdate system.
@ -107,9 +104,6 @@ class RESTModelMixin:
or or
CollectionElement.from_values(collection_string, id, deleted=True) 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 # We don't know how to fix this circular import
from .autoupdate import inform_changed_data, inform_deleted_data from .autoupdate import inform_changed_data, inform_deleted_data
@ -118,9 +112,9 @@ class RESTModelMixin:
if not skip_autoupdate: if not skip_autoupdate:
if self != self.get_root_rest_element(): if self != self.get_root_rest_element():
# The deletion of a included element is a change of the root 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: else:
inform_deleted_data([(self.get_collection_string(), instance_pk)], information=information) inform_deleted_data([(self.get_collection_string(), instance_pk)])
return return_value return return_value
@classmethod @classmethod

View File

@ -1,6 +1,4 @@
from typing import Any, Dict, Generator, Iterable, List, Optional, Type from typing import Any, Dict, Generator, Optional, Type
from .collection import CollectionElement
class ProjectorElement: class ProjectorElement:
@ -45,44 +43,6 @@ class ProjectorElement:
""" """
pass 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] = {} projector_elements: Dict[str, ProjectorElement] = {}

View File

@ -1,48 +1,9 @@
from unittest import TestCase from unittest import TestCase
from openslides.users.access_permissions import ( from openslides.users.access_permissions import PersonalNoteAccessPermissions
PersonalNoteAccessPermissions,
UserAccessPermissions,
)
from openslides.utils.collection import CollectionElement 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): class TestPersonalNoteAccessPermissions(TestCase):
def test_get_restricted_data(self): def test_get_restricted_data(self):
ap = PersonalNoteAccessPermissions() ap = PersonalNoteAccessPermissions()
@ -54,9 +15,6 @@ class TestPersonalNoteAccessPermissions(TestCase):
def test_get_restricted_data_for_anonymous(self): def test_get_restricted_data_for_anonymous(self):
ap = PersonalNoteAccessPermissions() ap = PersonalNoteAccessPermissions()
rd = ap.get_restricted_data( rd = ap.get_restricted_data(
[CollectionElement.from_values( [{'user_id': 1}],
'users/personal_note',
1,
full_data={'user_id': 1}).get_full_data()],
None) None)
self.assertEqual(rd, []) self.assertEqual(rd, [])

View File

@ -24,25 +24,6 @@ class TestCollectionElement(TestCase):
self.assertEqual(collection_element.collection_string, 'testmodule/model') self.assertEqual(collection_element.collection_string, 'testmodule/model')
self.assertEqual(collection_element.id, 42) 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') @patch.object(collection.CollectionElement, 'get_full_data')
def test_equal(self, mock_get_full_data): def test_equal(self, mock_get_full_data):
self.assertEqual( self.assertEqual(