Merge pull request #1624 from ostcar/agendaTree

Calculate agenda tree on the client side.
This commit is contained in:
Oskar Hahn 2015-09-06 15:04:38 +02:00
commit f0803f1c03
6 changed files with 133 additions and 72 deletions

View File

@ -5,7 +5,7 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError 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 as _
from django.utils.translation import ugettext_lazy, ugettext_noop 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)) yield from get_children(filter(lambda item: item.parent is None, item_queryset))
@transaction.atomic
def set_tree(self, tree): def set_tree(self, tree):
""" """
Sets the agenda tree. Sets the agenda tree.
@ -71,15 +72,25 @@ class ItemManager(models.Manager):
yield from walk_items(element.get('children', []), element['id']) yield from walk_items(element.get('children', []), element['id'])
touched_items = set() 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 # Check that the item is only once in the tree to prevent invalid trees
if item_pk in touched_items: if item_id in touched_items:
raise ValueError("Item %d is more then once in the tree" % item_pk) raise ValueError("Item {} is more then once in the tree.".format(item_id))
touched_items.add(item_pk) touched_items.add(item_id)
Item.objects.filter(pk=item_pk).update( try:
parent_id=parent_pk, db_item = db_items[item_id]
weight=weight) 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): class Item(RESTModelMixin, models.Model):

View File

@ -68,4 +68,6 @@ class ItemSerializer(ModelSerializer):
'speakers', 'speakers',
'speaker_list_closed', 'speaker_list_closed',
'content_object', 'content_object',
'tags',) 'tags',
'weight',
'parent',)

View File

@ -75,9 +75,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
resolve: { resolve: {
items: function(Agenda) { items: function(Agenda) {
return Agenda.findAll(); 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: { resolve: {
items: function(Agenda) { items: function(Agenda) {
return Agenda.findAll(); return Agenda.findAll();
},
tree: function($http) {
return $http.get('/rest/agenda/item/tree/');
} }
}, },
url: '/sort', url: '/sort',
@ -134,31 +128,76 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}); });
}) })
.controller('ItemListCtrl', function($scope, $http, Agenda, tree, Projector) { .factory('AgendaTree', [
Agenda.bindAll({}, $scope, 'items'); function () {
return {
getTree: function (items) {
// sortieren nach weight???
// get a 'flat' (ordered) array of agenda tree to display in table // Build a dict with all children (dict-value) to a specific
$scope.flattenedTree = buildTree(tree.data); // item id (dict-key).
function buildTree(tree, level) { var itemChildren = {};
var level = level || 0
var nodes = []; _.each(items, function (item) {
var defaultlevel = level; if (item.parent_id) {
_.each(tree, function(node) { // Add item to his parent. If it is the first child, then
level = defaultlevel; // create a new list.
if (node.id) { try {
nodes.push({ id: node.id, level: level }); itemChildren[item.parent_id].push(item);
} } catch (error) {
if (node.children) { itemChildren[item.parent_id] = [item];
level++;
var child = buildTree(node.children, level);
if (child.length) {
nodes = nodes.concat(child);
} }
} }
}); });
return nodes;
// 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;
} }
// 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 // save changed item
$scope.save = function (item) { $scope.save = function (item) {
Agenda.save(item); Agenda.save(item);
@ -269,14 +308,18 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}; };
}) })
.controller('AgendaSortCtrl', function($scope, $http, Agenda, tree) { .controller('AgendaSortCtrl', function($scope, $http, Agenda, AgendaTree) {
Agenda.bindAll({}, $scope, 'items'); // Bind agenda tree to the scope
$scope.tree = tree.data; $scope.$watch(function () {
return Agenda.lastModified();
}, function () {
$scope.items = AgendaTree.getTree(Agenda.getAll());
});
// set changed agenda tree // set changed agenda tree
$scope.treeOptions = { $scope.treeOptions = {
dropped: function(e) { dropped: function() {
$http.put('/rest/agenda/item/tree/', {tree: $scope.tree}); $http.put('/rest/agenda/item/tree/', {tree: $scope.items});
} }
}; };
}) })

View File

@ -52,42 +52,44 @@
<th os-perms="agenda.can_manage core.can_manage_projector" class="minimum"> <th os-perms="agenda.can_manage core.can_manage_projector" class="minimum">
<translate>Actions</translate> <translate>Actions</translate>
<tbody> <tbody>
<tr ng-repeat="node in flattenedTree | filter: filter.search" <tr ng-repeat="item in items"
ng-class="{ 'activeline': (items | filter: {id: node.id})[0].isProjected() }"> ng-class="{ 'activeline': item.isProjected() }">
<td><input type="checkbox" ng-model="(items | filter: {id: node.id})[0].closed" ng-click="save(node.id)">
<td> <td>
<span ng-repeat="n in [].constructor(node.level) track by $index">&ndash;</span> <!-- Please do not use ng-click, but ng-change -->
<a ui-sref="agenda.item.detail({id: node.id})"> <input type="checkbox" ng-model="item.closed" ng-click="save(item.id)">
{{ (items | filter: {id: node.id})[0].item_number }} <td>
{{ (items | filter: {id: node.id})[0].title }} <span ng-repeat="n in [].constructor(item.parentCount) track by $index">&ndash;</span>
<a ui-sref="agenda.item.detail({id: item.id})">
{{ item.item_number }}
{{ item.title }}
</a> </a>
<div ng-if="(items | filter: {id: node.id})[0].comment"> <div ng-if="item.comment">
<small><i class="fa fa-info-circle"></i> {{ (items | filter: {id: node.id})[0].comment }}</small> <small><i class="fa fa-info-circle"></i> {{ item.comment }}</small>
</div> </div>
<td os-perms="agenda.can_manage" class="optional"> <td os-perms="agenda.can_manage" class="optional">
<a href="#" editable-number="(items | filter: {id: node.id})[0].duration" e-min="1" onaftersave="save(node.id)"> <a href="#" editable-number="item.duration" e-min="1" onaftersave="save(item.id)">
{{ (items | filter: {id: node.id})[0].duration }} {{ item.duration }}
</a> </a>
<span ng-if="(items | filter: {id: node.id})[0].duration" translate>min</span> <span ng-if="item.duration" translate>min</span>
<td os-perms="agenda.can_manage core.can_manage_projector" class="nobr"> <td os-perms="agenda.can_manage core.can_manage_projector" class="nobr">
<!-- project --> <!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': (items | filter: {id: node.id})[0].isProjected() }" ng-class="{ 'btn-primary': item.isProjected() }"
ng-click="(items | filter: {id: node.id})[0].project()" ng-click="item.project()"
title="{{ 'Project item' | translate }}"> title="{{ 'Project item' | translate }}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</a> </a>
<!-- edit --> <!-- edit -->
<a ui-sref="agenda.item.detail.update({id: node.id })" os-perms="agenda.can_manage" <a ui-sref="agenda.item.detail.update({id: item.id })" os-perms="agenda.can_manage"
class="btn btn-default btn-sm" class="btn btn-default btn-sm"
title="{{ 'Edit' | translate}}"> title="{{ 'Edit' | translate }}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</a> </a>
<!-- delete --> <!-- delete -->
<a os-perms="agenda.can_manage" class="btn btn-danger btn-sm" <a os-perms="agenda.can_manage" class="btn btn-danger btn-sm"
ng-bootbox-confirm="Are you sure you want to delete ng-bootbox-confirm="Are you sure you want to delete
<b>{{ (items | filter: {id: node.id})[0].title }}</b>?" <b>{{ item.title }}</b>?"
ng-bootbox-confirm-action="delete(node.id)" ng-bootbox-confirm-action="delete(item.id)"
title="{{ 'Delete' | translate }}"> title="{{ 'Delete' | translate }}">
<i class="fa fa-trash-o"></i> <i class="fa fa-trash-o"></i>
</a> </a>

View File

@ -9,19 +9,21 @@
<p translate>Drag and drop items to change the order of the agenda. Your modification will be saved directly.</p> <p translate>Drag and drop items to change the order of the agenda. Your modification will be saved directly.</p>
<div ui-tree callbacks="treeOptions">
<ol ui-tree-nodes="" ng-model="items" id="tree-root">
<li ng-repeat="item in items" ui-tree-node ng-include="'nodes_renderer.html'">
</ol>
</div>
<!-- Nested node template --> <!-- Nested node template -->
<script type="text/ng-template" id="nodes_renderer.html"> <script type="text/ng-template" id="nodes_renderer.html">
<div ui-tree-handle> <div ui-tree-handle>
{{ (items | filter: {id: node.id})[0].item_number }} {{ item.item.item_number }}
{{ (items | filter: {id: node.id})[0].title }} {{ item.item.title }}
</div> </div>
<ol ui-tree-nodes="" ng-model="node.children"> <ol ui-tree-nodes="" ng-model="item.children">
<li ng-repeat="node in node.children" ui-tree-node ng-include="'nodes_renderer.html'"> <li ng-repeat="item in item.children" ui-tree-node ng-include="'nodes_renderer.html'">
</ol> </ol>
</script> </script>
<div ui-tree callbacks="treeOptions">
<ol ui-tree-nodes="" ng-model="tree" id="tree-root">
<li ng-repeat="node in tree" ui-tree-node ng-include="'nodes_renderer.html'">
</ol>
</div>

View File

@ -58,7 +58,7 @@ class AgendaTreeTest(TestCase):
response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json')
self.assertEqual(response.status_code, 400) 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): def test_tree_with_empty_children(self):
""" """
@ -72,13 +72,14 @@ class AgendaTreeTest(TestCase):
def test_tree_with_unknown_item(self): def test_tree_with_unknown_item(self):
""" """
Tests that unknown items are ignored. Tests that unknown items lead to an error.
""" """
tree = [{'id': 500}] tree = [{'id': 500}]
response = self.client.put('/rest/agenda/item/tree/', {'tree': tree}, format='json') 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): class TestAgendaPDF(TestCase):