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.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):
|
||||||
|
@ -68,4 +68,6 @@ class ItemSerializer(ModelSerializer):
|
|||||||
'speakers',
|
'speakers',
|
||||||
'speaker_list_closed',
|
'speaker_list_closed',
|
||||||
'content_object',
|
'content_object',
|
||||||
'tags',)
|
'tags',
|
||||||
|
'weight',
|
||||||
|
'parent',)
|
||||||
|
@ -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});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -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">–</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">–</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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user