Merge pull request #1637 from normanjaeckel/ProjectorScaleScroll

Added scale and scroll. Fixed #1633.
This commit is contained in:
Norman Jäckel 2015-09-18 10:36:28 +02:00
commit d7fd61e979
6 changed files with 131 additions and 18 deletions

View File

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

View File

@ -25,25 +25,37 @@ class Projector(RESTModelMixin, models.Model):
{ {
"881d875cf01741718ca926279ac9c99c": { "881d875cf01741718ca926279ac9c99c": {
"name": "core/customslide", "name": "core/customslide",
"id": 1}, "id": 1
},
"191c0878cdc04abfbd64f3177a21891a": { "191c0878cdc04abfbd64f3177a21891a": {
"name": "core/countdown", "name": "core/countdown",
"stable": true, "stable": true,
"status": "stop",
"countdown_time": 20, "countdown_time": 20,
"status": "stop"}, "visable": true,
"default": 42
},
"db670aa8d3ed4aabb348e752c75aeaaf": { "db670aa8d3ed4aabb348e752c75aeaaf": {
"name": "core/clock", "name": "core/clock",
"stable": true} "stable": true
}
} }
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.
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 The projector can be controlled using the REST API with POST requests
on e. g. the URL /rest/core/projector/1/activate_elements/. on e. g. the URL /rest/core/projector/1/activate_elements/.
""" """
config = JSONField() config = JSONField()
scale = models.IntegerField(default=0)
scroll = models.IntegerField(default=0)
class Meta: class Meta:
""" """
Contains general permissions that can not be placed in a specific app. Contains general permissions that can not be placed in a specific app.

View File

@ -45,11 +45,11 @@ class Countdown(ProjectorElement):
To start the countdown write into the config field: To start the countdown write into the config field:
{ {
"status": "running",
"countdown_time": <timestamp>, "countdown_time": <timestamp>,
"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 - time, server time offset and countdown duration (countdown_time = now -
serverTimeOffset + duration). serverTimeOffset + duration).
@ -58,13 +58,23 @@ class Countdown(ProjectorElement):
and set status to "stop". and set status to "stop".
To reset the countdown (it is not a reset in a functional way) just 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 Do not forget to send values for additional keywords like "stable" if
which is used for the internal reset method if the countdown is coupled you do not want to use the default.
with the list of speakers.
To hide a countdown add {"hidden": true}. The countdown backend supports an extra keyword "default".
{
"default": <seconds>
}
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' name = 'core/countdown'

View File

@ -30,7 +30,7 @@ class ProjectorSerializer(ModelSerializer):
class Meta: class Meta:
model = Projector model = Projector
fields = ('id', 'config', 'elements', ) fields = ('id', 'config', 'elements', 'scale', 'scroll', )
class CustomSlideSerializer(ModelSerializer): class CustomSlideSerializer(ModelSerializer):

View File

@ -7,6 +7,7 @@ from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.core.urlresolvers import get_resolver from django.core.urlresolvers import get_resolver
from django.db.models import F
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from openslides import __version__ as version from openslides import __version__ as version
@ -139,8 +140,9 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
""" """
API endpoint for the projector slide info. API endpoint for the projector slide info.
There are the following views: metadata, list, retrieve, activate_elements, There are the following views: metadata, list, retrieve,
prune_elements, update_elements, deactivate_elements and clear_elements. activate_elements, prune_elements, update_elements,
deactivate_elements, clear_elements and control_view.
""" """
queryset = Projector.objects.all() queryset = Projector.objects.all()
serializer_class = ProjectorSerializer serializer_class = ProjectorSerializer
@ -152,7 +154,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
if self.action in ('metadata', 'list', 'retrieve'): if self.action in ('metadata', 'list', 'retrieve'):
result = self.request.user.has_perm('core.can_see_projector') result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements', 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 result = (self.request.user.has_perm('core.can_see_projector') and
self.request.user.has_perm('core.can_manage_projector')) self.request.user.has_perm('core.can_manage_projector'))
else: else:
@ -211,8 +213,22 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
def update_elements(self, request, pk): def update_elements(self, request, pk):
""" """
REST API operation to update projector elements. It expects a POST REST API operation to update projector elements. It expects a POST
request to /rest/core/projector/<pk>/deactivate_elements/ with a request to /rest/core/projector/<pk>/update_elements/ with a
dictonary to update the projector config. 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): if not isinstance(request.data, dict):
raise ValidationError({'data': 'Data must be a dictionary.'}) raise ValidationError({'data': 'Data must be a dictionary.'})
@ -284,6 +300,56 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
serializer.save() serializer.save()
return Response(serializer.data) 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/<pk>/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): class CustomSlideViewSet(ModelViewSet):
""" """

View File

@ -34,7 +34,9 @@ class ProjectorAPI(TestCase):
'aae4a07b26534cfb9af4232f361dce73': 'aae4a07b26534cfb9af4232f361dce73':
{'id': customslide.id, {'id': customslide.id,
'name': 'core/customslide', 'name': 'core/customslide',
'context': None}}}) 'context': None}},
'scale': 0,
'scroll': 0})
def test_invalid_slide_on_default_projector(self): def test_invalid_slide_on_default_projector(self):
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
@ -51,7 +53,9 @@ class ProjectorAPI(TestCase):
'elements': { 'elements': {
'fc6ef43b624043068c8e6e7a86c5a1b0': 'fc6ef43b624043068c8e6e7a86c5a1b0':
{'name': 'invalid_slide', {'name': 'invalid_slide',
'error': 'Projector element does not exist.'}}}) 'error': 'Projector element does not exist.'}},
'scale': 0,
'scroll': 0})
class VersionView(TestCase): class VersionView(TestCase):