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