From 47a151c71e61d315ff09f311ae04aabda0ed5ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 5 Sep 2015 17:52:40 +0200 Subject: [PATCH] Added UUID to projector elements. Added update view. --- openslides/core/migrations/0002_countdown.py | 2 +- openslides/core/migrations/0003_uuid.py | 39 ++++++++++++ openslides/core/models.py | 18 ++++++ openslides/core/views.py | 65 +++++++++++++++----- tests/integration/core/test_views.py | 6 +- tests/unit/core/test_views.py | 48 ++++++++++++--- 6 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 openslides/core/migrations/0003_uuid.py diff --git a/openslides/core/migrations/0002_countdown.py b/openslides/core/migrations/0002_countdown.py index 7ae5cc588..41d159946 100644 --- a/openslides/core/migrations/0002_countdown.py +++ b/openslides/core/migrations/0002_countdown.py @@ -14,7 +14,7 @@ def add_default_projector_2(apps, schema_editor): 'name': 'core/countdown', 'stable': True, 'status': 'stop', - 'countdown_time': 60 + 'countdown_time': 60, }) projector.config = config projector.save() diff --git a/openslides/core/migrations/0003_uuid.py b/openslides/core/migrations/0003_uuid.py new file mode 100644 index 000000000..a66ca8881 --- /dev/null +++ b/openslides/core/migrations/0003_uuid.py @@ -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, + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index ea5d18952..a086edf65 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -1,3 +1,5 @@ +import uuid + from django.db import models from django.utils.translation import ugettext as _ 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_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 def elements(self): """ diff --git a/openslides/core/views.py b/openslides/core/views.py index d95688025..d709397df 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -1,4 +1,5 @@ import re +import uuid from collections import OrderedDict from operator import attrgetter @@ -194,30 +195,66 @@ class ProjectorViewSet(ReadOnlyModelViewSet): serializer.save() 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//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']) def deactivate_elements(self, request, pk): """ REST API operation to deactivate projector elements. It expects a POST request to /rest/core/projector//deactivate_elements/ with - a list of dictionaries. These are exactly the projector_elements in - the config that should be deleted. + a list of hex UUIDs. These are the projector_elements in the config + 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 - # deleted and try to serialize. This raises ValidationErrors if the + # deleted and try to serialize. This raises ValidationError if the # data is invalid. - if not isinstance(request.data, list) or list(filter(lambda item: not isinstance(item, dict), request.data)): - raise ValidationError({'config': ['Data must be a list of dictionaries.']}) + if not isinstance(request.data, list): + 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_config = projector_instance.config - for entry_to_be_deleted in request.data: - try: - projector_config.remove(entry_to_be_deleted) - except ValueError: - # The entry that should be deleted is not on the projector. - pass - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + elements = [] + for element in projector_instance.config: + if not element['uuid'] in request.data: + elements.append(element) + serializer = self.get_serializer(projector_instance, data={'config': elements}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index 85edaf4dc..79318ccba 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -23,13 +23,14 @@ class ProjectorAPI(TestCase): default_projector = Projector.objects.get(pk=1) default_projector.config = [{'name': 'core/customslide', 'id': customslide.id}] default_projector.save() + element_uuid = Projector.objects.get(pk=1).config[0]['uuid'] response = self.client.get(reverse('projector-detail', args=['1'])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(json.loads(response.content.decode()), { 'id': 1, - 'config': [{'name': 'core/customslide', 'id': customslide.id}], + 'config': [{'name': 'core/customslide', 'id': customslide.id, 'uuid': element_uuid}], 'elements': [ {'name': 'core/customslide', 'context': {'id': customslide.id}}]}) @@ -39,13 +40,14 @@ class ProjectorAPI(TestCase): default_projector = Projector.objects.get(pk=1) default_projector.config = [{'name': 'invalid_slide'}] default_projector.save() + element_uuid = Projector.objects.get(pk=1).config[0]['uuid'] response = self.client.get(reverse('projector-detail', args=['1'])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(json.loads(response.content.decode()), { 'id': 1, - 'config': [{'name': 'invalid_slide'}], + 'config': [{'name': 'invalid_slide', 'uuid': element_uuid}], 'elements': [ {'name': 'invalid_slide', 'error': 'Projector element does not exist.'}]}) diff --git a/tests/unit/core/test_views.py b/tests/unit/core/test_views.py index 5b24250f8..3a03e88ae 100644 --- a/tests/unit/core/test_views.py +++ b/tests/unit/core/test_views.py @@ -83,14 +83,43 @@ class ProjectorAPI(TestCase): self.viewset.prune_elements(request=request, pk=MagicMock()) 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): mock_object.return_value.config = [{ 'name': 'test_projector_element_c6oohooxugiphuuM6Wee', - 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}] + 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo', + 'uuid': '874aaf279be346ff85a9b456ce1d1128'}] request = MagicMock() - request.data = [{ - 'name': 'test_projector_element_c6oohooxugiphuuM6Wee', - 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}] + request.data = ['874aaf279be346ff85a9b456ce1d1128'] self.viewset.request = request self.viewset.deactivate_elements(request=request, pk=MagicMock()) 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): mock_object.return_value.config = [{ 'name': 'test_projector_element_c6oohooxugiphuuM6Wee', - 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}] + 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo', + 'uuid': 'd867b2557ad041b8848e95981c5671b7'}] request = MagicMock() - request.data = [{'name': 'wrong name'}] + request.data = ['1179ea09ba2b4559a41272efb1346c86'] # Wrong UUID. self.viewset.request = request self.viewset.deactivate_elements(request=request, pk=MagicMock()) 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): mock_object.return_value.config = [{ 'name': 'test_projector_element_Au1ce9nevaeX7zo4ye2w', - 'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor'}] + 'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor', + 'uuid': '0f3b8f8df38b4bbc90f4beba9393d2db'}] request = MagicMock() request.data = 'bad_value_no_list_ohchohWee1fie0SieTha' self.viewset.request = request @@ -118,7 +149,8 @@ class ProjectorAPI(TestCase): def test_deactivate_elements_bad_list(self, mock_object): mock_object.return_value.config = [{ 'name': 'test_projector_element_teibaeRaim1heiCh6Ohv', - 'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae'}] + 'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae', + 'uuid': '8ae42a09f585480e8b4a53194d4d1fba'}] request = MagicMock() # Value 1 is not an dictionary so we expect ValidationError. request.data = [1]