Merge pull request #1701 from normanjaeckel/AutoNumbering
Added REST API viewpoint to number the agenda.
This commit is contained in:
commit
ec600c98f8
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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/', {});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -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()"
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user