commit
24bb4ad0ad
@ -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].
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
51
openslides/core/migrations/0010_auto_20190118_1908.py
Normal file
51
openslides/core/migrations/0010_auto_20190118_1908.py
Normal 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,
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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/<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"])
|
||||
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/<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"])
|
||||
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})
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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(
|
||||
|
@ -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"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
@ -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())
|
Loading…
Reference in New Issue
Block a user