diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py
index 035e0ad3f..288c32713 100644
--- a/openslides/agenda/forms.py
+++ b/openslides/agenda/forms.py
@@ -16,7 +16,6 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
"""
Form to create of update an item.
"""
-
clean_html_fields = ('text', )
parent = TreeNodeChoiceField(
@@ -32,7 +31,7 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
class Meta:
model = Item
- exclude = ('closed', 'weight', 'content_type', 'object_id')
+ exclude = ('closed', 'weight', 'content_type', 'object_id', 'item_number')
class RelatedItemForm(ItemForm):
@@ -41,7 +40,7 @@ class RelatedItemForm(ItemForm):
"""
class Meta:
model = Item
- exclude = ('closed', 'type', 'weight', 'content_type', 'object_id', 'title', 'text')
+ exclude = ('closed', 'type', 'weight', 'content_type', 'object_id', 'title', 'text', 'item_number')
class ItemOrderForm(CssClassMixin, forms.Form):
diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py
index ffc90430f..348ef9944 100644
--- a/openslides/agenda/models.py
+++ b/openslides/agenda/models.py
@@ -19,6 +19,7 @@ from openslides.projector.models import SlideMixin
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.person.models import PersonField
+from openslides.utils.utils import toRoman
class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
@@ -41,6 +42,11 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
Title of the agenda item.
"""
+ item_number = models.CharField(null=True, max_length=255, verbose_name=ugettext_lazy("Number"))
+ """
+ Number of agenda item.
+ """
+
text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Text"))
"""
The optional text of the agenda item.
@@ -147,6 +153,9 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
Return the title of this item.
"""
if not self.content_object:
+ if config['agenda_enable_auto_numbering']:
+ item_no = self.item_no
+ return item_no + ' ' + self.title if item_no else self.title
return self.title
try:
return self.content_object.get_agenda_title()
@@ -284,6 +293,40 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
value = False
return value
+ @property
+ def item_no(self):
+ if config['agenda_agenda_fixed']:
+ item_no = self.item_number
+ else:
+ item_no = self.calc_item_no()
+ if item_no:
+ return '%s %s' % (config['agenda_number_prefix'], item_no)
+
+ def calc_item_no(self):
+ """
+ Returns the number of this agenda item
+ """
+ if self.type == self.AGENDA_ITEM:
+ if self.is_root_node():
+ if config['agenda_numeral_system'] == 'a':
+ return str(self._calc_sibling_no())
+ else:
+ return toRoman(self._calc_sibling_no())
+ else:
+ return '%s.%s' % (self.parent.calc_item_no(), self._calc_sibling_no())
+
+ def _calc_sibling_no(self):
+ """
+ Counts all siblings on the same level which are AGENDA_ITEMs
+ """
+ sibling_no = 0
+ prev_sibling = self.get_previous_sibling()
+ while not prev_sibling is None:
+ if prev_sibling.type == self.AGENDA_ITEM:
+ sibling_no += 1
+ prev_sibling = prev_sibling.get_previous_sibling()
+ return sibling_no + 1
+
class SpeakerManager(models.Manager):
def add(self, person, item):
diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py
index 854a6cfee..b14c69ddd 100644
--- a/openslides/agenda/signals.py
+++ b/openslides/agenda/signals.py
@@ -60,6 +60,37 @@ def setup_agenda_config(sender, **kwargs):
help_text=ugettext_lazy('[Begin speach] starts the countdown, [End speach] stops the countdown.'),
required=False))
+ agenda_enable_auto_numbering = ConfigVariable(
+ name='agenda_enable_auto_numbering',
+ default_value=False,
+ form_field=forms.BooleanField(
+ label=ugettext_lazy('Enable automatic numbering of agenda items'),
+ required=False))
+
+ agenda_number_prefix = ConfigVariable(
+ name='agenda_number_prefix',
+ default_value='',
+ form_field=forms.CharField(
+ label=ugettext_lazy('Numbering prefix for agenda items'),
+ max_length=20,
+ required=False))
+
+ agenda_numeral_system = ConfigVariable(
+ name='agenda_numeral_system',
+ default_value='a',
+ form_field=forms.ChoiceField(
+ label=ugettext_lazy('Numeral System for Top items'),
+ widget=forms.Select(),
+ choices=(
+ ('a', ugettext_lazy('Arabic')),
+ ('r', ugettext_lazy('Roman'))),
+ required=False))
+
+ agenda_agenda_fixed = ConfigVariable(
+ name='agenda_agenda_fixed',
+ default_value=False,
+ form_field=None)
+
extra_stylefiles = ['css/jquery-ui-timepicker.css']
extra_javascript = ['js/jquery/jquery-ui-timepicker-addon.min.js',
'js/jquery/jquery-ui-sliderAccess.min.js',
@@ -71,7 +102,11 @@ def setup_agenda_config(sender, **kwargs):
weight=20,
variables=(agenda_start_event_date_time,
agenda_show_last_speakers,
- agenda_couple_countdown_and_speakers),
+ agenda_couple_countdown_and_speakers,
+ agenda_enable_auto_numbering,
+ agenda_number_prefix,
+ agenda_numeral_system,
+ agenda_agenda_fixed),
extra_context={'extra_stylefiles': extra_stylefiles,
'extra_javascript': extra_javascript})
diff --git a/openslides/agenda/templates/agenda/overview.html b/openslides/agenda/templates/agenda/overview.html
index c67fb0309..148c7910a 100644
--- a/openslides/agenda/templates/agenda/overview.html
+++ b/openslides/agenda/templates/agenda/overview.html
@@ -70,6 +70,12 @@
{% else %}
{% trans 'Set start time of event' %}
{% endif %}
+ {% if perms.agenda.can_manage_agenda and agenda_enable_auto_numbering %}
+ {% trans 'Fix numbering' %}
+ {% trans 'Reset numbering' %}
+ {% endif %}
{% endif %}
diff --git a/openslides/agenda/urls.py b/openslides/agenda/urls.py
index 0e854f1c6..c311f8aad 100644
--- a/openslides/agenda/urls.py
+++ b/openslides/agenda/urls.py
@@ -40,6 +40,14 @@ urlpatterns = patterns(
views.AgendaPDF.as_view(),
name='print_agenda'),
+ url(r'^fix/$',
+ views.FixAgendaView.as_view(),
+ name='fix_agenda'),
+
+ url(r'^reset/$',
+ views.ResetAgendaView.as_view(),
+ name='reset_agenda'),
+
# List of speakers
url(r'^(?P\d+)/speaker/$',
views.SpeakerAppendView.as_view(),
diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py
index 6f41917e6..0ba12c83b 100644
--- a/openslides/agenda/views.py
+++ b/openslides/agenda/views.py
@@ -34,6 +34,7 @@ from openslides.utils.views import (
DeleteView,
FormView,
PDFView,
+ QuestionView,
RedirectView,
SingleObjectMixin,
TemplateView,
@@ -108,9 +109,14 @@ class Overview(TemplateView):
agenda_is_active = None
active_type = None
+ agenda_enable_auto_numbering = True if config['agenda_enable_auto_numbering'] else False
+ agenda_numbering_fixed = True if config['agenda_agenda_fixed'] else False
+
context.update({
'items': items,
'agenda_is_active': agenda_is_active,
+ 'agenda_enable_auto_numbering': agenda_enable_auto_numbering,
+ 'agenda_numbering_fixed': agenda_numbering_fixed,
'duration': duration,
'start': start,
'end': end,
@@ -336,6 +342,48 @@ class CreateRelatedAgendaItemView(SingleObjectMixin, RedirectView):
self.item = Item.objects.create(content_object=self.object)
+class FixAgendaView(QuestionView):
+ permission_required = 'agenda.can_manage_agenda'
+ question_url_name = 'item_overview'
+ url_name = 'item_overview'
+ question_message = ugettext_lazy('Do you really want to fix the agenda numbering?')
+ url_name_args = []
+
+ def get(self, request, *args, **kwargs):
+ self.items = Item.objects.all()
+ return super(FixAgendaView, self).get(request, *args, **kwargs)
+
+ def on_clicked_yes(self):
+ config['agenda_agenda_fixed'] = True
+ for item in self.items:
+ item.item_number = item.calc_item_no()
+ item.save()
+
+ def get_final_message(self):
+ return ugettext_lazy('The agenda has been fixed.')
+
+
+class ResetAgendaView(QuestionView):
+ permission_required = 'agenda.can_manage_agenda'
+ question_url_name = 'item_overview'
+ url_name = 'item_overview'
+ question_message = ugettext_lazy('Do you really want to reset the agenda numbering?')
+ url_name_args = []
+
+ def get(self, request, *args, **kwargs):
+ self.items = Item.objects.all()
+ return super(ResetAgendaView, self).get(request, *args, **kwargs)
+
+ def on_clicked_yes(self):
+ config['agenda_agenda_fixed'] = False
+ for item in self.items:
+ item.item_number = ''
+ item.save()
+
+ def get_final_message(self):
+ return ugettext_lazy('The agenda has been reset.')
+
+
class AgendaPDF(PDFView):
"""
Create a full agenda-PDF.
diff --git a/openslides/utils/utils.py b/openslides/utils/utils.py
index 832dbc0d7..66f8044bc 100644
--- a/openslides/utils/utils.py
+++ b/openslides/utils/utils.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import difflib
+import roman
from django.contrib.auth.models import Permission
from django.shortcuts import render_to_response
@@ -71,3 +72,14 @@ def int_or_none(var):
return int(var)
except (TypeError, ValueError):
return None
+
+
+def toRoman(number):
+ """
+ Converts an arabic number within range from 1 to 4999 to the corresponding roman number.
+ Returns None on error conditions.
+ """
+ try:
+ return roman.toRoman(number)
+ except (roman.NotIntegerError, roman.OutOfRangeError):
+ return None
diff --git a/requirements_production.txt b/requirements_production.txt
index 2745c22dd..3e2480a56 100644
--- a/requirements_production.txt
+++ b/requirements_production.txt
@@ -7,6 +7,7 @@ django-mptt>=0.6,<0.7
jsonfield>=0.9,<0.10
natsort>=3.1,<3.2
reportlab>=2.7,<2.8
+roman>=2.0,<2.1
setuptools>=2.1,<3.5
sockjs-tornado>=1.0,<1.1
tornado>=3.1,<3.3