diff --git a/openslides/agenda/__init__.py b/openslides/agenda/__init__.py index 004f26495..c72be1dc6 100644 --- a/openslides/agenda/__init__.py +++ b/openslides/agenda/__init__.py @@ -8,7 +8,7 @@ It includes time-management and list 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..37b12f0ee 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. """ @@ -157,13 +157,21 @@ class Item(MPTTModel, SlideMixin): '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') + + speaker_query = Speaker.objects.filter(item=self) + + coming_speakers = speaker_query.filter(begin_time=None).order_by('weight') + old_speakers = speaker_query.exclude(begin_time=None).exclude(end_time=None).order_by('end_time') + try: + actual_speaker = speaker_query.filter(end_time=None).exclude(begin_time=None).get() + except Speaker.DoesNotExist: + actual_speaker = None slice_items = max(0, old_speakers.count()-2) data = {'title': self.get_title(), 'template': 'projector/agenda_list_of_speaker.html', - 'speakers': speakers, - 'old_speakers': old_speakers[slice_items:]} + 'coming_speakers': coming_speakers, + 'old_speakers': old_speakers[slice_items:], + 'actual_speaker': actual_speaker} elif self.related_sid: data = self.get_related_slide().slide() else: @@ -246,7 +254,7 @@ class Item(MPTTModel, SlideMixin): 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 +278,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 +308,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..50ab1ece8 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -80,7 +80,7 @@ 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] + speakers = Speaker.objects.filter(begin_time=None, item=slide)[:5] context = {'speakers': speakers} return render_to_string('agenda/overlay_speaker_projector.html', context) diff --git a/openslides/agenda/templates/agenda/view.html b/openslides/agenda/templates/agenda/view.html index 46ec94e29..aaac1ef1c 100644 --- a/openslides/agenda/templates/agenda/view.html +++ b/openslides/agenda/templates/agenda/view.html @@ -77,42 +77,40 @@ {% if old_speakers %}
{% trans "Last speakers" %}: - {% if old_speakers|length > 1 %} -
- -
- {% endif %} -
+
+ +
+
{% for speaker in old_speakers %} - {% if not forloop.last %} - {{forloop.counter}}. - [{{ speaker.time }}h] - {{ speaker }} - {% if perms.agenda.can_manage_agenda %} - - - - {% endif %} -
- {% endif %} + {{forloop.counter}}. + [{{ speaker.begin_time }} – {{ speaker.end_time }}] + {{ speaker }} + {% if perms.agenda.can_manage_agenda %} + + + + {% endif %} +
{% endfor %}
- {% if old_speakers %} - {% with last=old_speakers|last %} - {{ old_speakers|length }}. - [{{ last.time }}h] - {{ last }} - {% if perms.agenda.can_manage_agenda %} - - - - {% endif %} - - {% endwith %} - {% endif %} + +
+{% endif %} + +{% if actual_speaker %} +
+ {% trans 'Actual speaker' %}:
+ [{{ actual_speaker.begin_time }}] + {{ actual_speaker }} + {% if perms.agenda.can_manage_agenda %} + {% trans 'End speach' %} + + + + {% endif %}
{% endif %} @@ -128,11 +126,10 @@ {% endif %} -
- {% trans "Next speakers:" %} + {% trans "Next speakers" %}:

- {% 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" %} diff --git a/openslides/agenda/templates/projector/agenda_list_of_speaker.html b/openslides/agenda/templates/projector/agenda_list_of_speaker.html index edd00ae2f..857c42c96 100644 --- a/openslides/agenda/templates/projector/agenda_list_of_speaker.html +++ b/openslides/agenda/templates/projector/agenda_list_of_speaker.html @@ -1,8 +1,8 @@ -{% extends "base-projector.html" %} +{% extends 'base-projector.html' %} {% load i18n %} -{% block title %}{{ block.super }} - {{ item }}{% endblock %} +{% block title %}{{ block.super }} – {{ item }}{% endblock %} {% block content %}

{{ title }}

@@ -10,17 +10,18 @@ {% endblock %} {% block scrollcontent %} - {% if old_speakers|length > 0 %} + {% if old_speakers|length > 0 or actual_speaker %} {% endif %} - {% if speakers %} + {% if coming_speakers %}
    - {% for speaker in speakers %} + {% for speaker in coming_speakers %}
  1. {{ speaker }}
  2. {% endfor %}
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\d+)/speaker/end_speach/$', + SpeakerEndSpeachView.as_view(), + name='agenda_speaker_end_speach', + ), + url(r'^(?P\d+)/speaker/change_order/$', SpeakerChangeOrderView.as_view(), name='agenda_speaker_change_order', diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 234be94b0..0f68a332b 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -6,7 +6,7 @@ Views for the agenda app. - :copyright: 2011, 2012 by the OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ # TODO: Rename all views and template names @@ -127,15 +127,19 @@ class AgendaItemView(SingleObjectMixin, FormView): def get_context_data(self, **kwargs): self.object = self.get_object() - speakers = Speaker.objects.filter(time=None, item=self.object.pk).order_by('weight') - old_speakers = list(Speaker.objects.filter(item=self.object.pk) - .exclude(time=None).order_by('time')) + speaker_query = Speaker.objects.filter(item=self.object) + coming_speakers = speaker_query.filter(begin_time=None).order_by('weight') + old_speakers = speaker_query.exclude(begin_time=None).exclude(end_time=None).order_by('end_time') + try: + actual_speaker = speaker_query.filter(end_time=None).exclude(begin_time=None).get() + except Speaker.DoesNotExist: + actual_speaker = None kwargs.update({ 'object': self.object, - 'speakers': speakers, + 'coming_speakers': coming_speakers, 'old_speakers': old_speakers, - 'is_speaker': Speaker.objects.filter( - time=None, person=self.request.user, item=self.object).exists(), + 'actual_speaker': actual_speaker, + 'is_on_the_list_of_speakers': speaker_query.filter(begin_time=None, person=self.request.user).exists(), 'show_list': config['presentation_argument'] == 'show_list_of_speakers', }) return super(AgendaItemView, self).get_context_data(**kwargs) @@ -334,15 +338,41 @@ class SpeakerSpeakView(SingleObjectMixin, RedirectView): try: speaker = Speaker.objects.filter( person=kwargs['person_id'], - item=self.object.pk).exclude( - weight=None).get() - except Speaker.DoesNotExist: + item=self.object, + begin_time=None).get() + except Speaker.DoesNotExist: # TODO: Check the MultipleObjectsReturned error here? messages.error( self.request, _('%(person)s is not on the list of %(item)s.') % {'person': kwargs['person_id'], 'item': self.object}) else: - speaker.speak() + speaker.begin_speach() + + def get_url_name_args(self): + return [self.object.pk] + + +class SpeakerEndSpeachView(SingleObjectMixin, RedirectView): + """ + The speach of the actual speaker is finished. + """ + permission_required = 'agenda.can_manage_agenda' + url_name = 'item_view' + model = Item + + def pre_redirect(self, *args, **kwargs): + self.object = self.get_object() + try: + speaker = Speaker.objects.filter( + item=self.object, + end_time=None).exclude(begin_time=None).get() + except Speaker.DoesNotExist: + messages.error( + self.request, + _('There is no one speaking at the moment according to %(item)s.') + % {'item': self.object}) + else: + speaker.end_speach() def get_url_name_args(self): return [self.object.pk] diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py index 9233d6fd0..8a6d82ca4 100644 --- a/tests/agenda/test_list_of_speakers.py +++ b/tests/agenda/test_list_of_speakers.py @@ -41,7 +41,8 @@ class ListOfSpeakerModelTests(TestCase): # Check time and weight for object in (speaker1_item1, speaker2_item1, speaker1_item2): - self.assertIsNone(object.time) + self.assertIsNone(object.begin_time) + self.assertIsNone(object.end_time) self.assertEqual(speaker1_item1.weight, 1) self.assertEqual(speaker1_item2.weight, 1) self.assertEqual(speaker2_item1.weight, 2) @@ -52,13 +53,25 @@ class ListOfSpeakerModelTests(TestCase): self.item1.save() self.assertTrue(Item.objects.get(pk=self.item1.pk).speaker_list_closed) - def test_speak(self): + def test_speak_and_finish(self): speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) - - self.assertIsNone(speaker1_item1.time) - speaker1_item1.speak() - self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).time) + self.assertIsNone(speaker1_item1.begin_time) + self.assertIsNone(speaker1_item1.end_time) + speaker1_item1.begin_speach() + self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).begin_time) self.assertIsNone(Speaker.objects.get(pk=speaker1_item1.pk).weight) + speaker1_item1.end_speach() + self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).end_time) + + def test_finish_when_other_speaker_begins(self): + speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) + speaker2_item1 = Speaker.objects.add(self.speaker2, self.item1) + speaker1_item1.begin_speach() + self.assertIsNone(speaker1_item1.end_time) + self.assertIsNone(speaker2_item1.begin_time) + speaker2_item1.begin_speach() + self.assertIsNotNone(Speaker.objects.get(person=self.speaker1, item=self.item1).end_time) + self.assertIsNotNone(speaker2_item1.begin_time) class SpeakerViewTestCase(TestCase): @@ -160,7 +173,21 @@ class TestSpeakerSpeakView(SpeakerViewTestCase): speaker = Speaker.objects.add(self.speaker1, self.item1) response = self.check_url(url, self.admin_client, 302) speaker = Speaker.objects.get(pk=speaker.pk) - self.assertIsNotNone(speaker.time) + self.assertIsNotNone(speaker.begin_time) + self.assertIsNone(speaker.weight) + + +class TestSpeakerEndSpeachView(SpeakerViewTestCase): + def test_get(self): + url = '/agenda/1/speaker/end_speach/' + response = self.check_url(url, self.admin_client, 302) + self.assertMessage(response, 'There is no one speaking at the moment according to item1.') + speaker = Speaker.objects.add(self.speaker1, self.item1) + speaker.begin_speach() + response = self.check_url(url, self.admin_client, 302) + speaker = Speaker.objects.get(pk=speaker.pk) + self.assertIsNotNone(speaker.begin_time) + self.assertIsNotNone(speaker.end_time) self.assertIsNone(speaker.weight)