Remove old projector code
This commit is contained in:
parent
f0d60a6a96
commit
b034839ac8
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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:
|
||||||
|
@ -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),
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
@ -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",
|
||||||
{
|
{
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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] = {}
|
||||||
|
|
||||||
|
@ -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, [])
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user