diff --git a/openslides/core/migrations/0006_auto_20150914_2232.py b/openslides/core/migrations/0006_auto_20150914_2232.py new file mode 100644 index 000000000..20be13ba7 --- /dev/null +++ b/openslides/core/migrations/0006_auto_20150914_2232.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_add_chat_message_model'), + ] + + operations = [ + migrations.AddField( + model_name='projector', + name='scale', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='projector', + name='scroll', + field=models.IntegerField(default=0), + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index bc3864d63..9fb928eae 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -25,25 +25,37 @@ class Projector(RESTModelMixin, models.Model): { "881d875cf01741718ca926279ac9c99c": { "name": "core/customslide", - "id": 1}, + "id": 1 + }, "191c0878cdc04abfbd64f3177a21891a": { "name": "core/countdown", "stable": true, + "status": "stop", "countdown_time": 20, - "status": "stop"}, + "visable": true, + "default": 42 + }, "db670aa8d3ed4aabb348e752c75aeaaf": { "name": "core/clock", - "stable": true} + "stable": true + } } If the config field is empty or invalid the projector shows a default slide. + There are two additional fields to control the behavior of the projector + view itself: scale and scroll. + The projector can be controlled using the REST API with POST requests on e. g. the URL /rest/core/projector/1/activate_elements/. """ config = JSONField() + scale = models.IntegerField(default=0) + + scroll = models.IntegerField(default=0) + class Meta: """ Contains general permissions that can not be placed in a specific app. diff --git a/openslides/core/projector.py b/openslides/core/projector.py index ef1d7b1a0..7efbc65f9 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -45,11 +45,11 @@ class Countdown(ProjectorElement): To start the countdown write into the config field: { + "status": "running", "countdown_time": , - "status": "running" } - The timestamp is a POSIX timestamp (seconds) calculated from server + The timestamp is a POSIX timestamp (seconds) calculated from client time, server time offset and countdown duration (countdown_time = now - serverTimeOffset + duration). @@ -58,13 +58,23 @@ class Countdown(ProjectorElement): and set status to "stop". To reset the countdown (it is not a reset in a functional way) just - change the countdown_time. The status value remains "stop". + change the countdown time. The status value remains "stop". - There might be an additional value for the "default" countdown time - which is used for the internal reset method if the countdown is coupled - with the list of speakers. + Do not forget to send values for additional keywords like "stable" if + you do not want to use the default. - To hide a countdown add {"hidden": true}. + The countdown backend supports an extra keyword "default". + + { + "default": + } + + This is used for the internal reset method if the countdown is coupled + with the list of speakers. The default of this default value can be + customized in OpenSlides config 'projector_default_countdown'. + + Use additional keywords to control view behavior like "visable" and + "label". These keywords are not handles by the backend. """ name = 'core/countdown' diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index d541d6cbe..4dab834a7 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -30,7 +30,7 @@ class ProjectorSerializer(ModelSerializer): class Meta: model = Projector - fields = ('id', 'config', 'elements', ) + fields = ('id', 'config', 'elements', 'scale', 'scroll', ) class CustomSlideSerializer(ModelSerializer): diff --git a/openslides/core/views.py b/openslides/core/views.py index 6ec3572e9..b80ab3660 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -7,6 +7,7 @@ from django.apps import apps from django.conf import settings from django.contrib.staticfiles import finders from django.core.urlresolvers import get_resolver +from django.db.models import F from django.http import Http404, HttpResponse from openslides import __version__ as version @@ -139,8 +140,9 @@ class ProjectorViewSet(ReadOnlyModelViewSet): """ API endpoint for the projector slide info. - There are the following views: metadata, list, retrieve, activate_elements, - prune_elements, update_elements, deactivate_elements and clear_elements. + There are the following views: metadata, list, retrieve, + activate_elements, prune_elements, update_elements, + deactivate_elements, clear_elements and control_view. """ queryset = Projector.objects.all() serializer_class = ProjectorSerializer @@ -152,7 +154,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet): if self.action in ('metadata', 'list', 'retrieve'): result = self.request.user.has_perm('core.can_see_projector') elif self.action in ('activate_elements', 'prune_elements', 'update_elements', - 'deactivate_elements', 'clear_elements'): + 'deactivate_elements', 'clear_elements', 'control_view'): result = (self.request.user.has_perm('core.can_see_projector') and self.request.user.has_perm('core.can_manage_projector')) else: @@ -211,8 +213,22 @@ class ProjectorViewSet(ReadOnlyModelViewSet): def update_elements(self, request, pk): """ REST API operation to update projector elements. It expects a POST - request to /rest/core/projector//deactivate_elements/ with a - dictonary to update the projector config. + request to /rest/core/projector//update_elements/ with a + dictonary to update the projector config. This must be a dictionary + with UUIDs as keys and projector element dictionaries as values. + + Example: + + { + "191c0878cdc04abfbd64f3177a21891a": { + "name": "core/countdown", + "stable": true, + "status": "running", + "countdown_time": 1374321600.0, + "visable": true, + "default": 42 + } + } """ if not isinstance(request.data, dict): raise ValidationError({'data': 'Data must be a dictionary.'}) @@ -284,6 +300,56 @@ class ProjectorViewSet(ReadOnlyModelViewSet): serializer.save() return Response(serializer.data) + @detail_route(methods=['post']) + def control_view(self, request, pk): + """ + REST API operation to control the projector view, i. e. scale and + scroll the projector. + + It expects a POST request to + /rest/core/projector//control_view/ with a dictionary with an + action ('scale' or 'scroll') and a direction ('up', 'down' or + 'reset'). + + Example: + + { + "action": "scale", + "direction": "up" + } + """ + if not isinstance(request.data, dict): + raise ValidationError({'data': 'Data must be a dictionary.'}) + if (request.data.get('action') not in ('scale', 'scroll') or + request.data.get('direction') not in ('up', 'down', 'reset')): + raise ValidationError({'data': "Data must be a dictionary with an action ('scale' or 'scroll') " + "and a direction ('up', 'down' or 'reset')."}) + + projector_instance = self.get_object() + if request.data['action'] == 'scale': + if request.data['direction'] == 'up': + projector_instance.scale = F('scale') + 1 + elif request.data['direction'] == 'down': + projector_instance.scale = F('scale') - 1 + else: + # request.data['direction'] == 'reset' + projector_instance.scale = 0 + else: + # request.data['action'] == 'scroll' + if request.data['direction'] == 'up': + projector_instance.scroll = F('scroll') + 1 + elif request.data['direction'] == 'down': + projector_instance.scroll = F('scroll') - 1 + else: + # request.data['direction'] == 'reset' + projector_instance.scroll = 0 + + projector_instance.save() + message = '{action} {direction} was successful.'.format( + action=request.data['action'].capitalize(), + direction=request.data['direction']) + return Response({'detail': message}) + class CustomSlideViewSet(ModelViewSet): """ diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index bd95f2b50..61e97868a 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -34,7 +34,9 @@ class ProjectorAPI(TestCase): 'aae4a07b26534cfb9af4232f361dce73': {'id': customslide.id, 'name': 'core/customslide', - 'context': None}}}) + 'context': None}}, + 'scale': 0, + 'scroll': 0}) def test_invalid_slide_on_default_projector(self): self.client.login(username='admin', password='admin') @@ -51,7 +53,9 @@ class ProjectorAPI(TestCase): 'elements': { 'fc6ef43b624043068c8e6e7a86c5a1b0': {'name': 'invalid_slide', - 'error': 'Projector element does not exist.'}}}) + 'error': 'Projector element does not exist.'}}, + 'scale': 0, + 'scroll': 0}) class VersionView(TestCase):