Merge pull request #1618 from normanjaeckel/Projector

Added UUID to projector elements. Added update view.
This commit is contained in:
Norman Jäckel 2015-09-05 21:23:00 +02:00
commit 973d3fa653
6 changed files with 153 additions and 25 deletions

View File

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

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

View File

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

View File

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

View File

@ -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.'}]})

View File

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