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 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(
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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).
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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).
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
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