Add second time field for list of speakers. Now you can click begin and end speach.
This commit is contained in:
parent
0df5c5a2e6
commit
0b85164b84
@ -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.
|
||||
"""
|
||||
|
||||
|
@ -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)))
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -77,42 +77,40 @@
|
||||
{% if old_speakers %}
|
||||
<div class="well">
|
||||
<b>{% trans "Last speakers" %}:</b>
|
||||
{% if old_speakers|length > 1 %}
|
||||
<div class="btn-group" data-toggle="buttons-checkbox">
|
||||
<button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#all_speakers">
|
||||
{% trans "Show all speakers" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<br>
|
||||
<div class="btn-group" data-toggle="buttons-checkbox">
|
||||
<button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#all_speakers">
|
||||
{% trans "Show all speakers" %}
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<div id="all_speakers" class="collapse out">
|
||||
{% for speaker in old_speakers %}
|
||||
{% if not forloop.last %}
|
||||
<small>{{forloop.counter}}.
|
||||
[{{ speaker.time }}h]
|
||||
<a href="{% model_url speaker %}">{{ speaker }}</a>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</small><br>
|
||||
{% endif %}
|
||||
<small>{{forloop.counter}}.
|
||||
[{{ speaker.begin_time }} – {{ speaker.end_time }}]
|
||||
<a href="{% model_url speaker %}">{{ speaker }}</a>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</small><br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if old_speakers %}
|
||||
{% with last=old_speakers|last %}
|
||||
<small>{{ old_speakers|length }}.
|
||||
[{{ last.time }}h]
|
||||
<a href="{% model_url last %}">{{ last }}</a>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
<a href="{% model_url last 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</small>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if actual_speaker %}
|
||||
<div class="well">
|
||||
<b>{% trans 'Actual speaker' %}:</b><br />
|
||||
[{{ actual_speaker.begin_time }}]
|
||||
<a href="{% model_url actual_speaker %}">{{ actual_speaker }}</a>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
<a href="{% url 'agenda_speaker_end_speach' item.pk %}" class="btn btn-mini"><i class="icon-bell"></i> {% trans 'End speach' %}</a>
|
||||
<a href="{% model_url actual_speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
|
||||
<i class="icon-remove"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -128,11 +126,10 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<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 %}>
|
||||
{% for speaker in speakers %}
|
||||
{% for speaker in coming_speakers %}
|
||||
<li id="speaker_{{ speaker.pk }}">
|
||||
<span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span>
|
||||
<span>{{ forloop.counter }}.</span>
|
||||
@ -150,7 +147,7 @@
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
{% 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>
|
||||
|
@ -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 %}
|
||||
<h1>{{ title }}</h1>
|
||||
@ -10,17 +10,18 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scrollcontent %}
|
||||
{% if old_speakers|length > 0 %}
|
||||
{% if old_speakers|length > 0 or actual_speaker %}
|
||||
<ul class="list_of_speakers last_speakers">
|
||||
{% for speaker in old_speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
{% endfor %}
|
||||
<li>{{ actual_speaker }} – {% trans 'Actual Speaker' %}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if speakers %}
|
||||
{% if coming_speakers %}
|
||||
<ol class="list_of_speakers">
|
||||
{% for speaker in speakers %}
|
||||
{% for speaker in coming_speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
@ -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<pk>\d+)/speaker/end_speach/$',
|
||||
SpeakerEndSpeachView.as_view(),
|
||||
name='agenda_speaker_end_speach',
|
||||
),
|
||||
|
||||
url(r'^(?P<pk>\d+)/speaker/change_order/$',
|
||||
SpeakerChangeOrderView.as_view(),
|
||||
name='agenda_speaker_change_order',
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user