diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 267a7b4bb..0d61e014e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ Core: - Switch from Yarn back to npm [#3964]. - Added password reset link (password reset via email) [#3914]. - Added global history mode [#3977]. + - Projector Refactor [4119, #4130]. Agenda: - Added viewpoint to assign multiple items to a new parent item [#4037]. diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index 547dde6c8..ea0ebd609 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -41,14 +41,14 @@ def get_tree( return get_children(children[parent_id]) -def items(config: Dict[str, Any], all_data: AllData) -> Dict[str, Any]: +def items(element: Dict[str, Any], all_data: AllData) -> Dict[str, Any]: """ 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 + root_item_id = element.get("id") or None + show_tree = element.get("tree") or False if show_tree: agenda_items = get_tree(all_data, root_item_id or 0) @@ -63,13 +63,13 @@ def items(config: Dict[str, Any], all_data: AllData) -> Dict[str, Any]: return {"items": agenda_items} -def list_of_speakers(config: Dict[str, Any], all_data: AllData) -> Dict[str, Any]: +def list_of_speakers(element: Dict[str, Any], all_data: AllData) -> Dict[str, Any]: """ 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 + item_id = element.get("id") or 0 # item_id 0 means current_list_of_speakers # TODO: handle item_id == 0 diff --git a/openslides/assignments/projector.py b/openslides/assignments/projector.py index ae854d11a..35f54a7d8 100644 --- a/openslides/assignments/projector.py +++ b/openslides/assignments/projector.py @@ -10,12 +10,12 @@ from ..utils.projector import register_projector_element def assignment( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Assignment slide. """ - poll_id = config.get("tree") # noqa + poll_id = element.get("tree") # noqa return {"error": "TODO"} diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 0bfb9f32f..d5108c62a 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -185,15 +185,6 @@ def get_config_variables(): group="Projector", ) - yield ConfigVariable( - name="projector_enable_clock", - default_value=True, - input_type="boolean", - label="Show the clock on projector", - weight=154, - group="Projector", - ) - yield ConfigVariable( name="projector_enable_title", default_value=True, @@ -249,7 +240,7 @@ def get_config_variables(): ) yield ConfigVariable( - name="projector_blank_color", + name="projector_background_color", default_value="#FFFFFF", input_type="colorpicker", label="Color for blanked projector", @@ -257,16 +248,6 @@ def get_config_variables(): group="Projector", ) - yield ConfigVariable( - name="projector_broadcast", - default_value=0, - input_type="integer", - label="Projector which is broadcasted", - weight=200, - group="Projector", - hidden=True, - ) - yield ConfigVariable( name="projector_currentListOfSpeakers_reference", default_value=1, diff --git a/openslides/core/migrations/0010_auto_20190118_1908.py b/openslides/core/migrations/0010_auto_20190118_1908.py new file mode 100644 index 000000000..ec4bc1334 --- /dev/null +++ b/openslides/core/migrations/0010_auto_20190118_1908.py @@ -0,0 +1,51 @@ +# Generated by Django 2.1.5 on 2019-01-18 18:08 + +import jsonfield.encoder +import jsonfield.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [("core", "0009_auto_20181118_2126")] + + operations = [ + migrations.RemoveField(model_name="projector", name="blank"), + migrations.RemoveField(model_name="projector", name="config"), + migrations.AddField( + model_name="projector", + name="elements", + field=jsonfield.fields.JSONField( + dump_kwargs={ + "cls": jsonfield.encoder.JSONEncoder, + "separators": (",", ":"), + }, + load_kwargs={}, + ), + preserve_default=False, + ), + migrations.AddField( + model_name="projector", + name="elements_history", + field=jsonfield.fields.JSONField( + dump_kwargs={ + "cls": jsonfield.encoder.JSONEncoder, + "separators": (",", ":"), + }, + load_kwargs={}, + ), + preserve_default=False, + ), + migrations.AddField( + model_name="projector", + name="elements_preview", + field=jsonfield.fields.JSONField( + dump_kwargs={ + "cls": jsonfield.encoder.JSONEncoder, + "separators": (",", ":"), + }, + load_kwargs={}, + ), + preserve_default=False, + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index e9693581e..0f92303db 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -32,35 +32,27 @@ class ProjectorManager(models.Manager): class Projector(RESTModelMixin, models.Model): - # TODO: Fix docstring """ Model for all projectors. - The config field contains a dictionary which uses UUIDs as keys. Every - element must have at least the property "name". The property "stable" - is to set whether this element should disappear on prune or clear - requests. + The elements field contains a list. Every element must have at least the + property "name". Example: - - { - "881d875cf01741718ca926279ac9c99c": { + [ + { "name": "topics/topic", - "id": 1 + "id": 1, }, - "191c0878cdc04abfbd64f3177a21891a": { + { "name": "core/countdown", - "stable": true, - "status": "stop", - "countdown_time": 20, - "visable": true, - "default": 42 + "id": 1, }, - "db670aa8d3ed4aabb348e752c75aeaaf": { + { "name": "core/clock", - "stable": true - } - } + "id": 1, + }, + ] If the config field is empty or invalid the projector shows a default slide. @@ -76,20 +68,18 @@ class Projector(RESTModelMixin, models.Model): objects = ProjectorManager() - config = JSONField() + elements = JSONField() + elements_preview = JSONField() + elements_history = JSONField() scale = models.IntegerField(default=0) - scroll = models.IntegerField(default=0) width = models.PositiveIntegerField(default=1024) - height = models.PositiveIntegerField(default=768) name = models.CharField(max_length=255, unique=True, blank=True) - blank = models.BooleanField(blank=False, default=False) - class Meta: """ Contains general permissions that can not be placed in a specific app. diff --git a/openslides/core/projector.py b/openslides/core/projector.py index 6e0b5ccbd..cab2d301a 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -10,19 +10,19 @@ from ..utils.projector import register_projector_element def countdown( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Countdown slide. Returns the full_data of the countdown element. - config = { + element = { name: 'core/countdown', id: 5, # Countdown ID } """ - countdown_id = config.get("id") or 1 + countdown_id = element.get("id") or 1 try: return all_data["core/countdown"][countdown_id] @@ -31,19 +31,19 @@ def countdown( def message( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Message slide. Returns the full_data of the message element. - config = { + element = { name: 'core/projector-message', id: 5, # ProjectorMessage ID } """ - message_id = config.get("id") or 1 + message_id = element.get("id") or 1 try: return all_data["core/projector-message"][message_id] @@ -54,4 +54,4 @@ def message( 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 + # TODO: Add clock slide diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index aba0c4266..2daed6b82 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -1,6 +1,8 @@ -from openslides.utils.rest_api import Field, ModelSerializer, ValidationError -from openslides.utils.validate import validate_html +from typing import Any +from ..utils.projector import projector_elements +from ..utils.rest_api import Field, IntegerField, ModelSerializer, ValidationError +from ..utils.validate import validate_html from .models import ( ChatMessage, ConfigStore, @@ -20,18 +22,8 @@ class JSONSerializerField(Field): def to_internal_value(self, data): """ - Checks that data is a dictionary. The key is a hex UUID and the - value is a dictionary with must have a key 'name'. + Returns the value. It is encoded from the Django JSONField. """ - if type(data) is not dict: - raise ValidationError({"detail": "Data must be a dictionary."}) - for element in data.values(): - if type(element) is not dict: - raise ValidationError({"detail": "Data must be a dictionary."}) - elif element.get("name") is None: - raise ValidationError( - {"detail": "Every dictionary must have a key 'name'."} - ) return data def to_representation(self, value): @@ -51,28 +43,69 @@ class ProjectionDefaultSerializer(ModelSerializer): fields = ("id", "name", "display_name", "projector") +def elements_validator(value: Any) -> None: + """ + Checks the format of the elements field. + """ + if not isinstance(value, list): + raise ValidationError({"detail": "Data must be a list."}) + for element in value: + if not isinstance(element, dict): + raise ValidationError({"detail": "Data must be a dictionary."}) + if element.get("name") is None: + raise ValidationError( + {"detail": "Every dictionary must have a key 'name'."} + ) + if element["name"] not in projector_elements: + raise ValidationError( + {"detail": f"Unknown projector element {element['name']},"} + ) + + +def elements_array_validator(value: Any) -> None: + """ + Validates the value of the element field of the projector model. + """ + if not isinstance(value, list): + raise ValidationError({"detail": "Data must be a list."}) + for element in value: + elements_validator(element) + + class ProjectorSerializer(ModelSerializer): """ Serializer for core.models.Projector objects. """ - config = JSONSerializerField() + elements = JSONSerializerField(validators=[elements_validator]) + elements_preview = JSONSerializerField(validators=[elements_array_validator]) + elements_history = JSONSerializerField(validators=[elements_array_validator]) + projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) + width = IntegerField(min_value=800, max_value=3840, required=False) + height = IntegerField(min_value=340, max_value=2880, required=False) class Meta: model = Projector fields = ( "id", - "config", + "elements", + "elements_preview", + "elements_history", "scale", "scroll", "name", - "blank", "width", "height", "projectiondefaults", ) - read_only_fields = ("scale", "scroll", "blank", "width", "height") + read_only_fields = ("scale", "scroll") + + def validate_elements_history(self, value): + """ + Validates the value of the element field of the projector model. + """ + self.validate_elements_preview(value) class TagSerializer(ModelSerializer): diff --git a/openslides/core/views.py b/openslides/core/views.py index 10bc9621c..2270f1e88 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -1,6 +1,5 @@ import datetime import os -import uuid from typing import Any, Dict, List from django.conf import settings @@ -127,17 +126,9 @@ class ProjectorViewSet(ModelViewSet): "update", "partial_update", "destroy", - "activate_elements", - "prune_elements", - "update_elements", - "deactivate_elements", - "clear_elements", - "project", "control_view", "set_resolution", "set_scroll", - "control_blank", - "broadcast", "set_projectiondefault", ): result = has_perm(self.request.user, "core.can_see_projector") and has_perm( @@ -152,309 +143,15 @@ class ProjectorViewSet(ModelViewSet): REST API operation for DELETE requests. Assigns all ProjectionDefault objects from this projector to the - default projector (pk=1). Resets broadcast if set to this projector. + default projector (pk=1). """ projector_instance = self.get_object() for projection_default in ProjectionDefault.objects.all(): if projection_default.projector.id == projector_instance.id: projection_default.projector_id = 1 projection_default.save() - if config["projector_broadcast"] == projector_instance.pk: - config["projector_broadcast"] = 0 return super(ProjectorViewSet, self).destroy(*args, **kwargs) - @detail_route(methods=["post"]) - def activate_elements(self, request, pk): - """ - REST API operation to activate projector elements. It expects a POST - request to /rest/core/projector//activate_elements/ with a list - of dictionaries to be appended to the projector config entry. - """ - if not isinstance(request.data, list): - raise ValidationError({"detail": "Data must be a list."}) - - projector_instance = self.get_object() - projector_config = projector_instance.config - for element in request.data: - if element.get("name") is None: - raise ValidationError( - {"detail": "Invalid projector element. Name is missing."} - ) - projector_config[uuid.uuid4().hex] = element - - serializer = self.get_serializer( - projector_instance, data={"config": projector_config}, partial=False - ) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data) - - @detail_route(methods=["post"]) - def prune_elements(self, request, pk): - """ - REST API operation to activate projector elements. It expects a POST - request to /rest/core/projector//prune_elements/ with a list of - dictionaries to write them to the projector config entry. All old - entries are deleted but not entries with stable == True. - """ - if not isinstance(request.data, list): - raise ValidationError({"detail": "Data must be a list."}) - - projector = self.get_object() - elements = request.data - if not isinstance(elements, list): - raise ValidationError({"detail": "The data has to be a list."}) - for element in elements: - if not isinstance(element, dict): - raise ValidationError({"detail": "All elements have to be dicts."}) - if element.get("name") is None: - raise ValidationError( - {"detail": "Invalid projector element. Name is missing."} - ) - return Response(self.prune(projector, elements)) - - def prune(self, projector, elements): - """ - Prunes all non stable elements from the projector and adds the given elements. - The elements have to a list of dicts, each gict containing at least a name. This - is not validated at this point! Should be done before. - Returns the new serialized data. - """ - projector_config = {} - for key, value in projector.config.items(): - if value.get("stable"): - projector_config[key] = value - for element in elements: - projector_config[uuid.uuid4().hex] = element - - serializer = self.get_serializer( - projector, data={"config": projector_config}, partial=False - ) - serializer.is_valid(raise_exception=True) - serializer.save() - # reset scroll level - if projector.scroll != 0: - projector.scroll = 0 - projector.save() - return serializer.data - - @detail_route(methods=["post"]) - def update_elements(self, request, pk): - """ - REST API operation to update projector elements. It expects a POST - request to /rest/core/projector//update_elements/ with a - dictonary to update the projector config. This must be a dictionary - with UUIDs as keys and projector element dictionaries as values. - - Example: - - { - "191c0878cdc04abfbd64f3177a21891a": { - "name": "core/countdown", - "stable": true, - "status": "running", - "countdown_time": 1374321600.0, - "visable": true, - "default": 42 - } - } - """ - if not isinstance(request.data, dict): - raise ValidationError({"detail": "Data must be a dictionary."}) - error = { - "detail": "Data must be a dictionary with UUIDs as keys and dictionaries as values." - } - for key, value in request.data.items(): - try: - uuid.UUID(hex=str(key)) - except ValueError: - raise ValidationError(error) - if not isinstance(value, dict): - raise ValidationError(error) - - projector_instance = self.get_object() - projector_config = projector_instance.config - for key, value in request.data.items(): - if key not in projector_config: - raise ValidationError( - {"detail": "Invalid projector element. Wrong UUID."} - ) - projector_config[key].update(request.data[key]) - - serializer = self.get_serializer( - projector_instance, data={"config": projector_config}, partial=False - ) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data) - - @detail_route(methods=["post"]) - def deactivate_elements(self, request, pk): - """ - REST API operation to deactivate projector elements. It expects a - POST request to /rest/core/projector//deactivate_elements/ with - a list of hex UUIDs. These are the projector_elements in the config - that should be deleted. - """ - if not isinstance(request.data, list): - raise ValidationError({"detail": "Data must be a list of hex UUIDs."}) - for item in request.data: - try: - uuid.UUID(hex=str(item)) - except ValueError: - raise ValidationError({"detail": "Data must be a list of hex UUIDs."}) - - projector_instance = self.get_object() - projector_config = projector_instance.config - for key in request.data: - try: - del projector_config[key] - except KeyError: - raise ValidationError({"detail": "Invalid UUID."}) - - serializer = self.get_serializer( - projector_instance, data={"config": projector_config}, partial=False - ) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data) - - @detail_route(methods=["post"]) - def clear_elements(self, request, pk): - """ - REST API operation to deactivate all projector elements but not - entries with stable == True. It expects a POST request to - /rest/core/projector//clear_elements/. - """ - projector = self.get_object() - return Response(self.clear(projector)) - - def clear(self, projector): - projector_config = {} - for key, value in projector.config.items(): - if value.get("stable"): - projector_config[key] = value - - serializer = self.get_serializer( - projector, data={"config": projector_config}, partial=False - ) - serializer.is_valid(raise_exception=True) - serializer.save() - return serializer.data - - @list_route(methods=["post"]) - def project(self, request, *args, **kwargs): - """ - REST API operation. Does a combination of clear_elements and prune_elements: - In the most cases when projecting an element it first need to be removed from - all projectors where it is projected. In a second step the new element (which - may be not given if the element is just deprojected) needs to be projected on - a maybe different projector. The request data has to have this scheme: - { - clear_ids: [, ...], # May be an empty list - prune: { # May not be given. - id: , - element: - } - } - """ - # The data has to be a dict. - if not isinstance(request.data, dict): - raise ValidationError({"detail": "The data has to be a dict."}) - - # Get projector ids to clear - clear_projector_ids = request.data.get("clear_ids", []) - for id in clear_projector_ids: - if not isinstance(id, int): - raise ValidationError({"detail": f'The id "{id}" has to be int.'}) - - # Get the projector id and validate element to prune. This is optional. - prune = request.data.get("prune") - if prune is not None: - if not isinstance(prune, dict): - raise ValidationError({"detail": "Prune has to be an object."}) - prune_projector_id = prune.get("id") - if not isinstance(prune_projector_id, int): - raise ValidationError( - {"detail": "The prune projector id has to be int."} - ) - - # Get the projector after all clear operations, but check, if it exist. - if not Projector.objects.filter(pk=prune_projector_id).exists(): - raise ValidationError( - { - "detail": f'The projector with id "{prune_projector_id}" does not exist' - } - ) - - prune_element = prune.get("element", {}) - if not isinstance(prune_element, dict): - raise ValidationError( - {"detail": "Prune element has to be a dict or not given."} - ) - if prune_element.get("name") is None: - raise ValidationError( - {"detail": "Invalid projector element. Name is missing."} - ) - - # First step: Clear all given projectors - for projector in Projector.objects.filter(pk__in=clear_projector_ids): - self.clear(projector) - - # Second step: optionally prune - if prune is not None: - # This get is save. We checked that the projector exists above. - prune_projector = Projector.objects.get(pk=prune_projector_id) - self.prune(prune_projector, [prune_element]) - - return Response() - - @detail_route(methods=["post"]) - def set_resolution(self, request, pk): - """ - REST API operation to set the resolution. - - It is actually unused, because the resolution is currently set in the config. - But with the multiprojector feature this will become importent to set the - resolution per projector individually. - - It expects a POST request to - /rest/core/projector//set_resolution/ with a dictionary with the width - and height and the values. - - Example: - - { - "width": "1024", - "height": "768" - } - """ - if not isinstance(request.data, dict): - raise ValidationError({"detail": "Data must be a dictionary."}) - if request.data.get("width") is None or request.data.get("height") is None: - raise ValidationError({"detail": "A width and a height have to be given."}) - if not isinstance(request.data["width"], int) or not isinstance( - request.data["height"], int - ): - raise ValidationError({"detail": "Data has to be integers."}) - if ( - request.data["width"] < 800 - or request.data["width"] > 3840 - or request.data["height"] < 340 - or request.data["height"] > 2880 - ): - raise ValidationError( - {"detail": "The Resolution have to be between 800x340 and 3840x2880."} - ) - - projector_instance = self.get_object() - projector_instance.width = request.data["width"] - projector_instance.height = request.data["height"] - projector_instance.save() - - message = f"Changing resolution to {request.data['width']}x{request.data['height']} was successful." - return Response({"detail": message}) - @detail_route(methods=["post"]) def control_view(self, request, pk): """ @@ -507,10 +204,7 @@ class ProjectorViewSet(ModelViewSet): projector_instance.save(skip_autoupdate=True) projector_instance.refresh_from_db() inform_changed_data(projector_instance) - action = (request.data["action"].capitalize(),) - direction = (request.data["direction"],) - message = f"{action} {direction} was successful." - return Response({"detail": message}) + return Response() @detail_route(methods=["post"]) def set_scroll(self, request, pk): @@ -530,40 +224,6 @@ class ProjectorViewSet(ModelViewSet): message = f"Setting scroll to {request.data} was successful." return Response({"detail": message}) - @detail_route(methods=["post"]) - def control_blank(self, request, pk): - """ - REST API operation to blank the projector. - - It expects a POST request to - /rest/core/projector//control_blank/ with a value for blank. - """ - if not isinstance(request.data, bool): - raise ValidationError({"detail": "Data must be a bool."}) - - projector_instance = self.get_object() - projector_instance.blank = request.data - projector_instance.save() - message = f"Setting 'blank' to {request.data} was successful." - return Response({"detail": message}) - - @detail_route(methods=["post"]) - def broadcast(self, request, pk): - """ - REST API operation to (un-)broadcast the given projector. - This method takes care, that all other projectors get the new requirements. - - It expects a POST request to - /rest/core/projector//broadcast/ without an argument - """ - if config["projector_broadcast"] == 0: - config["projector_broadcast"] = pk - message = f"Setting projector {pk} as broadcast projector was successful." - else: - config["projector_broadcast"] = 0 - message = "Disabling broadcast was successful." - return Response({"detail": message}) - @detail_route(methods=["post"]) def set_projectiondefault(self, request, pk): """ @@ -589,9 +249,7 @@ class ProjectorViewSet(ModelViewSet): projectiondefault.projector = projector_instance projectiondefault.save() - return Response( - f'Setting projectiondefault "{projectiondefault.display_name}" to projector {projector_instance.pk} was successful.' - ) + return Response() class TagViewSet(ModelViewSet): @@ -673,8 +331,8 @@ class ConfigViewSet(ModelViewSet): config[key] = value except ConfigNotFound: raise Http404 - except ConfigError as e: - raise ValidationError({"detail": str(e)}) + except ConfigError as err: + raise ValidationError({"detail": str(err)}) # Return response. return Response({"key": key, "value": value}) diff --git a/openslides/mediafiles/projector.py b/openslides/mediafiles/projector.py index 80683ff9f..5779059a2 100644 --- a/openslides/mediafiles/projector.py +++ b/openslides/mediafiles/projector.py @@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element def mediafile( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Slide for Mediafile. diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 75ca6565f..47b279201 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element def motion( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Motion slide. @@ -19,7 +19,7 @@ def motion( def motion_block( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Motion slide. diff --git a/openslides/topics/projector.py b/openslides/topics/projector.py index 3e39aa7ba..bc4343d1e 100644 --- a/openslides/topics/projector.py +++ b/openslides/topics/projector.py @@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element def topic( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ Topic slide. diff --git a/openslides/users/projector.py b/openslides/users/projector.py index 4dac2304a..6e799c383 100644 --- a/openslides/users/projector.py +++ b/openslides/users/projector.py @@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element def user( - config: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] + element: Dict[str, Any], all_data: Dict[str, Dict[int, Dict[str, Any]]] ) -> Dict[str, Any]: """ User slide. diff --git a/openslides/utils/consumers.py b/openslides/utils/consumers.py index 66876139f..232fdd87d 100644 --- a/openslides/utils/consumers.py +++ b/openslides/utils/consumers.py @@ -121,7 +121,7 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): projector_id, {"error": f"No data for projector {projector_id}"} ) new_hash = hash(str(data)) - if new_hash != self.projector_hash[projector_id]: + if new_hash != self.projector_hash.get(projector_id): projector_data[projector_id] = data self.projector_hash[projector_id] = new_hash diff --git a/openslides/utils/projector.py b/openslides/utils/projector.py index 14bdf094d..f78e02269 100644 --- a/openslides/utils/projector.py +++ b/openslides/utils/projector.py @@ -28,72 +28,58 @@ def register_projector_element(name: str, element: ProjectorElementCallable) -> async def get_projectot_data( projector_ids: List[int] = None -) -> Dict[int, Dict[str, Dict[str, Any]]]: +) -> Dict[int, List[Dict[str, Any]]]: """ - Callculates and returns the data for one or all projectors. + Calculates and returns the data for one or all projectors. The keys of the returned data are the projector ids as int. When converted to json, the numbers will changed to strings like "1". - The data for each projector is a dict. The keys are the uuids of the - elements as strings. If there is a generell problem with the projector, the - key can be 'error'. + The data for each projector is a list of elements. - Each element is a dict where the keys are "config", "data". "config" - contains the projector config. It is the same as the projector config in the - database. "data" contains all necessary data to render the projector + Each element is a dict where the keys are "elements", "data". "elements" + contains the projector elements. It is the same as the projector elements in + the database. "data" contains all necessary data to render the projector element. The key can also be "error" if there is a generall error for the - slide. In this case the values "config" and "data" are optional. + slide. In this case the values "elements" and "data" are optional. The returned value looks like this: projector_data = { - 1: { - "UnIqUe-UUID": { - "config": { + 1: [ + { + "element": { "name": "agenda/item-list", }, "data": { "items": [] }, }, - }, - 2: { - "error": { - "error": "Projector has no config", - }, - }, + ], } """ 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]]] = {} + projector_data: Dict[int, List[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"} - } + if not projector["elements"]: + # Skip empty elements. continue - for uuid, projector_config in projector["config"].items(): - projector_data[projector_id][uuid] = {"config": projector_config} - projector_element = projector_elements.get(projector_config["name"]) - if projector_element is None: - projector_data[projector_id][uuid][ - "error" - ] = f"Projector element {projector_config['name']} does not exist" - else: - projector_data[projector_id][uuid]["data"] = projector_element( - projector_config, all_data - ) + projector_data[projector_id] = [] + for element in projector["elements"]: + projector_element = projector_elements[element["name"]] + projector_data[projector_id].append( + {"data": projector_element(element, all_data), "element": element} + ) + return projector_data diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index a42c962f7..a9d7cf6f3 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -6,60 +6,67 @@ from rest_framework.test import APIClient from openslides import __license__ as license, __url__ as url, __version__ as version from openslides.core.config import ConfigVariable, config -from openslides.core.models import Projector -from openslides.topics.models import Topic from openslides.utils.rest_api import ValidationError from openslides.utils.test import TestCase class ProjectorAPI(TestCase): - """ - Tests requests from the anonymous user. - """ - def test_slide_on_default_projector(self): self.client.login(username="admin", password="admin") - topic = Topic.objects.create( - title="title_que1olaish5Wei7que6i", text="text_aishah8Eh7eQuie5ooji" + self.client.put( + reverse("projector-detail", args=["1"]), + {"elements": [{"name": "topics/topic", "id": 1}]}, + content_type="application/json", ) - default_projector = Projector.objects.get(pk=1) - default_projector.config = { - "aae4a07b26534cfb9af4232f361dce73": {"name": "topics/topic", "id": topic.id} - } - default_projector.save() response = self.client.get(reverse("projector-detail", args=["1"])) + self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_invalid_slide_on_default_projector(self): + def test_invalid_element_non_existing_slide(self): self.client.login(username="admin", password="admin") - default_projector = Projector.objects.get(pk=1) - default_projector.config = { - "fc6ef43b624043068c8e6e7a86c5a1b0": {"name": "invalid_slide"} - } - default_projector.save() - response = self.client.get(reverse("projector-detail", args=["1"])) - content = json.loads(response.content.decode()) - del content["projectiondefaults"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - content, - { - "id": 1, - "config": { - "fc6ef43b624043068c8e6e7a86c5a1b0": {"name": "invalid_slide"} - }, - "scale": 0, - "scroll": 0, - "name": "Default projector", - "blank": False, - "width": 1220, - "height": 915, - }, + response = self.client.put( + reverse("projector-detail", args=["1"]), + {"elements": [{"name": "invalid_slide_name", "id": 1}]}, + content_type="application/json", ) + self.assertEqual(response.status_code, 400) + + def test_invalid_element_no_name_attribute(self): + self.client.login(username="admin", password="admin") + + response = self.client.put( + reverse("projector-detail", args=["1"]), + {"elements": [{"id": 1}]}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + + def test_invalid_element_not_a_inner_dict(self): + self.client.login(username="admin", password="admin") + + response = self.client.put( + reverse("projector-detail", args=["1"]), + {"elements": ["not a dict"]}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + + def test_invalid_element_a_list(self): + self.client.login(username="admin", password="admin") + + response = self.client.put( + reverse("projector-detail", args=["1"]), + {"elements": {"name": "invalid_slide_name", "id": 1}}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + class VersionView(TestCase): """ diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 343c56309..8642bf0c3 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -80,8 +80,8 @@ class TProjector: 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}}}, + {"id": 1, "elements": [{"name": "test/slide1", "id": 1}]}, + {"id": 2, "elements": [{"name": "test/slide2", "id": 1}]}, ] async def restrict_elements( diff --git a/tests/integration/utils/test_consumers.py b/tests/integration/utils/test_consumers.py index 543521d59..88bba1b90 100644 --- a/tests/integration/utils/test_consumers.py +++ b/tests/integration/utils/test_consumers.py @@ -532,12 +532,12 @@ async def test_listen_to_projector(communicator, set_config): content = response.get("content") assert type == "projector" assert content == { - "1": { - "uid1": { + "1": [ + { "data": {"name": "slide1", "event_name": "OpenSlides"}, - "config": {"id": 1, "name": "test/slide1"}, + "element": {"id": 1, "name": "test/slide1"}, } - } + ] } @@ -562,12 +562,12 @@ async def test_update_projector(communicator, set_config): content = response.get("content") assert type == "projector" assert content == { - "1": { - "uid1": { + "1": [ + { "data": {"name": "slide1", "event_name": "Test Event"}, - "config": {"id": 1, "name": "test/slide1"}, + "element": {"id": 1, "name": "test/slide1"}, } - } + ] } diff --git a/tests/unit/core/test_views.py b/tests/unit/core/test_views.py deleted file mode 100644 index 9dc7b6223..000000000 --- a/tests/unit/core/test_views.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock, patch - -from openslides.core import views -from openslides.utils.rest_api import ValidationError - - -@patch("openslides.core.views.ProjectorViewSet.get_object") -class ProjectorAPI(TestCase): - def setUp(self): - self.viewset = views.ProjectorViewSet() - self.viewset.format_kwarg = None - - def test_activate_elements_no_list(self, mock_object): - mock_object.return_value.config = { - "3979c9fc3bee432fb25f354d6b4868b3": { - "name": "test_projector_element_ahshaiTie8xie3eeThu9", - "test_key_ohwa7ooze2angoogieM9": "test_value_raiL2ohsheij1seiqua5", - } - } - request = MagicMock() - request.data = {"name": "new_test_projector_element_buuDohphahWeeR2eeQu0"} - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.activate_elements(request=request, pk=MagicMock()) - - def test_activate_elements_bad_element(self, mock_object): - mock_object.return_value.config = { - "374000ee236a41e09cce22ffad29b455": { - "name": "test_projector_element_ieroa7eu3aechaip3eeD", - "test_key_mie3Eeroh9rooKeinga6": "test_value_gee1Uitae6aithaiphoo", - } - } - request = MagicMock() - request.data = [{"bad_quangah1ahoo6oKaeBai": "value_doh8ahwe0Zooc1eefu0o"}] - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.activate_elements(request=request, pk=MagicMock())