Merge pull request #1637 from normanjaeckel/ProjectorScaleScroll
Added scale and scroll. Fixed #1633.
This commit is contained in:
commit
d7fd61e979
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": {
|
"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.
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user