From 5254cc83a6b005e1afe7259832640c928a6af40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Mon, 28 Apr 2014 01:03:10 +0200 Subject: [PATCH] Updated numbering feature. Fixed organizational item structuring. Prevented organizational items from having agenda items as descendents. Some coding style changes. Added CHANGELOG and README entries. --- CHANGELOG | 2 ++ README.rst | 2 ++ openslides/agenda/forms.py | 4 ++-- openslides/agenda/models.py | 44 ++++++++++++++++++++++++------------ openslides/agenda/signals.py | 11 ++++----- openslides/agenda/views.py | 27 +++++++++------------- tests/agenda/tests.py | 40 ++++++++++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e387a527a..11b2ed524 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,8 @@ Version 1.6 (unreleased) Agenda: - New projector view with the current list of speakers. - Added CSV import. +- Added automatic numbering of agenda items. +- Fixed organizational item structuring. Assignment: - Coupled assignment candidates with list of speakers. Dashboard: diff --git a/README.rst b/README.rst index b54c3446d..105cfc79a 100644 --- a/README.rst +++ b/README.rst @@ -272,6 +272,8 @@ OpenSlides uses the following projects or parts of them: * `ReportLab `_, License: BSD +* `roman `_, License: Python 2.1.1 + * `sockjs-client `_, License: MIT diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py index 288c32713..839cfeed6 100644 --- a/openslides/agenda/forms.py +++ b/openslides/agenda/forms.py @@ -31,7 +31,7 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm): class Meta: model = Item - exclude = ('closed', 'weight', 'content_type', 'object_id', 'item_number') + fields = ('item_number', 'title', 'text', 'comment', 'type', 'duration', 'parent', 'speaker_list_closed') class RelatedItemForm(ItemForm): @@ -40,7 +40,7 @@ class RelatedItemForm(ItemForm): """ class Meta: model = Item - exclude = ('closed', 'type', 'weight', 'content_type', 'object_id', 'title', 'text', 'item_number') + fields = ('comment', 'duration', 'parent', 'speaker_list_closed') class ItemOrderForm(CssClassMixin, forms.Form): diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 348ef9944..b6857e0c0 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -5,6 +5,7 @@ from datetime import datetime from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.utils.translation import ugettext as _ @@ -37,16 +38,16 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): (AGENDA_ITEM, ugettext_lazy('Agenda item')), (ORGANIZATIONAL_ITEM, ugettext_lazy('Organizational item'))) + item_number = models.CharField(blank=True, max_length=255, verbose_name=ugettext_lazy("Number")) + """ + Number of agenda item. + """ + title = models.CharField(null=True, max_length=255, verbose_name=ugettext_lazy("Title")) """ 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. @@ -122,6 +123,16 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): if self.parent and self.parent.is_active_slide(): update_projector() + def clean(self): + """ + Ensures that the children of orga items are only orga items. + """ + if self.type == self.AGENDA_ITEM and self.parent is not None and self.parent.type == self.ORGANIZATIONAL_ITEM: + raise ValidationError(_('Agenda items can not be descendents of an organizational item.')) + if self.type == self.ORGANIZATIONAL_ITEM and self.get_descendants().filter(type=self.AGENDA_ITEM).exists(): + raise ValidationError(_('Organizational items can not have agenda items as descendents.')) + return super(Item, self).clean() + def __unicode__(self): return self.get_title() @@ -155,7 +166,7 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): 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 '%s %s' % (item_no, self.title) if item_no else self.title return self.title try: return self.content_object.get_agenda_title() @@ -296,32 +307,35 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): @property def item_no(self): if config['agenda_agenda_fixed']: - item_no = self.item_number + item_number = self.item_number else: - item_no = self.calc_item_no() - if item_no: - return '%s %s' % (config['agenda_number_prefix'], item_no) + item_number = self.calc_item_no() + if item_number: + item_no = '%s %s' % (config['agenda_number_prefix'], item_number) + else: + item_no = None + return item_no def calc_item_no(self): """ - Returns the number of this agenda item + Returns the number of this agenda item. """ if self.type == self.AGENDA_ITEM: if self.is_root_node(): - if config['agenda_numeral_system'] == 'a': + if config['agenda_numeral_system'] == 'arabic': return str(self._calc_sibling_no()) - else: + else: # config['agenda_numeral_system'] == 'roman' 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 + 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: + while prev_sibling is not None: if prev_sibling.type == self.AGENDA_ITEM: sibling_no += 1 prev_sibling = prev_sibling.get_previous_sibling() diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index b14c69ddd..d5c14cabb 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -77,19 +77,18 @@ def setup_agenda_config(sender, **kwargs): agenda_numeral_system = ConfigVariable( name='agenda_numeral_system', - default_value='a', + default_value='arabic', form_field=forms.ChoiceField( - label=ugettext_lazy('Numeral System for Top items'), + label=ugettext_lazy('Numeral system for agenda items'), widget=forms.Select(), choices=( - ('a', ugettext_lazy('Arabic')), - ('r', ugettext_lazy('Roman'))), + ('arabic', ugettext_lazy('Arabic')), + ('roman', ugettext_lazy('Roman'))), required=False)) agenda_agenda_fixed = ConfigVariable( name='agenda_agenda_fixed', - default_value=False, - form_field=None) + default_value=False) extra_stylefiles = ['css/jquery-ui-timepicker.css'] extra_javascript = ['js/jquery/jquery-ui-timepicker-addon.min.js', diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 0ba12c83b..8a708bcec 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -109,14 +109,11 @@ 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, + 'agenda_enable_auto_numbering': config['agenda_enable_auto_numbering'], + 'agenda_numbering_fixed': config['agenda_agenda_fixed'], 'duration': duration, 'start': start, 'end': end, @@ -140,8 +137,14 @@ class Overview(TemplateView): parent = Item.objects.get(id=form.cleaned_data['parent']) except Item.DoesNotExist: parent = None - item.weight = form.cleaned_data['weight'] + else: + if item.type == item.AGENDA_ITEM and parent.type == item.ORGANIZATIONAL_ITEM: + transaction.rollback() + messages.error( + request, _('Agenda items can not be descendents of an organizational item.')) + break item.parent = parent + item.weight = form.cleaned_data['weight'] Model.save(item) else: transaction.rollback() @@ -349,13 +352,9 @@ class FixAgendaView(QuestionView): 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: + for item in Item.objects.all(): item.item_number = item.calc_item_no() item.save() @@ -370,13 +369,9 @@ class ResetAgendaView(QuestionView): 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: + for item in Item.objects.all(): item.item_number = '' item.save() diff --git a/tests/agenda/tests.py b/tests/agenda/tests.py index 48c868a28..cec6a6351 100644 --- a/tests/agenda/tests.py +++ b/tests/agenda/tests.py @@ -229,6 +229,20 @@ class ViewTest(TestCase): self.assertIsNone(Item.objects.get(pk=1).parent) self.assertEqual(Item.objects.get(pk=2).parent_id, 1) + def test_change_item_order_with_orga_item(self): + self.item1.type = 2 + self.item1.save() + data = { + 'i1-self': 1, + 'i1-weight': 50, + 'i1-parent': 0, + 'i2-self': 2, + 'i2-weight': 50, + 'i2-parent': 1} + response = self.adminClient.post('/agenda/', data) + self.assertNotEqual(Item.objects.get(pk=2).parent_id, 1) + self.assertContains(response, 'Agenda items can not be descendents of an organizational item.') + def test_delete(self): response = self.adminClient.get('/agenda/%s/del/' % self.item1.pk) self.assertRedirects(response, '/agenda/') @@ -276,6 +290,32 @@ class ViewTest(TestCase): response = client.get('/agenda/2/') self.assertEqual(response.status_code, 200) + def test_orga_item_with_orga_parent_one(self): + item1 = Item.objects.create(title='item1_Taeboog1de1sahSeiM8y', type=2) + response = self.adminClient.post( + '/agenda/new/', + {'title': 'item2_faelohD2uK7ohNgeepi2', + 'type': '1', + 'parent': item1.pk}) + self.assertFormError( + response, + 'form', + None, + 'Agenda items can not be descendents of an organizational item.') + + def test_orga_item_with_orga_parent_two(self): + item1 = Item.objects.create(title='item1_aeNg4Heibee8ULooneep') + Item.objects.create(title='item2_fooshaeroo7Ohvoow0hoo', parent=item1) + response = self.adminClient.post( + '/agenda/%s/edit/' % item1.pk, + {'title': 'item1_aeNg4Heibee8ULooneep_changed', + 'type': '2'}) + self.assertFormError( + response, + 'form', + None, + 'Organizational items can not have agenda items as descendents.') + def test_csv_import(self): item_number = Item.objects.all().count() new_csv_file = SimpleUploadedFile(