New projector system. Add first slides
This commit is contained in:
parent
73e1853758
commit
a0f554674b
@ -2,21 +2,18 @@ from typing import Any, Dict, Set
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class AgendaAppConfig(AppConfig):
|
||||
name = "openslides.agenda"
|
||||
verbose_name = "OpenSlides Agenda"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from django.db.models.signals import pre_delete, post_save
|
||||
from ..core.signals import permission_change
|
||||
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,
|
||||
listen_to_related_object_post_delete,
|
||||
@ -27,7 +24,7 @@ class AgendaAppConfig(AppConfig):
|
||||
from ..utils.access_permissions import required_user
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
post_save.connect(
|
||||
|
@ -10,7 +10,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
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.exceptions import OpenSlidesError
|
||||
from openslides.utils.models import RESTModelMixin
|
||||
@ -291,18 +291,6 @@ class Item(RESTModelMixin, models.Model):
|
||||
def __str__(self):
|
||||
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
|
||||
def title(self):
|
||||
"""
|
||||
|
@ -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 ProjectorElement
|
||||
from .models import Item
|
||||
from ..utils.projector import AllData, register_projector_element
|
||||
|
||||
|
||||
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
|
||||
integer to get a list slide of the children of the metioned item.
|
||||
|
||||
Additionally set 'tree' to True to get also children of children.
|
||||
Returns a list of two element tuples where the first element is the item title
|
||||
and the second a List with children as two element tuples.
|
||||
"""
|
||||
|
||||
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):
|
||||
pk = self.config_entry.get("id")
|
||||
if pk is not None:
|
||||
# Children slide.
|
||||
if not Item.objects.filter(pk=pk).exists():
|
||||
raise ProjectorException("Item does not exist.")
|
||||
def get_children(
|
||||
item_ids: List[int]
|
||||
) -> List[Tuple[int, List[Tuple[int, List[Any]]]]]:
|
||||
return [
|
||||
(all_data["agenda/item"][item_id]["title"], get_children(children[item_id]))
|
||||
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.
|
||||
This is only for list of speakers slide. You have to set 'id'.
|
||||
Item list slide.
|
||||
|
||||
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):
|
||||
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")}
|
||||
return {"items": items}
|
||||
|
||||
|
||||
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]:
|
||||
yield ItemListSlide
|
||||
yield ListOfSpeakersSlide
|
||||
yield CurrentListOfSpeakersSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("agenda/item-list", items)
|
||||
register_projector_element("agenda/list-of-speakers", list_of_speakers)
|
||||
|
@ -3,14 +3,11 @@ from typing import Any, Dict, List, Set
|
||||
from django.apps import AppConfig
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class AssignmentsAppConfig(AppConfig):
|
||||
name = "openslides.assignments"
|
||||
verbose_name = "OpenSlides Assignments"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
@ -18,12 +15,12 @@ class AssignmentsAppConfig(AppConfig):
|
||||
from ..utils.access_permissions import required_user
|
||||
from ..utils.rest_api import router
|
||||
from . import serializers # noqa
|
||||
from .projector import get_projector_elements
|
||||
from .projector import register_projector_elements
|
||||
from .signals import get_permission_change_data
|
||||
from .views import AssignmentViewSet, AssignmentPollViewSet
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
permission_change.connect(
|
||||
|
@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _, ugettext_noop
|
||||
|
||||
from openslides.agenda.models import Item, Speaker
|
||||
from openslides.core.config import config
|
||||
from openslides.core.models import Projector, Tag
|
||||
from openslides.core.models import Tag
|
||||
from openslides.poll.models import (
|
||||
BaseOption,
|
||||
BasePoll,
|
||||
@ -159,18 +159,6 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
def __str__(self):
|
||||
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
|
||||
def candidates(self):
|
||||
"""
|
||||
@ -432,21 +420,6 @@ class AssignmentPoll( # type: ignore
|
||||
class Meta:
|
||||
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):
|
||||
return self.assignment
|
||||
|
||||
|
@ -1,45 +1,23 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..core.exceptions import ProjectorException
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .models import Assignment, AssignmentPoll
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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.
|
||||
|
||||
You can send a poll id to get a poll slide.
|
||||
Assignment slide.
|
||||
"""
|
||||
|
||||
name = "assignments/assignment"
|
||||
|
||||
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
|
||||
poll_id = config.get("tree") # noqa
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
yield AssignmentSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("assignments/assignment", assignment)
|
||||
|
@ -7,21 +7,16 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class CoreAppConfig(AppConfig):
|
||||
name = "openslides.core"
|
||||
verbose_name = "OpenSlides Core"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from .config import config
|
||||
from ..utils.rest_api import router
|
||||
from ..utils.cache import element_cache
|
||||
from .projector import get_projector_elements
|
||||
from .projector import register_projector_elements
|
||||
from . import serializers # noqa
|
||||
from .signals import (
|
||||
delete_django_app_permissions,
|
||||
@ -38,15 +33,18 @@ class CoreAppConfig(AppConfig):
|
||||
ProjectorViewSet,
|
||||
TagViewSet,
|
||||
)
|
||||
from ..utils.constants import set_constants, get_constants_from_apps
|
||||
from .websocket import (
|
||||
NotifyWebsocketClientMessage,
|
||||
ConstantsWebsocketClientMessage,
|
||||
GetElementsWebsocketClientMessage,
|
||||
AutoupdateWebsocketClientMessage,
|
||||
ListenToProjectors,
|
||||
)
|
||||
from ..utils.websocket import register_client_message
|
||||
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.
|
||||
config.collect_config_variables_from_apps()
|
||||
@ -64,7 +62,7 @@ class CoreAppConfig(AppConfig):
|
||||
set_constants(get_constants_from_apps())
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
post_permission_creation.connect(
|
||||
@ -114,6 +112,7 @@ class CoreAppConfig(AppConfig):
|
||||
register_client_message(ConstantsWebsocketClientMessage())
|
||||
register_client_message(GetElementsWebsocketClientMessage())
|
||||
register_client_message(AutoupdateWebsocketClientMessage())
|
||||
register_client_message(ListenToProjectors())
|
||||
|
||||
# register required_users
|
||||
required_user.add_collection_string(
|
||||
|
@ -1,10 +1,6 @@
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
|
||||
class ProjectorException(OpenSlidesError):
|
||||
pass
|
||||
|
||||
|
||||
class TagException(OpenSlidesError):
|
||||
pass
|
||||
|
||||
|
@ -7,7 +7,6 @@ from jsonfield import JSONField
|
||||
from ..utils.autoupdate import Element
|
||||
from ..utils.cache import element_cache, get_element_id
|
||||
from ..utils.models import RESTModelMixin
|
||||
from ..utils.projector import get_all_projector_elements
|
||||
from .access_permissions import (
|
||||
ChatMessageAccessPermissions,
|
||||
ConfigAccessPermissions,
|
||||
@ -17,7 +16,6 @@ from .access_permissions import (
|
||||
ProjectorMessageAccessPermissions,
|
||||
TagAccessPermissions,
|
||||
)
|
||||
from .exceptions import ProjectorException
|
||||
|
||||
|
||||
class ProjectorManager(models.Manager):
|
||||
@ -34,6 +32,7 @@ class ProjectorManager(models.Manager):
|
||||
|
||||
|
||||
class Projector(RESTModelMixin, models.Model):
|
||||
# TODO: Fix docstring
|
||||
"""
|
||||
Model for all projectors.
|
||||
|
||||
@ -103,66 +102,6 @@ class Projector(RESTModelMixin, models.Model):
|
||||
("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):
|
||||
"""
|
||||
@ -275,18 +214,6 @@ class ProjectorMessage(RESTModelMixin, models.Model):
|
||||
class Meta:
|
||||
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):
|
||||
"""
|
||||
@ -306,18 +233,6 @@ class Countdown(RESTModelMixin, models.Model):
|
||||
class Meta:
|
||||
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):
|
||||
if action not in ("start", "stop", "reset"):
|
||||
raise ValueError(
|
||||
|
@ -1,43 +1,57 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .exceptions import ProjectorException
|
||||
from .models import Countdown, ProjectorMessage
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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"
|
||||
|
||||
def check_data(self):
|
||||
if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Countdown does not exists.")
|
||||
try:
|
||||
return all_data["core/projector-message"][message_id]
|
||||
except KeyError:
|
||||
return {"error": "Message {} does not exist".format(message_id)}
|
||||
|
||||
|
||||
class ProjectorMessageElement(ProjectorElement):
|
||||
"""
|
||||
Short message on the projector. Rendered as overlay.
|
||||
"""
|
||||
|
||||
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
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("core/countdown", countdown)
|
||||
register_projector_element("core/projector-message", message)
|
||||
# TODO: Deside if we need a clock slide
|
||||
|
@ -56,7 +56,7 @@ class ProjectorSerializer(ModelSerializer):
|
||||
Serializer for core.models.Projector objects.
|
||||
"""
|
||||
|
||||
config = JSONSerializerField(write_only=True)
|
||||
config = JSONSerializerField()
|
||||
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -64,7 +64,6 @@ class ProjectorSerializer(ModelSerializer):
|
||||
fields = (
|
||||
"id",
|
||||
"config",
|
||||
"elements",
|
||||
"scale",
|
||||
"scroll",
|
||||
"name",
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from ..utils.constants import get_constants
|
||||
from ..utils.projector import get_projectot_data
|
||||
from ..utils.websocket import (
|
||||
BaseWebsocketClientMessage,
|
||||
ProtocollAsyncJsonWebsocketConsumer,
|
||||
@ -111,3 +112,51 @@ class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
await consumer.channel_layer.group_discard(
|
||||
"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
|
||||
)
|
||||
|
@ -2,27 +2,24 @@ from typing import Any, Dict, Set
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class MediafilesAppConfig(AppConfig):
|
||||
name = "openslides.mediafiles"
|
||||
verbose_name = "OpenSlides Mediafiles"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from openslides.core.signals import permission_change
|
||||
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 .views import MediafileViewSet
|
||||
from . import serializers # noqa
|
||||
from ..utils.access_permissions import required_user
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
permission_change.connect(
|
||||
|
@ -3,7 +3,6 @@ from django.db import models
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from ..core.config import config
|
||||
from ..core.models import Projector
|
||||
from ..utils.autoupdate import inform_changed_data
|
||||
from ..utils.models import RESTModelMixin
|
||||
from .access_permissions import MediafileAccessPermissions
|
||||
@ -67,18 +66,6 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
inform_changed_data(self.uploader)
|
||||
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):
|
||||
"""
|
||||
Transforms bytes to kilobytes or megabytes. Returns the size as string.
|
||||
|
@ -1,21 +1,22 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..core.exceptions import ProjectorException
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .models import Mediafile
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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.")
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
yield MediafileSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("mediafiles/mediafile", mediafile)
|
||||
|
@ -3,20 +3,17 @@ from typing import Any, Dict, Set
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class MotionsAppConfig(AppConfig):
|
||||
name = "openslides.motions"
|
||||
verbose_name = "OpenSlides Motion"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from openslides.core.signals import permission_change
|
||||
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 . import serializers # noqa
|
||||
from .views import (
|
||||
@ -33,7 +30,7 @@ class MotionsAppConfig(AppConfig):
|
||||
from ..utils.access_permissions import required_user
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
post_migrate.connect(
|
||||
|
@ -12,7 +12,7 @@ from jsonfield import JSONField
|
||||
|
||||
from openslides.agenda.models import Item
|
||||
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.poll.models import (
|
||||
BaseOption,
|
||||
@ -310,18 +310,6 @@ class Motion(RESTModelMixin, models.Model):
|
||||
if not skip_autoupdate:
|
||||
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):
|
||||
"""
|
||||
Sets the motion identifier automaticly according to the config value if
|
||||
@ -893,18 +881,6 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||
def __str__(self):
|
||||
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).
|
||||
"""
|
||||
|
@ -1,56 +1,32 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..core.exceptions import ProjectorException
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .models import Motion, MotionBlock
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
yield MotionSlide
|
||||
yield MotionBlockSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("motion/motion", motion)
|
||||
register_projector_element("motion/motion-block", motion_block)
|
||||
|
@ -1,25 +1,22 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class TopicsAppConfig(AppConfig):
|
||||
name = "openslides.topics"
|
||||
verbose_name = "OpenSlides Topics"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from openslides.core.signals import permission_change
|
||||
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 .views import TopicViewSet
|
||||
from . import serializers # noqa
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
permission_change.connect(
|
||||
|
@ -4,7 +4,6 @@ from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
|
||||
from ..agenda.models import Item
|
||||
from ..core.models import Projector
|
||||
from ..mediafiles.models import Mediafile
|
||||
from ..utils.models import RESTModelMixin
|
||||
from .access_permissions import TopicAccessPermissions
|
||||
@ -47,18 +46,6 @@ class Topic(RESTModelMixin, models.Model):
|
||||
def __str__(self):
|
||||
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).
|
||||
"""
|
||||
|
@ -1,32 +1,22 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..core.exceptions import ProjectorException
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .models import Topic
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
yield TopicSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("topics/topic", topic)
|
||||
|
@ -2,26 +2,23 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
|
||||
from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class UsersAppConfig(AppConfig):
|
||||
name = "openslides.users"
|
||||
verbose_name = "OpenSlides Users"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
def ready(self):
|
||||
# Import all required stuff.
|
||||
from . import serializers # noqa
|
||||
from ..core.signals import post_permission_creation, permission_change
|
||||
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 .views import GroupViewSet, PersonalNoteViewSet, UserViewSet
|
||||
|
||||
# Define projector elements.
|
||||
register_projector_elements(get_projector_elements())
|
||||
register_projector_elements()
|
||||
|
||||
# Connect signals.
|
||||
post_permission_creation.connect(
|
||||
|
@ -18,7 +18,6 @@ from django.utils import timezone
|
||||
from jsonfield import JSONField
|
||||
|
||||
from ..core.config import config
|
||||
from ..core.models import Projector
|
||||
from ..utils.auth import GROUP_ADMIN_PK
|
||||
from ..utils.models import RESTModelMixin
|
||||
from .access_permissions import (
|
||||
@ -197,18 +196,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
kwargs["skip_autoupdate"] = True
|
||||
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):
|
||||
"""
|
||||
This method is closed. Do not use it but use openslides.utils.auth.has_perm.
|
||||
|
@ -1,21 +1,22 @@
|
||||
from typing import Generator, Type
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..core.exceptions import ProjectorException
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .models import User
|
||||
from ..utils.projector import register_projector_element
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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.")
|
||||
return {"error": "TODO"}
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
yield UserSlide
|
||||
def register_projector_elements() -> None:
|
||||
register_projector_element("users/user", user)
|
||||
|
@ -8,6 +8,7 @@ from django.db.models import Model
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
from .cache import element_cache, get_element_id
|
||||
from .projector import get_projectot_data
|
||||
|
||||
|
||||
Element = TypedDict(
|
||||
@ -192,6 +193,13 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
||||
"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:
|
||||
# Save histroy here using sync code.
|
||||
history_instances = save_history(elements)
|
||||
|
@ -154,16 +154,28 @@ class ElementCache:
|
||||
|
||||
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 value is a list of data.
|
||||
"""
|
||||
all_data = await self.get_all_full_data_ordered()
|
||||
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()
|
||||
for element_id, data in full_data.items():
|
||||
collection_string, __ = split_element_id(element_id)
|
||||
out[collection_string].append(json.loads(data.decode()))
|
||||
collection_string, id = split_element_id(element_id)
|
||||
out[collection_string][id] = json.loads(data.decode())
|
||||
return dict(out)
|
||||
|
||||
async def get_full_data(
|
||||
|
@ -15,6 +15,10 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
|
||||
|
||||
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:
|
||||
"""
|
||||
A user connects to the site.
|
||||
@ -110,3 +114,21 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
|
||||
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)
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
Registers a projector element.
|
||||
|
||||
Has to be called in the app.ready method.
|
||||
"""
|
||||
for AppProjectorElement in elements:
|
||||
element = AppProjectorElement()
|
||||
projector_elements[element.name] = element # type: ignore
|
||||
projector_elements[name] = element
|
||||
|
||||
|
||||
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
|
||||
register_projector_elements()
|
||||
Callculates and returns the data for one or all projectors.
|
||||
"""
|
||||
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"
|
||||
]
|
||||
|
@ -29,19 +29,7 @@ class ProjectorAPI(TestCase):
|
||||
default_projector.save()
|
||||
|
||||
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(
|
||||
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):
|
||||
self.client.login(username="admin", password="admin")
|
||||
@ -60,12 +48,8 @@ class ProjectorAPI(TestCase):
|
||||
content,
|
||||
{
|
||||
"id": 1,
|
||||
"elements": {
|
||||
"fc6ef43b624043068c8e6e7a86c5a1b0": {
|
||||
"name": "invalid_slide",
|
||||
"uuid": "fc6ef43b624043068c8e6e7a86c5a1b0",
|
||||
"error": "Projector element does not exist.",
|
||||
}
|
||||
"config": {
|
||||
"fc6ef43b624043068c8e6e7a86c5a1b0": {"name": "invalid_slide"}
|
||||
},
|
||||
"scale": 0,
|
||||
"scroll": 0,
|
||||
|
@ -4,7 +4,9 @@ from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.core.models import Projector
|
||||
from openslides.users.models import User
|
||||
from openslides.utils.projector import get_config, register_projector_element
|
||||
|
||||
|
||||
class TConfig:
|
||||
@ -33,7 +35,7 @@ class TConfig:
|
||||
|
||||
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:
|
||||
@ -68,6 +70,45 @@ class TUser:
|
||||
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:
|
||||
context = CaptureQueriesContext(connections[DEFAULT_DB_ALIAS])
|
||||
with context:
|
||||
|
@ -18,7 +18,7 @@ from openslides.utils.autoupdate import (
|
||||
from openslides.utils.cache import element_cache
|
||||
|
||||
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)
|
||||
@ -31,7 +31,7 @@ async def prepare_element_cache(settings):
|
||||
await element_cache.cache_provider.clear_cache()
|
||||
orig_cachable_provider = element_cache.cachable_provider
|
||||
element_cache.cachable_provider = get_cachable_provider(
|
||||
[Collection1(), Collection2(), TConfig(), TUser()]
|
||||
[Collection1(), Collection2(), TConfig(), TUser(), TProjector()]
|
||||
)
|
||||
element_cache._cachables = None
|
||||
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
|
||||
await set_config("general_event_name", "Test Event")
|
||||
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()
|
||||
|
117
tests/unit/agenda/test_projector.py
Normal file
117
tests/unit/agenda/test_projector.py
Normal 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", [])]}
|
Loading…
Reference in New Issue
Block a user