Merge pull request #588 from ostcar/speaker_list

List of speakers
This commit is contained in:
Oskar Hahn 2013-04-15 11:01:21 -07:00
commit 1e8e5334a5
25 changed files with 953 additions and 59 deletions

View File

@ -6,8 +6,10 @@
The OpenSlides agenda app appends the functionality to OpenSlides to The OpenSlides agenda app appends the functionality to OpenSlides to
manage agendas. manage agendas.
It includes time-management and list of speakers.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS. :copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from . import signals from . import signals, slides

View File

@ -17,7 +17,8 @@ from django.utils.translation import ugettext_lazy
from mptt.forms import TreeNodeChoiceField from mptt.forms import TreeNodeChoiceField
from openslides.utils.forms import CssClassMixin 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): class ItemForm(forms.ModelForm, CssClassMixin):
@ -57,3 +58,27 @@ class ItemOrderForm(CssClassMixin, forms.Form):
widget=forms.HiddenInput(attrs={'class': 'menu-mlid'})) widget=forms.HiddenInput(attrs={'class': 'menu-mlid'}))
parent = forms.IntegerField( parent = forms.IntegerField(
widget=forms.HiddenInput(attrs={'class': 'menu-plid'})) 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

View File

@ -10,17 +10,20 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from datetime import datetime
from django.db import models from django.db import models
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from openslides.utils.exceptions import OpenSlidesError
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.projector import SlideMixin from openslides.projector.projector import SlideMixin
from openslides.projector.api import ( from openslides.projector.api import (
register_slidemodel, get_slide_from_sid, register_slidefunc) register_slidemodel, get_slide_from_sid, register_slidefunc)
from .slides import agenda_show from openslides.utils.person.models import PersonField
class Item(MPTTModel, SlideMixin): class Item(MPTTModel, SlideMixin):
@ -39,15 +42,62 @@ class Item(MPTTModel, SlideMixin):
(ORGANIZATIONAL_ITEM, _('Organizational item'))) (ORGANIZATIONAL_ITEM, _('Organizational item')))
title = models.CharField(null=True, max_length=255, verbose_name=_("Title")) 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")) 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")) 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")) closed = models.BooleanField(default=False, verbose_name=_("Closed"))
"""
Flag, if the item is finished.
"""
weight = models.IntegerField(default=0, verbose_name=_("Weight")) weight = models.IntegerField(default=0, verbose_name=_("Weight"))
"""
Weight to sort the item in the agenda.
"""
parent = TreeForeignKey('self', null=True, blank=True, parent = TreeForeignKey('self', null=True, blank=True,
related_name='children') 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) 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): def get_related_slide(self):
""" """
@ -106,6 +156,11 @@ class Item(MPTTModel, SlideMixin):
'items': self.get_children(), 'items': self.get_children(),
'template': 'projector/AgendaSummary.html', '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: elif self.related_sid:
data = self.get_related_slide().slide() data = self.get_related_slide().slide()
else: else:
@ -186,5 +241,63 @@ class Item(MPTTModel, SlideMixin):
order_insertion_by = ['weight'] order_insertion_by = ['weight']
register_slidemodel(Item, control_template='agenda/control_item.html') class SpeakerManager(models.Manager):
register_slidefunc('agenda', agenda_show, weight=-1, name=_('Agenda')) 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()

View File

@ -13,10 +13,17 @@
from django.dispatch import receiver from django.dispatch import receiver
from django import forms from django import forms
from django.utils.translation import ugettext_lazy, ugettext_noop, ugettext as _ 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.signals import config_signal
from openslides.config.api import ConfigVariable, ConfigPage 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 # 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,), variables=(agenda_start_event_date_time,),
extra_context={'extra_stylefiles': extra_stylefiles, extra_context={'extra_stylefiles': extra_stylefiles,
'extra_javascript': extra_javascript}) '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)

View File

@ -12,12 +12,18 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.projector.api import register_slidemodel, register_slidefunc
from .models import Item
def agenda_show(): def agenda_show():
from openslides.agenda.models import Item
data = {} data = {}
items = Item.objects.filter(parent=None, type__exact=Item.AGENDA_ITEM) items = Item.objects.filter(parent=None, type__exact=Item.AGENDA_ITEM)
data['title'] = _("Agenda") data['title'] = _("Agenda")
data['items'] = items data['items'] = items
data['template'] = 'projector/AgendaSummary.html' data['template'] = 'projector/AgendaSummary.html'
return data return data
register_slidemodel(Item, control_template='agenda/control_item.html')
register_slidefunc('agenda', agenda_show, weight=-1, name=_('Agenda'))

View File

@ -34,6 +34,10 @@ function hideClosedSlides(hide) {
return false; return false;
} }
$('#speaker_list_changed_form').submit(function() {
$('#sort_order').val($('#list_of_speakers').sortable("toArray"));
});
$(function() { $(function() {
// change participant status (on/off) // change participant status (on/off)
$('.close_link').click(function(event) { $('.close_link').click(function(event) {
@ -72,11 +76,18 @@ $(function() {
$('#hide_closed_items').attr('checked', true); $('#hide_closed_items').attr('checked', true);
} }
}); });
if ($.cookie('Slide.HideClosed') === null) {
$('#hide_closed_items').attr('checked', false); // TODO: Fix this code and reactivate it again
$.cookie('Slide.HideClosed', 0); //# if ($.cookie('Slide.HideClosed') === null) {
} else if ($.cookie('Slide.HideClosed') == 1) { //# $('#hide_closed_items').attr('checked', false);
hideClosedSlides(true); //# $.cookie('Slide.HideClosed', 0);
$('#hide_closed_items').attr('checked', true); //# } 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();
}); });

View File

@ -21,3 +21,17 @@ table#agendatime td {
padding: 3px; padding: 3px;
white-space: nowrap; 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;
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
{% load i18n %}
{% load tags %}
<span>
{% trans "List of speakers" %}
</span>

View 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>

View File

@ -1,21 +1,36 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load tags %}
{% load staticfiles %}
{% block title %}{{ block.super }} {{ item.title }}{% endblock %} {% block title %}{{ block.super }} {{ item.title }}{% endblock %}
{% 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 %}
{{ block.super }}
{% comment %} TODO: import the sortable-plugin in our custom jquery-file {% endcomment %}
<script src="http://code.jquery.com/ui/1.10.2/jquery-ui.js"></script>
<script src="{% static 'javascript/agenda.js' %}"></script>
{% endblock %}
{% block content %} {% block content %}
<h1> <h1>
{{ item }} {{ item }}
<small class="pull-right"> <small class="pull-right">
<div class="btn-toolbar"> <div class="btn-toolbar">
<a href="{% url 'item_overview' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a> <a href="{% url 'item_overview' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a>
{% if perms.agenda.can_manage_agenda %}
<div class="btn-group"> <div class="btn-group">
<a data-toggle="dropdown" href="#" class="btn btn-mini dropdown-toggle"> <a data-toggle="dropdown" href="#" class="btn btn-mini dropdown-toggle">
{% trans 'More actions' %} {% trans 'More actions' %}
<span class="caret"></span> <span class="caret"></span>
</a> </a>
{% if perms.agenda.can_manage_agenda %}
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
<li><a href="{% url 'item_edit' item.id %}"><i class="icon-edit"></i> {% trans 'Edit item' %}</a></li> <li><a href="{% url 'item_edit' item.id %}"><i class="icon-edit"></i> {% trans 'Edit item' %}</a></li>
<li><a href="{% url 'item_delete' item.id %}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li> <li><a href="{% url 'item_delete' item.id %}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li>
@ -23,19 +38,143 @@
{% endif %} {% endif %}
</div> </div>
{% if perms.projector.can_manage_projector %} {% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' item.sid %}" class="activate_link btn btn-mini {% if item.active %}btn-primary{% endif %}" rel="tooltip" data-original-title="{% trans 'Show' %}"> <a href="{% url 'projector_activate_slide' item.sid %}"
<i class="icon icon-facetime-video {% if item.active %}icon-white{% endif %}"></i> class="activate_link btn btn-mini {% if item.active and not show_list %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon icon-facetime-video {% if item.active and not show_list %}icon-white{% endif %}"></i>
</a> </a>
{% endif %} {% endif %}
</div> </div>
</small> </small>
</h1> </h1>
<p>{{ item.text|safe|linebreaks }}</p> <p>{{ item.text|safe|linebreaks }}</p>
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
{% if item.comment %} {% if item.comment %}
<h2>{% trans "Comment" %}</h2> <h3>{% trans "Comment" %}</h3>
<p>{{ item.comment|linebreaks }}</p> <p>{{ item.comment|linebreaks }}</p>
{% endif %} {% endif %}
{% endif %}
{# List of Speakers #}
<h3>{% trans "List of speakers" %} {% if item.speaker_list_closed %}<span class="label label-important">{% trans 'closed' %}</span>{% endif %}</h3>
<p>
{% if perms.agenda.can_manage_agenda %}
{% if item.speaker_list_closed %}
<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 %} {% endif %}
{% endif %}
{% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' item.sid 'show_list_of_speakers' %}"
class="activate_link btn btn-mini {% if item.active and show_list %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}">
<i class="icon icon-facetime-video {% if item.active and show_list %}icon-white{% endif %}"></i>
{% trans 'Show list' %}
</a>
{% endif %}
</p>
{% if old_speakers %}
<div class="well">
<b>{% trans "Last speakers:" %}</b>
<div class="btn-group" data-toggle="buttons-checkbox">
<button type="button" class="btn btn-mini" data-toggle="collapse" data-target="#all_speakers">
{% trans "Show all speakers" %}
</button>
</div>
<br>
<div id="all_speakers" class="collapse out">
{% for speaker in old_speakers %}
{% if not forloop.last %}
<small>{{forloop.counter}}.
[{{ speaker.time }}h]
<a href="{% model_url speaker %}">{{ speaker }}</a>
{% if perms.agenda.can_manage_agenda %}
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</small><br>
{% endif %}
{% endfor %}
</div>
{% if old_speakers %}
{% with last=old_speakers|last %}
<small>{{ old_speakers|length }}.
[{{ last.time }}h]
<a href="{% model_url last %}">{{ last }}</a>
{% if perms.agenda.can_manage_agenda %}
<a href="{% model_url last 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</small>
{% endwith %}
{% endif %}
</div>
{% endif %}
{% if perms.agenda.can_manage_agenda %}
<form id="speaker_list_changed_form" action="{% url 'agenda_speaker_change_order' item.pk %}" method="post" style="display:none" class="alert alert-warning">{% csrf_token %}
<button type="button" class="close" data-dismiss="alert">×</button>
<p>{% trans "Do you want to save the changed order of speakers?" %}</p>
<input id="sort_order" name="sort_order" type="hidden"></hidden>
<p>
<button class="btn" type="submit">{% trans 'Yes' %}</button>
<a href="{% url 'item_view' item.pk %}" class="btn">{% trans 'No' %}</a>
</p>
</form>
{% endif %}
<div class="well">
<b>{% trans "Next speakers:" %}</b>
<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>
<a href="{% model_url speaker 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</li>
{% empty %}
<i>{% trans "The list of speakers is empty." %}</i>
{% endfor %}
</ul>
<p>
{% if is_speaker %}
<a href="{% url 'agenda_speaker_delete' object.id %}" class="btn">{% trans "Remove me from the list" %}</a>
{% elif not object.speaker_list_closed %}
<a href="{% url 'agenda_speaker_append' object.id %}" class="btn">{% trans "Put me on the list" %}</a>
{% endif %}
</p>
{% if perms.can_manage_agenda %}
<form action="" method="post">{% csrf_token %}
{% for field in form %}
<label>{{ field.label }}:</label>
<div class="control-group input-append {% if field.errors %}error{% endif %}">
{{ field }}
<button class="btn btn-primary" type="submit" title="{% trans 'Apply' %}"><i class="icon-ok icon-white"></i></button>
{% if perms.participant.can_see_participant and perms.participant.can_manage_participant %}
<a href="{% url 'user_new' %}" class="btn" title="{% trans 'Add new participant' %}"><i class="icon-add-user"></i></a>
{% endif %}
{% if field.errors %}
<span class="help-inline">{{ field.errors }}</span>
{% endif %}
</div>
{% endfor %}
</form>
{% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% 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 %}
{% block scrollcontent %}
{% if speakers %}
<ol id="list_of_speakers">
{% for speaker in speakers %}
<li>{{ speaker }}</li>
{% endfor %}
</ol>
{% else %}
{% trans 'The list of speakers is empty' %}
{% endif %}
{% endblock %}

View File

@ -12,8 +12,9 @@
from django.conf.urls import url, patterns from django.conf.urls import url, patterns
from openslides.agenda.views import ( from openslides.agenda.views import (
Overview, View, SetClosed, ItemUpdate, Overview, AgendaItemView, SetClosed, ItemUpdate, SpeakerSpeakView,
ItemCreate, ItemDelete, AgendaPDF) ItemCreate, ItemDelete, AgendaPDF, SpeakerAppendView, SpeakerDeleteView,
SpeakerListCloseView, SpeakerChangeOrderView, CurrentListOfSpeakersView)
urlpatterns = patterns( urlpatterns = patterns(
'', '',
@ -23,7 +24,7 @@ urlpatterns = patterns(
), ),
url(r'^(?P<pk>\d+)/$', url(r'^(?P<pk>\d+)/$',
View.as_view(), AgendaItemView.as_view(),
name='item_view', name='item_view',
), ),
@ -58,4 +59,45 @@ urlpatterns = patterns(
AgendaPDF.as_view(), AgendaPDF.as_view(),
name='print_agenda', name='print_agenda',
), ),
# Speaker List
url(r'^(?P<pk>\d+)/speaker/$',
SpeakerAppendView.as_view(),
name='agenda_speaker_append',
),
url(r'^(?P<pk>\d+)/speaker/close/$',
SpeakerListCloseView.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/$',
SpeakerDeleteView.as_view(),
name='agenda_speaker_delete',
),
url(r'^(?P<pk>\d+)/speaker/(?P<speaker>\d+)/del/$',
SpeakerDeleteView.as_view(),
name='agenda_speaker_delete',
),
url(r'^(?P<pk>\d+)/speaker/(?P<person_id>[^/]+)/speak/$',
SpeakerSpeakView.as_view(),
name='agenda_speaker_speak',
),
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',
),
) )

View File

@ -9,6 +9,7 @@
:copyright: 2011, 2012 by the OpenSlides team, see AUTHORS. :copyright: 2011, 2012 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
# TODO: Rename all views and template names
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -22,15 +23,16 @@ from django.views.generic.detail import SingleObjectMixin
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.views import ( from openslides.utils.views import (
TemplateView, RedirectView, UpdateView, CreateView, DeleteView, PDFView, TemplateView, RedirectView, UpdateView, CreateView, DeleteView, PDFView,
DetailView, FormView) DetailView, FormView, SingleObjectMixin)
from openslides.utils.template import Tab from openslides.utils.template import Tab
from openslides.utils.utils import html_strong 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 openslides.projector.projector import Widget, SLIDE
from .models import Item from .models import Item, Speaker
from .forms import ItemOrderForm, ItemForm from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm
class Overview(TemplateView): class Overview(TemplateView):
@ -112,14 +114,40 @@ class Overview(TemplateView):
return self.render_to_response(context) return self.render_to_response(context)
class View(DetailView): class AgendaItemView(SingleObjectMixin, FormView):
""" """
Show an agenda item. Show an agenda item.
""" """
# TODO: use 'SingleObjectTemplateResponseMixin' to choose the right template name
permission_required = 'agenda.can_see_agenda' permission_required = 'agenda.can_see_agenda'
template_name = 'agenda/view.html' template_name = 'agenda/view.html'
model = Item model = Item
context_object_name = '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): class SetClosed(RedirectView, SingleObjectMixin):
@ -149,6 +177,9 @@ class SetClosed(RedirectView, SingleObjectMixin):
self.object.set_closed(closed) self.object.set_closed(closed)
return super(SetClosed, self).pre_redirect(request, *args, **kwargs) return super(SetClosed, self).pre_redirect(request, *args, **kwargs)
def get_url_name_args(self):
return []
class ItemUpdate(UpdateView): class ItemUpdate(UpdateView):
""" """
@ -224,6 +255,209 @@ class AgendaPDF(PDFView):
story.append(Paragraph(item.get_title(), stylesheet['Item'])) 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): def register_tab(request):
""" """
Registers the agenda tab. Registers the agenda tab.
@ -242,11 +476,19 @@ def get_widgets(request):
""" """
Returns the agenda widget for the projector tab. Returns the agenda widget for the projector tab.
""" """
return [Widget( return [
Widget(
name='agenda', name='agenda',
display_name=_('Agenda'), display_name=_('Agenda'),
template='agenda/widget.html', template='agenda/widget.html',
context={ context={
'agenda': SLIDE['agenda'], 'agenda': SLIDE['agenda'],
'items': Item.objects.all()}, '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')]

View File

@ -115,8 +115,6 @@
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
<p>
</p>
{% endif %} {% endif %}
</form> </form>
{% endif %} {% endif %}

View File

@ -86,16 +86,16 @@ class User(PersonMixin, Person, SlideMixin, DjangoUser):
return self.last_name.lower() return self.last_name.lower()
@models.permalink @models.permalink
def get_absolute_url(self, link='view'): def get_absolute_url(self, link='detail'):
""" """
Return the URL to this user. Return the URL to this user.
link can be: link can be:
* view * detail
* edit * edit
* delete * delete
""" """
if link == 'view': if link == 'detail' or link == 'view':
return ('user_view', [str(self.id)]) return ('user_view', [str(self.id)])
if link == 'edit': if link == 'edit':
return ('user_edit', [str(self.id)]) return ('user_edit', [str(self.id)])

View File

@ -77,8 +77,10 @@ def create_builtin_groups(sender, **kwargs):
perm_2 = Permission.objects.get(content_type=ct_projector, codename='can_see_dashboard') perm_2 = Permission.objects.get(content_type=ct_projector, codename='can_see_dashboard')
ct_agenda = ContentType.objects.get(app_label='agenda', model='item') 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_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') 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') ct_motion = ContentType.objects.get(app_label='motion', model='motion')
perm_4 = Permission.objects.get(content_type=ct_motion, codename='can_see_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 = 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_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 = 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 # Delegates
perm_7 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') perm_7 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion')

View File

@ -169,4 +169,4 @@ class Overlay(object):
return self.name in config['projector_active_overlays'] return self.name in config['projector_active_overlays']
def show_on_projector(self): 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

View File

@ -84,7 +84,7 @@ def setup_projector_config_variables(sender, **kwargs):
@receiver(projector_overlays, dispatch_uid="projector_countdown") @receiver(projector_overlays, dispatch_uid="projector_countdown")
def countdown(sender, **kwargs): def countdown(sender, **kwargs):
""" """
Reveiver for the countdown. Receiver for the countdown.
""" """
name = 'projector_countdown' name = 'projector_countdown'
request = kwargs.get('request', None) request = kwargs.get('request', None)

View File

@ -147,7 +147,6 @@ body{
list-style-type: none; list-style-type: none;
} }
/* Table */ /* Table */
table { table {
border-collapse:collapse; border-collapse:collapse;

View File

@ -16,4 +16,4 @@
<div id="overlay_message_inner"> <div id="overlay_message_inner">
{{ message }} {{ message }}
</span> </div>

View File

@ -23,7 +23,7 @@ class Person(object):
""" """
raise NotImplementedError('Any person object needs a person_id') raise NotImplementedError('Any person object needs a person_id')
def __repr__(self): def __unicode__(self):
""" """
Return a string for this person. Return a string for this person.
""" """

View File

@ -42,6 +42,8 @@ class PersonField(models.fields.Field):
""" """
if value is None: if value is None:
return None return None
elif isinstance(value, basestring):
return value
else: else:
return value.person_id return value.person_id

View File

@ -149,15 +149,26 @@ class QuestionMixin(object):
def get_redirect_url(self, **kwargs): def get_redirect_url(self, **kwargs):
if self.request.method == 'GET': 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: 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): def get_url_name_args(self):
try:
return [self.object.pk]
except AttributeError:
return [] return []
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
# Prints the question in a GET request """
Prints the question in a GET request.
"""
self.confirm_form() self.confirm_form()
def get_question(self): def get_question(self):
@ -251,6 +262,9 @@ class RedirectView(PermissionMixin, AjaxMixin, _RedirectView):
return super(RedirectView, self).get_redirect_url(**kwargs) return super(RedirectView, self).get_redirect_url(**kwargs)
def get_url_name_args(self): def get_url_name_args(self):
try:
return [self.object.pk]
except AttributeError:
return [] return []
@ -319,6 +333,9 @@ class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView):
def get_success_message(self): def get_success_message(self):
return _('%s was successfully deleted.') % html_strong(self.object) return _('%s was successfully deleted.') % html_strong(self.object)
def get_url_name_args(self):
return []
class DetailView(PermissionMixin, ExtraContextMixin, _DetailView): class DetailView(PermissionMixin, ExtraContextMixin, _DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View File

@ -0,0 +1,175 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Unit test for the list of speakers
:copyright: 20112013 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)