List of speakers. Part 2

This commit is contained in:
Oskar Hahn 2013-03-18 12:34:47 +01:00
parent 1a416e5726
commit 33f74c3025
16 changed files with 247 additions and 49 deletions

View File

@ -61,6 +61,9 @@ class ItemOrderForm(CssClassMixin, forms.Form):
class AppendSpeakerForm(CssClassMixin, forms.Form): class AppendSpeakerForm(CssClassMixin, forms.Form):
"""
Form to set an user to a list of speakers.
"""
speaker = PersonFormField( speaker = PersonFormField(
widget=forms.Select(attrs={'class': 'medium-input'}), widget=forms.Select(attrs={'class': 'medium-input'}),
label=ugettext_lazy("Add participant")) label=ugettext_lazy("Add participant"))
@ -70,6 +73,9 @@ class AppendSpeakerForm(CssClassMixin, forms.Form):
return super(AppendSpeakerForm, self).__init__(*args, **kwargs) return super(AppendSpeakerForm, self).__init__(*args, **kwargs)
def clean_speaker(self): def clean_speaker(self):
"""
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, time=None).exists():
raise forms.ValidationError(ugettext_lazy( raise forms.ValidationError(ugettext_lazy(

View File

@ -42,23 +42,35 @@ class Item(MPTTModel, SlideMixin):
(ORGANIZATIONAL_ITEM, _('Organizational item'))) (ORGANIZATIONAL_ITEM, _('Organizational item')))
title = models.CharField(null=True, max_length=255, verbose_name=_("Title")) title = models.CharField(null=True, max_length=255, verbose_name=_("Title"))
"""Title of the agenda item.""" """
Title of the agenda item.
"""
text = models.TextField(null=True, blank=True, verbose_name=_("Text")) text = models.TextField(null=True, blank=True, verbose_name=_("Text"))
"""The optional text of the agenda item.""" """
The optional text of the agenda item.
"""
comment = models.TextField(null=True, blank=True, verbose_name=_("Comment")) comment = models.TextField(null=True, blank=True, verbose_name=_("Comment"))
"""Optional comment to the agenda item. Will not be shoun to normal users.""" """
Optional comment to the agenda item. Will not be shoun to normal users.
"""
closed = models.BooleanField(default=False, verbose_name=_("Closed")) closed = models.BooleanField(default=False, verbose_name=_("Closed"))
"""Flag, if the item is finished.""" """
Flag, if the item is finished.
"""
weight = models.IntegerField(default=0, verbose_name=_("Weight")) weight = models.IntegerField(default=0, verbose_name=_("Weight"))
"""Weight to sort the item in the agenda.""" """
Weight to sort the item in the agenda.
"""
parent = TreeForeignKey('self', null=True, blank=True, parent = TreeForeignKey('self', null=True, blank=True,
related_name='children') related_name='children')
"""The parent item in the agenda tree.""" """
The parent item in the agenda tree.
"""
type = models.IntegerField(max_length=1, choices=ITEM_TYPE, type = models.IntegerField(max_length=1, choices=ITEM_TYPE,
default=AGENDA_ITEM, verbose_name=_("Type")) default=AGENDA_ITEM, verbose_name=_("Type"))
@ -70,7 +82,9 @@ class Item(MPTTModel, SlideMixin):
duration = models.CharField(null=True, blank=True, max_length=5, duration = models.CharField(null=True, blank=True, max_length=5,
verbose_name=_("Duration (hh:mm)")) verbose_name=_("Duration (hh:mm)"))
"""The intended duration for the topic.""" """
The intended duration for the topic.
"""
related_sid = models.CharField(null=True, blank=True, max_length=63) related_sid = models.CharField(null=True, blank=True, max_length=63)
""" """
@ -230,7 +244,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, time=None).exists():
raise OpenSlidesError(_('%s is allready on the list of speakers from item %d') % (person, item.id)) raise OpenSlidesError(_('%s is already on the list of speakers of item %d.') % (person, 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)
return self.create(item=item, person=person, weight=weight + 1) return self.create(item=item, person=person, weight=weight + 1)
@ -244,9 +258,24 @@ class Speaker(models.Model):
objects = SpeakerManager() objects = SpeakerManager()
person = PersonField() person = PersonField()
"""
ForeinKey to the person who speaks.
"""
item = models.ForeignKey(Item) item = models.ForeignKey(Item)
time = models.TimeField(null=True) """
ForeinKey to the AgendaItem to which the person want to speak.
"""
time = models.DateTimeField(null=True)
"""
Saves the time, when the speaker has spoken. None, if he has not spoken yet.
"""
weight = models.IntegerField(null=True) weight = models.IntegerField(null=True)
"""
The sort order of the list of speakers. None, if he has already spoken.
"""
class Meta: class Meta:
permissions = ( permissions = (
@ -264,6 +293,11 @@ class Speaker(models.Model):
args=[self.item.pk, self.pk]) args=[self.item.pk, self.pk])
def speak(self): def speak(self):
"""
Let the person speak.
Set the weight to None and the time to now.
"""
self.weight = None self.weight = None
self.time = datetime.now() self.time = datetime.now()
self.save() self.save()

View File

@ -13,10 +13,17 @@
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, ugettext as _ 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.signals import config_signal
from openslides.config.api import ConfigVariable, ConfigPage from openslides.config.api import ConfigVariable, ConfigPage
from openslides.projector.signals import projector_overlays
from openslides.projector.projector import Overlay
from openslides.projector.api import get_active_slide, get_slide_from_sid
from .models import Speaker, Item
# TODO: Reinsert the datepicker scripts in the template # TODO: Reinsert the datepicker scripts in the template
@ -48,3 +55,31 @@ def setup_agenda_config_page(sender, **kwargs):
variables=(agenda_start_event_date_time,), variables=(agenda_start_event_date_time,),
extra_context={'extra_stylefiles': extra_stylefiles, extra_context={'extra_stylefiles': extra_stylefiles,
'extra_javascript': extra_javascript}) 'extra_javascript': extra_javascript})
@receiver(projector_overlays, dispatch_uid="agenda_list_of_speakers")
def agenda_list_of_speakers(sender, **kwargs):
"""
Receiver for the list of speaker overlay.
"""
name = 'agenda_speaker'
def get_widget_html():
"""
Returns the the html-code to show in the overly-widget.
"""
return render_to_string('agenda/overlay_speaker_widget.html')
def get_projector_html():
"""
Returns an html-code to show on the projector.
"""
slide = get_slide_from_sid(get_active_slide(only_sid=True), element=True)
if not isinstance(slide, Item):
# Only show list of speakers on Agenda-Items
return None
speakers = Speaker.objects.filter(time=None, item=slide)[:5]
context = {'speakers': speakers}
return render_to_string('agenda/overlay_speaker_projector.html', context)
return Overlay(name, get_widget_html, get_projector_html)

View File

@ -25,3 +25,13 @@ table#agendatime td {
#list_of_speakers li { #list_of_speakers li {
line-height: 30px; line-height: 30px;
} }
#list_of_speakers {
list-style-type: none;
}
#list_of_speakers span.ui-icon {
position: absolute;
margin-left: -15px;
margin-top: 6px;
}

View File

@ -0,0 +1,29 @@
{% load i18n %}
<style type="text/css">
#overlay_speaker_inner {
position: fixed;
bottom: 0;
right: 0;
border-radius: 0.2em;
background-color: #cccccc;
opacity: 0.6;
padding: 0.2em 0;
margin: 1em;
z-index:2;
padding: 3px;
}
</style>
<div id="overlay_speaker_inner">
{% if speakers %}
<header>{% trans "List of speakers:" %}</header>
<ol>
{% for speaker in speakers %}
<li>{{ speaker }}</li>
{% endfor %}
</ol>
{% else %}
<i>{% trans 'The list of speakers is empty.' %}</i>
{% endif %}
</div>

View File

@ -0,0 +1,6 @@
{% load i18n %}
{% load tags %}
<span>
{% trans "List of speakers" %}
</span>

View File

@ -0,0 +1,6 @@
{% load i18n %}
{% load tags %}
<div>
<a href="{% url 'agenda_current_list_of_speakers' %}">{% trans 'Put me on the current list of speakers' %}</a>
</div>

View File

@ -8,6 +8,8 @@
{% block header %} {% block header %}
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/agenda.css' %}" /> <link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/agenda.css' %}" />
{% comment %} TODO: include stylesheet in our repo{% endcomment %}
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css" />
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}
@ -59,7 +61,7 @@
<p> <p>
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
{% if item.speaker_list_closed %} {% if item.speaker_list_closed %}
<a href="{% url 'agenda_speaker_open' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Open list' %}</a> <a href="{% url 'agenda_speaker_reopen' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Open list' %}</a>
{% else %} {% else %}
<a href="{% url 'agenda_speaker_close' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Close list' %}</a> <a href="{% url 'agenda_speaker_close' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Close list' %}</a>
{% endif %} {% endif %}
@ -130,9 +132,11 @@
<div class="well"> <div class="well">
<b>{% trans "Next speakers:" %}</b> <b>{% trans "Next speakers:" %}</b>
<ol {% 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 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>{{ forloop.counter }}.</span>
<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="{% url 'agenda_speaker_speak' item.pk speaker.person.person_id %}" class="btn btn-mini"><i class="icon-bell"></i> Finished speech</a> <a href="{% url 'agenda_speaker_speak' item.pk speaker.person.person_id %}" class="btn btn-mini"><i class="icon-bell"></i> Finished speech</a>
@ -144,7 +148,7 @@
{% empty %} {% empty %}
<i>{% trans "The list of speakers is empty." %}</i> <i>{% trans "The list of speakers is empty." %}</i>
{% endfor %} {% endfor %}
</ol> </ul>
<p> <p>
{% if is_speaker %} {% if is_speaker %}

View File

@ -4,6 +4,16 @@
{% block title %}{{ block.super }} - {{ item }}{% endblock %} {% block title %}{{ block.super }} - {{ item }}{% endblock %}
{% block header %}
<style type="text/css">
#list_of_speakers li
{
font-size: 130%;
line-height: 160%;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
{% endblock %} {% endblock %}

View File

@ -14,7 +14,7 @@ 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,
ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView, ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView,
SpeakerListOpenView, SpeakerChangeOrderView) SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView)
urlpatterns = patterns( urlpatterns = patterns(
'', '',
@ -66,14 +66,14 @@ urlpatterns = patterns(
name='agenda_speaker_append', name='agenda_speaker_append',
), ),
url(r'^(?P<pk>\d+)/speaker/open/$', url(r'^(?P<pk>\d+)/speaker/close/$',
SpeakerListOpenView.as_view(open_list=True), SpeakerListCloseView.as_view(),
name='agenda_speaker_open', name='agenda_speaker_close',
), ),
url(r'^(?P<pk>\d+)/speaker/close/$', url(r'^(?P<pk>\d+)/speaker/reopen/$',
SpeakerListOpenView.as_view(), SpeakerListCloseView.as_view(reopen=True),
name='agenda_speaker_close', name='agenda_speaker_reopen',
), ),
url(r'^(?P<pk>\d+)/speaker/del/$', url(r'^(?P<pk>\d+)/speaker/del/$',
@ -91,8 +91,13 @@ urlpatterns = patterns(
name='agenda_speaker_speak', name='agenda_speaker_speak',
), ),
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',
), ),
url(r'^list_of_speakers/$',
CurrentListOfSpeakersView.as_view(),
name='agenda_current_list_of_speakers',
),
) )

View File

@ -29,7 +29,7 @@ from openslides.utils.views import (
DetailView, FormView, SingleObjectMixin) DetailView, FormView, SingleObjectMixin)
from openslides.utils.template import Tab from openslides.utils.template import Tab
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.projector.api import get_active_slide from openslides.projector.api import get_active_slide, get_slide_from_sid
from openslides.projector.projector import Widget, SLIDE from openslides.projector.projector import Widget, SLIDE
from .models import Item, Speaker from .models import Item, Speaker
from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm
@ -128,7 +128,8 @@ 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') speakers = Speaker.objects.filter(time=None, item=self.object.pk).order_by('weight')
old_speakers = list(Speaker.objects.exclude(time=None).order_by('time')) 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, 'speakers': speakers,
@ -258,7 +259,6 @@ class SpeakerAppendView(SingleObjectMixin, RedirectView):
""" """
Set the request.user to the speaker list. Set the request.user to the speaker list.
""" """
permission_required = 'agenda.can_be_speaker' permission_required = 'agenda.can_be_speaker'
url_name = 'item_view' url_name = 'item_view'
model = Item model = Item
@ -278,7 +278,6 @@ class SpeakerDeleteView(DeleteView):
""" """
Delete the request.user or a specific user from the speaker list. Delete the request.user or a specific user from the speaker list.
""" """
success_url_name = 'item_view' success_url_name = 'item_view'
question_url_name = 'item_view' question_url_name = 'item_view'
@ -324,7 +323,7 @@ class SpeakerDeleteView(DeleteView):
class SpeakerSpeakView(SingleObjectMixin, RedirectView): class SpeakerSpeakView(SingleObjectMixin, RedirectView):
""" """
Mark a speaker, that he can speak. Mark the speaking person.
""" """
permission_required = 'agenda.can_manage_agenda' permission_required = 'agenda.can_manage_agenda'
url_name = 'item_view' url_name = 'item_view'
@ -347,18 +346,18 @@ class SpeakerSpeakView(SingleObjectMixin, RedirectView):
return [self.object.pk] return [self.object.pk]
class SpeakerListOpenView(SingleObjectMixin, RedirectView): class SpeakerListCloseView(SingleObjectMixin, RedirectView):
""" """
View to open and close a list of speakers. View to close and reopen a list of speakers.
""" """
permission_required = 'agenda.can_manage_agenda' permission_required = 'agenda.can_manage_agenda'
model = Item model = Item
open_list = False reopen = False
url_name = 'item_view' url_name = 'item_view'
def pre_redirect(self, *args, **kwargs): def pre_redirect(self, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.speaker_list_closed = not self.open_list self.object.speaker_list_closed = not self.reopen
self.object.save() self.object.save()
def get_url_name_args(self): def get_url_name_args(self):
@ -402,11 +401,63 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView):
speaker.save() speaker.save()
else: else:
transaction.commit() transaction.commit()
return None
messages.error(request, _('Could not change order. Invalid data.'))
def get_url_name_args(self): def get_url_name_args(self):
return [self.object.pk] return [self.object.pk]
class CurrentListOfSpeakersView(RedirectView):
"""
Redirect to the current list of speakers and set the request.user on it.
"""
def get_item(self):
"""
Returns the current Item, or None, if the current Slide is not an Agenda Item.
"""
slide = get_slide_from_sid(get_active_slide(only_sid=True), element=True)
if not isinstance(slide, Item):
return None
else:
return slide
def get_redirect_url(self):
"""
Returns the URL to the item_view if:
* the current slide is an item and
* the user has the permission to see the item
in other case, it returns the URL to the dashboard.
This method also add the request.user to the list of speakers, if he
has the right permissions.
"""
item = self.get_item()
request = self.request
if item is None:
messages.error(request, _(
'There is no list of speakers for the current slide. '
'Please choose your agenda item manually from the agenda.'))
return reverse('dashboard')
if self.request.user.has_perm('agenda.can_be_speaker'):
try:
Speaker.objects.add(self.request.user, item)
except OpenSlidesError:
messages.error(request, _('You are already on the list of speakers.'))
else:
messages.success(request, _('You are now on the list of speakers.'))
else:
messages.error(request, _('You can not put yourself on the list of speakers.'))
if not self.request.user.has_perm('agenda.can_see_agenda'):
return reverse('dashboard')
else:
return reverse('item_view', args=[item.pk])
def register_tab(request): def register_tab(request):
""" """
Registers the agenda tab. Registers the agenda tab.
@ -425,11 +476,19 @@ def get_widgets(request):
""" """
Returns the agenda widget for the projector tab. Returns the agenda widget for the projector tab.
""" """
return [Widget( return [
Widget(
name='agenda', name='agenda',
display_name=_('Agenda'), display_name=_('Agenda'),
template='agenda/widget.html', template='agenda/widget.html',
context={ context={
'agenda': SLIDE['agenda'], 'agenda': SLIDE['agenda'],
'items': Item.objects.all()}, 'items': Item.objects.all()},
permission_required='projector.can_manage_projector')] permission_required='projector.can_manage_projector'),
Widget(
name='append_to_list_of_speakers',
display_name=_('To the current list of speakers'),
template='agenda/speaker_widget.html',
context={},
permission_required='agenda.can_be_speaker')]

View File

@ -169,4 +169,4 @@ class Overlay(object):
return self.name in config['projector_active_overlays'] return self.name in config['projector_active_overlays']
def show_on_projector(self): def show_on_projector(self):
return self.is_active and self.get_projector_html() is not None return self.is_active() and self.get_projector_html() is not None

View File

@ -84,7 +84,7 @@ def setup_projector_config_variables(sender, **kwargs):
@receiver(projector_overlays, dispatch_uid="projector_countdown") @receiver(projector_overlays, dispatch_uid="projector_countdown")
def countdown(sender, **kwargs): def countdown(sender, **kwargs):
""" """
Reveiver for the countdown. Receiver for the countdown.
""" """
name = 'projector_countdown' name = 'projector_countdown'
request = kwargs.get('request', None) request = kwargs.get('request', None)

View File

@ -146,12 +146,6 @@ body{
color: #9FA9B7; color: #9FA9B7;
list-style-type: none; list-style-type: none;
} }
/* list of speakers */
#list_of_speakers li
{
font-size: 130%;
line-height: 160%;
}
/* Table */ /* Table */
table { table {

View File

@ -16,4 +16,4 @@
<div id="overlay_message_inner"> <div id="overlay_message_inner">
{{ message }} {{ message }}
</span> </div>

View File

@ -106,7 +106,7 @@ class TestSpeakerAppendView(SpeakerViewTestCase):
# Try to set speaker 1 to item 1 again # Try to set speaker 1 to item 1 again
response = self.check_url('/agenda/1/speaker/', self.speaker1_client, 302) response = self.check_url('/agenda/1/speaker/', self.speaker1_client, 302)
self.assertEqual(Speaker.objects.filter(item=self.item1).count(), 1) self.assertEqual(Speaker.objects.filter(item=self.item1).count(), 1)
self.assertMessage(response, 'speaker1 is allready on the list of speakers from item 1') self.assertMessage(response, 'speaker1 is already on the list of speakers of item 1.')
def test_closed_list(self): def test_closed_list(self):
self.item1.speaker_list_closed = True self.item1.speaker_list_closed = True
@ -127,7 +127,7 @@ class TestAgendaItemView(SpeakerViewTestCase):
# Try it again # Try it again
response = self.admin_client.post( response = self.admin_client.post(
'/agenda/1/', {'speaker': self.speaker1.person_id}) '/agenda/1/', {'speaker': self.speaker1.person_id})
self.assertFormError(response, 'form', 'speaker', 'speaker1 is allready on the list of speakers.') self.assertFormError(response, 'form', 'speaker', 'speaker1 is already on the list of speakers.')
class TestSpeakerDeleteView(SpeakerViewTestCase): class TestSpeakerDeleteView(SpeakerViewTestCase):
@ -170,6 +170,6 @@ class SpeakerListOpenView(SpeakerViewTestCase):
item = Item.objects.get(pk=self.item1.pk) item = Item.objects.get(pk=self.item1.pk)
self.assertTrue(item.speaker_list_closed) self.assertTrue(item.speaker_list_closed)
response = self.check_url('/agenda/1/speaker/open/', self.admin_client, 302) response = self.check_url('/agenda/1/speaker/reopen/', self.admin_client, 302)
item = Item.objects.get(pk=self.item1.pk) item = Item.objects.get(pk=self.item1.pk)
self.assertFalse(item.speaker_list_closed) self.assertFalse(item.speaker_list_closed)