diff --git a/openslides/agenda/__init__.py b/openslides/agenda/__init__.py index 004f26495..320161958 100644 --- a/openslides/agenda/__init__.py +++ b/openslides/agenda/__init__.py @@ -6,9 +6,9 @@ The OpenSlides agenda app appends the functionality to OpenSlides to manage agendas. - It includes time-management and list of speakers. + It includes time-management and lists of speakers. - :copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS. + :copyright: (c) 2011–2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py index 38f4ae81a..783398765 100644 --- a/openslides/agenda/forms.py +++ b/openslides/agenda/forms.py @@ -6,7 +6,7 @@ Forms for the agenda app. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -69,7 +69,7 @@ class AppendSpeakerForm(CssClassMixin, forms.Form): Checks, that the user is not already on the list. """ speaker = self.cleaned_data['speaker'] - if Speaker.objects.filter(person=speaker, item=self.item, time=None).exists(): + if Speaker.objects.filter(person=speaker, item=self.item, begin_time=None).exists(): raise forms.ValidationError(ugettext_lazy( '%s is already on the list of speakers.' % unicode(speaker))) diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index f21886910..9de46ee4d 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -6,7 +6,7 @@ Models for the agenda app. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -99,10 +99,44 @@ class Item(MPTTModel, SlideMixin): True, if the list of speakers is closed. """ + class Meta: + permissions = ( + ('can_see_agenda', ugettext_noop("Can see agenda")), + ('can_manage_agenda', ugettext_noop("Can manage agenda")), + ('can_see_orga_items', ugettext_noop("Can see orga items and time scheduling of agenda"))) + + class MPTTMeta: + order_insertion_by = ['weight'] + + def __unicode__(self): + return self.get_title() + + def get_absolute_url(self, link='view'): + """ + Return the URL to this item. By default it is the link to its + view or the view of a related object. + + The link can be: + * view + * edit + * delete + """ + if link == 'view': + if self.related_sid: + return self.get_related_slide().get_absolute_url(link) + return reverse('item_view', args=[str(self.id)]) + if link == 'edit': + if self.related_sid: + return self.get_related_slide().get_absolute_url(link) + return reverse('item_edit', args=[str(self.id)]) + if link == 'delete': + return reverse('item_delete', args=[str(self.id)]) + def get_related_slide(self): """ - return the object, of which the item points. + Return the object at which the item points. """ + # TODO: Rename it to 'get_related_object' object = get_slide_from_sid(self.related_sid, element=True) if object is None: self.title = 'Item for deleted slide: %s' % self.related_sid @@ -114,7 +148,7 @@ class Item(MPTTModel, SlideMixin): def get_related_type(self): """ - return the type of the releated slide. + Return the type of the releated slide. """ return self.get_related_slide().prefix @@ -129,7 +163,7 @@ class Item(MPTTModel, SlideMixin): def get_title(self): """ - return the title of this item. + Return the title of this item. """ if self.related_sid is None: return self.title @@ -137,7 +171,7 @@ class Item(MPTTModel, SlideMixin): def get_title_supplement(self): """ - return a supplement for the title. + Return a supplement for the title. """ if self.related_sid is None: return '' @@ -148,30 +182,36 @@ class Item(MPTTModel, SlideMixin): def slide(self): """ - Return a map with all Data for the Slide + Return a map with all data for the slide. + + There are four cases: + * summary slide + * list of speakers + * related slide, i. e. the slide of the related object + * normal slide of the item + + The method returns only one of them according to the config value + 'presentation_argument' and the attribut 'related_sid'. """ if config['presentation_argument'] == 'summary': - data = { - 'title': self.get_title(), - 'items': self.get_children(), - 'template': 'projector/AgendaSummary.html', - } + data = {'title': self.get_title(), + 'items': self.get_children(), + 'template': 'projector/AgendaSummary.html'} + elif config['presentation_argument'] == 'show_list_of_speakers': - speakers = Speaker.objects.filter(time=None, item=self.pk).order_by('weight') - old_speakers = Speaker.objects.filter(item=self.pk).exclude(time=None).order_by('time') - slice_items = max(0, old_speakers.count()-2) + list_of_speakers = self.get_list_of_speakers( + old_speakers_count=config['agenda_show_last_speakers']) data = {'title': self.get_title(), 'template': 'projector/agenda_list_of_speaker.html', - 'speakers': speakers, - 'old_speakers': old_speakers[slice_items:]} + 'list_of_speakers': list_of_speakers} elif self.related_sid: data = self.get_related_slide().slide() + else: - data = { - 'item': self, - 'title': self.get_title(), - 'template': 'projector/AgendaText.html', - } + data = {'item': self, + 'title': self.get_title(), + 'template': 'projector/AgendaText.html'} + return data def set_closed(self, closed=True): @@ -209,44 +249,72 @@ class Item(MPTTModel, SlideMixin): super(Item, self).delete() Item.objects.rebuild() - def get_absolute_url(self, link='view'): + def get_list_of_speakers(self, old_speakers_count=None, coming_speakers_count=None): """ - Return the URL to this item. By default it is the Link to its - slide - - link can be: - * view - * edit - * delete + Returns the list of speakers as a list of dictionaries. Each + dictionary contains a prefix, the speaker and its type. Types + are old_speaker, actual_speaker and coming_speaker. """ - if link == 'view': - if self.related_sid: - return self.get_related_slide().get_absolute_url(link) - return reverse('item_view', args=[str(self.id)]) - if link == 'edit': - if self.related_sid: - return self.get_related_slide().get_absolute_url(link) - return reverse('item_edit', args=[str(self.id)]) - if link == 'delete': - return reverse('item_delete', args=[str(self.id)]) + speaker_query = Speaker.objects.filter(item=self) + list_of_speakers = [] - def __unicode__(self): - return self.get_title() + # Parse old speakers + old_speakers = speaker_query.exclude(begin_time=None).exclude(end_time=None).order_by('end_time') + if old_speakers_count is None: + old_speakers_count = old_speakers.count() + last_old_speakers_count = max(0, old_speakers.count() - old_speakers_count) + old_speakers = old_speakers[last_old_speakers_count:] + for number, speaker in enumerate(old_speakers): + prefix = old_speakers_count - number + speaker_dict = { + 'prefix': '-%d' % prefix, + 'speaker': speaker, + 'type': 'old_speaker', + 'first_in_group': False, + 'last_in_group': False} + if number == 0: + speaker_dict['first_in_group'] = True + if number == old_speakers_count - 1: + speaker_dict['last_in_group'] = True + list_of_speakers.append(speaker_dict) - class Meta: - permissions = ( - ('can_see_agenda', ugettext_noop("Can see agenda")), - ('can_manage_agenda', ugettext_noop("Can manage agenda")), - ('can_see_orga_items', ugettext_noop("Can see orga items and time scheduling of agenda")), - ) + # Parse actual speaker + try: + actual_speaker = speaker_query.filter(end_time=None).exclude(begin_time=None).get() + except Speaker.DoesNotExist: + pass + else: + list_of_speakers.append({ + 'prefix': '0', + 'speaker': actual_speaker, + 'type': 'actual_speaker', + 'first_in_group': True, + 'last_in_group': True}) - class MPTTMeta: - order_insertion_by = ['weight'] + # Parse coming speakers + coming_speakers = speaker_query.filter(begin_time=None).order_by('weight') + if coming_speakers_count is None: + coming_speakers_count = coming_speakers.count() + coming_speakers = coming_speakers[:max(0, coming_speakers_count)] + for number, speaker in enumerate(coming_speakers): + speaker_dict = { + 'prefix': number + 1, + 'speaker': speaker, + 'type': 'coming_speaker', + 'first_in_group': False, + 'last_in_group': False} + if number == 0: + speaker_dict['first_in_group'] = True + if number == coming_speakers_count - 1: + speaker_dict['last_in_group'] = True + list_of_speakers.append(speaker_dict) + + return list_of_speakers class SpeakerManager(models.Manager): def add(self, person, item): - if self.filter(person=person, item=item, time=None).exists(): + if self.filter(person=person, item=item, begin_time=None).exists(): raise OpenSlidesError(_('%(person)s is already on the list of speakers of item %(id)s.') % {'person': person, 'id': item.id}) weight = (self.filter(item=item).aggregate( models.Max('weight'))['weight__max'] or 0) @@ -270,9 +338,14 @@ class Speaker(models.Model): ForeinKey to the AgendaItem to which the person want to speak. """ - time = models.DateTimeField(null=True) + begin_time = models.DateTimeField(null=True) """ - Saves the time, when the speaker has spoken. None, if he has not spoken yet. + Saves the time, when the speaker begins to speak. None, if he has not spoken yet. + """ + + end_time = models.DateTimeField(null=True) + """ + Saves the time, when the speaker ends his speach. None, if he is not finished yet. """ weight = models.IntegerField(null=True) @@ -295,12 +368,26 @@ class Speaker(models.Model): return reverse('agenda_speaker_delete', args=[self.item.pk, self.pk]) - def speak(self): + def begin_speach(self): """ Let the person speak. - Set the weight to None and the time to now. + Set the weight to None and the time to now. If anyone is still + speaking, end his speach. """ + try: + actual_speaker = Speaker.objects.filter(item=self.item, end_time=None).exclude(begin_time=None).get() + except Speaker.DoesNotExist: + pass + else: + actual_speaker.end_speach() self.weight = None - self.time = datetime.now() + self.begin_time = datetime.now() + self.save() + + def end_speach(self): + """ + The speach is finished. Set the time to now. + """ + self.end_time = datetime.now() self.save() diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index 4d9d99527..e7f92afed 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -12,11 +12,11 @@ from django.dispatch import receiver from django import forms -from django.utils.translation import ugettext_lazy, ugettext_noop +from django.utils.translation import ugettext_lazy, ugettext_noop, ugettext as _ from django.template.loader import render_to_string from openslides.config.signals import config_signal -from openslides.config.api import ConfigVariable, ConfigPage +from openslides.config.api import config, ConfigVariable, ConfigPage from openslides.projector.signals import projector_overlays from openslides.projector.projector import Overlay @@ -40,8 +40,15 @@ def setup_agenda_config_page(sender, **kwargs): form_field=forms.CharField( widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M'), required=False, - label=ugettext_lazy('Begin of event'), - help_text=ugettext_lazy('Input format: DD.MM.YYYY HH:MM'))) + label=_('Begin of event'), + help_text=_('Input format: DD.MM.YYYY HH:MM'))) + + agenda_show_last_speakers = ConfigVariable( + name='agenda_show_last_speakers', + default_value=1, + form_field=forms.IntegerField( + min_value=0, + label=_('Number of last speakers to be shown on the projector'))) extra_stylefiles = ['styles/timepicker.css', 'styles/jquery-ui/jquery-ui.custom.min.css'] extra_javascript = ['javascript/jquery-ui.custom.min.js', @@ -53,7 +60,7 @@ def setup_agenda_config_page(sender, **kwargs): url='agenda', required_permission='config.can_manage', weight=20, - variables=(agenda_start_event_date_time,), + variables=(agenda_start_event_date_time, agenda_show_last_speakers), extra_context={'extra_stylefiles': extra_stylefiles, 'extra_javascript': extra_javascript}) @@ -80,8 +87,10 @@ def agenda_list_of_speakers(sender, **kwargs): # Only show list of speakers on Agenda-Items return None clear_projector_cache() - speakers = Speaker.objects.filter(time=None, item=slide)[:5] - context = {'speakers': speakers} + list_of_speakers = slide.get_list_of_speakers( + old_speakers_count=config['agenda_show_last_speakers'], + coming_speakers_count=5) + context = {'list_of_speakers': list_of_speakers} return render_to_string('agenda/overlay_speaker_projector.html', context) return Overlay(name, get_widget_html, get_projector_html) diff --git a/openslides/agenda/static/javascript/agenda.js b/openslides/agenda/static/javascript/agenda.js index fd12a897d..068f5fb2e 100644 --- a/openslides/agenda/static/javascript/agenda.js +++ b/openslides/agenda/static/javascript/agenda.js @@ -24,8 +24,8 @@ function hideClosedSlides(hide) { return false; } -$('#speaker_list_changed_form').submit(function() { - $('#sort_order').val($('#list_of_speakers').sortable("toArray")); +$('#coming_speakers_changed_form').submit(function() { + $('#sort_order').val($('#coming_speakers').sortable("toArray")); }); $(function() { @@ -76,10 +76,10 @@ $(function() { //# $('#hide_closed_items').attr('checked', true); //# } - if ($('#list_of_speakers').length > 0) { - $('#list_of_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) { - $('#speaker_list_changed_form').show(); + if ($('#coming_speakers').length > 0) { + $('#coming_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) { + $('#coming_speakers_changed_form').show(); }}); - $('#list_of_speakers').disableSelection(); + $('#coming_speakers').disableSelection(); } }); diff --git a/openslides/agenda/static/styles/agenda.css b/openslides/agenda/static/styles/agenda.css index f5d1e311b..682330f15 100644 --- a/openslides/agenda/static/styles/agenda.css +++ b/openslides/agenda/static/styles/agenda.css @@ -22,15 +22,15 @@ table#agendatime td { white-space: nowrap; } -#list_of_speakers li { - line-height: 30px; -} - -#list_of_speakers { +div#complete_list_of_speakers li { list-style-type: none; } -#list_of_speakers span.ui-icon { +div#complete_list_of_speakers li { + line-height: 30px; +} + +#coming_speakers span.ui-icon { position: absolute; margin-left: -15px; margin-top: 6px; diff --git a/openslides/agenda/templates/agenda/overlay_speaker_projector.html b/openslides/agenda/templates/agenda/overlay_speaker_projector.html index e1596a4cb..287536aa0 100644 --- a/openslides/agenda/templates/agenda/overlay_speaker_projector.html +++ b/openslides/agenda/templates/agenda/overlay_speaker_projector.html @@ -1,13 +1,57 @@ {% load i18n %} +{% load tags %} + +
- {% if is_speaker %} + {% if is_on_the_list_of_speakers %} {% trans "Remove me from the list" %} {% elif not object.speaker_list_closed and perms.can_be_speaker %} {% trans "Put me on the list" %} @@ -174,6 +157,6 @@ {% endfor %} {% endif %} -
{% trans 'The list of speakers is empty.' %}
+ {% trans 'The list of speakers is empty.' %} {% endif %} {% endblock %} diff --git a/openslides/agenda/urls.py b/openslides/agenda/urls.py index bf6416b67..2d7ad1e9f 100644 --- a/openslides/agenda/urls.py +++ b/openslides/agenda/urls.py @@ -6,13 +6,13 @@ URL list for the agenda app. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ from django.conf.urls import url, patterns from openslides.agenda.views import ( - Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView, + Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView, SpeakerEndSpeachView, ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView, SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView) @@ -91,6 +91,11 @@ urlpatterns = patterns( name='agenda_speaker_speak', ), + url(r'^(?P