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:
- 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:

View File

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

View File

@ -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):

View File

@ -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()

View File

@ -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',

View File

@ -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()

View File

@ -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(