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": {
"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.

View File

@ -45,11 +45,11 @@ class Countdown(ProjectorElement):
To start the countdown write into the config field:
{
"status": "running",
"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 -
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": <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'

View File

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

View File

@ -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/<pk>/deactivate_elements/ with a
dictonary to update the projector config.
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({'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/<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):
"""

View File

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