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..0ee615f65 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,27 @@ 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):
+ """
+ Form to set an user to a list of speakers.
+ """
+ speaker = PersonFormField(
+ widget=forms.Select(attrs={'class': 'medium-input'}),
+ label=ugettext_lazy("Add participant"))
+
+ def __init__(self, item, *args, **kwargs):
+ self.item = item
+ 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(
+ '%s is already on the list of speakers.'
+ % unicode(speaker)))
+ return speaker
diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py
index 6a994e3d4..8abed0914 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,62 @@ 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 +156,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 +241,63 @@ 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 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)
+
+
+class Speaker(models.Model):
+ """
+ Model for the Speaker list.
+ """
+
+ objects = SpeakerManager()
+
+ person = PersonField()
+ """
+ ForeinKey to the person who speaks.
+ """
+
+ item = models.ForeignKey(Item)
+ """
+ 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 = (
+ ('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):
+ """
+ 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/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..35cb30765 100644
--- a/openslides/agenda/static/javascript/agenda.js
+++ b/openslides/agenda/static/javascript/agenda.js
@@ -34,6 +34,10 @@ function hideClosedSlides(hide) {
return false;
}
+$('#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 +76,18 @@ $(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').sortable({axis: "y", containment: "parent", update: function(event, ui) {
+ $('#speaker_list_changed_form').show();
+ }});
+ $('#list_of_speakers').disableSelection();
});
diff --git a/openslides/agenda/static/styles/agenda.css b/openslides/agenda/static/styles/agenda.css
index 3c6ead5ea..f5d1e311b 100644
--- a/openslides/agenda/static/styles/agenda.css
+++ b/openslides/agenda/static/styles/agenda.css
@@ -21,3 +21,17 @@ table#agendatime td {
padding: 3px;
white-space: nowrap;
}
+
+#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 %}
+
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 229298f33..400f2542e 100644
--- a/openslides/agenda/templates/agenda/view.html
+++ b/openslides/agenda/templates/agenda/view.html
@@ -1,41 +1,180 @@
{% extends "base.html" %}
{% load i18n %}
+{% load tags %}
+{% load staticfiles %}
{% block title %}{{ block.super }} – {{ item.title }}{% endblock %}
+{% block header %}
+
+ {% comment %} TODO: include stylesheet in our repo{% endcomment %}
+
+{% endblock %}
+
+{% block javascript %}
+ {{ block.super }}
+ {% comment %} TODO: import the sortable-plugin in our custom jquery-file {% endcomment %}
+
+
+{% endblock %}
+
{% block content %}
{{ item }}
- {{ 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 "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' %}
+
+
+{% endif %}
+
+
+{% if old_speakers %}
+
+
{% trans "Last speakers:" %}
+
+
+
+
+
+ {% for speaker in old_speakers %}
+ {% if not forloop.last %}
+
{{forloop.counter}}.
+ [{{ speaker.time }}h]
+ {{ speaker }}
+ {% if perms.agenda.can_manage_agenda %}
+
+
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+ {% if old_speakers %}
+ {% with last=old_speakers|last %}
+
{{ old_speakers|length }}.
+ [{{ last.time }}h]
+ {{ last }}
+ {% if perms.agenda.can_manage_agenda %}
+
+
+
+ {% endif %}
+
+ {% endwith %}
+ {% endif %}
+
+{% endif %}
+
+{% if perms.agenda.can_manage_agenda %}
+
+{% 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..7d667dc6f
--- /dev/null
+++ b/openslides/agenda/templates/projector/agenda_list_of_speaker.html
@@ -0,0 +1,31 @@
+{% extends "base-projector.html" %}
+
+{% load i18n %}
+
+{% block title %}{{ block.super }} - {{ item }}{% endblock %}
+
+{% block header %}
+
+{% endblock %}
+
+{% block content %}
+ {{ title }}
+{% endblock %}
+
+{% block scrollcontent %}
+ {% if 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 7ca03dc8f..bf6416b67 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,
+ SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView)
urlpatterns = patterns(
'',
@@ -23,7 +24,7 @@ urlpatterns = patterns(
),
url(r'^(?P\d+)/$',
- View.as_view(),
+ AgendaItemView.as_view(),
name='item_view',
),
@@ -58,4 +59,45 @@ 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/close/$',
+ SpeakerListCloseView.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/$',
+ 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',
+ ),
+
+ 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 cb471119e..c8afc4da3 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.api import get_active_slide, get_slide_from_sid
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,40 @@ 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.filter(item=self.object.pk)
+ .exclude(time=None).order_by('time'))
+ kwargs.update({
+ 'object': self.object,
+ 'speakers': speakers,
+ 'old_speakers': old_speakers,
+ 'is_speaker': Speaker.objects.filter(
+ time=None, person=self.request.user, item=self.object).exists(),
+ 'show_list': config['presentation_argument'] == 'show_list_of_speakers',
+ })
+ 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 +177,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 +255,209 @@ 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 the speaking person.
+ """
+ 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 SpeakerListCloseView(SingleObjectMixin, RedirectView):
+ """
+ View to close and reopen a list of speakers.
+ """
+ permission_required = 'agenda.can_manage_agenda'
+ model = Item
+ reopen = False
+ url_name = 'item_view'
+
+ def pre_redirect(self, *args, **kwargs):
+ self.object = self.get_object()
+ self.object.speaker_list_closed = not self.reopen
+ 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()
+ 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.
@@ -242,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/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/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 b956d7b6d..62e5c9622 100644
--- a/openslides/projector/static/styles/projector.css
+++ b/openslides/projector/static/styles/projector.css
@@ -147,7 +147,6 @@ body{
list-style-type: none;
}
-
/* Table */
table {
border-collapse:collapse;
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/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..fe8e28386
--- /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 already on the list of speakers of 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 already 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/reopen/', self.admin_client, 302)
+ item = Item.objects.get(pk=self.item1.pk)
+ self.assertFalse(item.speaker_list_closed)