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.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):

View File

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

View File

@ -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) {

View File

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