diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py
index 3578f95c5..0ee615f65 100644
--- a/openslides/agenda/forms.py
+++ b/openslides/agenda/forms.py
@@ -61,6 +61,9 @@ class ItemOrderForm(CssClassMixin, forms.Form):
class AppendSpeakerForm(CssClassMixin, forms.Form):
+ """
+ Form to set an user to a list of speakers.
+ """
speaker = PersonFormField(
widget=forms.Select(attrs={'class': 'medium-input'}),
label=ugettext_lazy("Add participant"))
@@ -70,6 +73,9 @@ class AppendSpeakerForm(CssClassMixin, forms.Form):
return super(AppendSpeakerForm, self).__init__(*args, **kwargs)
def clean_speaker(self):
+ """
+ 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():
raise forms.ValidationError(ugettext_lazy(
diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py
index 7a52c706f..8abed0914 100644
--- a/openslides/agenda/models.py
+++ b/openslides/agenda/models.py
@@ -42,23 +42,35 @@ class Item(MPTTModel, SlideMixin):
(ORGANIZATIONAL_ITEM, _('Organizational item')))
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"))
- """The optional text of the agenda item."""
+ """
+ The optional text of the agenda item.
+ """
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"))
- """Flag, if the item is finished."""
+ """
+ Flag, if the item is finished.
+ """
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,
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,
default=AGENDA_ITEM, verbose_name=_("Type"))
@@ -70,7 +82,9 @@ class Item(MPTTModel, SlideMixin):
duration = models.CharField(null=True, blank=True, max_length=5,
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)
"""
@@ -230,7 +244,7 @@ class Item(MPTTModel, SlideMixin):
class SpeakerManager(models.Manager):
def add(self, person, item):
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(
models.Max('weight'))['weight__max'] or 0)
return self.create(item=item, person=person, weight=weight + 1)
@@ -244,9 +258,24 @@ class Speaker(models.Model):
objects = SpeakerManager()
person = PersonField()
+ """
+ ForeinKey to the person who speaks.
+ """
+
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)
+ """
+ The sort order of the list of speakers. None, if he has already spoken.
+ """
class Meta:
permissions = (
@@ -264,6 +293,11 @@ class Speaker(models.Model):
args=[self.item.pk, self.pk])
def speak(self):
+ """
+ Let the person speak.
+
+ Set the weight to None and the time to now.
+ """
self.weight = None
self.time = datetime.now()
self.save()
diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py
index 457a59d9d..59c1c2ddd 100644
--- a/openslides/agenda/signals.py
+++ b/openslides/agenda/signals.py
@@ -13,10 +13,17 @@
from django.dispatch import receiver
from django import forms
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.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
@@ -48,3 +55,31 @@ def setup_agenda_config_page(sender, **kwargs):
variables=(agenda_start_event_date_time,),
extra_context={'extra_stylefiles': extra_stylefiles,
'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)
diff --git a/openslides/agenda/static/styles/agenda.css b/openslides/agenda/static/styles/agenda.css
index 2b8343ac9..f5d1e311b 100644
--- a/openslides/agenda/static/styles/agenda.css
+++ b/openslides/agenda/static/styles/agenda.css
@@ -25,3 +25,13 @@ table#agendatime td {
#list_of_speakers li {
line-height: 30px;
}
+
+#list_of_speakers {
+ list-style-type: none;
+}
+
+#list_of_speakers span.ui-icon {
+ position: absolute;
+ margin-left: -15px;
+ margin-top: 6px;
+}
diff --git a/openslides/agenda/templates/agenda/overlay_speaker_projector.html b/openslides/agenda/templates/agenda/overlay_speaker_projector.html
new file mode 100644
index 000000000..47a52dacd
--- /dev/null
+++ b/openslides/agenda/templates/agenda/overlay_speaker_projector.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+
+
+
+ {% if speakers %}
+ {% trans "List of speakers:" %}
+
+ {% for speaker in speakers %}
+
{{ speaker }}
+ {% endfor %}
+
+ {% else %}
+ {% trans 'The list of speakers is empty.' %}
+ {% endif %}
+
{% endblock %}
diff --git a/openslides/agenda/urls.py b/openslides/agenda/urls.py
index 47ffbbae7..bf6416b67 100644
--- a/openslides/agenda/urls.py
+++ b/openslides/agenda/urls.py
@@ -14,7 +14,7 @@ from django.conf.urls import url, patterns
from openslides.agenda.views import (
Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView,
ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView,
- SpeakerListOpenView, SpeakerChangeOrderView)
+ SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView)
urlpatterns = patterns(
'',
@@ -66,14 +66,14 @@ urlpatterns = patterns(
name='agenda_speaker_append',
),
- url(r'^(?P\d+)/speaker/open/$',
- SpeakerListOpenView.as_view(open_list=True),
- name='agenda_speaker_open',
+ url(r'^(?P\d+)/speaker/close/$',
+ SpeakerListCloseView.as_view(),
+ name='agenda_speaker_close',
),
- url(r'^(?P\d+)/speaker/close/$',
- SpeakerListOpenView.as_view(),
- name='agenda_speaker_close',
+ url(r'^(?P\d+)/speaker/reopen/$',
+ SpeakerListCloseView.as_view(reopen=True),
+ name='agenda_speaker_reopen',
),
url(r'^(?P\d+)/speaker/del/$',
@@ -91,8 +91,13 @@ urlpatterns = patterns(
name='agenda_speaker_speak',
),
- url(r'^(?P\d+)/speaker/change_order$',
+ url(r'^(?P\d+)/speaker/change_order/$',
SpeakerChangeOrderView.as_view(),
name='agenda_speaker_change_order',
),
+
+ url(r'^list_of_speakers/$',
+ CurrentListOfSpeakersView.as_view(),
+ name='agenda_current_list_of_speakers',
+ ),
)
diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py
index a5fb9b8bf..c8afc4da3 100644
--- a/openslides/agenda/views.py
+++ b/openslides/agenda/views.py
@@ -29,7 +29,7 @@ from openslides.utils.views import (
DetailView, FormView, SingleObjectMixin)
from openslides.utils.template import Tab
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 .models import Item, Speaker
from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm
@@ -128,7 +128,8 @@ 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.exclude(time=None).order_by('time'))
+ old_speakers = list(Speaker.objects.filter(item=self.object.pk)
+ .exclude(time=None).order_by('time'))
kwargs.update({
'object': self.object,
'speakers': speakers,
@@ -258,7 +259,6 @@ class SpeakerAppendView(SingleObjectMixin, RedirectView):
"""
Set the request.user to the speaker list.
"""
-
permission_required = 'agenda.can_be_speaker'
url_name = 'item_view'
model = Item
@@ -278,7 +278,6 @@ class SpeakerDeleteView(DeleteView):
"""
Delete the request.user or a specific user from the speaker list.
"""
-
success_url_name = 'item_view'
question_url_name = 'item_view'
@@ -324,7 +323,7 @@ class SpeakerDeleteView(DeleteView):
class SpeakerSpeakView(SingleObjectMixin, RedirectView):
"""
- Mark a speaker, that he can speak.
+ Mark the speaking person.
"""
permission_required = 'agenda.can_manage_agenda'
url_name = 'item_view'
@@ -347,18 +346,18 @@ class SpeakerSpeakView(SingleObjectMixin, RedirectView):
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'
model = Item
- open_list = False
+ reopen = False
url_name = 'item_view'
def pre_redirect(self, *args, **kwargs):
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()
def get_url_name_args(self):
@@ -402,11 +401,63 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView):
speaker.save()
else:
transaction.commit()
+ return None
+ messages.error(request, _('Could not change order. Invalid data.'))
def get_url_name_args(self):
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):
"""
Registers the agenda tab.
@@ -425,11 +476,19 @@ def get_widgets(request):
"""
Returns the agenda widget for the projector tab.
"""
- return [Widget(
- name='agenda',
- display_name=_('Agenda'),
- template='agenda/widget.html',
- context={
- 'agenda': SLIDE['agenda'],
- 'items': Item.objects.all()},
- permission_required='projector.can_manage_projector')]
+ return [
+ Widget(
+ name='agenda',
+ display_name=_('Agenda'),
+ template='agenda/widget.html',
+ context={
+ 'agenda': SLIDE['agenda'],
+ 'items': Item.objects.all()},
+ 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')]
diff --git a/openslides/projector/projector.py b/openslides/projector/projector.py
index b8ca8bef6..ab495db51 100644
--- a/openslides/projector/projector.py
+++ b/openslides/projector/projector.py
@@ -169,4 +169,4 @@ class Overlay(object):
return self.name in config['projector_active_overlays']
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
diff --git a/openslides/projector/signals.py b/openslides/projector/signals.py
index b8136e797..116972e77 100644
--- a/openslides/projector/signals.py
+++ b/openslides/projector/signals.py
@@ -84,7 +84,7 @@ def setup_projector_config_variables(sender, **kwargs):
@receiver(projector_overlays, dispatch_uid="projector_countdown")
def countdown(sender, **kwargs):
"""
- Reveiver for the countdown.
+ Receiver for the countdown.
"""
name = 'projector_countdown'
request = kwargs.get('request', None)
diff --git a/openslides/projector/static/styles/projector.css b/openslides/projector/static/styles/projector.css
index 84b8aeb3d..62e5c9622 100644
--- a/openslides/projector/static/styles/projector.css
+++ b/openslides/projector/static/styles/projector.css
@@ -146,12 +146,6 @@ body{
color: #9FA9B7;
list-style-type: none;
}
-/* list of speakers */
-#list_of_speakers li
-{
- font-size: 130%;
- line-height: 160%;
-}
/* Table */
table {
diff --git a/openslides/projector/templates/projector/overlay_message_projector.html b/openslides/projector/templates/projector/overlay_message_projector.html
index 7178e84ad..20a7616cf 100644
--- a/openslides/projector/templates/projector/overlay_message_projector.html
+++ b/openslides/projector/templates/projector/overlay_message_projector.html
@@ -16,4 +16,4 @@
{{ message }}
-
+
diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py
index 04ac5af88..fe8e28386 100644
--- a/tests/agenda/test_list_of_speakers.py
+++ b/tests/agenda/test_list_of_speakers.py
@@ -106,7 +106,7 @@ class TestSpeakerAppendView(SpeakerViewTestCase):
# Try to set speaker 1 to item 1 again
response = self.check_url('/agenda/1/speaker/', self.speaker1_client, 302)
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):
self.item1.speaker_list_closed = True
@@ -127,7 +127,7 @@ class TestAgendaItemView(SpeakerViewTestCase):
# Try it again
response = self.admin_client.post(
'/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):
@@ -170,6 +170,6 @@ class SpeakerListOpenView(SpeakerViewTestCase):
item = Item.objects.get(pk=self.item1.pk)
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)
self.assertFalse(item.speaker_list_closed)