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 %} +
  1. {{ speaker }}
  2. + {% endfor %} +
+ {% else %} + {% trans 'The list of speakers is empty.' %} + {% endif %} +
diff --git a/openslides/agenda/templates/agenda/overlay_speaker_widget.html b/openslides/agenda/templates/agenda/overlay_speaker_widget.html new file mode 100644 index 000000000..2600818b0 --- /dev/null +++ b/openslides/agenda/templates/agenda/overlay_speaker_widget.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% load tags %} + + + {% trans "List of speakers" %} + diff --git a/openslides/agenda/templates/agenda/speaker_widget.html b/openslides/agenda/templates/agenda/speaker_widget.html new file mode 100644 index 000000000..ee4d0c012 --- /dev/null +++ b/openslides/agenda/templates/agenda/speaker_widget.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% load tags %} + +
+ {% trans 'Put me on the current list of speakers' %} +
diff --git a/openslides/agenda/templates/agenda/view.html b/openslides/agenda/templates/agenda/view.html index 1e5337580..400f2542e 100644 --- a/openslides/agenda/templates/agenda/view.html +++ b/openslides/agenda/templates/agenda/view.html @@ -8,6 +8,8 @@ {% block header %} + {% comment %} TODO: include stylesheet in our repo{% endcomment %} + {% endblock %} {% block javascript %} @@ -59,7 +61,7 @@

{% if perms.agenda.can_manage_agenda %} {% if item.speaker_list_closed %} - {% trans 'Open list' %} + {% trans 'Open list' %} {% else %} {% trans 'Close list' %} {% endif %} @@ -130,9 +132,11 @@

{% trans "Next speakers:" %} -
    +
+

{% if is_speaker %} diff --git a/openslides/agenda/templates/projector/agenda_list_of_speaker.html b/openslides/agenda/templates/projector/agenda_list_of_speaker.html index 3bddb3791..7d667dc6f 100644 --- a/openslides/agenda/templates/projector/agenda_list_of_speaker.html +++ b/openslides/agenda/templates/projector/agenda_list_of_speaker.html @@ -4,6 +4,16 @@ {% block title %}{{ block.super }} - {{ item }}{% endblock %} +{% block header %} + +{% endblock %} + {% block content %}

{{ title }}

{% 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)