Merge pull request #1624 from ostcar/agendaTree
Calculate agenda tree on the client side.
This commit is contained in:
commit
f0803f1c03
@ -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):
|
||||
|
@ -68,4 +68,6 @@ class ItemSerializer(ModelSerializer):
|
||||
'speakers',
|
||||
'speaker_list_closed',
|
||||
'content_object',
|
||||
'tags',)
|
||||
'tags',
|
||||
'weight',
|
||||
'parent',)
|
||||
|
@ -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) {
|
||||
|
@ -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">–</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">–</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>
|
||||
|
@ -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>
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user