From fa95119936912121f3257d8bf1b33a3d4b2943d5 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Mon, 18 Mar 2013 12:34:47 +0100 Subject: [PATCH 1/3] New Feature: List of speakers. --- openslides/agenda/__init__.py | 4 +- openslides/agenda/forms.py | 21 +- openslides/agenda/models.py | 89 +++++++- openslides/agenda/slides.py | 8 +- openslides/agenda/static/javascript/agenda.js | 37 +++- openslides/agenda/templates/agenda/view.html | 99 ++++++++- .../projector/agenda_list_of_speaker.html | 21 ++ openslides/agenda/urls.py | 43 +++- openslides/agenda/views.py | 195 +++++++++++++++++- .../assignment/templates/assignment/view.html | 4 +- openslides/participant/models.py | 6 +- openslides/participant/signals.py | 4 +- openslides/utils/person/api.py | 2 +- openslides/utils/person/models.py | 2 + openslides/utils/views.py | 27 ++- tests/agenda/test_list_of_speakers.py | 175 ++++++++++++++++ 16 files changed, 698 insertions(+), 39 deletions(-) create mode 100644 openslides/agenda/templates/projector/agenda_list_of_speaker.html create mode 100644 tests/agenda/test_list_of_speakers.py diff --git a/openslides/agenda/__init__.py b/openslides/agenda/__init__.py index a9676dd1a..004f26495 100644 --- a/openslides/agenda/__init__.py +++ b/openslides/agenda/__init__.py @@ -6,8 +6,10 @@ The OpenSlides agenda app appends the functionality to OpenSlides to manage agendas. + It includes time-management and list of speakers. + :copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ -from . import signals +from . import signals, slides diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py index 42752f91b..0f27e254c 100644 --- a/openslides/agenda/forms.py +++ b/openslides/agenda/forms.py @@ -17,7 +17,8 @@ from django.utils.translation import ugettext_lazy from mptt.forms import TreeNodeChoiceField from openslides.utils.forms import CssClassMixin -from .models import Item +from openslides.utils.person.forms import PersonFormField +from .models import Item, Speaker class ItemForm(forms.ModelForm, CssClassMixin): @@ -57,3 +58,21 @@ class ItemOrderForm(CssClassMixin, forms.Form): widget=forms.HiddenInput(attrs={'class': 'menu-mlid'})) parent = forms.IntegerField( widget=forms.HiddenInput(attrs={'class': 'menu-plid'})) + + +class AppendSpeakerForm(CssClassMixin, forms.Form): + speaker = PersonFormField( + widget=forms.Select(attrs={'class': 'medium-input'}), + label=ugettext_lazy("Set a person to the speaker list.")) + + def __init__(self, item, *args, **kwargs): + self.item = item + return super(AppendSpeakerForm, self).__init__(*args, **kwargs) + + def clean_speaker(self): + speaker = self.cleaned_data['speaker'] + if Speaker.objects.filter(person=speaker, item=self.item, time=None).exists(): + raise forms.ValidationError(ugettext_lazy( + '%s is allready on the list of speakers.' + % speaker)) + return speaker diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 6a994e3d4..7a52c706f 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -10,17 +10,20 @@ :license: GNU GPL, see LICENSE for more details. """ +from datetime import datetime + from django.db import models from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext from mptt.models import MPTTModel, TreeForeignKey +from openslides.utils.exceptions import OpenSlidesError from openslides.config.api import config from openslides.projector.projector import SlideMixin from openslides.projector.api import ( register_slidemodel, get_slide_from_sid, register_slidefunc) -from .slides import agenda_show +from openslides.utils.person.models import PersonField class Item(MPTTModel, SlideMixin): @@ -39,15 +42,48 @@ class Item(MPTTModel, SlideMixin): (ORGANIZATIONAL_ITEM, _('Organizational item'))) title = models.CharField(null=True, max_length=255, verbose_name=_("Title")) + """Title of the agenda item.""" + text = models.TextField(null=True, blank=True, verbose_name=_("Text")) + """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.""" + closed = models.BooleanField(default=False, verbose_name=_("Closed")) + """Flag, if the item is finished.""" + weight = models.IntegerField(default=0, verbose_name=_("Weight")) + """Weight to sort the item in the agenda.""" + parent = TreeForeignKey('self', null=True, blank=True, related_name='children') - type = models.IntegerField(max_length=1, choices=ITEM_TYPE, default=AGENDA_ITEM, verbose_name=_("Type")) - duration = models.CharField(null=True, blank=True, max_length=5, verbose_name=_("Duration (hh:mm)")) + """The parent item in the agenda tree.""" + + type = models.IntegerField(max_length=1, choices=ITEM_TYPE, + default=AGENDA_ITEM, verbose_name=_("Type")) + """ + Type of the agenda item. + + See Agenda.ITEM_TYPE for more informations. + """ + + duration = models.CharField(null=True, blank=True, max_length=5, + verbose_name=_("Duration (hh:mm)")) + """The intended duration for the topic.""" + related_sid = models.CharField(null=True, blank=True, max_length=63) + """ + Slide-ID to another object to show it in the agenda. + + For example a motion or assignment. + """ + + speaker_list_closed = models.BooleanField( + default=False, verbose_name=_("List of speakers is closed")) + """ + True, if the list of speakers is closed. + """ def get_related_slide(self): """ @@ -106,6 +142,11 @@ class Item(MPTTModel, SlideMixin): 'items': self.get_children(), 'template': 'projector/AgendaSummary.html', } + elif config['presentation_argument'] == 'show_list_of_speakers': + speakers = Speaker.objects.filter(time=None, item=self.pk).order_by('weight') + data = {'title': _('List of speakers for %s') % self.get_title(), + 'template': 'projector/agenda_list_of_speaker.html', + 'speakers': speakers} elif self.related_sid: data = self.get_related_slide().slide() else: @@ -186,5 +227,43 @@ class Item(MPTTModel, SlideMixin): order_insertion_by = ['weight'] -register_slidemodel(Item, control_template='agenda/control_item.html') -register_slidefunc('agenda', agenda_show, weight=-1, name=_('Agenda')) +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)) + weight = (self.filter(item=item).aggregate( + models.Max('weight'))['weight__max'] or 0) + return self.create(item=item, person=person, weight=weight + 1) + + +class Speaker(models.Model): + """ + Model for the Speaker list. + """ + + objects = SpeakerManager() + + person = PersonField() + item = models.ForeignKey(Item) + time = models.TimeField(null=True) + weight = models.IntegerField(null=True) + + class Meta: + permissions = ( + ('can_be_speaker', ugettext_noop('Can be speaker')), + ) + + def __unicode__(self): + return unicode(self.person) + + def get_absolute_url(self, link='detail'): + if link == 'detail' or link == 'view': + return self.person.get_absolute_url('detail') + if link == 'delete': + return reverse('agenda_speaker_delete', + args=[self.item.pk, self.pk]) + + def speak(self): + self.weight = None + self.time = datetime.now() + self.save() diff --git a/openslides/agenda/slides.py b/openslides/agenda/slides.py index 51acb35c2..70e5f9d28 100644 --- a/openslides/agenda/slides.py +++ b/openslides/agenda/slides.py @@ -12,12 +12,18 @@ from django.utils.translation import ugettext as _ +from openslides.projector.api import register_slidemodel, register_slidefunc + +from .models import Item + def agenda_show(): - from openslides.agenda.models import Item data = {} items = Item.objects.filter(parent=None, type__exact=Item.AGENDA_ITEM) data['title'] = _("Agenda") data['items'] = items data['template'] = 'projector/AgendaSummary.html' return data + +register_slidemodel(Item, control_template='agenda/control_item.html') +register_slidefunc('agenda', agenda_show, weight=-1, name=_('Agenda')) diff --git a/openslides/agenda/static/javascript/agenda.js b/openslides/agenda/static/javascript/agenda.js index 69432803f..9de70b69a 100644 --- a/openslides/agenda/static/javascript/agenda.js +++ b/openslides/agenda/static/javascript/agenda.js @@ -34,6 +34,19 @@ function hideClosedSlides(hide) { return false; } +function toggleOldSpeakers() { + $('#show_old_speakers').toggle(); + $('#old_speakers').toggle(); +} + +$('.toggle_old_speakers > a').click(function() { + toggleOldSpeakers(); +}); + +$('#speaker_list_changed_form').submit(function() { + $('#sort_order').val($('#list_of_speakers').sortable("toArray")); +}); + $(function() { // change participant status (on/off) $('.close_link').click(function(event) { @@ -72,11 +85,21 @@ $(function() { $('#hide_closed_items').attr('checked', true); } }); - if ($.cookie('Slide.HideClosed') === null) { - $('#hide_closed_items').attr('checked', false); - $.cookie('Slide.HideClosed', 0); - } else if ($.cookie('Slide.HideClosed') == 1) { - hideClosedSlides(true); - $('#hide_closed_items').attr('checked', true); - } + + // TODO: Fix this code and reactivate it again + //# if ($.cookie('Slide.HideClosed') === null) { + //# $('#hide_closed_items').attr('checked', false); + //# $.cookie('Slide.HideClosed', 0); + //# } else if ($.cookie('Slide.HideClosed') == 1) { + //# hideClosedSlides(true); + //# $('#hide_closed_items').attr('checked', true); + //# } + + // List of Speakers + toggleOldSpeakers(); + + $('#list_of_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) { + $('#speaker_list_changed_form').show(); + }}); + $('#list_of_speakers').disableSelection(); }); diff --git a/openslides/agenda/templates/agenda/view.html b/openslides/agenda/templates/agenda/view.html index 229298f33..66d40ee9e 100644 --- a/openslides/agenda/templates/agenda/view.html +++ b/openslides/agenda/templates/agenda/view.html @@ -1,9 +1,18 @@ {% extends "base.html" %} {% load i18n %} +{% load tags %} +{% load staticfiles %} {% block title %}{{ block.super }} – {{ item.title }}{% endblock %} +{% block javascript %} + {{ block.super }} + {% comment %} TODO: import the sortable-plugin in our custom jquery-file {% endcomment %} + + +{% endblock %} + {% block content %}

{{ item }} @@ -30,12 +39,94 @@

-

{{ item.text|safe|linebreaks }}

+

{{ item.text|safe|linebreaks }}

- {% if perms.agenda.can_manage_agenda %} +{% if perms.agenda.can_manage_agenda %} {% if item.comment %} -

{% trans "Comment" %}

-

{{ item.comment|linebreaks }}

+

{% trans "Comment" %}

+

{{ item.comment|linebreaks }}

{% endif %} +{% endif %} + +{# List of Speakers #} +

{% trans "Speaker List" %}{% if item.speaker_list_closed %}({% trans 'Closed' %}){% endif %}

+ +{% if item.speaker_list_closed %} + {% trans 'Open list of speakers' %} +{% else %} + {% trans 'Close list of speakers' %} +{% endif %} + +

Show list of speakers

+ +{% if old_speakers %} + + +
+ {% trans "Hide old speakers" %} +
    + {% for speaker in old_speakers %} +
  1. + {{ speaker.time }} + {{ speaker }} + + + +
  2. + {% endfor %} +
+
+{% endif %} + +{% if perms.agenda.can_manage_agenda %} + +{% endif %} + +{% if speakers %} +
    + {% for speaker in speakers %} +
  1. + {{ speaker }} + speak + + + +
  2. + {% endfor %} +
+{% else %} +

{% trans 'The list of speakers is empty' %}

+{% endif %} + + + +{% if is_speaker %} + {% trans "Remove me vom speakers list." %} +{% elif not object.speaker_list_closed %} + {% trans "Put me on speakers list." %} +{% endif %} + +{% if perms.can_manage_agenda %} +
{% csrf_token %} + {{ form.as_p }} + + {% if perms.participant.can_see_participant and perms.participant.can_manage_participant %} + + {% endif %} +
+{% endif %} + {% endblock %} diff --git a/openslides/agenda/templates/projector/agenda_list_of_speaker.html b/openslides/agenda/templates/projector/agenda_list_of_speaker.html new file mode 100644 index 000000000..bb4401ebe --- /dev/null +++ b/openslides/agenda/templates/projector/agenda_list_of_speaker.html @@ -0,0 +1,21 @@ +{% extends "base-projector.html" %} + +{% load i18n %} + +{% block title %}{{ block.super }} - {{ item }}{% endblock %} + +{% block content %} +

{{ title }}

+{% endblock %} + +{% block scrollcontent %} + {% if speakers %} +
    + {% for speaker in speakers %} +
  1. {{ speaker }}
  2. + {% endfor %} +
+ {% else %} + {% trans 'The list of speakers is empty' %} + {% endif %} +{% endblock %} diff --git a/openslides/agenda/urls.py b/openslides/agenda/urls.py index 7ca03dc8f..47ffbbae7 100644 --- a/openslides/agenda/urls.py +++ b/openslides/agenda/urls.py @@ -12,8 +12,9 @@ from django.conf.urls import url, patterns from openslides.agenda.views import ( - Overview, View, SetClosed, ItemUpdate, - ItemCreate, ItemDelete, AgendaPDF) + Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView, + ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView, + SpeakerListOpenView, SpeakerChangeOrderView) urlpatterns = patterns( '', @@ -23,7 +24,7 @@ urlpatterns = patterns( ), url(r'^(?P\d+)/$', - View.as_view(), + AgendaItemView.as_view(), name='item_view', ), @@ -58,4 +59,40 @@ urlpatterns = patterns( AgendaPDF.as_view(), name='print_agenda', ), + + # Speaker List + url(r'^(?P\d+)/speaker/$', + SpeakerAppendView.as_view(), + 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/$', + SpeakerListOpenView.as_view(), + name='agenda_speaker_close', + ), + + url(r'^(?P\d+)/speaker/del/$', + SpeakerDeleteView.as_view(), + name='agenda_speaker_delete', + ), + + url(r'^(?P\d+)/speaker/(?P\d+)/del/$', + SpeakerDeleteView.as_view(), + name='agenda_speaker_delete', + ), + + url(r'^(?P\d+)/speaker/(?P[^/]+)/speak/$', + SpeakerSpeakView.as_view(), + name='agenda_speaker_speak', + ), + + url(r'^(?P\d+)/speaker/change_order$', + SpeakerChangeOrderView.as_view(), + name='agenda_speaker_change_order', + ), ) diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index cb471119e..7348d9fe2 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -9,6 +9,7 @@ :copyright: 2011, 2012 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ +# TODO: Rename all views and template names from reportlab.platypus import Paragraph from datetime import datetime, timedelta @@ -22,15 +23,16 @@ from django.views.generic.detail import SingleObjectMixin from openslides.config.api import config from openslides.utils.pdf import stylesheet +from openslides.utils.exceptions import OpenSlidesError from openslides.utils.views import ( TemplateView, RedirectView, UpdateView, CreateView, DeleteView, PDFView, - DetailView, FormView) + 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.projector import Widget, SLIDE -from .models import Item -from .forms import ItemOrderForm, ItemForm +from .models import Item, Speaker +from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm class Overview(TemplateView): @@ -112,14 +114,43 @@ class Overview(TemplateView): return self.render_to_response(context) -class View(DetailView): +class AgendaItemView(SingleObjectMixin, FormView): """ Show an agenda item. """ + # TODO: use 'SingleObjectTemplateResponseMixin' to choose the right template name permission_required = 'agenda.can_see_agenda' template_name = 'agenda/view.html' model = Item context_object_name = 'item' + form_class = AppendSpeakerForm + + 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')) + try: + last_speaker = old_speakers[-1] + except IndexError: + last_speaker = None + kwargs.update({ + 'object': self.object, + 'speakers': speakers, + 'old_speakers': old_speakers, + 'last_speaker': last_speaker, + 'is_speaker': Speaker.objects.filter( + time=None, person=self.request.user, item=self.object).exists(), + }) + return super(AgendaItemView, self).get_context_data(**kwargs) + + def form_valid(self, form): + Speaker.objects.add(person=form.cleaned_data['speaker'], item=self.get_object()) + return self.render_to_response(self.get_context_data(form=form)) + + def get_form_kwargs(self): + kwargs = super(AgendaItemView, self).get_form_kwargs() + kwargs['item'] = self.get_object() + return kwargs class SetClosed(RedirectView, SingleObjectMixin): @@ -149,6 +180,9 @@ class SetClosed(RedirectView, SingleObjectMixin): self.object.set_closed(closed) return super(SetClosed, self).pre_redirect(request, *args, **kwargs) + def get_url_name_args(self): + return [] + class ItemUpdate(UpdateView): """ @@ -224,6 +258,159 @@ class AgendaPDF(PDFView): story.append(Paragraph(item.get_title(), stylesheet['Item'])) +class SpeakerAppendView(SingleObjectMixin, RedirectView): + """ + Set the request.user to the speaker list. + """ + + permission_required = 'agenda.can_be_speaker' + url_name = 'item_view' + model = Item + + def pre_redirect(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.speaker_list_closed: + messages.error(request, _('List of speakers is closed.')) + else: + try: + Speaker.objects.add(item=self.object, person=request.user) + except OpenSlidesError, e: + messages.error(request, e) + + +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' + + def has_permission(self, request, *args, **kwargs): + """ + Check the permission to delete a speaker. + """ + 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 + return True + + def get(self, *args, **kwargs): + try: + return super(SpeakerDeleteView, self).get(*args, **kwargs) + except Speaker.DoesNotExist: + messages.error(self.request, _('You are not on the list of speakers.')) + return super(RedirectView, self).get(*args, **kwargs) + + def get_object(self): + """ + Returns the speaker object. + + If 'speaker' is in kwargs, this speaker object is returnd. Else, a speaker + object with the request.user as speaker. + """ + try: + return Speaker.objects.get(pk=self.kwargs['speaker']) + except KeyError: + return Speaker.objects.filter( + item=self.kwargs['pk'], person=self.request.user).exclude(weight=None).get() + + def get_url_name_args(self): + return [self.kwargs['pk']] + + def get_question(self): + if 'speaker' in self.kwargs: + return super(SpeakerDeleteView, self).get_question() + else: + return _('Do you really want to remove yourself from the list of speakers?') + + +class SpeakerSpeakView(SingleObjectMixin, RedirectView): + """ + Mark a speaker, that he can speak. + """ + 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( + person=kwargs['person_id'], + item=self.object.pk).exclude( + weight=None).get() + except Speaker.DoesNotExist: + messages.error(self.request, _('Person %s is not on the list of item %s.' + % (kwargs['person_id'], self.object))) + else: + speaker.speak() + + def get_url_name_args(self): + return [self.object.pk] + + +class SpeakerListOpenView(SingleObjectMixin, RedirectView): + """ + View to open and close a list of speakers. + """ + permission_required = 'agenda.can_manage_agenda' + model = Item + open_list = 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.save() + + def get_url_name_args(self): + return [self.object.pk] + + +class SpeakerChangeOrderView(SingleObjectMixin, RedirectView): + """ + Change the order of the speakers. + + Has to be called as post-request with the new order of the speaker ids. + """ + permission_required = 'agenda.can_manage_agenda' + model = Item + url_name = 'item_view' + + def pre_redirect(self, args, **kwargs): + self.object = self.get_object() + + @transaction.commit_manually + def pre_post_redirect(self, request, *args, **kwargs): + """ + Reorder the list of speaker. + + Take the string 'sort_order' from the post-data, and use this order. + """ + self.object = self.get_object() + transaction.commit() + for (counter, speaker) in enumerate(self.request.POST['sort_order'].split(',')): + try: + speaker_pk = int(speaker.split('_')[1]) + except IndexError: + transaction.rollback() + break + try: + speaker = Speaker.objects.filter(item=self.object).get(pk=speaker_pk) + except: + transaction.rollback() + break + speaker.weight = counter + 1 + speaker.save() + else: + transaction.commit() + + def get_url_name_args(self): + return [self.object.pk] + + def register_tab(request): """ Registers the agenda tab. diff --git a/openslides/assignment/templates/assignment/view.html b/openslides/assignment/templates/assignment/view.html index b62eb98e5..186f808bf 100644 --- a/openslides/assignment/templates/assignment/view.html +++ b/openslides/assignment/templates/assignment/view.html @@ -115,8 +115,6 @@ {% endif %} {% endfor %} -

-

{% endif %} {% endif %} @@ -235,7 +233,7 @@ {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} {% endif %} - + {% trans 'Votes cast' %} {% for poll in polls %} diff --git a/openslides/participant/models.py b/openslides/participant/models.py index 37aa13c9d..b90ce7963 100644 --- a/openslides/participant/models.py +++ b/openslides/participant/models.py @@ -86,16 +86,16 @@ class User(PersonMixin, Person, SlideMixin, DjangoUser): return self.last_name.lower() @models.permalink - def get_absolute_url(self, link='view'): + def get_absolute_url(self, link='detail'): """ Return the URL to this user. link can be: - * view + * detail * edit * delete """ - if link == 'view': + if link == 'detail' or link == 'view': return ('user_view', [str(self.id)]) if link == 'edit': return ('user_edit', [str(self.id)]) diff --git a/openslides/participant/signals.py b/openslides/participant/signals.py index 9fb0f4297..ce9a153b4 100644 --- a/openslides/participant/signals.py +++ b/openslides/participant/signals.py @@ -77,8 +77,10 @@ def create_builtin_groups(sender, **kwargs): perm_2 = Permission.objects.get(content_type=ct_projector, codename='can_see_dashboard') ct_agenda = ContentType.objects.get(app_label='agenda', model='item') + ct_speaker = ContentType.objects.get(app_label='agenda', model='speaker') perm_3 = Permission.objects.get(content_type=ct_agenda, codename='can_see_agenda') perm_3a = Permission.objects.get(content_type=ct_agenda, codename='can_see_orga_items') + can_speak = Permission.objects.get(content_type=ct_speaker, codename='can_be_speaker') ct_motion = ContentType.objects.get(app_label='motion', model='motion') perm_4 = Permission.objects.get(content_type=ct_motion, codename='can_see_motion') @@ -95,7 +97,7 @@ def create_builtin_groups(sender, **kwargs): group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1) group_anonymous.permissions.add(perm_1, perm_2, perm_3, perm_3a, perm_4, perm_5, perm_6, perm_6a) group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2) - group_registered.permissions.add(perm_1, perm_2, perm_3, perm_3a, perm_4, perm_5, perm_6, perm_6a) + group_registered.permissions.add(perm_1, perm_2, perm_3, perm_3a, perm_4, perm_5, perm_6, perm_6a, can_speak) # Delegates perm_7 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') diff --git a/openslides/utils/person/api.py b/openslides/utils/person/api.py index 24a759551..e063f13b5 100644 --- a/openslides/utils/person/api.py +++ b/openslides/utils/person/api.py @@ -23,7 +23,7 @@ class Person(object): """ raise NotImplementedError('Any person object needs a person_id') - def __repr__(self): + def __unicode__(self): """ Return a string for this person. """ diff --git a/openslides/utils/person/models.py b/openslides/utils/person/models.py index 35f969705..f4beadd05 100644 --- a/openslides/utils/person/models.py +++ b/openslides/utils/person/models.py @@ -42,6 +42,8 @@ class PersonField(models.fields.Field): """ if value is None: return None + elif isinstance(value, basestring): + return value else: return value.person_id diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 47bcd1157..e57ffe559 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -149,15 +149,26 @@ class QuestionMixin(object): def get_redirect_url(self, **kwargs): if self.request.method == 'GET': - return reverse(self.question_url_name, args=self.get_url_name_args()) + return reverse(self.question_url_name, args=self.get_question_url_name_args()) else: - return reverse(self.success_url_name, args=self.get_url_name_args()) + return reverse(self.success_url_name, args=self.get_success_url_name_args()) + + def get_question_url_name_args(self): + return self.get_url_name_args() + + def get_success_url_name_args(self): + return self.get_url_name_args() def get_url_name_args(self): - return [] + try: + return [self.object.pk] + except AttributeError: + return [] def pre_redirect(self, request, *args, **kwargs): - # Prints the question in a GET request + """ + Prints the question in a GET request. + """ self.confirm_form() def get_question(self): @@ -251,7 +262,10 @@ class RedirectView(PermissionMixin, AjaxMixin, _RedirectView): return super(RedirectView, self).get_redirect_url(**kwargs) def get_url_name_args(self): - return [] + try: + return [self.object.pk] + except AttributeError: + return [] class FormView(PermissionMixin, ExtraContextMixin, UrlMixin, _FormView): @@ -319,6 +333,9 @@ class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView): def get_success_message(self): return _('%s was successfully deleted.') % html_strong(self.object) + def get_url_name_args(self): + return [] + class DetailView(PermissionMixin, ExtraContextMixin, _DetailView): def get(self, request, *args, **kwargs): diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py new file mode 100644 index 000000000..04ac5af88 --- /dev/null +++ b/tests/agenda/test_list_of_speakers.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + Unit test for the list of speakers + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.test.client import Client + +from openslides.utils.exceptions import OpenSlidesError +from openslides.utils.test import TestCase +from openslides.participant.models import User +from openslides.agenda.models import Item, Speaker + + +class ListOfSpeakerModelTests(TestCase): + def setUp(self): + self.item1 = Item.objects.create(title='item1') + self.item2 = Item.objects.create(title='item2') + self.speaker1 = User.objects.create(username='user1') + self.speaker2 = User.objects.create(username='user2') + + def test_append_speaker(self): + # Append speaker1 to the list of item1 + speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) + self.assertTrue(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + + # Append speaker1 to the list of item2 + speaker1_item2 = Speaker.objects.add(self.speaker1, self.item2) + self.assertTrue(Speaker.objects.filter(person=self.speaker1, item=self.item2).exists()) + + # Append speaker2 to the list of item1 + speaker2_item1 = Speaker.objects.add(self.speaker2, self.item1) + self.assertTrue(Speaker.objects.filter(person=self.speaker2, item=self.item1).exists()) + + # Try to append speaker 1 again to the list of item1 + with self.assertRaises(OpenSlidesError): + Speaker.objects.add(self.speaker1, self.item1) + + # Check time and weight + for object in (speaker1_item1, speaker2_item1, speaker1_item2): + self.assertIsNone(object.time) + self.assertEqual(speaker1_item1.weight, 1) + self.assertEqual(speaker1_item2.weight, 1) + self.assertEqual(speaker2_item1.weight, 2) + + def test_open_close_list_of_speaker(self): + self.assertFalse(Item.objects.get(pk=self.item1.pk).speaker_list_closed) + self.item1.speaker_list_closed = True + self.item1.save() + self.assertTrue(Item.objects.get(pk=self.item1.pk).speaker_list_closed) + + def test_speak(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(Speaker.objects.get(pk=speaker1_item1.pk).weight) + + +class SpeakerViewTestCase(TestCase): + def setUp(self): + # Admin + self.admin = User.objects.create_superuser('admin', 'admin@admin.admin', 'admin') + self.admin_client = Client() + self.admin_client.login(username='admin', password='admin') + + # Speaker1 + self.speaker1 = User.objects.create_user('speaker1', 'speaker1@user.user', 'speaker') + self.speaker1_client = Client() + self.speaker1_client.login(username='speaker1', password='speaker') + + # Speaker2 + self.speaker2 = User.objects.create_user('speaker2', 'speaker2@user.user', 'speaker') + self.speaker2_client = Client() + self.speaker2_client.login(username='speaker2', password='speaker') + + # Items + self.item1 = Item.objects.create(title='item1') + self.item2 = Item.objects.create(title='item2') + + def check_url(self, url, test_client, response_cose): + response = test_client.get(url) + self.assertEqual(response.status_code, response_cose) + return response + + def assertMessage(self, response, message): + self.assertTrue(message in response.cookies['messages'].value, + '"%s" is not a message of the response. (But: %s)' + % (message, response.cookies['messages'].value)) + + +class TestSpeakerAppendView(SpeakerViewTestCase): + def test_get(self): + self.assertFalse(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + self.assertEqual(Speaker.objects.filter(item=self.item1).count(), 0) + + # Set speaker1 to item1 + self.check_url('/agenda/1/speaker/', self.speaker1_client, 302) + self.assertTrue(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + self.assertEqual(Speaker.objects.filter(item=self.item1).count(), 1) + + # 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') + + def test_closed_list(self): + self.item1.speaker_list_closed = True + self.item1.save() + + response = self.check_url('/agenda/1/speaker/', self.speaker1_client, 302) + self.assertEqual(Speaker.objects.filter(item=self.item1).count(), 0) + self.assertMessage(response, 'List of speakers is closed.') + + +class TestAgendaItemView(SpeakerViewTestCase): + def test_post(self): + # Set speaker1 to item1 + response = self.admin_client.post( + '/agenda/1/', {'speaker': self.speaker1.person_id}) + self.assertTrue(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + + # 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.') + + +class TestSpeakerDeleteView(SpeakerViewTestCase): + def test_get(self): + self.check_url('/agenda/1/speaker/del/', self.speaker1_client, 302) + + def test_post_as_admin(self): + speaker = Speaker.objects.add(self.speaker1, self.item1) + + response = self.admin_client.post( + '/agenda/1/speaker/%d/del/' % speaker.pk, {'yes': 'yes'}) + self.assertEqual(response.status_code, 302) + self.assertFalse(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + + def test_post_as_user(self): + speaker = Speaker.objects.add(self.speaker1, self.item1) + + response = self.speaker1_client.post( + '/agenda/1/speaker/del/', {'yes': 'yes'}) + self.assertEqual(response.status_code, 302) + self.assertFalse(Speaker.objects.filter(person=self.speaker1, item=self.item1).exists()) + + +class TestSpeakerSpeakView(SpeakerViewTestCase): + def test_get(self): + url = '/agenda/1/speaker/%s/speak/' % self.speaker1.person_id + response = self.check_url(url, self.admin_client, 302) + self.assertMessage(response, 'Person user:2 is not on the list of item item1.') + + 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.assertIsNone(speaker.weight) + + +class SpeakerListOpenView(SpeakerViewTestCase): + def test_get(self): + response = self.check_url('/agenda/1/speaker/close/', self.admin_client, 302) + 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) + item = Item.objects.get(pk=self.item1.pk) + self.assertFalse(item.speaker_list_closed) From 1a416e5726332abf991de9bacb89c8340706811f Mon Sep 17 00:00:00 2001 From: Emanuel Schuetze Date: Sun, 14 Apr 2013 00:20:35 +0200 Subject: [PATCH 2/3] Updated template and some minor fixed for list of speakers. Updated projector slide. --- openslides/agenda/forms.py | 6 +- openslides/agenda/static/javascript/agenda.js | 12 -- openslides/agenda/static/styles/agenda.css | 4 + openslides/agenda/templates/agenda/view.html | 168 +++++++++++------- .../projector/agenda_list_of_speaker.html | 2 +- openslides/agenda/views.py | 6 +- .../projector/static/styles/projector.css | 7 +- 7 files changed, 121 insertions(+), 84 deletions(-) diff --git a/openslides/agenda/forms.py b/openslides/agenda/forms.py index 0f27e254c..3578f95c5 100644 --- a/openslides/agenda/forms.py +++ b/openslides/agenda/forms.py @@ -63,7 +63,7 @@ class ItemOrderForm(CssClassMixin, forms.Form): class AppendSpeakerForm(CssClassMixin, forms.Form): speaker = PersonFormField( widget=forms.Select(attrs={'class': 'medium-input'}), - label=ugettext_lazy("Set a person to the speaker list.")) + label=ugettext_lazy("Add participant")) def __init__(self, item, *args, **kwargs): self.item = item @@ -73,6 +73,6 @@ class AppendSpeakerForm(CssClassMixin, forms.Form): speaker = self.cleaned_data['speaker'] if Speaker.objects.filter(person=speaker, item=self.item, time=None).exists(): raise forms.ValidationError(ugettext_lazy( - '%s is allready on the list of speakers.' - % speaker)) + '%s is already on the list of speakers.' + % unicode(speaker))) return speaker diff --git a/openslides/agenda/static/javascript/agenda.js b/openslides/agenda/static/javascript/agenda.js index 9de70b69a..35cb30765 100644 --- a/openslides/agenda/static/javascript/agenda.js +++ b/openslides/agenda/static/javascript/agenda.js @@ -34,15 +34,6 @@ function hideClosedSlides(hide) { return false; } -function toggleOldSpeakers() { - $('#show_old_speakers').toggle(); - $('#old_speakers').toggle(); -} - -$('.toggle_old_speakers > a').click(function() { - toggleOldSpeakers(); -}); - $('#speaker_list_changed_form').submit(function() { $('#sort_order').val($('#list_of_speakers').sortable("toArray")); }); @@ -95,9 +86,6 @@ $(function() { //# $('#hide_closed_items').attr('checked', true); //# } - // List of Speakers - toggleOldSpeakers(); - $('#list_of_speakers').sortable({axis: "y", containment: "parent", update: function(event, ui) { $('#speaker_list_changed_form').show(); }}); diff --git a/openslides/agenda/static/styles/agenda.css b/openslides/agenda/static/styles/agenda.css index 3c6ead5ea..2b8343ac9 100644 --- a/openslides/agenda/static/styles/agenda.css +++ b/openslides/agenda/static/styles/agenda.css @@ -21,3 +21,7 @@ table#agendatime td { padding: 3px; white-space: nowrap; } + +#list_of_speakers li { + line-height: 30px; +} diff --git a/openslides/agenda/templates/agenda/view.html b/openslides/agenda/templates/agenda/view.html index 66d40ee9e..1e5337580 100644 --- a/openslides/agenda/templates/agenda/view.html +++ b/openslides/agenda/templates/agenda/view.html @@ -6,6 +6,10 @@ {% block title %}{{ block.super }} – {{ item.title }}{% endblock %} +{% block header %} + +{% endblock %} + {% block javascript %} {{ block.super }} {% comment %} TODO: import the sortable-plugin in our custom jquery-file {% endcomment %} @@ -19,23 +23,25 @@
{% trans "Back to overview" %} + {% if perms.agenda.can_manage_agenda %}
{% trans 'More actions' %} - {% if perms.agenda.can_manage_agenda %} - {% endif %} + {% endif %}
- {% if perms.projector.can_manage_projector %} - - - - {% endif %} + {% if perms.projector.can_manage_projector %} + + + + {% endif %}
@@ -43,90 +49,128 @@ {% if perms.agenda.can_manage_agenda %} {% if item.comment %} -

{% trans "Comment" %}

+

{% trans "Comment" %}

{{ item.comment|linebreaks }}

{% endif %} {% endif %} {# List of Speakers #} -

{% trans "Speaker List" %}{% if item.speaker_list_closed %}({% trans 'Closed' %}){% endif %}

- -{% if item.speaker_list_closed %} - {% trans 'Open list of speakers' %} -{% else %} - {% trans 'Close list of speakers' %} +

{% trans "List of speakers" %} {% if item.speaker_list_closed %}{% trans 'closed' %}{% endif %}

+

+{% if perms.agenda.can_manage_agenda %} + {% if item.speaker_list_closed %} + {% trans 'Open list' %} + {% else %} + {% trans 'Close list' %} + {% endif %} {% endif %} +{% if perms.projector.can_manage_projector %} + + + {% trans 'Show list' %} + -

Show list of speakers

+{% endif %} +

{% if old_speakers %} - - -
- {% trans "Hide old speakers" %} -
    +
    + {% trans "Last speakers:" %} +
    + +
    +
    +
    {% for speaker in old_speakers %} -
  1. - {{ speaker.time }} - {{ speaker }} - + {% if not forloop.last %} + {{forloop.counter}}. + [{{ speaker.time }}h] + {{ speaker }} + {% if perms.agenda.can_manage_agenda %} + + + + {% endif %} +
    + {% endif %} + {% endfor %} +
  2. + {% if old_speakers %} + {% with last=old_speakers|last %} + {{ old_speakers|length }}. + [{{ last.time }}h] + {{ last }} + {% if perms.agenda.can_manage_agenda %} + - - {% endfor %} -
+ {% endif %} + + {% endwith %} + {% endif %}
{% endif %} {% if perms.agenda.can_manage_agenda %} {% endif %} -{% if speakers %} + +
+ {% trans "Next speakers:" %}
    {% for speaker in speakers %}
  1. {{ speaker }} - speak - - - + {% if perms.agenda.can_manage_agenda %} + Finished speech + + + + {% endif %}
  2. + {% empty %} + {% trans "The list of speakers is empty." %} {% endfor %}
-{% else %} -

{% trans 'The list of speakers is empty' %}

-{% endif %} +

+ {% if is_speaker %} + {% trans "Remove me from the list" %} + {% elif not object.speaker_list_closed %} + {% trans "Put me on the list" %} + {% endif %} +

+ {% if perms.can_manage_agenda %} +
{% csrf_token %} + {% for field in form %} + +
+ {{ field }} + + {% if perms.participant.can_see_participant and perms.participant.can_manage_participant %} + + {% endif %} + {% if field.errors %} + {{ field.errors }} + {% endif %} +
+ {% endfor %} +
+ {% endif %} -{% if is_speaker %} - {% trans "Remove me vom speakers list." %} -{% elif not object.speaker_list_closed %} - {% trans "Put me on speakers list." %} -{% endif %} - -{% if perms.can_manage_agenda %} -
{% csrf_token %} - {{ form.as_p }} - - {% if perms.participant.can_see_participant and perms.participant.can_manage_participant %} - - {% endif %} -
-{% endif %} - +
{% endblock %} diff --git a/openslides/agenda/templates/projector/agenda_list_of_speaker.html b/openslides/agenda/templates/projector/agenda_list_of_speaker.html index bb4401ebe..3bddb3791 100644 --- a/openslides/agenda/templates/projector/agenda_list_of_speaker.html +++ b/openslides/agenda/templates/projector/agenda_list_of_speaker.html @@ -10,7 +10,7 @@ {% block scrollcontent %} {% if speakers %} -
    +
      {% for speaker in speakers %}
    1. {{ speaker }}
    2. {% endfor %} diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 7348d9fe2..a5fb9b8bf 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -129,17 +129,13 @@ class AgendaItemView(SingleObjectMixin, FormView): 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')) - try: - last_speaker = old_speakers[-1] - except IndexError: - last_speaker = None kwargs.update({ 'object': self.object, 'speakers': speakers, 'old_speakers': old_speakers, - 'last_speaker': last_speaker, 'is_speaker': Speaker.objects.filter( time=None, person=self.request.user, item=self.object).exists(), + 'show_list': config['presentation_argument'] == 'show_list_of_speakers', }) return super(AgendaItemView, self).get_context_data(**kwargs) diff --git a/openslides/projector/static/styles/projector.css b/openslides/projector/static/styles/projector.css index b956d7b6d..84b8aeb3d 100644 --- a/openslides/projector/static/styles/projector.css +++ b/openslides/projector/static/styles/projector.css @@ -146,7 +146,12 @@ body{ color: #9FA9B7; list-style-type: none; } - +/* list of speakers */ +#list_of_speakers li +{ + font-size: 130%; + line-height: 160%; +} /* Table */ table { From 33f74c30250e01ec2d28ccab1ba3152a6d2d3276 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Mon, 18 Mar 2013 12:34:47 +0100 Subject: [PATCH 3/3] List of speakers. Part 2 --- openslides/agenda/forms.py | 6 ++ openslides/agenda/models.py | 52 +++++++++-- openslides/agenda/signals.py | 35 +++++++ openslides/agenda/static/styles/agenda.css | 10 ++ .../agenda/overlay_speaker_projector.html | 29 ++++++ .../agenda/overlay_speaker_widget.html | 6 ++ .../templates/agenda/speaker_widget.html | 6 ++ openslides/agenda/templates/agenda/view.html | 10 +- .../projector/agenda_list_of_speaker.html | 10 ++ openslides/agenda/urls.py | 21 +++-- openslides/agenda/views.py | 93 +++++++++++++++---- openslides/projector/projector.py | 2 +- openslides/projector/signals.py | 2 +- .../projector/static/styles/projector.css | 6 -- .../projector/overlay_message_projector.html | 2 +- tests/agenda/test_list_of_speakers.py | 6 +- 16 files changed, 247 insertions(+), 49 deletions(-) create mode 100644 openslides/agenda/templates/agenda/overlay_speaker_projector.html create mode 100644 openslides/agenda/templates/agenda/overlay_speaker_widget.html create mode 100644 openslides/agenda/templates/agenda/speaker_widget.html 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 %} + + 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:" %} -
        +
          {% for speaker in speakers %}
        • + + {{ forloop.counter }}. {{ speaker }} {% if perms.agenda.can_manage_agenda %} Finished speech @@ -144,7 +148,7 @@ {% empty %} {% trans "The list of speakers is empty." %} {% endfor %} -
      +

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