Merge pull request #4130 from ostcar/new_projector_ii

New projector II
This commit is contained in:
Oskar Hahn 2019-01-19 09:01:43 +01:00 committed by GitHub
commit 24bb4ad0ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 218 additions and 549 deletions

View File

@ -21,6 +21,7 @@ Core:
- Switch from Yarn back to npm [#3964]. - Switch from Yarn back to npm [#3964].
- Added password reset link (password reset via email) [#3914]. - Added password reset link (password reset via email) [#3914].
- Added global history mode [#3977]. - Added global history mode [#3977].
- Projector Refactor [4119, #4130].
Agenda: Agenda:
- Added viewpoint to assign multiple items to a new parent item [#4037]. - Added viewpoint to assign multiple items to a new parent item [#4037].

View File

@ -41,14 +41,14 @@ def get_tree(
return get_children(children[parent_id]) 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. Item list slide.
Returns all root items or all children of an item. Returns all root items or all children of an item.
""" """
root_item_id = config.get("id") or None root_item_id = element.get("id") or None
show_tree = config.get("tree") or False show_tree = element.get("tree") or False
if show_tree: if show_tree:
agenda_items = get_tree(all_data, root_item_id or 0) 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} 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. List of speakers slide.
Returns all usernames, that are on the list of speaker of a 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 # TODO: handle item_id == 0

View File

@ -10,12 +10,12 @@ from ..utils.projector import register_projector_element
def assignment( 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]: ) -> Dict[str, Any]:
""" """
Assignment slide. Assignment slide.
""" """
poll_id = config.get("tree") # noqa poll_id = element.get("tree") # noqa
return {"error": "TODO"} return {"error": "TODO"}

View File

@ -185,15 +185,6 @@ def get_config_variables():
group="Projector", 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( yield ConfigVariable(
name="projector_enable_title", name="projector_enable_title",
default_value=True, default_value=True,
@ -249,7 +240,7 @@ def get_config_variables():
) )
yield ConfigVariable( yield ConfigVariable(
name="projector_blank_color", name="projector_background_color",
default_value="#FFFFFF", default_value="#FFFFFF",
input_type="colorpicker", input_type="colorpicker",
label="Color for blanked projector", label="Color for blanked projector",
@ -257,16 +248,6 @@ def get_config_variables():
group="Projector", 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( yield ConfigVariable(
name="projector_currentListOfSpeakers_reference", name="projector_currentListOfSpeakers_reference",
default_value=1, default_value=1,

View File

@ -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,
),
]

View File

@ -32,35 +32,27 @@ 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.
The config field contains a dictionary which uses UUIDs as keys. Every The elements field contains a list. Every element must have at least the
element must have at least the property "name". The property "stable" property "name".
is to set whether this element should disappear on prune or clear
requests.
Example: Example:
[
{ {
"881d875cf01741718ca926279ac9c99c": {
"name": "topics/topic", "name": "topics/topic",
"id": 1 "id": 1,
}, },
"191c0878cdc04abfbd64f3177a21891a": { {
"name": "core/countdown", "name": "core/countdown",
"stable": true, "id": 1,
"status": "stop",
"countdown_time": 20,
"visable": true,
"default": 42
}, },
"db670aa8d3ed4aabb348e752c75aeaaf": { {
"name": "core/clock", "name": "core/clock",
"stable": true "id": 1,
} },
} ]
If the config field is empty or invalid the projector shows a default If the config field is empty or invalid the projector shows a default
slide. slide.
@ -76,20 +68,18 @@ class Projector(RESTModelMixin, models.Model):
objects = ProjectorManager() objects = ProjectorManager()
config = JSONField() elements = JSONField()
elements_preview = JSONField()
elements_history = JSONField()
scale = models.IntegerField(default=0) scale = models.IntegerField(default=0)
scroll = models.IntegerField(default=0) scroll = models.IntegerField(default=0)
width = models.PositiveIntegerField(default=1024) width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768) height = models.PositiveIntegerField(default=768)
name = models.CharField(max_length=255, unique=True, blank=True) name = models.CharField(max_length=255, unique=True, blank=True)
blank = models.BooleanField(blank=False, default=False)
class Meta: class Meta:
""" """
Contains general permissions that can not be placed in a specific app. Contains general permissions that can not be placed in a specific app.

View File

@ -10,19 +10,19 @@ from ..utils.projector import register_projector_element
def countdown( 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]: ) -> Dict[str, Any]:
""" """
Countdown slide. Countdown slide.
Returns the full_data of the countdown element. Returns the full_data of the countdown element.
config = { element = {
name: 'core/countdown', name: 'core/countdown',
id: 5, # Countdown ID id: 5, # Countdown ID
} }
""" """
countdown_id = config.get("id") or 1 countdown_id = element.get("id") or 1
try: try:
return all_data["core/countdown"][countdown_id] return all_data["core/countdown"][countdown_id]
@ -31,19 +31,19 @@ def countdown(
def message( 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]: ) -> Dict[str, Any]:
""" """
Message slide. Message slide.
Returns the full_data of the message element. Returns the full_data of the message element.
config = { element = {
name: 'core/projector-message', name: 'core/projector-message',
id: 5, # ProjectorMessage ID id: 5, # ProjectorMessage ID
} }
""" """
message_id = config.get("id") or 1 message_id = element.get("id") or 1
try: try:
return all_data["core/projector-message"][message_id] return all_data["core/projector-message"][message_id]
@ -54,4 +54,4 @@ def message(
def register_projector_elements() -> None: def register_projector_elements() -> None:
register_projector_element("core/countdown", countdown) register_projector_element("core/countdown", countdown)
register_projector_element("core/projector-message", message) register_projector_element("core/projector-message", message)
# TODO: Deside if we need a clock slide # TODO: Add clock slide

View File

@ -1,6 +1,8 @@
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError from typing import Any
from openslides.utils.validate import validate_html
from ..utils.projector import projector_elements
from ..utils.rest_api import Field, IntegerField, ModelSerializer, ValidationError
from ..utils.validate import validate_html
from .models import ( from .models import (
ChatMessage, ChatMessage,
ConfigStore, ConfigStore,
@ -20,18 +22,8 @@ class JSONSerializerField(Field):
def to_internal_value(self, data): def to_internal_value(self, data):
""" """
Checks that data is a dictionary. The key is a hex UUID and the Returns the value. It is encoded from the Django JSONField.
value is a dictionary with must have a key 'name'.
""" """
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 return data
def to_representation(self, value): def to_representation(self, value):
@ -51,28 +43,69 @@ class ProjectionDefaultSerializer(ModelSerializer):
fields = ("id", "name", "display_name", "projector") 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): class ProjectorSerializer(ModelSerializer):
""" """
Serializer for core.models.Projector objects. 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) 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: class Meta:
model = Projector model = Projector
fields = ( fields = (
"id", "id",
"config", "elements",
"elements_preview",
"elements_history",
"scale", "scale",
"scroll", "scroll",
"name", "name",
"blank",
"width", "width",
"height", "height",
"projectiondefaults", "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): class TagSerializer(ModelSerializer):

View File

@ -1,6 +1,5 @@
import datetime import datetime
import os import os
import uuid
from typing import Any, Dict, List from typing import Any, Dict, List
from django.conf import settings from django.conf import settings
@ -127,17 +126,9 @@ class ProjectorViewSet(ModelViewSet):
"update", "update",
"partial_update", "partial_update",
"destroy", "destroy",
"activate_elements",
"prune_elements",
"update_elements",
"deactivate_elements",
"clear_elements",
"project",
"control_view", "control_view",
"set_resolution", "set_resolution",
"set_scroll", "set_scroll",
"control_blank",
"broadcast",
"set_projectiondefault", "set_projectiondefault",
): ):
result = has_perm(self.request.user, "core.can_see_projector") and has_perm( 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. REST API operation for DELETE requests.
Assigns all ProjectionDefault objects from this projector to the 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() projector_instance = self.get_object()
for projection_default in ProjectionDefault.objects.all(): for projection_default in ProjectionDefault.objects.all():
if projection_default.projector.id == projector_instance.id: if projection_default.projector.id == projector_instance.id:
projection_default.projector_id = 1 projection_default.projector_id = 1
projection_default.save() projection_default.save()
if config["projector_broadcast"] == projector_instance.pk:
config["projector_broadcast"] = 0
return super(ProjectorViewSet, self).destroy(*args, **kwargs) 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/<pk>/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/<pk>/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/<pk>/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/<pk>/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/<pk>/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: [<projector id1>, ...], # May be an empty list
prune: { # May not be given.
id: <projector id>,
element: <projector element to add>
}
}
"""
# 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/<pk>/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"]) @detail_route(methods=["post"])
def control_view(self, request, pk): def control_view(self, request, pk):
""" """
@ -507,10 +204,7 @@ class ProjectorViewSet(ModelViewSet):
projector_instance.save(skip_autoupdate=True) projector_instance.save(skip_autoupdate=True)
projector_instance.refresh_from_db() projector_instance.refresh_from_db()
inform_changed_data(projector_instance) inform_changed_data(projector_instance)
action = (request.data["action"].capitalize(),) return Response()
direction = (request.data["direction"],)
message = f"{action} {direction} was successful."
return Response({"detail": message})
@detail_route(methods=["post"]) @detail_route(methods=["post"])
def set_scroll(self, request, pk): def set_scroll(self, request, pk):
@ -530,40 +224,6 @@ class ProjectorViewSet(ModelViewSet):
message = f"Setting scroll to {request.data} was successful." message = f"Setting scroll to {request.data} was successful."
return Response({"detail": message}) 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/<pk>/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/<pk>/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"]) @detail_route(methods=["post"])
def set_projectiondefault(self, request, pk): def set_projectiondefault(self, request, pk):
""" """
@ -589,9 +249,7 @@ class ProjectorViewSet(ModelViewSet):
projectiondefault.projector = projector_instance projectiondefault.projector = projector_instance
projectiondefault.save() projectiondefault.save()
return Response( return Response()
f'Setting projectiondefault "{projectiondefault.display_name}" to projector {projector_instance.pk} was successful.'
)
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
@ -673,8 +331,8 @@ class ConfigViewSet(ModelViewSet):
config[key] = value config[key] = value
except ConfigNotFound: except ConfigNotFound:
raise Http404 raise Http404
except ConfigError as e: except ConfigError as err:
raise ValidationError({"detail": str(e)}) raise ValidationError({"detail": str(err)})
# Return response. # Return response.
return Response({"key": key, "value": value}) return Response({"key": key, "value": value})

View File

@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element
def mediafile( 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]: ) -> Dict[str, Any]:
""" """
Slide for Mediafile. Slide for Mediafile.

View File

@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element
def motion( 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]: ) -> Dict[str, Any]:
""" """
Motion slide. Motion slide.
@ -19,7 +19,7 @@ def motion(
def motion_block( 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]: ) -> Dict[str, Any]:
""" """
Motion slide. Motion slide.

View File

@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element
def topic( 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]: ) -> Dict[str, Any]:
""" """
Topic slide. Topic slide.

View File

@ -10,7 +10,7 @@ from ..utils.projector import register_projector_element
def user( 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]: ) -> Dict[str, Any]:
""" """
User slide. User slide.

View File

@ -121,7 +121,7 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
projector_id, {"error": f"No data for projector {projector_id}"} projector_id, {"error": f"No data for projector {projector_id}"}
) )
new_hash = hash(str(data)) 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 projector_data[projector_id] = data
self.projector_hash[projector_id] = new_hash self.projector_hash[projector_id] = new_hash

View File

@ -28,72 +28,58 @@ def register_projector_element(name: str, element: ProjectorElementCallable) ->
async def get_projectot_data( async def get_projectot_data(
projector_ids: List[int] = None 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 The keys of the returned data are the projector ids as int. When converted
to json, the numbers will changed to strings like "1". 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 The data for each projector is a list of elements.
elements as strings. If there is a generell problem with the projector, the
key can be 'error'.
Each element is a dict where the keys are "config", "data". "config" Each element is a dict where the keys are "elements", "data". "elements"
contains the projector config. It is the same as the projector config in the contains the projector elements. It is the same as the projector elements in
database. "data" contains all necessary data to render the projector 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 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: The returned value looks like this:
projector_data = { projector_data = {
1: { 1: [
"UnIqUe-UUID": { {
"config": { "element": {
"name": "agenda/item-list", "name": "agenda/item-list",
}, },
"data": { "data": {
"items": [] "items": []
}, },
}, },
}, ],
2: {
"error": {
"error": "Projector has no config",
},
},
} }
""" """
if projector_ids is None: if projector_ids is None:
projector_ids = [] projector_ids = []
all_data = await element_cache.get_all_full_data_ordered() 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(): for projector_id, projector in all_data.get("core/projector", {}).items():
if projector_ids and projector_id not in projector_ids: if projector_ids and projector_id not in projector_ids:
# only render the projector in question. # only render the projector in question.
continue continue
projector_data[projector_id] = {} if not projector["elements"]:
if not projector["config"]: # Skip empty elements.
projector_data[projector_id] = {
"error": {"error": "Projector has no config"}
}
continue continue
for uuid, projector_config in projector["config"].items(): projector_data[projector_id] = []
projector_data[projector_id][uuid] = {"config": projector_config} for element in projector["elements"]:
projector_element = projector_elements.get(projector_config["name"]) projector_element = projector_elements[element["name"]]
if projector_element is None: projector_data[projector_id].append(
projector_data[projector_id][uuid][ {"data": projector_element(element, all_data), "element": element}
"error" )
] = f"Projector element {projector_config['name']} does not exist"
else:
projector_data[projector_id][uuid]["data"] = projector_element(
projector_config, all_data
)
return projector_data return projector_data

View File

@ -6,60 +6,67 @@ from rest_framework.test import APIClient
from openslides import __license__ as license, __url__ as url, __version__ as version from openslides import __license__ as license, __url__ as url, __version__ as version
from openslides.core.config import ConfigVariable, config 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.rest_api import ValidationError
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
class ProjectorAPI(TestCase): class ProjectorAPI(TestCase):
"""
Tests requests from the anonymous user.
"""
def test_slide_on_default_projector(self): def test_slide_on_default_projector(self):
self.client.login(username="admin", password="admin") self.client.login(username="admin", password="admin")
topic = Topic.objects.create( self.client.put(
title="title_que1olaish5Wei7que6i", text="text_aishah8Eh7eQuie5ooji" 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"])) response = self.client.get(reverse("projector-detail", args=["1"]))
self.assertEqual(response.status_code, status.HTTP_200_OK) 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") 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"])) response = self.client.put(
content = json.loads(response.content.decode()) reverse("projector-detail", args=["1"]),
del content["projectiondefaults"] {"elements": [{"name": "invalid_slide_name", "id": 1}]},
content_type="application/json",
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,
},
) )
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): class VersionView(TestCase):
""" """

View File

@ -80,8 +80,8 @@ class TProjector:
def get_elements(self) -> List[Dict[str, Any]]: def get_elements(self) -> List[Dict[str, Any]]:
return [ return [
{"id": 1, "config": {"uid1": {"name": "test/slide1", "id": 1}}}, {"id": 1, "elements": [{"name": "test/slide1", "id": 1}]},
{"id": 2, "config": {"uid2": {"name": "test/slide2", "id": 1}}}, {"id": 2, "elements": [{"name": "test/slide2", "id": 1}]},
] ]
async def restrict_elements( async def restrict_elements(

View File

@ -532,12 +532,12 @@ async def test_listen_to_projector(communicator, set_config):
content = response.get("content") content = response.get("content")
assert type == "projector" assert type == "projector"
assert content == { assert content == {
"1": { "1": [
"uid1": { {
"data": {"name": "slide1", "event_name": "OpenSlides"}, "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") content = response.get("content")
assert type == "projector" assert type == "projector"
assert content == { assert content == {
"1": { "1": [
"uid1": { {
"data": {"name": "slide1", "event_name": "Test Event"}, "data": {"name": "slide1", "event_name": "Test Event"},
"config": {"id": 1, "name": "test/slide1"}, "element": {"id": 1, "name": "test/slide1"},
} }
} ]
} }

View File

@ -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())