Merge pull request #625 from normanjaeckel/ListOfSpeakersRework
Add second time field for list of speakers.
This commit is contained in:
commit
8af50a36cc
@ -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.
|
||||
"""
|
||||
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
@ -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(),
|
||||
data = {'title': self.get_title(),
|
||||
'items': self.get_children(),
|
||||
'template': 'projector/AgendaSummary.html',
|
||||
}
|
||||
'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,
|
||||
data = {'item': self,
|
||||
'title': self.get_title(),
|
||||
'template': 'projector/AgendaText.html',
|
||||
}
|
||||
'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()
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -1,13 +1,57 @@
|
||||
{% 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">
|
||||
{% if speakers %}
|
||||
{% if list_of_speakers %}
|
||||
<h3>{% trans "List of speakers" %}:</h3>
|
||||
<ol>
|
||||
{% for speaker in speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
<ul>
|
||||
{% 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 %}
|
||||
</ol>
|
||||
</ul>
|
||||
{% else %}
|
||||
<i>{% trans 'The list of speakers is empty.' %}</i>
|
||||
{% endif %}
|
||||
|
@ -70,54 +70,11 @@
|
||||
<i class="icon icon-facetime-video {% if item.active and show_list %}icon-white{% endif %}"></i>
|
||||
{% trans 'Show list' %}
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
</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 %}
|
||||
<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>
|
||||
<p>{% trans "Do you want to save the changed order of speakers?" %}</p>
|
||||
<input id="sort_order" name="sort_order" type="hidden"></hidden>
|
||||
@ -128,29 +85,55 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="well">
|
||||
<b>{% trans "Next speakers:" %}</b>
|
||||
<ul {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}>
|
||||
{% for speaker in speakers %}
|
||||
<li id="speaker_{{ speaker.pk }}">
|
||||
<div id="complete_list_of_speakers" class="well">
|
||||
{% for speaker_dict in list_of_speakers %}
|
||||
{% if speaker_dict.first_in_group %}
|
||||
{% if speaker_dict.type == 'old_speaker' %}
|
||||
<b>{% trans "Last speakers" %}:</b>
|
||||
<div class="btn-group" data-toggle="buttons-checkbox">
|
||||
<button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#old_speakers">
|
||||
{% trans "Show all speakers" %}
|
||||
</button>
|
||||
</div>
|
||||
{% elif speaker_dict.type == 'actual_speaker' %}
|
||||
<b>{% trans 'Actual speaker' %}:</b>
|
||||
{% else %}
|
||||
<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 id="speaker_{{ speaker_dict.speaker.pk }}">
|
||||
{% if speaker_dict.type == 'coming_speaker' %}
|
||||
<span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span>
|
||||
<span>{{ forloop.counter }}.</span>
|
||||
<a href="{% model_url speaker %}">{{ speaker }}</a>
|
||||
{{ 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 %}
|
||||
<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>
|
||||
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<i>{% trans "The list of speakers is empty." %}</i>
|
||||
{% endfor %}
|
||||
{% if speaker_dict.last_in_group %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<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>
|
||||
@ -174,6 +157,6 @@
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% extends "base-projector.html" %}
|
||||
{% extends 'base-projector.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block title %}{{ block.super }} - {{ item }}{% endblock %}
|
||||
{% block title %}{{ block.super }} – {{ item }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
@ -10,21 +11,36 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scrollcontent %}
|
||||
{% if old_speakers|length > 0 %}
|
||||
<ul class="list_of_speakers last_speakers">
|
||||
{% for speaker in old_speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
<style type="text/css">
|
||||
ul#list_of_speakers {
|
||||
list-style-type: None;
|
||||
}
|
||||
#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>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if speakers %}
|
||||
<ol class="list_of_speakers">
|
||||
{% for speaker in speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p><i>{% trans 'The list of speakers is empty.' %}</i></p>
|
||||
<i>{% trans 'The list of speakers is empty.' %}</i>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -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
|
||||
@ -37,7 +37,7 @@ from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm
|
||||
|
||||
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'
|
||||
template_name = 'agenda/overview.html'
|
||||
@ -127,15 +127,11 @@ 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'))
|
||||
list_of_speakers = self.object.get_list_of_speakers()
|
||||
kwargs.update({
|
||||
'object': self.object,
|
||||
'speakers': speakers,
|
||||
'old_speakers': old_speakers,
|
||||
'is_speaker': Speaker.objects.filter(
|
||||
time=None, person=self.request.user, item=self.object).exists(),
|
||||
'list_of_speakers': list_of_speakers,
|
||||
'is_on_the_list_of_speakers': Speaker.objects.filter(item=self.object, 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)
|
||||
@ -288,7 +284,7 @@ class SpeakerDeleteView(DeleteView):
|
||||
if 'speaker' in kwargs:
|
||||
return request.user.has_perm('agenda.can_manage_agenda')
|
||||
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
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
@ -334,15 +330,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]
|
||||
|
@ -73,19 +73,6 @@ body{
|
||||
width: 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 ***/
|
||||
#contentwrapper {
|
||||
|
@ -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