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.
This commit is contained in:
Norman Jäckel 2014-04-28 01:03:10 +02:00
parent 2c5b3a8e4f
commit 5254cc83a6
7 changed files with 91 additions and 39 deletions

View File

@ -11,6 +11,8 @@ Version 1.6 (unreleased)
Agenda: Agenda:
- New projector view with the current list of speakers. - New projector view with the current list of speakers.
- Added CSV import. - Added CSV import.
- Added automatic numbering of agenda items.
- Fixed organizational item structuring.
Assignment: Assignment:
- Coupled assignment candidates with list of speakers. - Coupled assignment candidates with list of speakers.
Dashboard: Dashboard:

View File

@ -272,6 +272,8 @@ OpenSlides uses the following projects or parts of them:
* `ReportLab <http://www.reportlab.com/software/opensource/rl-toolkit/>`_, * `ReportLab <http://www.reportlab.com/software/opensource/rl-toolkit/>`_,
License: BSD License: BSD
* `roman <https://pypi.python.org/pypi/roman>`_, License: Python 2.1.1
* `sockjs-client <https://github.com/sockjs/sockjs-client>`_, * `sockjs-client <https://github.com/sockjs/sockjs-client>`_,
License: MIT License: MIT

View File

@ -31,7 +31,7 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
class Meta: class Meta:
model = Item 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): class RelatedItemForm(ItemForm):
@ -40,7 +40,7 @@ class RelatedItemForm(ItemForm):
""" """
class Meta: class Meta:
model = Item 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): class ItemOrderForm(CssClassMixin, forms.Form):

View File

@ -5,6 +5,7 @@ from datetime import datetime
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -37,16 +38,16 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
(AGENDA_ITEM, ugettext_lazy('Agenda item')), (AGENDA_ITEM, ugettext_lazy('Agenda item')),
(ORGANIZATIONAL_ITEM, ugettext_lazy('Organizational 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 = models.CharField(null=True, max_length=255, verbose_name=ugettext_lazy("Title"))
""" """
Title of the agenda item. 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")) text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Text"))
""" """
The optional text of the agenda item. 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(): if self.parent and self.parent.is_active_slide():
update_projector() 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): def __unicode__(self):
return self.get_title() return self.get_title()
@ -155,7 +166,7 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
if not self.content_object: if not self.content_object:
if config['agenda_enable_auto_numbering']: if config['agenda_enable_auto_numbering']:
item_no = self.item_no 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 return self.title
try: try:
return self.content_object.get_agenda_title() return self.content_object.get_agenda_title()
@ -296,32 +307,35 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
@property @property
def item_no(self): def item_no(self):
if config['agenda_agenda_fixed']: if config['agenda_agenda_fixed']:
item_no = self.item_number item_number = self.item_number
else: else:
item_no = self.calc_item_no() item_number = self.calc_item_no()
if item_no: if item_number:
return '%s %s' % (config['agenda_number_prefix'], item_no) item_no = '%s %s' % (config['agenda_number_prefix'], item_number)
else:
item_no = None
return item_no
def calc_item_no(self): 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.type == self.AGENDA_ITEM:
if self.is_root_node(): if self.is_root_node():
if config['agenda_numeral_system'] == 'a': if config['agenda_numeral_system'] == 'arabic':
return str(self._calc_sibling_no()) return str(self._calc_sibling_no())
else: else: # config['agenda_numeral_system'] == 'roman'
return toRoman(self._calc_sibling_no()) return toRoman(self._calc_sibling_no())
else: else:
return '%s.%s' % (self.parent.calc_item_no(), self._calc_sibling_no()) return '%s.%s' % (self.parent.calc_item_no(), self._calc_sibling_no())
def _calc_sibling_no(self): 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 sibling_no = 0
prev_sibling = self.get_previous_sibling() 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: if prev_sibling.type == self.AGENDA_ITEM:
sibling_no += 1 sibling_no += 1
prev_sibling = prev_sibling.get_previous_sibling() prev_sibling = prev_sibling.get_previous_sibling()

View File

@ -77,19 +77,18 @@ def setup_agenda_config(sender, **kwargs):
agenda_numeral_system = ConfigVariable( agenda_numeral_system = ConfigVariable(
name='agenda_numeral_system', name='agenda_numeral_system',
default_value='a', default_value='arabic',
form_field=forms.ChoiceField( form_field=forms.ChoiceField(
label=ugettext_lazy('Numeral System for Top items'), label=ugettext_lazy('Numeral system for agenda items'),
widget=forms.Select(), widget=forms.Select(),
choices=( choices=(
('a', ugettext_lazy('Arabic')), ('arabic', ugettext_lazy('Arabic')),
('r', ugettext_lazy('Roman'))), ('roman', ugettext_lazy('Roman'))),
required=False)) required=False))
agenda_agenda_fixed = ConfigVariable( agenda_agenda_fixed = ConfigVariable(
name='agenda_agenda_fixed', name='agenda_agenda_fixed',
default_value=False, default_value=False)
form_field=None)
extra_stylefiles = ['css/jquery-ui-timepicker.css'] extra_stylefiles = ['css/jquery-ui-timepicker.css']
extra_javascript = ['js/jquery/jquery-ui-timepicker-addon.min.js', extra_javascript = ['js/jquery/jquery-ui-timepicker-addon.min.js',

View File

@ -109,14 +109,11 @@ class Overview(TemplateView):
agenda_is_active = None agenda_is_active = None
active_type = 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({ context.update({
'items': items, 'items': items,
'agenda_is_active': agenda_is_active, 'agenda_is_active': agenda_is_active,
'agenda_enable_auto_numbering': agenda_enable_auto_numbering, 'agenda_enable_auto_numbering': config['agenda_enable_auto_numbering'],
'agenda_numbering_fixed': agenda_numbering_fixed, 'agenda_numbering_fixed': config['agenda_agenda_fixed'],
'duration': duration, 'duration': duration,
'start': start, 'start': start,
'end': end, 'end': end,
@ -140,8 +137,14 @@ class Overview(TemplateView):
parent = Item.objects.get(id=form.cleaned_data['parent']) parent = Item.objects.get(id=form.cleaned_data['parent'])
except Item.DoesNotExist: except Item.DoesNotExist:
parent = None 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.parent = parent
item.weight = form.cleaned_data['weight']
Model.save(item) Model.save(item)
else: else:
transaction.rollback() transaction.rollback()
@ -349,13 +352,9 @@ class FixAgendaView(QuestionView):
question_message = ugettext_lazy('Do you really want to fix the agenda numbering?') question_message = ugettext_lazy('Do you really want to fix the agenda numbering?')
url_name_args = [] 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): def on_clicked_yes(self):
config['agenda_agenda_fixed'] = True config['agenda_agenda_fixed'] = True
for item in self.items: for item in Item.objects.all():
item.item_number = item.calc_item_no() item.item_number = item.calc_item_no()
item.save() item.save()
@ -370,13 +369,9 @@ class ResetAgendaView(QuestionView):
question_message = ugettext_lazy('Do you really want to reset the agenda numbering?') question_message = ugettext_lazy('Do you really want to reset the agenda numbering?')
url_name_args = [] 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): def on_clicked_yes(self):
config['agenda_agenda_fixed'] = False config['agenda_agenda_fixed'] = False
for item in self.items: for item in Item.objects.all():
item.item_number = '' item.item_number = ''
item.save() item.save()

View File

@ -229,6 +229,20 @@ class ViewTest(TestCase):
self.assertIsNone(Item.objects.get(pk=1).parent) self.assertIsNone(Item.objects.get(pk=1).parent)
self.assertEqual(Item.objects.get(pk=2).parent_id, 1) 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): def test_delete(self):
response = self.adminClient.get('/agenda/%s/del/' % self.item1.pk) response = self.adminClient.get('/agenda/%s/del/' % self.item1.pk)
self.assertRedirects(response, '/agenda/') self.assertRedirects(response, '/agenda/')
@ -276,6 +290,32 @@ class ViewTest(TestCase):
response = client.get('/agenda/2/') response = client.get('/agenda/2/')
self.assertEqual(response.status_code, 200) 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): def test_csv_import(self):
item_number = Item.objects.all().count() item_number = Item.objects.all().count()
new_csv_file = SimpleUploadedFile( new_csv_file = SimpleUploadedFile(