Add second time field for list of speakers. Now you can click begin and end speach.

This commit is contained in:
Norman Jäckel 2013-04-25 16:18:16 +02:00
parent 0df5c5a2e6
commit 0b85164b84
9 changed files with 163 additions and 76 deletions

View File

@ -8,7 +8,7 @@
It includes time-management and list of speakers. It includes time-management and list of speakers.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS. :copyright: (c) 20112013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """

View File

@ -6,7 +6,7 @@
Forms for the agenda app. Forms for the agenda app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS. :copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :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. Checks, that the user is not already on the list.
""" """
speaker = self.cleaned_data['speaker'] 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( raise forms.ValidationError(ugettext_lazy(
'%s is already on the list of speakers.' '%s is already on the list of speakers.'
% unicode(speaker))) % unicode(speaker)))

View File

@ -6,7 +6,7 @@
Models for the agenda app. Models for the agenda app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS. :copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
@ -157,13 +157,21 @@ class Item(MPTTModel, SlideMixin):
'template': 'projector/AgendaSummary.html', 'template': 'projector/AgendaSummary.html',
} }
elif config['presentation_argument'] == 'show_list_of_speakers': 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) slice_items = max(0, old_speakers.count()-2)
data = {'title': self.get_title(), data = {'title': self.get_title(),
'template': 'projector/agenda_list_of_speaker.html', 'template': 'projector/agenda_list_of_speaker.html',
'speakers': speakers, 'coming_speakers': coming_speakers,
'old_speakers': old_speakers[slice_items:]} 'old_speakers': old_speakers[slice_items:],
'actual_speaker': actual_speaker}
elif self.related_sid: elif self.related_sid:
data = self.get_related_slide().slide() data = self.get_related_slide().slide()
else: else:
@ -246,7 +254,7 @@ class Item(MPTTModel, SlideMixin):
class SpeakerManager(models.Manager): class SpeakerManager(models.Manager):
def add(self, person, item): 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}) 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( weight = (self.filter(item=item).aggregate(
models.Max('weight'))['weight__max'] or 0) 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. 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) weight = models.IntegerField(null=True)
@ -295,12 +308,26 @@ class Speaker(models.Model):
return reverse('agenda_speaker_delete', return reverse('agenda_speaker_delete',
args=[self.item.pk, self.pk]) args=[self.item.pk, self.pk])
def speak(self): def begin_speach(self):
""" """
Let the person speak. 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.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() self.save()

View File

@ -80,7 +80,7 @@ def agenda_list_of_speakers(sender, **kwargs):
# Only show list of speakers on Agenda-Items # Only show list of speakers on Agenda-Items
return None return None
clear_projector_cache() 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} context = {'speakers': speakers}
return render_to_string('agenda/overlay_speaker_projector.html', context) return render_to_string('agenda/overlay_speaker_projector.html', context)

View File

@ -77,42 +77,40 @@
{% if old_speakers %} {% if old_speakers %}
<div class="well"> <div class="well">
<b>{% trans "Last speakers" %}:</b> <b>{% trans "Last speakers" %}:</b>
{% if old_speakers|length > 1 %} <div class="btn-group" data-toggle="buttons-checkbox">
<div class="btn-group" data-toggle="buttons-checkbox"> <button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#all_speakers">
<button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#all_speakers"> {% trans "Show all speakers" %}
{% trans "Show all speakers" %} </button>
</button> </div>
</div> <br />
{% endif %}
<br>
<div id="all_speakers" class="collapse out"> <div id="all_speakers" class="collapse out">
{% for speaker in old_speakers %} {% for speaker in old_speakers %}
{% if not forloop.last %} <small>{{forloop.counter}}.
<small>{{forloop.counter}}. [{{ speaker.begin_time }} {{ speaker.end_time }}]
[{{ speaker.time }}h] <a href="{% model_url speaker %}">{{ speaker }}</a>
<a href="{% model_url speaker %}">{{ speaker }}</a> {% if perms.agenda.can_manage_agenda %}
{% if perms.agenda.can_manage_agenda %} <a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini"> <i class="icon-remove"></i>
<i class="icon-remove"></i> </a>
</a> {% endif %}
{% endif %} </small><br />
</small><br>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% if old_speakers %}
{% with last=old_speakers|last %} </div>
<small>{{ old_speakers|length }}. {% endif %}
[{{ last.time }}h]
<a href="{% model_url last %}">{{ last }}</a> {% if actual_speaker %}
{% if perms.agenda.can_manage_agenda %} <div class="well">
<a href="{% model_url last 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini"> <b>{% trans 'Actual speaker' %}:</b><br />
<i class="icon-remove"></i> [{{ actual_speaker.begin_time }}]
</a> <a href="{% model_url actual_speaker %}">{{ actual_speaker }}</a>
{% endif %} {% if perms.agenda.can_manage_agenda %}
</small> <a href="{% url 'agenda_speaker_end_speach' item.pk %}" class="btn btn-mini"><i class="icon-bell"></i> {% trans 'End speach' %}</a>
{% endwith %} <a href="{% model_url actual_speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
{% endif %} <i class="icon-remove"></i>
</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
@ -128,11 +126,10 @@
</form> </form>
{% endif %} {% endif %}
<div class="well"> <div class="well">
<b>{% trans "Next speakers:" %}</b> <b>{% trans "Next speakers" %}:</b>
<ul {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}> <ul {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}>
{% for speaker in speakers %} {% for speaker in coming_speakers %}
<li id="speaker_{{ speaker.pk }}"> <li id="speaker_{{ speaker.pk }}">
<span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span> <span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span>
<span>{{ forloop.counter }}.</span> <span>{{ forloop.counter }}.</span>
@ -150,7 +147,7 @@
</ul> </ul>
<p> <p>
{% if is_speaker %} {% if is_on_the_list_of_speakers %}
<a href="{% url 'agenda_speaker_delete' object.id %}" class="btn">{% trans "Remove me from the list" %}</a> <a href="{% url 'agenda_speaker_delete' object.id %}" class="btn">{% trans "Remove me from the list" %}</a>
{% elif not object.speaker_list_closed and perms.can_be_speaker %} {% elif not object.speaker_list_closed and perms.can_be_speaker %}
<a href="{% url 'agenda_speaker_append' object.id %}" class="btn">{% trans "Put me on the list" %}</a> <a href="{% url 'agenda_speaker_append' object.id %}" class="btn">{% trans "Put me on the list" %}</a>

View File

@ -1,8 +1,8 @@
{% extends "base-projector.html" %} {% extends 'base-projector.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{{ block.super }} - {{ item }}{% endblock %} {% block title %}{{ block.super }} {{ item }}{% endblock %}
{% block content %} {% block content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
@ -10,17 +10,18 @@
{% endblock %} {% endblock %}
{% block scrollcontent %} {% block scrollcontent %}
{% if old_speakers|length > 0 %} {% if old_speakers|length > 0 or actual_speaker %}
<ul class="list_of_speakers last_speakers"> <ul class="list_of_speakers last_speakers">
{% for speaker in old_speakers %} {% for speaker in old_speakers %}
<li>{{ speaker }}</li> <li>{{ speaker }}</li>
{% endfor %} {% endfor %}
<li>{{ actual_speaker }} {% trans 'Actual Speaker' %}</li>
</ul> </ul>
{% endif %} {% endif %}
{% if speakers %} {% if coming_speakers %}
<ol class="list_of_speakers"> <ol class="list_of_speakers">
{% for speaker in speakers %} {% for speaker in coming_speakers %}
<li>{{ speaker }}</li> <li>{{ speaker }}</li>
{% endfor %} {% endfor %}
</ol> </ol>

View File

@ -6,13 +6,13 @@
URL list for the agenda app. URL list for the agenda app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS. :copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from django.conf.urls import url, patterns from django.conf.urls import url, patterns
from openslides.agenda.views import ( from openslides.agenda.views import (
Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView, Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView, SpeakerEndSpeachView,
ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView, ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView,
SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView) SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView)
@ -91,6 +91,11 @@ urlpatterns = patterns(
name='agenda_speaker_speak', name='agenda_speaker_speak',
), ),
url(r'^(?P<pk>\d+)/speaker/end_speach/$',
SpeakerEndSpeachView.as_view(),
name='agenda_speaker_end_speach',
),
url(r'^(?P<pk>\d+)/speaker/change_order/$', url(r'^(?P<pk>\d+)/speaker/change_order/$',
SpeakerChangeOrderView.as_view(), SpeakerChangeOrderView.as_view(),
name='agenda_speaker_change_order', name='agenda_speaker_change_order',

View File

@ -6,7 +6,7 @@
Views for the agenda app. Views for the agenda app.
:copyright: 2011, 2012 by the OpenSlides team, see AUTHORS. :copyright: 20112013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
# TODO: Rename all views and template names # TODO: Rename all views and template names
@ -127,15 +127,19 @@ class AgendaItemView(SingleObjectMixin, FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.object = self.get_object() self.object = self.get_object()
speakers = Speaker.objects.filter(time=None, item=self.object.pk).order_by('weight') speaker_query = Speaker.objects.filter(item=self.object)
old_speakers = list(Speaker.objects.filter(item=self.object.pk) coming_speakers = speaker_query.filter(begin_time=None).order_by('weight')
.exclude(time=None).order_by('time')) 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({ kwargs.update({
'object': self.object, 'object': self.object,
'speakers': speakers, 'coming_speakers': coming_speakers,
'old_speakers': old_speakers, 'old_speakers': old_speakers,
'is_speaker': Speaker.objects.filter( 'actual_speaker': actual_speaker,
time=None, person=self.request.user, item=self.object).exists(), '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', 'show_list': config['presentation_argument'] == 'show_list_of_speakers',
}) })
return super(AgendaItemView, self).get_context_data(**kwargs) return super(AgendaItemView, self).get_context_data(**kwargs)
@ -334,15 +338,41 @@ class SpeakerSpeakView(SingleObjectMixin, RedirectView):
try: try:
speaker = Speaker.objects.filter( speaker = Speaker.objects.filter(
person=kwargs['person_id'], person=kwargs['person_id'],
item=self.object.pk).exclude( item=self.object,
weight=None).get() begin_time=None).get()
except Speaker.DoesNotExist: except Speaker.DoesNotExist: # TODO: Check the MultipleObjectsReturned error here?
messages.error( messages.error(
self.request, self.request,
_('%(person)s is not on the list of %(item)s.') _('%(person)s is not on the list of %(item)s.')
% {'person': kwargs['person_id'], 'item': self.object}) % {'person': kwargs['person_id'], 'item': self.object})
else: 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): def get_url_name_args(self):
return [self.object.pk] return [self.object.pk]

View File

@ -41,7 +41,8 @@ class ListOfSpeakerModelTests(TestCase):
# Check time and weight # Check time and weight
for object in (speaker1_item1, speaker2_item1, speaker1_item2): 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_item1.weight, 1)
self.assertEqual(speaker1_item2.weight, 1) self.assertEqual(speaker1_item2.weight, 1)
self.assertEqual(speaker2_item1.weight, 2) self.assertEqual(speaker2_item1.weight, 2)
@ -52,13 +53,25 @@ class ListOfSpeakerModelTests(TestCase):
self.item1.save() self.item1.save()
self.assertTrue(Item.objects.get(pk=self.item1.pk).speaker_list_closed) 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) speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1)
self.assertIsNone(speaker1_item1.begin_time)
self.assertIsNone(speaker1_item1.time) self.assertIsNone(speaker1_item1.end_time)
speaker1_item1.speak() speaker1_item1.begin_speach()
self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).time) self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).begin_time)
self.assertIsNone(Speaker.objects.get(pk=speaker1_item1.pk).weight) 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): class SpeakerViewTestCase(TestCase):
@ -160,7 +173,21 @@ class TestSpeakerSpeakView(SpeakerViewTestCase):
speaker = Speaker.objects.add(self.speaker1, self.item1) speaker = Speaker.objects.add(self.speaker1, self.item1)
response = self.check_url(url, self.admin_client, 302) response = self.check_url(url, self.admin_client, 302)
speaker = Speaker.objects.get(pk=speaker.pk) 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) self.assertIsNone(speaker.weight)