Added scale and scroll, up, down and reset. Fixed #1633.
This commit is contained in:
parent
7860345116
commit
e646cce91e
21
openslides/core/migrations/0006_auto_20150914_2232.py
Normal file
21
openslides/core/migrations/0006_auto_20150914_2232.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
@ -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'
|
||||
|
||||
|
@ -30,7 +30,7 @@ class ProjectorSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Projector
|
||||
fields = ('id', 'config', 'elements', )
|
||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', )
|
||||
|
||||
|
||||
class CustomSlideSerializer(ModelSerializer):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user