New projector system. Add first slides

This commit is contained in:
Oskar Hahn 2018-12-23 11:05:38 +01:00
parent 73e1853758
commit a0f554674b
32 changed files with 599 additions and 521 deletions

View File

@ -2,21 +2,18 @@ from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from ..utils.projector import register_projector_elements
class AgendaAppConfig(AppConfig): class AgendaAppConfig(AppConfig):
name = "openslides.agenda" name = "openslides.agenda"
verbose_name = "OpenSlides Agenda" verbose_name = "OpenSlides Agenda"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save
from ..core.signals import permission_change from ..core.signals import permission_change
from ..utils.rest_api import router from ..utils.rest_api import router
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import ( from .signals import (
get_permission_change_data, get_permission_change_data,
listen_to_related_object_post_delete, listen_to_related_object_post_delete,
@ -27,7 +24,7 @@ class AgendaAppConfig(AppConfig):
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
post_save.connect( post_save.connect(

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Countdown, Projector from openslides.core.models import Countdown
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
@ -291,18 +291,6 @@ class Item(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete an agenda item. Ensures that a respective
list of speakers projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="agenda/list-of-speakers", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
@property @property
def title(self): def title(self):
""" """

View File

@ -1,57 +1,89 @@
from typing import Generator, Type from collections import defaultdict
from typing import Any, Dict, List, Tuple
from ..core.exceptions import ProjectorException from ..utils.projector import AllData, register_projector_element
from ..utils.projector import ProjectorElement
from .models import Item
class ItemListSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def get_tree(
all_data: AllData, parent_id: int = 0
) -> List[Tuple[int, List[Tuple[int, List[Any]]]]]:
""" """
Slide definitions for Item model. Build the item tree from all_data.
This is only for item list slides. Only build the tree from elements unterneath parent_id.
Set 'id' to None to get a list slide of all root items. Set 'id' to an Returns a list of two element tuples where the first element is the item title
integer to get a list slide of the children of the metioned item. and the second a List with children as two element tuples.
Additionally set 'tree' to True to get also children of children.
""" """
name = "agenda/item-list" # Build a dict from an item_id to all its children
children: Dict[int, List[int]] = defaultdict(list)
for item in sorted(
all_data["agenda/item"].values(), key=lambda item: item["weight"]
):
if item["type"] == 1: # only normal items
children[item["parent_id"] or 0].append(item["id"])
def check_data(self): def get_children(
pk = self.config_entry.get("id") item_ids: List[int]
if pk is not None: ) -> List[Tuple[int, List[Tuple[int, List[Any]]]]]:
# Children slide. return [
if not Item.objects.filter(pk=pk).exists(): (all_data["agenda/item"][item_id]["title"], get_children(children[item_id]))
raise ProjectorException("Item does not exist.") for item_id in item_ids
]
return get_children(children[parent_id])
class ListOfSpeakersSlide(ProjectorElement): def items(config: Dict[str, Any], all_data: AllData) -> Dict[str, Any]:
""" """
Slide definitions for Item model. Item list slide.
This is only for list of speakers slide. You have to set 'id'.
Returns all root items or all children of an item.
""" """
root_item_id = config.get("id") or None
show_tree = config.get("tree") or False
name = "agenda/list-of-speakers" if show_tree:
items = get_tree(all_data, root_item_id or 0)
else:
items = []
for item in sorted(
all_data["agenda/item"].values(), key=lambda item: item["weight"]
):
if item["parent_id"] == root_item_id and item["type"] == 1:
items.append(item["title"])
def check_data(self): return {"items": items}
if not Item.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("Item does not exist.")
def update_data(self):
return {"agenda_item_id": self.config_entry.get("id")}
class CurrentListOfSpeakersSlide(ProjectorElement): def list_of_speakers(config: Dict[str, Any], all_data: AllData) -> Dict[str, Any]:
""" """
Slide for the current list of speakers. List of speakers slide.
Returns all usernames, that are on the list of speaker of a slide.
""" """
item_id = config.get("id") or 0 # item_id 0 means current_list_of_speakers
name = "agenda/current-list-of-speakers" # TODO: handle item_id == 0
try:
item = all_data["agenda/item"][item_id]
except KeyError:
return {"error": "Item {} does not exist".format(item_id)}
user_ids = []
for speaker in item["speakers"]:
user_ids.append(speaker["user"])
return {"user_ids": user_ids}
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield ItemListSlide register_projector_element("agenda/item-list", items)
yield ListOfSpeakersSlide register_projector_element("agenda/list-of-speakers", list_of_speakers)
yield CurrentListOfSpeakersSlide

View File

@ -3,14 +3,11 @@ from typing import Any, Dict, List, Set
from django.apps import AppConfig from django.apps import AppConfig
from mypy_extensions import TypedDict from mypy_extensions import TypedDict
from ..utils.projector import register_projector_elements
class AssignmentsAppConfig(AppConfig): class AssignmentsAppConfig(AppConfig):
name = "openslides.assignments" name = "openslides.assignments"
verbose_name = "OpenSlides Assignments" verbose_name = "OpenSlides Assignments"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
@ -18,12 +15,12 @@ class AssignmentsAppConfig(AppConfig):
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
from ..utils.rest_api import router from ..utils.rest_api import router
from . import serializers # noqa from . import serializers # noqa
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import get_permission_change_data from .signals import get_permission_change_data
from .views import AssignmentViewSet, AssignmentPollViewSet from .views import AssignmentViewSet, AssignmentPollViewSet
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(

View File

@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _, ugettext_noop
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Projector, Tag from openslides.core.models import Tag
from openslides.poll.models import ( from openslides.poll.models import (
BaseOption, BaseOption,
BasePoll, BasePoll,
@ -159,18 +159,6 @@ class Assignment(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete an assignment. Ensures that a respective
assignment projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="assignments/assignment", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
@property @property
def candidates(self): def candidates(self):
""" """
@ -432,21 +420,6 @@ class AssignmentPoll( # type: ignore
class Meta: class Meta:
default_permissions = () default_permissions = ()
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete an assignment poll. Ensures that a respective
assignment projector element (with poll, so called poll slide) is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate,
name="assignments/assignment",
id=self.assignment.pk,
poll=self.pk,
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def get_assignment(self): def get_assignment(self):
return self.assignment return self.assignment

View File

@ -1,45 +1,23 @@
from typing import Generator, Type from typing import Any, Dict
from ..core.exceptions import ProjectorException from ..utils.projector import register_projector_element
from ..utils.projector import ProjectorElement
from .models import Assignment, AssignmentPoll
class AssignmentSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def assignment(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for Assignment model. Assignment slide.
You can send a poll id to get a poll slide.
""" """
poll_id = config.get("tree") # noqa
name = "assignments/assignment" return {"error": "TODO"}
def check_data(self):
if not Assignment.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("Election does not exist.")
poll_id = self.config_entry.get("poll")
if poll_id:
# Poll slide.
try:
poll = AssignmentPoll.objects.get(pk=poll_id)
except AssignmentPoll.DoesNotExist:
raise ProjectorException("Poll does not exist.")
if poll.assignment_id != self.config_entry.get("id"):
raise ProjectorException(
"Assignment id and poll do not belong together."
)
def update_data(self):
data = None
try:
assignment = Assignment.objects.get(pk=self.config_entry.get("id"))
except Assignment.DoesNotExist:
# Assignment does not exist, so just do nothing.
pass
else:
data = {"agenda_item_id": assignment.agenda_item_id}
return data
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield AssignmentSlide register_projector_element("assignments/assignment", assignment)

View File

@ -7,21 +7,16 @@ from django.apps import AppConfig
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from ..utils.projector import register_projector_elements
class CoreAppConfig(AppConfig): class CoreAppConfig(AppConfig):
name = "openslides.core" name = "openslides.core"
verbose_name = "OpenSlides Core" verbose_name = "OpenSlides Core"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from .config import config from .config import config
from ..utils.rest_api import router from .projector import register_projector_elements
from ..utils.cache import element_cache
from .projector import get_projector_elements
from . import serializers # noqa from . import serializers # noqa
from .signals import ( from .signals import (
delete_django_app_permissions, delete_django_app_permissions,
@ -38,15 +33,18 @@ class CoreAppConfig(AppConfig):
ProjectorViewSet, ProjectorViewSet,
TagViewSet, TagViewSet,
) )
from ..utils.constants import set_constants, get_constants_from_apps
from .websocket import ( from .websocket import (
NotifyWebsocketClientMessage, NotifyWebsocketClientMessage,
ConstantsWebsocketClientMessage, ConstantsWebsocketClientMessage,
GetElementsWebsocketClientMessage, GetElementsWebsocketClientMessage,
AutoupdateWebsocketClientMessage, AutoupdateWebsocketClientMessage,
ListenToProjectors,
) )
from ..utils.websocket import register_client_message
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
from ..utils.cache import element_cache
from ..utils.constants import set_constants, get_constants_from_apps
from ..utils.rest_api import router
from ..utils.websocket import register_client_message
# Collect all config variables before getting the constants. # Collect all config variables before getting the constants.
config.collect_config_variables_from_apps() config.collect_config_variables_from_apps()
@ -64,7 +62,7 @@ class CoreAppConfig(AppConfig):
set_constants(get_constants_from_apps()) set_constants(get_constants_from_apps())
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
post_permission_creation.connect( post_permission_creation.connect(
@ -114,6 +112,7 @@ class CoreAppConfig(AppConfig):
register_client_message(ConstantsWebsocketClientMessage()) register_client_message(ConstantsWebsocketClientMessage())
register_client_message(GetElementsWebsocketClientMessage()) register_client_message(GetElementsWebsocketClientMessage())
register_client_message(AutoupdateWebsocketClientMessage()) register_client_message(AutoupdateWebsocketClientMessage())
register_client_message(ListenToProjectors())
# register required_users # register required_users
required_user.add_collection_string( required_user.add_collection_string(

View File

@ -1,10 +1,6 @@
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
class ProjectorException(OpenSlidesError):
pass
class TagException(OpenSlidesError): class TagException(OpenSlidesError):
pass pass

View File

@ -7,7 +7,6 @@ from jsonfield import JSONField
from ..utils.autoupdate import Element from ..utils.autoupdate import Element
from ..utils.cache import element_cache, get_element_id from ..utils.cache import element_cache, get_element_id
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from ..utils.projector import get_all_projector_elements
from .access_permissions import ( from .access_permissions import (
ChatMessageAccessPermissions, ChatMessageAccessPermissions,
ConfigAccessPermissions, ConfigAccessPermissions,
@ -17,7 +16,6 @@ from .access_permissions import (
ProjectorMessageAccessPermissions, ProjectorMessageAccessPermissions,
TagAccessPermissions, TagAccessPermissions,
) )
from .exceptions import ProjectorException
class ProjectorManager(models.Manager): class ProjectorManager(models.Manager):
@ -34,6 +32,7 @@ class ProjectorManager(models.Manager):
class Projector(RESTModelMixin, models.Model): class Projector(RESTModelMixin, models.Model):
# TODO: Fix docstring
""" """
Model for all projectors. Model for all projectors.
@ -103,66 +102,6 @@ class Projector(RESTModelMixin, models.Model):
("can_see_frontpage", "Can see the front page"), ("can_see_frontpage", "Can see the front page"),
) )
@property
def elements(self):
"""
Retrieve all projector elements given in the config field. For
every element the method check_and_update_data() is called and its
result is also used.
"""
# Get all elements from all apps.
elements = get_all_projector_elements()
# Parse result
result = {}
for key, value in self.config.items():
# Use a copy here not to change the origin value in the config field.
result[key] = value.copy()
result[key]["uuid"] = key
element = elements.get(value["name"])
if element is None:
result[key]["error"] = "Projector element does not exist."
else:
try:
result[key].update(
element.check_and_update_data(
projector_object=self, config_entry=value
)
)
except ProjectorException as e:
result[key]["error"] = str(e)
return result
@classmethod
def remove_any(cls, skip_autoupdate=False, **kwargs):
"""
Removes all projector elements from all projectors with matching kwargs.
Additional properties of active projector elements are ignored:
Example: Sending {'name': 'assignments/assignment', 'id': 1} will remove
also projector elements with {'name': 'assignments/assignment', 'id': 1, 'poll': 2}.
"""
# Loop over all projectors.
for projector in cls.objects.all():
change_projector_config = False
projector_config = {}
# Loop over all projector elements of this projector.
for key, value in projector.config.items():
# Check if the kwargs match this element.
for kwarg_key, kwarg_value in kwargs.items():
if not value.get(kwarg_key) == kwarg_value:
# No match so the element should stay. Write it into
# new config field and break the loop.
projector_config[key] = value
break
else:
# All kwargs match this projector element. So mark this
# projector to be changed. Do not write it into new config field.
change_projector_config = True
if change_projector_config:
projector.config = projector_config
projector.save(skip_autoupdate=skip_autoupdate)
class ProjectionDefault(RESTModelMixin, models.Model): class ProjectionDefault(RESTModelMixin, models.Model):
""" """
@ -275,18 +214,6 @@ class ProjectorMessage(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a projector message. Ensures that a respective
projector message projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="core/projector-message", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
class Countdown(RESTModelMixin, models.Model): class Countdown(RESTModelMixin, models.Model):
""" """
@ -306,18 +233,6 @@ class Countdown(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a countdown. Ensures that a respective
countdown projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="core/countdown", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def control(self, action, skip_autoupdate=False): def control(self, action, skip_autoupdate=False):
if action not in ("start", "stop", "reset"): if action not in ("start", "stop", "reset"):
raise ValueError( raise ValueError(

View File

@ -1,43 +1,57 @@
from typing import Generator, Type from typing import Any, Dict
from ..utils.projector import ProjectorElement from ..utils.projector import register_projector_element
from .exceptions import ProjectorException
from .models import Countdown, ProjectorMessage
class Clock(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def countdown(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Clock on the projector. Countdown slide.
Returns the full_data of the countdown element.
config = {
name: 'core/countdown',
id: 5, # Countdown ID
}
""" """
countdown_id = config.get("id") or 1
name = "core/clock" try:
return all_data["core/countdown"][countdown_id]
except KeyError:
return {"error": "Countdown {} does not exist".format(countdown_id)}
class CountdownElement(ProjectorElement): def message(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Countdown slide for the projector. Message slide.
Returns the full_data of the message element.
config = {
name: 'core/projector-message',
id: 5, # ProjectorMessage ID
}
""" """
message_id = config.get("id") or 1
name = "core/countdown" try:
return all_data["core/projector-message"][message_id]
def check_data(self): except KeyError:
if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists(): return {"error": "Message {} does not exist".format(message_id)}
raise ProjectorException("Countdown does not exists.")
class ProjectorMessageElement(ProjectorElement): def register_projector_elements() -> None:
""" register_projector_element("core/countdown", countdown)
Short message on the projector. Rendered as overlay. register_projector_element("core/projector-message", message)
""" # TODO: Deside if we need a clock slide
name = "core/projector-message"
def check_data(self):
if not ProjectorMessage.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("Message does not exists.")
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
yield Clock
yield CountdownElement
yield ProjectorMessageElement

View File

@ -56,7 +56,7 @@ class ProjectorSerializer(ModelSerializer):
Serializer for core.models.Projector objects. Serializer for core.models.Projector objects.
""" """
config = JSONSerializerField(write_only=True) config = JSONSerializerField()
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
class Meta: class Meta:
@ -64,7 +64,6 @@ class ProjectorSerializer(ModelSerializer):
fields = ( fields = (
"id", "id",
"config", "config",
"elements",
"scale", "scale",
"scroll", "scroll",
"name", "name",

View File

@ -1,6 +1,7 @@
from typing import Any from typing import Any
from ..utils.constants import get_constants from ..utils.constants import get_constants
from ..utils.projector import get_projectot_data
from ..utils.websocket import ( from ..utils.websocket import (
BaseWebsocketClientMessage, BaseWebsocketClientMessage,
ProtocollAsyncJsonWebsocketConsumer, ProtocollAsyncJsonWebsocketConsumer,
@ -111,3 +112,51 @@ class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
await consumer.channel_layer.group_discard( await consumer.channel_layer.group_discard(
"autoupdate", consumer.channel_name "autoupdate", consumer.channel_name
) )
class ListenToProjectors(BaseWebsocketClientMessage):
"""
The client tells, to which projector it listens.
Therefor it sends a list of projector ids. If it sends an empty list, it does
not want to get any projector information.
"""
identifier = "listenToProjectors"
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"titel": "Listen to projectors",
"description": "Listen to zero, one or more projectors..",
"type": "object",
"properties": {
"projector_ids": {
"type": "array",
"items": {"type": "integer"},
"uniqueItems": True,
}
},
"required": ["projector_ids"],
}
async def receive_content(
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
) -> None:
consumer.listen_projector_ids = content["projector_ids"]
if consumer.listen_projector_ids:
# listen to projector group
await consumer.channel_layer.group_add("projector", consumer.channel_name)
else:
# do not listen to projector group
await consumer.channel_layer.group_discard(
"projector", consumer.channel_name
)
# Send projector data
if consumer.listen_projector_ids:
projector_data = await get_projectot_data(consumer.listen_projector_ids)
for projector_id, data in projector_data.items():
consumer.projector_hash[projector_id] = hash(str(data))
await consumer.send_json(
type="projector", content=projector_data, in_response=id
)

View File

@ -2,27 +2,24 @@ from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from ..utils.projector import register_projector_elements
class MediafilesAppConfig(AppConfig): class MediafilesAppConfig(AppConfig):
name = "openslides.mediafiles" name = "openslides.mediafiles"
verbose_name = "OpenSlides Mediafiles" verbose_name = "OpenSlides Mediafiles"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change from openslides.core.signals import permission_change
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import get_permission_change_data from .signals import get_permission_change_data
from .views import MediafileViewSet from .views import MediafileViewSet
from . import serializers # noqa from . import serializers # noqa
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(

View File

@ -3,7 +3,6 @@ from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from ..core.config import config from ..core.config import config
from ..core.models import Projector
from ..utils.autoupdate import inform_changed_data from ..utils.autoupdate import inform_changed_data
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from .access_permissions import MediafileAccessPermissions from .access_permissions import MediafileAccessPermissions
@ -67,18 +66,6 @@ class Mediafile(RESTModelMixin, models.Model):
inform_changed_data(self.uploader) inform_changed_data(self.uploader)
return result return result
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a mediafile. Ensures that a respective
mediafile projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="mediafiles/mediafile", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def get_filesize(self): def get_filesize(self):
""" """
Transforms bytes to kilobytes or megabytes. Returns the size as string. Transforms bytes to kilobytes or megabytes. Returns the size as string.

View File

@ -1,21 +1,22 @@
from typing import Generator, Type from typing import Any, Dict
from ..core.exceptions import ProjectorException from ..utils.projector import register_projector_element
from ..utils.projector import ProjectorElement
from .models import Mediafile
class MediafileSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def mediafile(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for Mediafile model. Slide for Mediafile.
""" """
return {"error": "TODO"}
name = "mediafiles/mediafile"
def check_data(self):
if not Mediafile.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("File does not exist.")
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield MediafileSlide register_projector_element("mediafiles/mediafile", mediafile)

View File

@ -3,20 +3,17 @@ from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from ..utils.projector import register_projector_elements
class MotionsAppConfig(AppConfig): class MotionsAppConfig(AppConfig):
name = "openslides.motions" name = "openslides.motions"
verbose_name = "OpenSlides Motion" verbose_name = "OpenSlides Motion"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change from openslides.core.signals import permission_change
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import create_builtin_workflows, get_permission_change_data from .signals import create_builtin_workflows, get_permission_change_data
from . import serializers # noqa from . import serializers # noqa
from .views import ( from .views import (
@ -33,7 +30,7 @@ class MotionsAppConfig(AppConfig):
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
post_migrate.connect( post_migrate.connect(

View File

@ -12,7 +12,7 @@ from jsonfield import JSONField
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Projector, Tag from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile from openslides.mediafiles.models import Mediafile
from openslides.poll.models import ( from openslides.poll.models import (
BaseOption, BaseOption,
@ -310,18 +310,6 @@ class Motion(RESTModelMixin, models.Model):
if not skip_autoupdate: if not skip_autoupdate:
inform_changed_data(self) inform_changed_data(self)
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a motion. Ensures that a respective
motion projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="motions/motion", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def set_identifier(self): def set_identifier(self):
""" """
Sets the motion identifier automaticly according to the config value if Sets the motion identifier automaticly according to the config value if
@ -893,18 +881,6 @@ class MotionBlock(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a motion block. Ensures that a respective
motion block projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="motions/motion-block", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
""" """
Container for runtime information for agenda app (on create or update of this instance). Container for runtime information for agenda app (on create or update of this instance).
""" """

View File

@ -1,56 +1,32 @@
from typing import Generator, Type from typing import Any, Dict
from ..core.exceptions import ProjectorException from ..utils.projector import register_projector_element
from ..utils.projector import ProjectorElement
from .models import Motion, MotionBlock
class MotionSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def motion(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for Motion model. Motion slide.
""" """
return {"error": "TODO"}
name = "motions/motion"
def check_data(self):
if not Motion.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("Motion does not exist.")
def update_data(self):
data = None
try:
motion = Motion.objects.get(pk=self.config_entry.get("id"))
except Motion.DoesNotExist:
# Motion does not exist, so just do nothing.
pass
else:
data = {"agenda_item_id": motion.agenda_item_id}
return data
class MotionBlockSlide(ProjectorElement): def motion_block(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for a block of motions (MotionBlock model). Motion slide.
""" """
return {"error": "TODO"}
name = "motions/motion-block"
def check_data(self):
if not MotionBlock.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("MotionBlock does not exist.")
def update_data(self):
data = None
try:
motion_block = MotionBlock.objects.get(pk=self.config_entry.get("id"))
except MotionBlock.DoesNotExist:
# MotionBlock does not exist, so just do nothing.
pass
else:
data = {"agenda_item_id": motion_block.agenda_item_id}
return data
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield MotionSlide register_projector_element("motion/motion", motion)
yield MotionBlockSlide register_projector_element("motion/motion-block", motion_block)

View File

@ -1,25 +1,22 @@
from django.apps import AppConfig from django.apps import AppConfig
from ..utils.projector import register_projector_elements
class TopicsAppConfig(AppConfig): class TopicsAppConfig(AppConfig):
name = "openslides.topics" name = "openslides.topics"
verbose_name = "OpenSlides Topics" verbose_name = "OpenSlides Topics"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change from openslides.core.signals import permission_change
from ..utils.rest_api import router from ..utils.rest_api import router
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import get_permission_change_data from .signals import get_permission_change_data
from .views import TopicViewSet from .views import TopicViewSet
from . import serializers # noqa from . import serializers # noqa
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(

View File

@ -4,7 +4,6 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
from ..agenda.models import Item from ..agenda.models import Item
from ..core.models import Projector
from ..mediafiles.models import Mediafile from ..mediafiles.models import Mediafile
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from .access_permissions import TopicAccessPermissions from .access_permissions import TopicAccessPermissions
@ -47,18 +46,6 @@ class Topic(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete a topic. Ensures that a respective
topic projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="topics/topic", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
""" """
Container for runtime information for agenda app (on create or update of this instance). Container for runtime information for agenda app (on create or update of this instance).
""" """

View File

@ -1,32 +1,22 @@
from typing import Generator, Type from typing import Any, Dict
from ..core.exceptions import ProjectorException from ..utils.projector import register_projector_element
from ..utils.projector import ProjectorElement
from .models import Topic
class TopicSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def topic(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for topic model. Topic slide.
""" """
return {"error": "TODO"}
name = "topics/topic"
def check_data(self):
if not Topic.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("Topic does not exist.")
def update_data(self):
data = None
try:
topic = Topic.objects.get(pk=self.config_entry.get("id"))
except Topic.DoesNotExist:
# Topic does not exist, so just do nothing.
pass
else:
data = {"agenda_item_id": topic.agenda_item_id}
return data
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield TopicSlide register_projector_element("topics/topic", topic)

View File

@ -2,26 +2,23 @@ from django.apps import AppConfig
from django.conf import settings from django.conf import settings
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from ..utils.projector import register_projector_elements
class UsersAppConfig(AppConfig): class UsersAppConfig(AppConfig):
name = "openslides.users" name = "openslides.users"
verbose_name = "OpenSlides Users" verbose_name = "OpenSlides Users"
angular_site_module = True angular_site_module = True
angular_projector_module = True
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from . import serializers # noqa from . import serializers # noqa
from ..core.signals import post_permission_creation, permission_change from ..core.signals import post_permission_creation, permission_change
from ..utils.rest_api import router from ..utils.rest_api import router
from .projector import get_projector_elements from .projector import register_projector_elements
from .signals import create_builtin_groups_and_admin, get_permission_change_data from .signals import create_builtin_groups_and_admin, get_permission_change_data
from .views import GroupViewSet, PersonalNoteViewSet, UserViewSet from .views import GroupViewSet, PersonalNoteViewSet, UserViewSet
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements()
# Connect signals. # Connect signals.
post_permission_creation.connect( post_permission_creation.connect(

View File

@ -18,7 +18,6 @@ from django.utils import timezone
from jsonfield import JSONField from jsonfield import JSONField
from ..core.config import config from ..core.config import config
from ..core.models import Projector
from ..utils.auth import GROUP_ADMIN_PK from ..utils.auth import GROUP_ADMIN_PK
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from .access_permissions import ( from .access_permissions import (
@ -197,18 +196,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
kwargs["skip_autoupdate"] = True kwargs["skip_autoupdate"] = True
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def delete(self, skip_autoupdate=False, *args, **kwargs):
"""
Customized method to delete an user. Ensures that a respective
user projector element is disabled.
"""
Projector.remove_any(
skip_autoupdate=skip_autoupdate, name="users/user", id=self.pk
)
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def has_perm(self, perm): def has_perm(self, perm):
""" """
This method is closed. Do not use it but use openslides.utils.auth.has_perm. This method is closed. Do not use it but use openslides.utils.auth.has_perm.

View File

@ -1,21 +1,22 @@
from typing import Generator, Type from typing import Any, Dict
from ..core.exceptions import ProjectorException from ..utils.projector import register_projector_element
from ..utils.projector import ProjectorElement
from .models import User
class UserSlide(ProjectorElement): # Important: All functions have to be prune. This means, that thay can only
# access the data, that they get as argument and do not have any
# side effects. They are called from an async context. So they have
# to be fast!
def user(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
""" """
Slide definitions for User model. User slide.
""" """
return {"error": "TODO"}
name = "users/user"
def check_data(self):
if not User.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException("User does not exist.")
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def register_projector_elements() -> None:
yield UserSlide register_projector_element("users/user", user)

View File

@ -8,6 +8,7 @@ from django.db.models import Model
from mypy_extensions import TypedDict from mypy_extensions import TypedDict
from .cache import element_cache, get_element_id from .cache import element_cache, get_element_id
from .projector import get_projectot_data
Element = TypedDict( Element = TypedDict(
@ -192,6 +193,13 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
"autoupdate", {"type": "send_data", "change_id": change_id} "autoupdate", {"type": "send_data", "change_id": change_id}
) )
projector_data = await get_projectot_data()
# Send projector
channel_layer = get_channel_layer()
await channel_layer.group_send(
"projector", {"type": "projector_changed", "data": projector_data}
)
if elements: if elements:
# Save histroy here using sync code. # Save histroy here using sync code.
history_instances = save_history(elements) history_instances = save_history(elements)

View File

@ -154,16 +154,28 @@ class ElementCache:
async def get_all_full_data(self) -> Dict[str, List[Dict[str, Any]]]: async def get_all_full_data(self) -> Dict[str, List[Dict[str, Any]]]:
""" """
Returns all full_data. If it does not exist, it is created. Returns all full_data.
The returned value is a dict where the key is the collection_string and The returned value is a dict where the key is the collection_string and
the value is a list of data. the value is a list of data.
""" """
all_data = await self.get_all_full_data_ordered()
out: Dict[str, List[Dict[str, Any]]] = defaultdict(list) out: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
for collection_string, collection_data in all_data.items():
for data in collection_data.values():
out[collection_string].append(data)
return dict(out)
async def get_all_full_data_ordered(self) -> Dict[str, Dict[int, Dict[str, Any]]]:
"""
Like get_all_full_data but orders the element of one collection by there
id.
"""
out: Dict[str, Dict[int, Dict[str, Any]]] = defaultdict(dict)
full_data = await self.cache_provider.get_all_data() full_data = await self.cache_provider.get_all_data()
for element_id, data in full_data.items(): for element_id, data in full_data.items():
collection_string, __ = split_element_id(element_id) collection_string, id = split_element_id(element_id)
out[collection_string].append(json.loads(data.decode())) out[collection_string][id] = json.loads(data.decode())
return dict(out) return dict(out)
async def get_full_data( async def get_full_data(

View File

@ -15,6 +15,10 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
groups = ["site"] groups = ["site"]
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.projector_hash: Dict[int, int] = {}
super().__init__(*args, **kwargs)
async def connect(self) -> None: async def connect(self) -> None:
""" """
A user connects to the site. A user connects to the site.
@ -110,3 +114,21 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
all_data=False, all_data=False,
), ),
) )
async def projector_changed(self, event: Dict[str, Any]) -> None:
"""
The projector has changed.
"""
all_projector_data = event["data"]
projector_data: Dict[int, Dict[str, Any]] = {}
for projector_id in self.listen_projector_ids:
data = all_projector_data.get(
projector_id, {"error": "No data for projector {}".format(projector_id)}
)
new_hash = hash(str(data))
if new_hash != self.projector_hash[projector_id]:
projector_data[projector_id] = data
self.projector_hash[projector_id] = new_hash
if projector_data:
await self.send_json(type="projector", content=projector_data)

View File

@ -1,70 +1,69 @@
from typing import Any, Dict, Generator, Optional, Type from typing import Any, Callable, Dict, List
from .cache import element_cache
class ProjectorElement: AllData = Dict[str, Dict[int, Dict[str, Any]]]
ProjectorElementCallable = Callable[[Dict[str, Any], AllData], Dict[str, Any]]
projector_elements: Dict[str, ProjectorElementCallable] = {}
def register_projector_element(name: str, element: ProjectorElementCallable) -> None:
""" """
Base class for an element on the projector. Registers a projector element.
Every app which wants to add projector elements has to create classes
subclassing from this base class with different names. The name attribute
has to be set.
"""
name: Optional[str] = None
def check_and_update_data(self, projector_object: Any, config_entry: Any) -> Any:
"""
Checks projector element data via self.check_data() and updates
them via self.update_data(). The projector object and the config
entry have to be given.
"""
self.projector_object = projector_object
self.config_entry = config_entry
assert (
self.config_entry.get("name") == self.name
), "To get data of a projector element, the correct config entry has to be given."
self.check_data()
return self.update_data() or {}
def check_data(self) -> None:
"""
Method can be overridden to validate projector element data. This
may raise ProjectorException in case of an error.
Default: Does nothing.
"""
pass
def update_data(self) -> Dict[Any, Any]:
"""
Method can be overridden to update the projector element data
output. This should return a dictonary. Use this for server
calculated data which have to be forwared to the client.
Default: Does nothing.
"""
pass
projector_elements: Dict[str, ProjectorElement] = {}
def register_projector_elements(
elements: Generator[Type[ProjectorElement], None, None]
) -> None:
"""
Registers projector elements for later use.
Has to be called in the app.ready method. Has to be called in the app.ready method.
""" """
for AppProjectorElement in elements: projector_elements[name] = element
element = AppProjectorElement()
projector_elements[element.name] = element # type: ignore
def get_all_projector_elements() -> Dict[str, ProjectorElement]: async def get_projectot_data(
projector_ids: List[int] = None
) -> Dict[int, Dict[str, Any]]:
""" """
Returns all projector elements that where registered with Callculates and returns the data for one or all projectors.
register_projector_elements()
""" """
return projector_elements if projector_ids is None:
projector_ids = []
all_data = await element_cache.get_all_full_data_ordered()
projector_data: Dict[int, Dict[str, Dict[str, Any]]] = {}
for projector_id, projector in all_data.get("core/projector", {}).items():
if projector_ids and projector_id not in projector_ids:
# only render the projector in question.
continue
projector_data[projector_id] = {}
if not projector["config"]:
projector_data[projector_id] = {
"error": {"error": "Projector has no config"}
}
continue
for uuid, projector_config in projector["config"].items():
projector_element = projector_elements.get(projector_config["name"])
if projector_element is None:
projector_data[projector_id][uuid] = {
"error": "Projector element {} does not exist".format(
projector_config["name"]
)
}
else:
projector_data[projector_id][uuid] = projector_element(
projector_config, all_data
)
return projector_data
def get_config(all_data: AllData, key: str) -> Any:
"""
Returns the config value from all_data.
"""
from ..core.config import config
return all_data[config.get_collection_string()][config.get_key_to_id()[key]][
"value"
]

View File

@ -29,19 +29,7 @@ class ProjectorAPI(TestCase):
default_projector.save() default_projector.save()
response = self.client.get(reverse("projector-detail", args=["1"])) response = self.client.get(reverse("projector-detail", args=["1"]))
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
content["elements"],
{
"aae4a07b26534cfb9af4232f361dce73": {
"id": topic.id,
"uuid": "aae4a07b26534cfb9af4232f361dce73",
"name": "topics/topic",
"agenda_item_id": topic.agenda_item_id,
}
},
)
def test_invalid_slide_on_default_projector(self): def test_invalid_slide_on_default_projector(self):
self.client.login(username="admin", password="admin") self.client.login(username="admin", password="admin")
@ -60,12 +48,8 @@ class ProjectorAPI(TestCase):
content, content,
{ {
"id": 1, "id": 1,
"elements": { "config": {
"fc6ef43b624043068c8e6e7a86c5a1b0": { "fc6ef43b624043068c8e6e7a86c5a1b0": {"name": "invalid_slide"}
"name": "invalid_slide",
"uuid": "fc6ef43b624043068c8e6e7a86c5a1b0",
"error": "Projector element does not exist.",
}
}, },
"scale": 0, "scale": 0,
"scroll": 0, "scroll": 0,

View File

@ -4,7 +4,9 @@ from django.db import DEFAULT_DB_ALIAS, connections
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Projector
from openslides.users.models import User from openslides.users.models import User
from openslides.utils.projector import get_config, register_projector_element
class TConfig: class TConfig:
@ -33,7 +35,7 @@ class TConfig:
class TUser: class TUser:
""" """
Cachable, that fills the cache with the default values of the config variables. Cachable, that fills the cache with fake users.
""" """
def get_collection_string(self) -> str: def get_collection_string(self) -> str:
@ -68,6 +70,45 @@ class TUser:
return elements return elements
class TProjector:
"""
Cachable, that mocks the projector.
"""
def get_collection_string(self) -> str:
return Projector.get_collection_string()
def get_elements(self) -> List[Dict[str, Any]]:
return [
{"id": 1, "config": {"uid1": {"name": "test/slide1", "id": 1}}},
{"id": 2, "config": {"uid2": {"name": "test/slide2", "id": 1}}},
]
async def restrict_elements(
self, user_id: int, elements: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
return elements
def slide1(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
"""
Slide that shows the general_event_name.
"""
return {"name": "slide1", "event_name": get_config(all_data, "general_event_name")}
def slide2(
config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]]
) -> Dict[str, Any]:
return {"name": "slide2"}
register_projector_element("test/slide1", slide1)
register_projector_element("test/slide2", slide2)
def count_queries(func, *args, **kwargs) -> int: def count_queries(func, *args, **kwargs) -> int:
context = CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) context = CaptureQueriesContext(connections[DEFAULT_DB_ALIAS])
with context: with context:

View File

@ -18,7 +18,7 @@ from openslides.utils.autoupdate import (
from openslides.utils.cache import element_cache from openslides.utils.cache import element_cache
from ...unit.utils.cache_provider import Collection1, Collection2, get_cachable_provider from ...unit.utils.cache_provider import Collection1, Collection2, get_cachable_provider
from ..helpers import TConfig, TUser from ..helpers import TConfig, TProjector, TUser
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -31,7 +31,7 @@ async def prepare_element_cache(settings):
await element_cache.cache_provider.clear_cache() await element_cache.cache_provider.clear_cache()
orig_cachable_provider = element_cache.cachable_provider orig_cachable_provider = element_cache.cachable_provider
element_cache.cachable_provider = get_cachable_provider( element_cache.cachable_provider = get_cachable_provider(
[Collection1(), Collection2(), TConfig(), TUser()] [Collection1(), Collection2(), TConfig(), TUser(), TProjector()]
) )
element_cache._cachables = None element_cache._cachables = None
await sync_to_async(element_cache.ensure_cache)() await sync_to_async(element_cache.ensure_cache)()
@ -512,3 +512,68 @@ async def test_turn_off_autoupdate(get_communicator, set_config):
# Change a config value # Change a config value
await set_config("general_event_name", "Test Event") await set_config("general_event_name", "Test Event")
assert await communicator.receive_nothing() assert await communicator.receive_nothing()
@pytest.mark.asyncio
async def test_listen_to_projector(communicator, set_config):
await set_config("general_system_enable_anonymous", True)
await communicator.connect()
await communicator.send_json_to(
{
"type": "listenToProjectors",
"content": {"projector_ids": [1]},
"id": "test_id",
}
)
response = await communicator.receive_json_from()
type = response.get("type")
content = response.get("content")
assert type == "projector"
assert content == {"1": {"uid1": {"name": "slide1", "event_name": "OpenSlides"}}}
@pytest.mark.asyncio
async def test_update_projector(communicator, set_config):
await set_config("general_system_enable_anonymous", True)
await communicator.connect()
await communicator.send_json_to(
{
"type": "listenToProjectors",
"content": {"projector_ids": [1]},
"id": "test_id",
}
)
await communicator.receive_json_from()
# Change a config value
await set_config("general_event_name", "Test Event")
response = await communicator.receive_json_from()
type = response.get("type")
content = response.get("content")
assert type == "projector"
assert content == {"1": {"uid1": {"event_name": "Test Event", "name": "slide1"}}}
@pytest.mark.asyncio
async def test_update_projector_to_current_value(communicator, set_config):
"""
When a value does not change, the projector should not be updated.
"""
await set_config("general_system_enable_anonymous", True)
await communicator.connect()
await communicator.send_json_to(
{
"type": "listenToProjectors",
"content": {"projector_ids": [1]},
"id": "test_id",
}
)
await communicator.receive_json_from()
# Change a config value to current_value
await set_config("general_event_name", "OpenSlides")
assert await communicator.receive_nothing()

View File

@ -0,0 +1,117 @@
from typing import Any, Dict
import pytest
from openslides.agenda import projector
@pytest.fixture
def all_data():
all_data = {
"agenda/item": {
1: {
"id": 1,
"item_number": "",
"title": "Item1",
"title_with_type": "Item1",
"comment": None,
"closed": False,
"type": 1,
"is_internal": False,
"is_hidden": False,
"duration": None,
"speakers": [],
"speaker_list_closed": False,
"content_object": {"collection": "topics/topic", "id": 1},
"weight": 10,
"parent_id": None,
},
2: {
"id": 2,
"item_number": "",
"title": "item2",
"title_with_type": "item2",
"comment": None,
"closed": False,
"type": 1,
"is_internal": False,
"is_hidden": False,
"duration": None,
"speakers": [],
"speaker_list_closed": False,
"content_object": {"collection": "topics/topic", "id": 1},
"weight": 20,
"parent_id": None,
},
# hidden item
3: {
"id": 3,
"item_number": "",
"title": "item3",
"title_with_type": "item3",
"comment": None,
"closed": True,
"type": 2,
"is_internal": False,
"is_hidden": True,
"duration": None,
"speakers": [],
"speaker_list_closed": False,
"content_object": {"collection": "topics/topic", "id": 1},
"weight": 30,
"parent_id": None,
},
# Child of item 1
4: {
"id": 4,
"item_number": "",
"title": "item4",
"title_with_type": "item4",
"comment": None,
"closed": True,
"type": 1,
"is_internal": False,
"is_hidden": False,
"duration": None,
"speakers": [],
"speaker_list_closed": False,
"content_object": {"collection": "topics/topic", "id": 1},
"weight": 0,
"parent_id": 1,
},
}
}
return all_data
def test_items(all_data):
config: Dict[str, Any] = {}
data = projector.items(config, all_data)
assert data == {"items": ["Item1", "item2"]}
def test_items_parent(all_data):
config: Dict[str, Any] = {"id": 1}
data = projector.items(config, all_data)
assert data == {"items": ["item4"]}
def test_items_tree(all_data):
config: Dict[str, Any] = {"tree": True}
data = projector.items(config, all_data)
assert data == {"items": [("Item1", [("item4", [])]), ("item2", [])]}
def test_items_tree_parent(all_data):
config: Dict[str, Any] = {"tree": True, "id": 1}
data = projector.items(config, all_data)
assert data == {"items": [("item4", [])]}