diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 0000866e5..1e93d86fb 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -1,3 +1,4 @@ +from collections import defaultdict from datetime import datetime from django.contrib.auth.models import AnonymousUser @@ -17,10 +18,75 @@ from openslides.utils.rest_api import RESTModelMixin from openslides.utils.utils import to_roman +class ItemManager(models.Manager): + def get_tree(self, only_agenda_items=False, include_content=False): + """ + Generator that yields dictonaries. Each dictonary has two keys, id + and children, where id is the id of one agenda item and children is a + generator that yields dictonaries like the one discribed. + + If only_agenda_items is True, the tree hides ORGANIZATIONAL_ITEMs. + + If include_content is True, the yielded dictonaries have no key 'id' + but a key 'item' with the entire object. + """ + item_queryset = self.order_by('weight') + if only_agenda_items: + item_queryset = item_queryset.filter(type__exact=Item.AGENDA_ITEM) + + # Index the items to get the children for each item + item_children = defaultdict(list) + for item in item_queryset: + if item.parent: + item_children[item.parent_id].append(item) + + def get_children(items): + """ + Generator that yields the descibed diconaries. + """ + for item in items: + if include_content: + yield dict(item=item, children=get_children(item_children[item.pk])) + else: + yield dict(id=item.pk, children=get_children(item_children[item.pk])) + + yield from get_children(filter(lambda item: item.parent is None, item_queryset)) + + 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: + raise ValueError("Item %d is more then once in the tree" % item_pk) + touched_items.add(item_pk) + + Item.objects.filter(pk=item_pk).update( + parent_id=parent_pk, + weight=weight) + + class Item(RESTModelMixin, SlideMixin, models.Model): """ An Agenda Item """ + objects = ItemManager() slide_callback_name = 'agenda' AGENDA_ITEM = 1 diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 0cdf00d40..a4d1b7ce1 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -1,5 +1,4 @@ from cgi import escape -from collections import defaultdict from django.contrib.auth import get_user_model from django.utils.translation import ugettext as _ @@ -30,10 +29,20 @@ class AgendaPDF(PDFView): document_title = ugettext_lazy('Agenda') def append_to_pdf(self, story): - for item in Item.objects.filter(type__exact=Item.AGENDA_ITEM): - ancestors = item.get_ancestors() + tree = Item.objects.get_tree(only_agenda_items=True, include_content=True) + + def walk_tree(tree, ancestors=0): + """ + Generator that yields a two-element-tuple. The first element is an + agenda-item and the second a number for steps to the root element. + """ + for element in tree: + yield element['item'], ancestors + yield from walk_tree(element['children'], ancestors + 1) + + for item, ancestors in walk_tree(tree): if ancestors: - space = " " * 6 * ancestors.count() + space = " " * 6 * ancestors story.append(Paragraph( "%s%s" % (space, escape(item.get_title())), stylesheet['Subitem'])) @@ -218,64 +227,10 @@ class ItemViewSet(ModelViewSet): 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) + try: + Item.objects.set_tree(request.data['tree']) + except ValueError as error: + return Response({'detail': str(error)}, status=400) + else: + return Response({'detail': 'Agenda tree successfully updated.'}) + return Response(Item.objects.get_tree()) diff --git a/tests/integration/agenda/test_views.py b/tests/integration/agenda/test_views.py index 79e71eb9d..4120136dc 100644 --- a/tests/integration/agenda/test_views.py +++ b/tests/integration/agenda/test_views.py @@ -69,3 +69,26 @@ class AgendaTreeTest(TestCase): response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') self.assertEqual(response.status_code, 200) + + def test_tree_with_unknown_item(self): + """ + Tests that unknown items are ignored. + """ + tree = [{'id': 500}] + + response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') + + self.assertEqual(response.status_code, 200) + + +class TestAgendaPDF(TestCase): + def test_get(self): + """ + Tests that a requst on the pdf-page returns with statuscode 200. + """ + Item.objects.create(title='item1') + self.client.login(username='admin', password='admin') + + response = self.client.get('/agenda/print/') + + self.assertEqual(response.status_code, 200)