Merge pull request #625 from normanjaeckel/ListOfSpeakersRework

Add second time field for list of speakers.
This commit is contained in:
Oskar Hahn 2013-05-08 08:21:08 -07:00
commit 8af50a36cc
13 changed files with 380 additions and 200 deletions

View File

@ -6,9 +6,9 @@
The OpenSlides agenda app appends the functionality to OpenSlides to The OpenSlides agenda app appends the functionality to OpenSlides to
manage agendas. 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) 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.
""" """
@ -99,10 +99,44 @@ class Item(MPTTModel, SlideMixin):
True, if the list of speakers is closed. 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): 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) object = get_slide_from_sid(self.related_sid, element=True)
if object is None: if object is None:
self.title = 'Item for deleted slide: %s' % self.related_sid self.title = 'Item for deleted slide: %s' % self.related_sid
@ -114,7 +148,7 @@ class Item(MPTTModel, SlideMixin):
def get_related_type(self): 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 return self.get_related_slide().prefix
@ -129,7 +163,7 @@ class Item(MPTTModel, SlideMixin):
def get_title(self): def get_title(self):
""" """
return the title of this item. Return the title of this item.
""" """
if self.related_sid is None: if self.related_sid is None:
return self.title return self.title
@ -137,7 +171,7 @@ class Item(MPTTModel, SlideMixin):
def get_title_supplement(self): def get_title_supplement(self):
""" """
return a supplement for the title. Return a supplement for the title.
""" """
if self.related_sid is None: if self.related_sid is None:
return '' return ''
@ -148,30 +182,36 @@ class Item(MPTTModel, SlideMixin):
def slide(self): 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': if config['presentation_argument'] == 'summary':
data = { data = {'title': self.get_title(),
'title': self.get_title(), 'items': self.get_children(),
'items': self.get_children(), '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') list_of_speakers = self.get_list_of_speakers(
old_speakers = Speaker.objects.filter(item=self.pk).exclude(time=None).order_by('time') old_speakers_count=config['agenda_show_last_speakers'])
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, 'list_of_speakers': list_of_speakers}
'old_speakers': old_speakers[slice_items:]}
elif self.related_sid: elif self.related_sid:
data = self.get_related_slide().slide() data = self.get_related_slide().slide()
else: else:
data = { data = {'item': self,
'item': self, 'title': self.get_title(),
'title': self.get_title(), 'template': 'projector/AgendaText.html'}
'template': 'projector/AgendaText.html',
}
return data return data
def set_closed(self, closed=True): def set_closed(self, closed=True):
@ -209,44 +249,72 @@ class Item(MPTTModel, SlideMixin):
super(Item, self).delete() super(Item, self).delete()
Item.objects.rebuild() 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 Returns the list of speakers as a list of dictionaries. Each
slide dictionary contains a prefix, the speaker and its type. Types
are old_speaker, actual_speaker and coming_speaker.
link can be:
* view
* edit
* delete
""" """
if link == 'view': speaker_query = Speaker.objects.filter(item=self)
if self.related_sid: list_of_speakers = []
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 __unicode__(self): # Parse old speakers
return self.get_title() 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: # Parse actual speaker
permissions = ( try:
('can_see_agenda', ugettext_noop("Can see agenda")), actual_speaker = speaker_query.filter(end_time=None).exclude(begin_time=None).get()
('can_manage_agenda', ugettext_noop("Can manage agenda")), except Speaker.DoesNotExist:
('can_see_orga_items', ugettext_noop("Can see orga items and time scheduling of agenda")), pass
) else:
list_of_speakers.append({
'prefix': '0',
'speaker': actual_speaker,
'type': 'actual_speaker',
'first_in_group': True,
'last_in_group': True})
class MPTTMeta: # Parse coming speakers
order_insertion_by = ['weight'] 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): 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 +338,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 +368,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

@ -12,11 +12,11 @@
from django.dispatch import receiver from django.dispatch import receiver
from django import forms 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 django.template.loader import render_to_string
from openslides.config.signals import config_signal 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.signals import projector_overlays
from openslides.projector.projector import Overlay from openslides.projector.projector import Overlay
@ -40,8 +40,15 @@ def setup_agenda_config_page(sender, **kwargs):
form_field=forms.CharField( form_field=forms.CharField(
widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M'), widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M'),
required=False, required=False,
label=ugettext_lazy('Begin of event'), label=_('Begin of event'),
help_text=ugettext_lazy('Input format: DD.MM.YYYY HH:MM'))) 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_stylefiles = ['styles/timepicker.css', 'styles/jquery-ui/jquery-ui.custom.min.css']
extra_javascript = ['javascript/jquery-ui.custom.min.js', extra_javascript = ['javascript/jquery-ui.custom.min.js',
@ -53,7 +60,7 @@ def setup_agenda_config_page(sender, **kwargs):
url='agenda', url='agenda',
required_permission='config.can_manage', required_permission='config.can_manage',
weight=20, 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_context={'extra_stylefiles': extra_stylefiles,
'extra_javascript': extra_javascript}) 'extra_javascript': extra_javascript})
@ -80,8 +87,10 @@ 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] list_of_speakers = slide.get_list_of_speakers(
context = {'speakers': 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 render_to_string('agenda/overlay_speaker_projector.html', context)
return Overlay(name, get_widget_html, get_projector_html) return Overlay(name, get_widget_html, get_projector_html)

View File

@ -24,8 +24,8 @@ function hideClosedSlides(hide) {
return false; return false;
} }
$('#speaker_list_changed_form').submit(function() { $('#coming_speakers_changed_form').submit(function() {
$('#sort_order').val($('#list_of_speakers').sortable("toArray")); $('#sort_order').val($('#coming_speakers').sortable("toArray"));
}); });
$(function() { $(function() {
@ -76,10 +76,10 @@ $(function() {
//# $('#hide_closed_items').attr('checked', true); //# $('#hide_closed_items').attr('checked', true);
//# } //# }
if ($('#list_of_speakers').length > 0) { if ($('#coming_speakers').length > 0) {
$('#list_of_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) { $('#coming_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) {
$('#speaker_list_changed_form').show(); $('#coming_speakers_changed_form').show();
}}); }});
$('#list_of_speakers').disableSelection(); $('#coming_speakers').disableSelection();
} }
}); });

View File

@ -22,15 +22,15 @@ table#agendatime td {
white-space: nowrap; white-space: nowrap;
} }
#list_of_speakers li { div#complete_list_of_speakers li {
line-height: 30px;
}
#list_of_speakers {
list-style-type: none; 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; position: absolute;
margin-left: -15px; margin-left: -15px;
margin-top: 6px; margin-top: 6px;

View File

@ -1,13 +1,57 @@
{% load i18n %} {% load i18n %}
{% load tags %}
<style type="text/css">
#overlay_list_of_speaker_box {
position: fixed;
bottom: 0;
right: 0;
border-radius: 0.4em;
border: 0.1em solid #777777;
background-color: #cccccc;
opacity: 0.9;
padding: 0 .5em;
margin: 1em;
z-index: 2;
width: 30%;
min-width: 200px;
}
#overlay_list_of_speaker_box h3 {
margin: 5px;
}
#overlay_list_of_speaker_box ul {
list-style-type: None;
padding-left: 0;
margin: 5px;
}
#overlay_list_of_speaker_box li {
margin-bottom: 1px;
}
#overlay_list_of_speaker_box li span.number {
display: block;
float: left;
width: 1.3em;
}
</style>
<div id="overlay_list_of_speaker_box"> <div id="overlay_list_of_speaker_box">
{% if speakers %} {% if list_of_speakers %}
<h3>{% trans "List of speakers" %}:</h3> <h3>{% trans "List of speakers" %}:</h3>
<ol> <ul>
{% for speaker in speakers %} {% for speaker_dict in list_of_speakers %}
<li>{{ speaker }}</li> <li>
{% if speaker_dict.type == 'actual_speaker' %}
<span class="number"></span><strong>
{% else %}
<span class="number">{{ speaker_dict.prefix }}</span>
{% endif %}
<span class="name">{{ speaker_dict.speaker }}</span>
{% if speaker_dict.type == 'actual_speaker' %}
</strong>
{% endif %}
</li>
{% endfor %} {% endfor %}
</ol> </ul>
{% else %} {% else %}
<i>{% trans 'The list of speakers is empty.' %}</i> <i>{% trans 'The list of speakers is empty.' %}</i>
{% endif %} {% endif %}

View File

@ -70,54 +70,11 @@
<i class="icon icon-facetime-video {% if item.active and show_list %}icon-white{% endif %}"></i> <i class="icon icon-facetime-video {% if item.active and show_list %}icon-white{% endif %}"></i>
{% trans 'Show list' %} {% trans 'Show list' %}
</a> </a>
{% endif %} {% endif %}
</p> </p>
{% 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 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 %}
{% 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 perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
<form id="speaker_list_changed_form" action="{% url 'agenda_speaker_change_order' item.pk %}" method="post" style="display:none" class="alert alert-warning">{% csrf_token %} <form id="coming_speakers_changed_form" action="{% url 'agenda_speaker_change_order' item.pk %}" method="post" style="display:none" class="alert alert-warning">{% csrf_token %}
<button type="button" class="close" data-dismiss="alert">×</button> <button type="button" class="close" data-dismiss="alert">×</button>
<p>{% trans "Do you want to save the changed order of speakers?" %}</p> <p>{% trans "Do you want to save the changed order of speakers?" %}</p>
<input id="sort_order" name="sort_order" type="hidden"></hidden> <input id="sort_order" name="sort_order" type="hidden"></hidden>
@ -128,29 +85,55 @@
</form> </form>
{% endif %} {% endif %}
<div id="complete_list_of_speakers" class="well">
<div class="well"> {% for speaker_dict in list_of_speakers %}
<b>{% trans "Next speakers:" %}</b> {% if speaker_dict.first_in_group %}
<ul {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}> {% if speaker_dict.type == 'old_speaker' %}
{% for speaker in speakers %} <b>{% trans "Last speakers" %}:</b>
<li id="speaker_{{ speaker.pk }}"> <div class="btn-group" data-toggle="buttons-checkbox">
<span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span> <button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#old_speakers">
<span>{{ forloop.counter }}.</span> {% trans "Show all speakers" %}
<a href="{% model_url speaker %}">{{ speaker }}</a> </button>
{% if perms.agenda.can_manage_agenda %} </div>
<a href="{% url 'agenda_speaker_speak' item.pk speaker.person.person_id %}" class="btn btn-mini"><i class="icon-bell"></i> {% trans "Next speaker" %}</a> {% elif speaker_dict.type == 'actual_speaker' %}
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini"> <b>{% trans 'Actual speaker' %}:</b>
<i class="icon-remove"></i> {% else %}
</a> <b>{% trans "Next speakers" %}:</b>
{% endif %}
<ul
{% if speaker_dict.type == 'old_speaker' %}
id="old_speakers" class="collapse out"
{% elif speaker_dict.type == 'coming_speaker' %}
id="coming_speakers"
{% endif %} {% endif %}
</li> >
{% empty %} {% endif %}
<i>{% trans "The list of speakers is empty." %}</i> <li id="speaker_{{ speaker_dict.speaker.pk }}">
{% endfor %} {% if speaker_dict.type == 'coming_speaker' %}
</ul> <span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span>
{{ speaker_dict.prefix }}.
{% else %}
[{{ speaker_dict.speaker.begin_time }}{% if speaker_dict.type == 'old_speaker' %} {{ speaker_dict.speaker.end_time }}{% endif %}]
{% endif %}
<a href="{% model_url speaker_dict.speaker %}">{{ speaker_dict.speaker }}</a>
{% if perms.agenda.can_manage_agenda %}
{% if speaker_dict.type == 'actual_speaker' %}
<a href="{% url 'agenda_speaker_end_speach' item.pk %}" class="btn btn-mini"><i class="icon-bell"></i> {% trans 'End speach' %}</a>
{% elif speaker_dict.type == 'coming_speaker' %}
<a href="{% url 'agenda_speaker_speak' item.pk speaker_dict.speaker.person.person_id %}" class="btn btn-mini"><i class="icon-bell"></i> {% trans "Next speaker" %}</a>
{% endif %}
<a href="{% model_url speaker_dict.speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</li>
{% if speaker_dict.last_in_group %}
</ul>
{% endif %}
{% endfor %}
<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>
@ -174,6 +157,6 @@
{% endfor %} {% endfor %}
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,9 @@
{% extends "base-projector.html" %} {% extends 'base-projector.html' %}
{% load i18n %} {% load i18n %}
{% load tags %}
{% block title %}{{ block.super }} - {{ item }}{% endblock %} {% block title %}{{ block.super }} {{ item }}{% endblock %}
{% block content %} {% block content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
@ -10,21 +11,36 @@
{% endblock %} {% endblock %}
{% block scrollcontent %} {% block scrollcontent %}
{% if old_speakers|length > 0 %} <style type="text/css">
<ul class="list_of_speakers last_speakers"> ul#list_of_speakers {
{% for speaker in old_speakers %} list-style-type: None;
<li>{{ speaker }}</li> }
{% endfor %} #overlay_list_of_speaker_box li {
</ul> margin-bottom: 1px;
{% endif %} }
#overlay_list_of_speaker_box li span.number {
{% if speakers %} display: block;
<ol class="list_of_speakers"> float: left;
{% for speaker in speakers %} width: 1.3em;
<li>{{ speaker }}</li> }
</style>
{% if list_of_speakers %}
<ul id="list_of_speakers">
{% for speaker_dict in list_of_speakers %}
<li>
{% if speaker_dict.type == 'actual_speaker' %}
<span class="number"></span><strong>
{% else %}
<span class="number">{{ speaker_dict.prefix }} </span>
{% endif %}
<span class="name">{{ speaker_dict.speaker }}</span>
{% if speaker_dict.type == 'actual_speaker' %}
</strong>
{% endif %}
</li>
{% endfor %} {% endfor %}
</ol> </ul>
{% else %} {% else %}
<p><i>{% trans 'The list of speakers is empty.' %}</i></p> <i>{% trans 'The list of speakers is empty.' %}</i>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

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
@ -37,7 +37,7 @@ from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm
class Overview(TemplateView): class Overview(TemplateView):
""" """
Show all agenda items, and update there range via post. Show all agenda items, and update their range via post.
""" """
permission_required = 'agenda.can_see_agenda' permission_required = 'agenda.can_see_agenda'
template_name = 'agenda/overview.html' template_name = 'agenda/overview.html'
@ -127,15 +127,11 @@ 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') list_of_speakers = self.object.get_list_of_speakers()
old_speakers = list(Speaker.objects.filter(item=self.object.pk)
.exclude(time=None).order_by('time'))
kwargs.update({ kwargs.update({
'object': self.object, 'object': self.object,
'speakers': speakers, 'list_of_speakers': list_of_speakers,
'old_speakers': old_speakers, 'is_on_the_list_of_speakers': Speaker.objects.filter(item=self.object, begin_time=None, person=self.request.user).exists(),
'is_speaker': Speaker.objects.filter(
time=None, person=self.request.user, item=self.object).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)
@ -288,7 +284,7 @@ class SpeakerDeleteView(DeleteView):
if 'speaker' in kwargs: if 'speaker' in kwargs:
return request.user.has_perm('agenda.can_manage_agenda') return request.user.has_perm('agenda.can_manage_agenda')
else: else:
# Any person how is on the list of speakers can delete him self from the list # Any person who is on the list of speakers can delete himself from the list.
return True return True
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@ -334,15 +330,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

@ -73,19 +73,6 @@ body{
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#overlay_list_of_speaker_box {
position: fixed;
bottom: 0;
right: 0;
border-radius: 0.4em;
border: 0.1em solid #777777;
background-color: #cccccc;
opacity: 0.9;
padding: 0 1em;
margin: 1em;
z-index: 2;
width: 50%;
}
/*** CONTENT ***/ /*** CONTENT ***/
#contentwrapper { #contentwrapper {

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)