diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index d9ec490ed..4f5e243de 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -22,6 +22,7 @@ from openslides.utils.utils import to_roman from openslides.users.models import User +# TODO: remove mptt after removing the django views and forms class Item(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, MPTTModel): """ An Agenda Item diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index 0ced4060a..c00443b8a 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -61,10 +61,4 @@ class ItemSerializer(ModelSerializer): 'speaker_set', 'speaker_list_closed', 'content_object', - 'weight', - 'lft', - 'rght', - 'tree_id', - 'level', - 'parent', 'tags',) diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 1d0632985..bca70602f 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -1,6 +1,7 @@ # TODO: Rename all views and template names from cgi import escape +from collections import defaultdict from datetime import datetime, timedelta from json import dumps @@ -25,7 +26,7 @@ from openslides.projector.api import ( get_overlays) from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet -from openslides.utils.rest_api import ModelViewSet +from openslides.utils.rest_api import ModelViewSet, list_route, Response from openslides.utils.utils import html_strong from openslides.utils.views import ( AjaxMixin, @@ -811,3 +812,74 @@ class ItemViewSet(ModelViewSet): if not self.request.user.has_perm('agenda.can_see_orga_items'): queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM) return queryset + + @list_route(methods=['get', 'put']) + def tree(self, request): + """ + Returns or sets the agenda tree. + """ + if request.method == 'PUT': + if not (request.user.has_perm('agenda.can_manage') and + request.user.has_perm('agenda.can_see_orga_items')): + self.permission_denied(request) + return self.set_tree(request.data['tree']) + return self.get_tree() + + def get_tree(self): + """ + Returns the agenda tree. + """ + item_list = Item.objects.order_by('weight') + + # Index the items to get the children for each item + item_children = defaultdict(list) + for item in item_list: + if item.parent: + item_children[item.parent_id].append(item) + + def get_children(item): + """ + Returns a list with all the children for item. + + Returns an empty list if item has no children. + """ + return [dict(id=child.pk, children=get_children(child)) + for child in item_children[item.pk]] + + return Response(dict(id=item.pk, children=get_children(item)) + for item in item_list if not item.parent) + + def set_tree(self, tree): + """ + Sets the agenda tree. + + The tree has to be a nested object. For example: + [{"id": 1}, {"id": 2, "children": [{"id": 3}]}] + """ + + def walk_items(tree, parent=None): + """ + Generator that returns each item in the tree as tuple. + + This tuples have tree values. The item id, the item parent and the + weight of the item. + """ + for weight, element in enumerate(tree): + yield (element['id'], parent, weight) + yield from walk_items(element.get('children', []), element['id']) + + touched_items = set() + for item_pk, parent_pk, weight in walk_items(tree): + # Check that the item is only once in the tree to prevent invalid trees + if item_pk in touched_items: + detail = "Item %d is more then once in the tree" % item_pk + break + touched_items.add(item_pk) + + Item.objects.filter(pk=item_pk).update( + parent_id=parent_pk, + weight=weight) + else: + # Everithing is fine. Return a response with status_code 200 an no content + return Response() + return Response({'detail': detail}, status=400) diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index e5663da14..4ba2da617 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -14,6 +14,7 @@ from rest_framework.serializers import ( # noqa from rest_framework.response import Response # noqa from rest_framework.routers import DefaultRouter from rest_framework.viewsets import ModelViewSet, ViewSet # noqa +from rest_framework.decorators import list_route # noqa from .exceptions import OpenSlidesError diff --git a/tests/integration/agenda/__init__.py b/tests/integration/agenda/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/agenda/test_views.py b/tests/integration/agenda/test_views.py new file mode 100644 index 000000000..7916cb819 --- /dev/null +++ b/tests/integration/agenda/test_views.py @@ -0,0 +1,70 @@ +import json +from rest_framework.test import APIClient + +from openslides.utils.test import TestCase +from openslides.agenda.models import Item + + +class AgendaTreeTest(TestCase): + def setUp(self): + Item.objects.create(title='item1') + item2 = Item.objects.create(title='item2') + Item.objects.create(title='item2a', parent=item2) + self.client = APIClient() + self.client.login(username='admin', password='admin') + + def test_get(self): + response = self.client.get('/rest/agenda/item/tree/') + + self.assertEqual(json.loads(response.content.decode()), + [{'children': [], 'id': 1}, + {'children': [{'children': [], 'id': 3}], 'id': 2}]) + + def test_set(self): + tree = [{'id': 3}, + {'children': [{'id': 1}], 'id': 2}] + + response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') + + self.assertEqual(response.status_code, 200) + + item1 = Item.objects.get(pk=1) + item2 = Item.objects.get(pk=2) + item3 = Item.objects.get(pk=3) + self.assertEqual(item1.parent_id, 2) + self.assertEqual(item1.weight, 0) + self.assertEqual(item2.parent_id, None) + self.assertEqual(item2.weight, 1) + self.assertEqual(item3.parent_id, None) + self.assertEqual(item3.weight, 0) + + def test_set_without_perm(self): + self.client = APIClient() + + response = self.client.put('/rest/agenda/item/tree/', {'tree': []}, format='json') + + self.assertEqual(response.status_code, 403) + + def test_tree_with_double_item(self): + """ + Test to send a tree that has an item-pk more then once in it. + + It is expected, that the responsecode 400 is returned with a specific + content + """ + tree = [{'id': 1}, {'id': 1}] + + response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data, {'detail': "Item 1 is more then once in the tree"}) + + def test_tree_with_empty_children(self): + """ + Test that the chrildren element is not required in the tree + """ + tree = [{'id': 1}] + + response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') + + self.assertEqual(response.status_code, 200)