List of speakers. Part 2
This commit is contained in:
parent
1a416e5726
commit
33f74c3025
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
{% load i18n %}
|
||||
|
||||
<style type="text/css">
|
||||
#overlay_speaker_inner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 0.2em;
|
||||
background-color: #cccccc;
|
||||
opacity: 0.6;
|
||||
padding: 0.2em 0;
|
||||
margin: 1em;
|
||||
z-index:2;
|
||||
padding: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="overlay_speaker_inner">
|
||||
{% if speakers %}
|
||||
<header>{% trans "List of speakers:" %}</header>
|
||||
<ol>
|
||||
{% for speaker in speakers %}
|
||||
<li>{{ speaker }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<i>{% trans 'The list of speakers is empty.' %}</i>
|
||||
{% endif %}
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
<span>
|
||||
{% trans "List of speakers" %}
|
||||
</span>
|
6
openslides/agenda/templates/agenda/speaker_widget.html
Normal file
6
openslides/agenda/templates/agenda/speaker_widget.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
<div>
|
||||
<a href="{% url 'agenda_current_list_of_speakers' %}">{% trans 'Put me on the current list of speakers' %}</a>
|
||||
</div>
|
@ -8,6 +8,8 @@
|
||||
|
||||
{% block header %}
|
||||
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/agenda.css' %}" />
|
||||
{% comment %} TODO: include stylesheet in our repo{% endcomment %}
|
||||
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
@ -59,7 +61,7 @@
|
||||
<p>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
{% if item.speaker_list_closed %}
|
||||
<a href="{% url 'agenda_speaker_open' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Open list' %}</a>
|
||||
<a href="{% url 'agenda_speaker_reopen' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Open list' %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'agenda_speaker_close' item.pk %}" class="btn btn-mini btn-danger">{% trans 'Close list' %}</a>
|
||||
{% endif %}
|
||||
@ -130,9 +132,11 @@
|
||||
|
||||
<div class="well">
|
||||
<b>{% trans "Next speakers:" %}</b>
|
||||
<ol {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}>
|
||||
<ul {% if perms.agenda.can_manage_agenda %}id="list_of_speakers"{% endif %}>
|
||||
{% for speaker in speakers %}
|
||||
<li id="speaker_{{ speaker.pk }}">
|
||||
<span {% if perms.agenda.can_manage_agenda %}class="ui-icon ui-icon-arrowthick-2-n-s"{% endif %}></span>
|
||||
<span>{{ forloop.counter }}.</span>
|
||||
<a href="{% model_url speaker %}">{{ speaker }}</a>
|
||||
{% if perms.agenda.can_manage_agenda %}
|
||||
<a href="{% url 'agenda_speaker_speak' item.pk speaker.person.person_id %}" class="btn btn-mini"><i class="icon-bell"></i> Finished speech</a>
|
||||
@ -144,7 +148,7 @@
|
||||
{% empty %}
|
||||
<i>{% trans "The list of speakers is empty." %}</i>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{% if is_speaker %}
|
||||
|
@ -4,6 +4,16 @@
|
||||
|
||||
{% block title %}{{ block.super }} - {{ item }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<style type="text/css">
|
||||
#list_of_speakers li
|
||||
{
|
||||
font-size: 130%;
|
||||
line-height: 160%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
{% endblock %}
|
||||
|
@ -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<pk>\d+)/speaker/open/$',
|
||||
SpeakerListOpenView.as_view(open_list=True),
|
||||
name='agenda_speaker_open',
|
||||
url(r'^(?P<pk>\d+)/speaker/close/$',
|
||||
SpeakerListCloseView.as_view(),
|
||||
name='agenda_speaker_close',
|
||||
),
|
||||
|
||||
url(r'^(?P<pk>\d+)/speaker/close/$',
|
||||
SpeakerListOpenView.as_view(),
|
||||
name='agenda_speaker_close',
|
||||
url(r'^(?P<pk>\d+)/speaker/reopen/$',
|
||||
SpeakerListCloseView.as_view(reopen=True),
|
||||
name='agenda_speaker_reopen',
|
||||
),
|
||||
|
||||
url(r'^(?P<pk>\d+)/speaker/del/$',
|
||||
@ -91,8 +91,13 @@ urlpatterns = patterns(
|
||||
name='agenda_speaker_speak',
|
||||
),
|
||||
|
||||
url(r'^(?P<pk>\d+)/speaker/change_order$',
|
||||
url(r'^(?P<pk>\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',
|
||||
),
|
||||
)
|
||||
|
@ -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(
|
||||
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')]
|
||||
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')]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -16,4 +16,4 @@
|
||||
|
||||
<div id="overlay_message_inner">
|
||||
{{ message }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user