From e22b9984e34384d32d897144405b052cdb873bf7 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Sun, 6 Sep 2015 14:02:15 +0200 Subject: [PATCH] Calculate agenda tree on the client side. Update changed agenda tree via autoupdate. --- openslides/agenda/models.py | 27 +++-- openslides/agenda/serializers.py | 4 +- openslides/agenda/static/js/agenda/agenda.js | 109 ++++++++++++------ .../static/templates/agenda/item-list.html | 38 +++--- .../static/templates/agenda/item-sort.html | 20 ++-- tests/integration/agenda/test_views.py | 7 +- 6 files changed, 133 insertions(+), 72 deletions(-) diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index e407f9193..f65d595e1 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db import models +from django.db import models, transaction from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop @@ -52,6 +52,7 @@ class ItemManager(models.Manager): yield from get_children(filter(lambda item: item.parent is None, item_queryset)) + @transaction.atomic def set_tree(self, tree): """ Sets the agenda tree. @@ -71,15 +72,25 @@ class ItemManager(models.Manager): yield from walk_items(element.get('children', []), element['id']) touched_items = set() - for item_pk, parent_pk, weight in walk_items(tree): + db_items = dict((item.pk, item) for item in Item.objects.all()) + for item_id, parent_id, 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) + if item_id in touched_items: + raise ValueError("Item {} is more then once in the tree.".format(item_id)) + touched_items.add(item_id) - Item.objects.filter(pk=item_pk).update( - parent_id=parent_pk, - weight=weight) + try: + db_item = db_items[item_id] + except KeyError: + raise ValueError("Item {} is not in the database.".format(item_id)) + + # Check if the item has changed and update it + # Note: Do not use Item.objects.update, so that the items are sent + # to the clients via autoupdate + if db_item.parent_id != parent_id or db_item.weight != weight: + db_item.parent_id = parent_id + db_item.weight = weight + db_item.save() class Item(RESTModelMixin, models.Model): diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index 611a10520..919be9416 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -68,4 +68,6 @@ class ItemSerializer(ModelSerializer): 'speakers', 'speaker_list_closed', 'content_object', - 'tags',) + 'tags', + 'weight', + 'parent',) diff --git a/openslides/agenda/static/js/agenda/agenda.js b/openslides/agenda/static/js/agenda/agenda.js index 7934cbd80..e9b33b1bb 100644 --- a/openslides/agenda/static/js/agenda/agenda.js +++ b/openslides/agenda/static/js/agenda/agenda.js @@ -75,9 +75,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) resolve: { items: function(Agenda) { return Agenda.findAll(); - }, - tree: function($http) { - return $http.get('/rest/agenda/item/tree/'); } } }) @@ -120,9 +117,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) resolve: { items: function(Agenda) { return Agenda.findAll(); - }, - tree: function($http) { - return $http.get('/rest/agenda/item/tree/'); } }, url: '/sort', @@ -134,30 +128,75 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) }); }) -.controller('ItemListCtrl', function($scope, $http, Agenda, tree, Projector) { - Agenda.bindAll({}, $scope, 'items'); +.factory('AgendaTree', [ + function () { + return { + getTree: function (items) { + // sortieren nach weight??? - // get a 'flat' (ordered) array of agenda tree to display in table - $scope.flattenedTree = buildTree(tree.data); - function buildTree(tree, level) { - var level = level || 0 - var nodes = []; - var defaultlevel = level; - _.each(tree, function(node) { - level = defaultlevel; - if (node.id) { - nodes.push({ id: node.id, level: level }); - } - if (node.children) { - level++; - var child = buildTree(node.children, level); - if (child.length) { - nodes = nodes.concat(child); + // Build a dict with all children (dict-value) to a specific + // item id (dict-key). + var itemChildren = {}; + + _.each(items, function (item) { + if (item.parent_id) { + // Add item to his parent. If it is the first child, then + // create a new list. + try { + itemChildren[item.parent_id].push(item); + } catch (error) { + itemChildren[item.parent_id] = [item]; + } + } + + }); + + // Recursive function that generates a nested list with all + // items with there children + function getChildren(items) { + var returnItems = []; + _.each(items, function (item) { + returnItems.push({ + item: item, + children: getChildren(itemChildren[item.id]), + id: item.id, + }); + }); + return returnItems; } - } - }); - return nodes; + + // Generates the list of root items (with no parents) + var parentItems = items.filter(function (item) { + return !item.parent_id; + }); + return getChildren(parentItems); + }, + // Returns a list of all items as a flat tree the attribute parentCount + getFlatTree: function(items) { + var tree = this.getTree(items); + var flatItems = []; + + function generateFatTree(tree, parentCount) { + _.each(tree, function (item) { + item.item.parentCount = parentCount; + flatItems.push(item.item); + generateFatTree(item.children, parentCount + 1); + }); + } + generateFatTree(tree, 0); + return flatItems; + }, + } } +]) + +.controller('ItemListCtrl', function($scope, $http, Agenda, Projector, AgendaTree) { + // Bind agenda tree to the scope + $scope.$watch(function () { + return Agenda.lastModified(); + }, function () { + $scope.items = AgendaTree.getFlatTree(Agenda.getAll()); + }); // save changed item $scope.save = function (item) { @@ -269,16 +308,20 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) }; }) -.controller('AgendaSortCtrl', function($scope, $http, Agenda, tree) { - Agenda.bindAll({}, $scope, 'items'); - $scope.tree = tree.data; +.controller('AgendaSortCtrl', function($scope, $http, Agenda, AgendaTree) { + // Bind agenda tree to the scope + $scope.$watch(function () { + return Agenda.lastModified(); + }, function () { + $scope.items = AgendaTree.getTree(Agenda.getAll()); + }); // set changed agenda tree $scope.treeOptions = { - dropped: function(e) { - $http.put('/rest/agenda/item/tree/', {tree: $scope.tree}); + dropped: function() { + $http.put('/rest/agenda/item/tree/', {tree: $scope.items}); } - }; + }; }) .controller('AgendaImportCtrl', function($scope, $state, Agenda) { diff --git a/openslides/agenda/static/templates/agenda/item-list.html b/openslides/agenda/static/templates/agenda/item-list.html index 9eda8b96c..0fbc07f9d 100644 --- a/openslides/agenda/static/templates/agenda/item-list.html +++ b/openslides/agenda/static/templates/agenda/item-list.html @@ -52,42 +52,44 @@ Actions - - + - - - {{ (items | filter: {id: node.id})[0].item_number }} - {{ (items | filter: {id: node.id})[0].title }} + + + + + + {{ item.item_number }} + {{ item.title }} -
- {{ (items | filter: {id: node.id})[0].comment }} +
+ {{ item.comment }}
- - {{ (items | filter: {id: node.id})[0].duration }} + + {{ item.duration }} - min + min - + title="{{ 'Edit' | translate }}"> {{ item.title }}?" + ng-bootbox-confirm-action="delete(item.id)" title="{{ 'Delete' | translate }}"> diff --git a/openslides/agenda/static/templates/agenda/item-sort.html b/openslides/agenda/static/templates/agenda/item-sort.html index 934c047d4..aec04f62d 100644 --- a/openslides/agenda/static/templates/agenda/item-sort.html +++ b/openslides/agenda/static/templates/agenda/item-sort.html @@ -9,19 +9,21 @@

Drag and drop items to change the order of the agenda. Your modification will be saved directly.

+
+
    +
  1. +
+
+ + -
-
    -
  1. -
-
diff --git a/tests/integration/agenda/test_views.py b/tests/integration/agenda/test_views.py index 4120136dc..122b64cbc 100644 --- a/tests/integration/agenda/test_views.py +++ b/tests/integration/agenda/test_views.py @@ -58,7 +58,7 @@ class AgendaTreeTest(TestCase): 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"}) + self.assertEqual(response.data, {'detail': "Item 1 is more then once in the tree."}) def test_tree_with_empty_children(self): """ @@ -72,13 +72,14 @@ class AgendaTreeTest(TestCase): def test_tree_with_unknown_item(self): """ - Tests that unknown items are ignored. + Tests that unknown items lead to an error. """ tree = [{'id': 500}] response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data, {'detail': "Item 500 is not in the database."}) class TestAgendaPDF(TestCase):