Merge pull request #1701 from normanjaeckel/AutoNumbering

Added REST API viewpoint to number the agenda.
This commit is contained in:
Norman Jäckel 2015-11-25 13:31:17 +01:00
commit ec600c98f8
5 changed files with 102 additions and 35 deletions

View File

@ -17,6 +17,10 @@ from openslides.utils.utils import to_roman
class ItemManager(models.Manager): class ItemManager(models.Manager):
"""
Customized model manager with special methods for agenda tree and
numbering.
"""
def get_only_agenda_items(self, queryset=None): def get_only_agenda_items(self, queryset=None):
""" """
Generator, which yields only agenda items. Skips hidden items. Generator, which yields only agenda items. Skips hidden items.
@ -36,14 +40,14 @@ class ItemManager(models.Manager):
yield from yield_items(item_children[item.pk]) yield from yield_items(item_children[item.pk])
yield from yield_items(root_items) yield from yield_items(root_items)
def get_root_and_children(self, queryset=None, only_agenda_items=False): def get_root_and_children(self, only_agenda_items=False):
""" """
Returns a list with all root items and a dictonary where the key is an Returns a list with all root items and a dictonary where the key is an
item pk and the value is a list with all children of the item. item pk and the value is a list with all children of the item.
"""
if queryset is None:
queryset = self.order_by('weight')
If only_agenda_items is True, the tree hides HIDDEN_ITEM.
"""
queryset = self.order_by('weight')
item_children = defaultdict(list) item_children = defaultdict(list)
root_items = [] root_items = []
for item in queryset: for item in queryset:
@ -62,7 +66,7 @@ class ItemManager(models.Manager):
and children, where id is the id of one agenda item and children is a and children, where id is the id of one agenda item and children is a
generator that yields dictonaries like the one discribed. generator that yields dictonaries like the one discribed.
If only_agenda_items is True, the tree hides ORGANIZATIONAL_ITEMs. If only_agenda_items is True, the tree hides HIDDEN_ITEM.
If include_content is True, the yielded dictonaries have no key 'id' If include_content is True, the yielded dictonaries have no key 'id'
but a key 'item' with the entire object. but a key 'item' with the entire object.
@ -121,6 +125,26 @@ class ItemManager(models.Manager):
db_item.weight = weight db_item.weight = weight
db_item.save() db_item.save()
@transaction.atomic
def number_all(self, numeral_system='arabic'):
"""
Auto numbering of the agenda according to the numeral_system. Manually
added item numbers will be overwritten.
"""
def walk_tree(tree, number=None):
for index, tree_element in enumerate(tree):
if numeral_system == 'roman' and number is None:
item_number = to_roman(index + 1)
else:
item_number = str(index + 1)
if number is not None:
item_number = '.'.join((number, item_number))
tree_element['item'].item_number = item_number
tree_element['item'].save()
walk_tree(tree_element['children'], item_number)
walk_tree(self.get_tree(only_agenda_items=True, include_content=True))
class Item(RESTModelMixin, models.Model): class Item(RESTModelMixin, models.Model):
""" """
@ -263,34 +287,6 @@ class Item(RESTModelMixin, models.Model):
item_no = str(self.item_number) item_no = str(self.item_number)
return item_no return item_no
def calc_item_no(self):
"""
Returns the number of this agenda item.
"""
if self.type == self.AGENDA_ITEM:
if self.parent is None:
sibling_no = self.sibling_no()
if config['agenda_numeral_system'] == 'arabic':
return str(sibling_no)
else: # config['agenda_numeral_system'] == 'roman'
return to_roman(sibling_no)
else:
return '%s.%s' % (self.parent.calc_item_no(), self.sibling_no())
else:
return ''
def sibling_no(self):
"""
Counts how many AGENDA_ITEMS with the same parent (siblings) have a
smaller weight then this item.
Returns this number + 1 or 0 when self is not an AGENDA_ITEM.
"""
return Item.objects.filter(
parent=self.parent,
type=self.AGENDA_ITEM,
weight__lte=self.weight).count()
class SpeakerManager(models.Manager): class SpeakerManager(models.Manager):
""" """

View File

@ -217,6 +217,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
}; };
return typeof _.findKey(projector.elements, predicate) === 'string'; return typeof _.findKey(projector.elements, predicate) === 'string';
}; };
// auto numbering of agenda items
$scope.autoNumbering = function() {
$http.post('/rest/agenda/item/numbering/', {});
};
} }
]) ])

View File

@ -44,6 +44,13 @@
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
<translate>Project agenda</translate> <translate>Project agenda</translate>
</a> </a>
<!-- auto numbering button -->
<a ng-show="!isDeleteMode" os-perms="core.can_manage_projector"
class="btn btn-default btn-sm form-control"
ng-click="autoNumbering()">
<i class="fa fa-list-ol"></i>
<translate>Number agenda items</translate>
</a>
<!-- delete button --> <!-- delete button -->
<a ng-show="isDeleteMode && (items|filter:{selected:true}).length > 0" <a ng-show="isDeleteMode && (items|filter:{selected:true}).length > 0"
os-perms="agenda.can_manage" ng-click="delete()" os-perms="agenda.can_manage" ng-click="delete()"

View File

@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from openslides.core.config import config
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
@ -47,7 +48,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
result = (self.request.user.has_perm('agenda.can_see') and result = (self.request.user.has_perm('agenda.can_see') and
self.request.user.has_perm('agenda.can_see_hidden_items') and self.request.user.has_perm('agenda.can_see_hidden_items') and
self.request.user.has_perm('agenda.can_manage')) self.request.user.has_perm('agenda.can_manage'))
elif self.action == 'speak': elif self.action in ('speak', 'numbering'):
result = (self.request.user.has_perm('agenda.can_see') and result = (self.request.user.has_perm('agenda.can_see') and
self.request.user.has_perm('agenda.can_manage')) self.request.user.has_perm('agenda.can_manage'))
else: else:
@ -219,6 +220,15 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
response = Response(Item.objects.get_tree()) response = Response(Item.objects.get_tree())
return response return response
@list_route(methods=['post'])
def numbering(self, request):
"""
Auto numbering of the agenda according to the config. Manually added
item numbers will be overwritten.
"""
Item.objects.number_all(numeral_system=config['agenda_numeral_system'])
return Response({'detail': _('The agenda has been numbered.')})
# Views to generate PDFs # Views to generate PDFs

View File

@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.agenda.models import Speaker from openslides.agenda.models import Item, Speaker
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import CustomSlide, Projector from openslides.core.models import CustomSlide, Projector
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -233,3 +233,53 @@ class Speak(TestCase):
else: else:
success = False success = False
self.assertTrue(success) self.assertTrue(success)
class Numbering(TestCase):
"""
Tests view to number the agenda
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.item_1 = CustomSlide.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item
self.item_2 = CustomSlide.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item
self.item_2.weight = 2
self.item_2.save()
self.item_2_1 = CustomSlide.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item
self.item_2_1.parent = self.item_2
self.item_2_1.save()
self.item_3 = CustomSlide.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item
self.item_3.weight = 3
self.item_3.save()
def test_numbering(self):
response = self.client.post(reverse('item-numbering'))
self.assertEqual(response.status_code, 200)
self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, '1')
self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, '2')
self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '2.1')
self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '3')
def test_roman_numbering(self):
config['agenda_numeral_system'] = 'roman'
response = self.client.post(reverse('item-numbering'))
self.assertEqual(response.status_code, 200)
self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, 'I')
self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, 'II')
self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, 'II.1')
self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, 'III')
def test_with_hidden_item(self):
self.item_2.type = Item.HIDDEN_ITEM
self.item_2.save()
response = self.client.post(reverse('item-numbering'))
self.assertEqual(response.status_code, 200)
self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, '1')
self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, '')
self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '')
self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '2')