Merge pull request #1618 from normanjaeckel/Projector
Added UUID to projector elements. Added update view.
This commit is contained in:
commit
973d3fa653
@ -14,7 +14,7 @@ def add_default_projector_2(apps, schema_editor):
|
|||||||
'name': 'core/countdown',
|
'name': 'core/countdown',
|
||||||
'stable': True,
|
'stable': True,
|
||||||
'status': 'stop',
|
'status': 'stop',
|
||||||
'countdown_time': 60
|
'countdown_time': 60,
|
||||||
})
|
})
|
||||||
projector.config = config
|
projector.config = config
|
||||||
projector.save()
|
projector.save()
|
||||||
|
39
openslides/core/migrations/0003_uuid.py
Normal file
39
openslides/core/migrations/0003_uuid.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_projector_3(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Adds UUIDs to projector config.
|
||||||
|
"""
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
Projector = apps.get_model('core', 'Projector')
|
||||||
|
projector = Projector.objects.get()
|
||||||
|
|
||||||
|
def add_uuid(self):
|
||||||
|
"""
|
||||||
|
Adds an UUID to every element.
|
||||||
|
"""
|
||||||
|
for element in self.config:
|
||||||
|
if element.get('uuid') is None:
|
||||||
|
element['uuid'] = uuid.uuid4().hex
|
||||||
|
|
||||||
|
add_uuid(projector)
|
||||||
|
projector.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_countdown'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=add_default_projector_3,
|
||||||
|
reverse_code=None,
|
||||||
|
atomic=True,
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,5 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||||
@ -40,6 +42,22 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
('can_see_dashboard', ugettext_noop('Can see the dashboard')),
|
('can_see_dashboard', ugettext_noop('Can see the dashboard')),
|
||||||
('can_use_chat', ugettext_noop('Can use the chat')))
|
('can_use_chat', ugettext_noop('Can use the chat')))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Saves the projector. Ensures that every projector element in config
|
||||||
|
has an UUID.
|
||||||
|
"""
|
||||||
|
self.add_uuid()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def add_uuid(self):
|
||||||
|
"""
|
||||||
|
Adds an UUID to every element.
|
||||||
|
"""
|
||||||
|
for element in self.config:
|
||||||
|
if element.get('uuid') is None:
|
||||||
|
element['uuid'] = uuid.uuid4().hex
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elements(self):
|
def elements(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
@ -194,30 +195,66 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
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
|
||||||
|
list of dictonaries. Every dictonary contains the hex UUID (key
|
||||||
|
'uuid') and the new element data (key 'data').
|
||||||
|
"""
|
||||||
|
# Check the data. It must be a list of dictionaries. Get config
|
||||||
|
# entry from projector model. Change the entries that should be
|
||||||
|
# changed and try to serialize. This raises ValidationError if the
|
||||||
|
# data is invalid.
|
||||||
|
if not isinstance(request.data, list):
|
||||||
|
raise ValidationError({'config': ['Data must be a list of dictionaries.']})
|
||||||
|
error = {'config': ['Data must be a list of dictionaries with special keys and values. See docstring.']}
|
||||||
|
for item in request.data:
|
||||||
|
try:
|
||||||
|
uuid.UUID(hex=str(item.get('uuid')))
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(error)
|
||||||
|
if not isinstance(item['data'], dict):
|
||||||
|
raise ValidationError(error)
|
||||||
|
|
||||||
|
projector_instance = self.get_object()
|
||||||
|
projector_config = projector_instance.config
|
||||||
|
for entry_to_be_changed in request.data:
|
||||||
|
for index, element in enumerate(projector_config):
|
||||||
|
if element['uuid'] == entry_to_be_changed['uuid']:
|
||||||
|
projector_config[index] = entry_to_be_changed['data']
|
||||||
|
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['post'])
|
||||||
def deactivate_elements(self, request, pk):
|
def deactivate_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to deactivate projector elements. It expects a
|
REST API operation to deactivate projector elements. It expects a
|
||||||
POST request to /rest/core/projector/<pk>/deactivate_elements/ with
|
POST request to /rest/core/projector/<pk>/deactivate_elements/ with
|
||||||
a list of dictionaries. These are exactly the projector_elements in
|
a list of hex UUIDs. These are the projector_elements in the config
|
||||||
the config that should be deleted.
|
that should be deleted.
|
||||||
"""
|
"""
|
||||||
# Check the data. It must be a list of dictionaries. Get config
|
# Check the data. It must be a list of hex UUIDs. Get config
|
||||||
# entry from projector model. Pop out the entries that should be
|
# entry from projector model. Pop out the entries that should be
|
||||||
# deleted and try to serialize. This raises ValidationErrors if the
|
# deleted and try to serialize. This raises ValidationError if the
|
||||||
# data is invalid.
|
# data is invalid.
|
||||||
if not isinstance(request.data, list) or list(filter(lambda item: not isinstance(item, dict), request.data)):
|
if not isinstance(request.data, list):
|
||||||
raise ValidationError({'config': ['Data must be a list of dictionaries.']})
|
raise ValidationError({'config': ['Data must be a list of hex UUIDs.']})
|
||||||
|
for item in request.data:
|
||||||
|
try:
|
||||||
|
uuid.UUID(hex=str(item))
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError({'config': ['Data must be a list of hex UUIDs.']})
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_config = projector_instance.config
|
elements = []
|
||||||
for entry_to_be_deleted in request.data:
|
for element in projector_instance.config:
|
||||||
try:
|
if not element['uuid'] in request.data:
|
||||||
projector_config.remove(entry_to_be_deleted)
|
elements.append(element)
|
||||||
except ValueError:
|
serializer = self.get_serializer(projector_instance, data={'config': elements}, partial=False)
|
||||||
# The entry that should be deleted is not on the projector.
|
|
||||||
pass
|
|
||||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
@ -23,13 +23,14 @@ class ProjectorAPI(TestCase):
|
|||||||
default_projector = Projector.objects.get(pk=1)
|
default_projector = Projector.objects.get(pk=1)
|
||||||
default_projector.config = [{'name': 'core/customslide', 'id': customslide.id}]
|
default_projector.config = [{'name': 'core/customslide', 'id': customslide.id}]
|
||||||
default_projector.save()
|
default_projector.save()
|
||||||
|
element_uuid = Projector.objects.get(pk=1).config[0]['uuid']
|
||||||
|
|
||||||
response = self.client.get(reverse('projector-detail', args=['1']))
|
response = self.client.get(reverse('projector-detail', args=['1']))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(json.loads(response.content.decode()), {
|
self.assertEqual(json.loads(response.content.decode()), {
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'config': [{'name': 'core/customslide', 'id': customslide.id}],
|
'config': [{'name': 'core/customslide', 'id': customslide.id, 'uuid': element_uuid}],
|
||||||
'elements': [
|
'elements': [
|
||||||
{'name': 'core/customslide',
|
{'name': 'core/customslide',
|
||||||
'context': {'id': customslide.id}}]})
|
'context': {'id': customslide.id}}]})
|
||||||
@ -39,13 +40,14 @@ class ProjectorAPI(TestCase):
|
|||||||
default_projector = Projector.objects.get(pk=1)
|
default_projector = Projector.objects.get(pk=1)
|
||||||
default_projector.config = [{'name': 'invalid_slide'}]
|
default_projector.config = [{'name': 'invalid_slide'}]
|
||||||
default_projector.save()
|
default_projector.save()
|
||||||
|
element_uuid = Projector.objects.get(pk=1).config[0]['uuid']
|
||||||
|
|
||||||
response = self.client.get(reverse('projector-detail', args=['1']))
|
response = self.client.get(reverse('projector-detail', args=['1']))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(json.loads(response.content.decode()), {
|
self.assertEqual(json.loads(response.content.decode()), {
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'config': [{'name': 'invalid_slide'}],
|
'config': [{'name': 'invalid_slide', 'uuid': element_uuid}],
|
||||||
'elements': [
|
'elements': [
|
||||||
{'name': 'invalid_slide',
|
{'name': 'invalid_slide',
|
||||||
'error': 'Projector element does not exist.'}]})
|
'error': 'Projector element does not exist.'}]})
|
||||||
|
@ -83,14 +83,43 @@ class ProjectorAPI(TestCase):
|
|||||||
self.viewset.prune_elements(request=request, pk=MagicMock())
|
self.viewset.prune_elements(request=request, pk=MagicMock())
|
||||||
self.assertEqual(len(mock_object.return_value.config), 2)
|
self.assertEqual(len(mock_object.return_value.config), 2)
|
||||||
|
|
||||||
|
def test_update_elements(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_jbmgfnf657djcnsjdfkm',
|
||||||
|
'test_key_7mibir1Uoee7uhilohB1': 'test_value_mbhfn5zwhakbigjrns88',
|
||||||
|
'uuid': 'aacbb64acafc4ccc957240c871d4e77d'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{
|
||||||
|
'uuid': 'aacbb64acafc4ccc957240c871d4e77d',
|
||||||
|
'data': {
|
||||||
|
'name': 'test_projector_element_wdsexrvhgn67ezfjnfje'}}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.update_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
|
self.assertEqual(mock_object.return_value.config[0]['name'], 'test_projector_element_wdsexrvhgn67ezfjnfje')
|
||||||
|
|
||||||
|
def test_update_elements_wrong_element(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_njb657djcsjdmgfnffkm',
|
||||||
|
'test_key_uhilo7mir1Uoee7ibhB1': 'test_value_hjrnsmbhfn5zwakbig88',
|
||||||
|
'uuid': '5b5e5d3b35de4fff873925296c3093fc'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{
|
||||||
|
'uuid': '255fda68ca6f4f3f803b98405abfb710',
|
||||||
|
'data': {
|
||||||
|
'name': 'test_projector_element_wxrvhn67eebmfjjnkvds'}}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.update_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
|
self.assertNotEqual(mock_object.return_value.config[0]['name'], 'test_projector_element_wxrvhn67eebmfjjnkvds')
|
||||||
|
|
||||||
def test_deactivate_elements(self, mock_object):
|
def test_deactivate_elements(self, mock_object):
|
||||||
mock_object.return_value.config = [{
|
mock_object.return_value.config = [{
|
||||||
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
||||||
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo',
|
||||||
|
'uuid': '874aaf279be346ff85a9b456ce1d1128'}]
|
||||||
request = MagicMock()
|
request = MagicMock()
|
||||||
request.data = [{
|
request.data = ['874aaf279be346ff85a9b456ce1d1128']
|
||||||
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
|
||||||
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
|
||||||
self.viewset.request = request
|
self.viewset.request = request
|
||||||
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
self.assertEqual(len(mock_object.return_value.config), 0)
|
self.assertEqual(len(mock_object.return_value.config), 0)
|
||||||
@ -98,9 +127,10 @@ class ProjectorAPI(TestCase):
|
|||||||
def test_deactivate_elements_wrong_element(self, mock_object):
|
def test_deactivate_elements_wrong_element(self, mock_object):
|
||||||
mock_object.return_value.config = [{
|
mock_object.return_value.config = [{
|
||||||
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
||||||
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo',
|
||||||
|
'uuid': 'd867b2557ad041b8848e95981c5671b7'}]
|
||||||
request = MagicMock()
|
request = MagicMock()
|
||||||
request.data = [{'name': 'wrong name'}]
|
request.data = ['1179ea09ba2b4559a41272efb1346c86'] # Wrong UUID.
|
||||||
self.viewset.request = request
|
self.viewset.request = request
|
||||||
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
self.assertEqual(len(mock_object.return_value.config), 1)
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
@ -108,7 +138,8 @@ class ProjectorAPI(TestCase):
|
|||||||
def test_deactivate_elements_no_list(self, mock_object):
|
def test_deactivate_elements_no_list(self, mock_object):
|
||||||
mock_object.return_value.config = [{
|
mock_object.return_value.config = [{
|
||||||
'name': 'test_projector_element_Au1ce9nevaeX7zo4ye2w',
|
'name': 'test_projector_element_Au1ce9nevaeX7zo4ye2w',
|
||||||
'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor'}]
|
'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor',
|
||||||
|
'uuid': '0f3b8f8df38b4bbc90f4beba9393d2db'}]
|
||||||
request = MagicMock()
|
request = MagicMock()
|
||||||
request.data = 'bad_value_no_list_ohchohWee1fie0SieTha'
|
request.data = 'bad_value_no_list_ohchohWee1fie0SieTha'
|
||||||
self.viewset.request = request
|
self.viewset.request = request
|
||||||
@ -118,7 +149,8 @@ class ProjectorAPI(TestCase):
|
|||||||
def test_deactivate_elements_bad_list(self, mock_object):
|
def test_deactivate_elements_bad_list(self, mock_object):
|
||||||
mock_object.return_value.config = [{
|
mock_object.return_value.config = [{
|
||||||
'name': 'test_projector_element_teibaeRaim1heiCh6Ohv',
|
'name': 'test_projector_element_teibaeRaim1heiCh6Ohv',
|
||||||
'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae'}]
|
'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae',
|
||||||
|
'uuid': '8ae42a09f585480e8b4a53194d4d1fba'}]
|
||||||
request = MagicMock()
|
request = MagicMock()
|
||||||
# Value 1 is not an dictionary so we expect ValidationError.
|
# Value 1 is not an dictionary so we expect ValidationError.
|
||||||
request.data = [1]
|
request.data = [1]
|
||||||
|
Loading…
Reference in New Issue
Block a user