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