Rework of the projector with websocket

* Set a static projector title
* absolute_urls for the activate links
* update the projector when a slide changes (in save())
* insert the absolute_url template filter
* Preview to slides
* renamed is_active to is_active_slide
* The SlideMixin has to come before the PersonMixin
* Update list of speakers
* Render Countdown via JS
* Reconnect projector after connection lost
* Overlays can allways be active and do not appear in the widget
* Rewrote the clock as overlay
This commit is contained in:
Oskar Hahn 2013-08-04 12:59:11 +02:00
parent d2f5302e28
commit ecf5248962
68 changed files with 1242 additions and 1164 deletions

View File

@ -4,7 +4,7 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% trans 'I am on the list of speakers of the following items:' %} {% trans 'I am on the list of speakers of the following items:' %}
{% for item in items %} {% for item in items %}
<li><a href="{% model_url item %}">{{ item }}</a></li> <li><a href="{{ item|absolute_url }}">{{ item }}</a></li>
{% empty %} {% empty %}
<li><i>{% trans 'None' %}</i></li> <li><i>{% trans 'None' %}</i></li>
{% endfor %} {% endfor %}
@ -15,7 +15,7 @@
{% trans 'I submitted the following motions:' %} {% trans 'I submitted the following motions:' %}
{% for motion in submitted_motions %} {% for motion in submitted_motions %}
<li> <li>
<a href="{% model_url motion %}"> <a href="{{ motion|absolute_url }}">
{{ motion.identifier|add:' | '|default:'' }} {{ motion.identifier|add:' | '|default:'' }}
{{ motion }} {{ motion }}
</a> </a>
@ -31,7 +31,7 @@
{% trans 'I support the following motions:' %} {% trans 'I support the following motions:' %}
{% for motion in supported_motions %} {% for motion in supported_motions %}
<li> <li>
<a href="{% model_url motion %}"> <a href="{{ motion|absolute_url }}">
{% if motion.identifier %} {% if motion.identifier %}
[# {{ motion.identifier }}] [# {{ motion.identifier }}]
{% else %} {% else %}
@ -50,7 +50,7 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% trans 'I am candidate for the following elections:' %} {% trans 'I am candidate for the following elections:' %}
{% for assignment in assignments %} {% for assignment in assignments %}
<li><a href="{% model_url assignment %}">{{ assignment }}</a></li> <li><a href="{{ assignment|absolute_url }}">{{ assignment }}</a></li>
{% empty %} {% empty %}
<li><i>{% trans 'None' %}</i></li> <li><i>{% trans 'None' %}</i></li>
{% endfor %} {% endfor %}

View File

@ -23,19 +23,19 @@ from mptt.models import MPTTModel, TreeForeignKey
from openslides.utils.exceptions import OpenSlidesError 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.api import (
register_slidemodel, get_slide_from_sid, register_slidefunc)
from openslides.utils.person.models import PersonField from openslides.utils.person.models import PersonField
from openslides.projector.api import (
update_projector, get_active_slide, update_projector_overlay)
from openslides.projector.models import SlideMixin
class Item(MPTTModel, SlideMixin): class Item(SlideMixin, MPTTModel):
""" """
An Agenda Item An Agenda Item
MPTT-model. See http://django-mptt.github.com/django-mptt/ MPTT-model. See http://django-mptt.github.com/django-mptt/
""" """
prefix = 'item' slide_callback_name = 'agenda'
AGENDA_ITEM = 1 AGENDA_ITEM = 1
ORGANIZATIONAL_ITEM = 2 ORGANIZATIONAL_ITEM = 2
@ -98,6 +98,7 @@ class Item(MPTTModel, SlideMixin):
""" """
Field for generic relation to a related object. Id of the object. Field for generic relation to a related object. Id of the object.
""" """
# TODO: rename it to object_pk
content_object = generic.GenericForeignKey() content_object = generic.GenericForeignKey()
""" """
@ -119,6 +120,14 @@ class Item(MPTTModel, SlideMixin):
class MPTTMeta: class MPTTMeta:
order_insertion_by = ['weight'] order_insertion_by = ['weight']
def save(self, *args, **kwargs):
super(Item, self).save(*args, **kwargs)
active_slide = get_active_slide()
active_slide_pk = active_slide.get('pk', None)
if (active_slide['callback'] == 'agenda' and
unicode(self.parent_id) == unicode(active_slide_pk)):
update_projector()
def __unicode__(self): def __unicode__(self):
return self.get_title() return self.get_title()
@ -134,6 +143,11 @@ class Item(MPTTModel, SlideMixin):
return reverse('item_edit', args=[str(self.id)]) return reverse('item_edit', args=[str(self.id)])
if link == 'delete': if link == 'delete':
return reverse('item_delete', args=[str(self.id)]) return reverse('item_delete', args=[str(self.id)])
if link == 'projector_list_of_speakers':
return '%s&type=list_of_speakers' % super(Item, self).get_absolute_url('projector')
if link == 'projector_summary':
return '%s&type=summary' % super(Item, self).get_absolute_url('projector')
return super(Item, self).get_absolute_url(link)
def get_title(self): def get_title(self):
""" """
@ -157,41 +171,6 @@ class Item(MPTTModel, SlideMixin):
except AttributeError: except AttributeError:
raise NotImplementedError('You have to provide a get_agenda_title_supplement method on your related model.') raise NotImplementedError('You have to provide a get_agenda_title_supplement method on your related model.')
def slide(self):
"""
Return a map with all data for the slide.
There are four cases:
* summary slide
* list of speakers
* related item, i. e. the slide of the related object
* normal slide of the item
The method returns only one of them according to the config value
'presentation_argument' and the attribute 'content_object'.
"""
if config['presentation_argument'] == 'summary':
data = {'title': self.get_title(),
'items': self.get_children().filter(type__exact=Item.AGENDA_ITEM),
'template': 'projector/AgendaSummary.html'}
elif config['presentation_argument'] == 'show_list_of_speakers':
list_of_speakers = self.get_list_of_speakers(
old_speakers_count=config['agenda_show_last_speakers'])
data = {'title': self.get_title(),
'item': self,
'template': 'projector/agenda_list_of_speaker.html',
'list_of_speakers': list_of_speakers}
elif self.content_object:
data = self.content_object.slide()
else:
data = {'item': self,
'title': self.get_title(),
'template': 'projector/AgendaText.html'}
return data
def set_closed(self, closed=True): def set_closed(self, closed=True):
""" """
Changes the closed-status of the item. Changes the closed-status of the item.
@ -351,6 +330,14 @@ class Speaker(models.Model):
('can_be_speaker', ugettext_noop('Can put oneself on the list of speakers')), ('can_be_speaker', ugettext_noop('Can put oneself on the list of speakers')),
) )
def save(self, *args, **kwargs):
super(Speaker, self).save(*args, **kwargs)
self.check_and_update_projector()
def delete(self, *args, **kwargs):
super(Speaker, self).delete(*args, **kwargs)
self.check_and_update_projector()
def __unicode__(self): def __unicode__(self):
return unicode(self.person) return unicode(self.person)
@ -361,6 +348,21 @@ class Speaker(models.Model):
return reverse('agenda_speaker_delete', return reverse('agenda_speaker_delete',
args=[self.item.pk, self.pk]) args=[self.item.pk, self.pk])
def check_and_update_projector(self):
"""
Checks, if the agenda item, or parts of it, is on the projector.
If yes, it updates the projector.
"""
active_slide = get_active_slide()
active_slide_pk = active_slide.get('pk', None)
slide_type = active_slide.get('type', None)
if (active_slide['callback'] == 'agenda' and
unicode(self.item_id) == unicode(active_slide_pk)):
if slide_type == 'list_of_speakers':
update_projector()
elif slide_type is None:
update_projector_overlay('agenda_speaker')
def begin_speach(self): def begin_speach(self):
""" """
Let the person speak. Let the person speak.

View File

@ -25,8 +25,7 @@ from openslides.config.api import config, ConfigVariable, ConfigPage
from openslides.projector.signals import projector_overlays from openslides.projector.signals import projector_overlays
from openslides.projector.projector import Overlay from openslides.projector.projector import Overlay
from openslides.projector.api import (get_active_slide, get_slide_from_sid, from openslides.projector.api import get_active_slide
clear_projector_cache)
from .models import Speaker, Item from .models import Speaker, Item
@ -94,23 +93,27 @@ def agenda_list_of_speakers(sender, **kwargs):
def get_projector_html(): def get_projector_html():
""" """
Returns an html-code to show on the projector. Returns an html-code to show on the projector.
The overlay is only shown on agenda-items and not on the
list-of-speakers slide.
""" """
slide = get_slide_from_sid(get_active_slide(only_sid=True), element=True) active_slide = get_active_slide()
if not isinstance(slide, Item): slide_type = active_slide.get('type', None)
# Only show list of speakers overlay on agenda items active_slide_pk = active_slide.get('pk', None)
if (active_slide['callback'] == 'agenda' and
slide_type != 'list_of_speakers' and
active_slide_pk is not None):
item = Item.objects.get(pk=active_slide_pk)
list_of_speakers = item.get_list_of_speakers(
old_speakers_count=config['agenda_show_last_speakers'],
coming_speakers_count=5)
context = {
'list_of_speakers': list_of_speakers,
'closed': item.speaker_list_closed,
}
return render_to_string('agenda/overlay_speaker_projector.html', context)
else:
return None return None
if config['presentation_argument'] == 'show_list_of_speakers':
# Do not show list of speakers overlay on the list of speakers slide
return None
clear_projector_cache()
list_of_speakers = slide.get_list_of_speakers(
old_speakers_count=config['agenda_show_last_speakers'],
coming_speakers_count=5)
context = {
'list_of_speakers': list_of_speakers,
'closed': slide.speaker_list_closed,
}
return render_to_string('agenda/overlay_speaker_projector.html', context)
return Overlay(name, get_widget_html, get_projector_html) return Overlay(name, get_widget_html, get_projector_html)

View File

@ -10,20 +10,63 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from django.utils.translation import ugettext_lazy, ugettext as _ from django.template.loader import render_to_string
from openslides.projector.api import register_slidemodel, register_slidefunc from openslides.config.api import config
from openslides.projector.api import register_slide, get_projector_content
from .models import Item from .models import Item
def agenda_show(): def agenda_slide(**kwargs):
data = {} """
items = Item.objects.filter(parent=None, type__exact=Item.AGENDA_ITEM) Return the html code for all slides of the agenda app.
data['title'] = _("Agenda")
data['items'] = items
data['template'] = 'projector/AgendaSummary.html'
return data
register_slidemodel(Item, control_template='agenda/control_item.html') If no id is given, show a summary of all parent items.
register_slidefunc('agenda', agenda_show, weight=-1, name=ugettext_lazy('Agenda'))
If an id is given, show the item depending of the argument 'type'.
If 'type' is not set, show only the item.
If 'type' is 'summary', show a summary of all children of the item.
If 'type' is 'list_of_speakers', show the list of speakers for the item.
"""
item_pk = kwargs.get('pk', None)
slide_type = kwargs.get('type', None)
try:
item = Item.objects.get(pk=item_pk)
except Item.DoesNotExist:
item = None
if slide_type == 'summary' or item is None:
context = {}
if item is None:
items = Item.objects.filter(parent=None, type__exact=Item.AGENDA_ITEM)
else:
items = item.get_children().filter(type__exact=Item.AGENDA_ITEM)
context['title'] = item.get_title()
context['items'] = items
return render_to_string('agenda/item_slide_summary.html', context)
elif slide_type == 'list_of_speakers':
list_of_speakers = item.get_list_of_speakers(
old_speakers_count=config['agenda_show_last_speakers'])
context = {'title': item.get_title(),
'item': item,
'list_of_speakers': list_of_speakers}
return render_to_string('agenda/item_slide_list_of_speaker.html', context)
elif item.content_object:
slide_dict = {
'callback': item.content_object.slide_callback_name,
'pk': item.content_object.pk}
return get_projector_content(slide_dict)
else:
context = {'item': item}
return render_to_string('agenda/item_slide.html', context)
register_slide('agenda', agenda_slide)

View File

@ -1,27 +1,27 @@
{% load i18n %} {% load i18n %}
{% load tags %} {% load tags %}
<div class="{% if node.active %}activeline{% endif %}"> <div class="{% if node.is_active_slide %}activeline{% endif %}">
{% if perms.agenda.can_manage_agenda or perms.projector.can_manage_projector %} {% if perms.agenda.can_manage_agenda or perms.projector.can_manage_projector %}
<div class="manage"> <div class="manage">
<span style="width: 1px; white-space: nowrap;"> <span style="width: 1px; white-space: nowrap;">
{% if perms.projector.can_manage_projector %} {% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' node.sid %}" <a href="{{ node|absolute_url:'projector' }}"
class="activate_link btn {% if node.active and not show_list and not summary %}btn-primary{% endif %} btn-mini" class="activate_link btn {% if node.is_active_slide and active_type == 'text' %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show agenda item' %}"> rel="tooltip" data-original-title="{% trans 'Show agenda item' %}">
<i class="icon-facetime-video {% if node.active and not show_list and not summary %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if node.is_active_slide and active_type == 'text' %}icon-white{% endif %}"></i>
</a> </a>
<a href="{% url 'projector_activate_slide' node.sid 'show_list_of_speakers' %}" <a href="{{ node|absolute_url:'projector_list_of_speakers' }}"
class="activate_link btn btn-mini {% if node.active and show_list %}btn-primary{% endif %}" class="activate_link btn btn-mini {% if node.is_active_slide and active_type == 'list_of_speakers' %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}"> rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}">
<i class="icon icon-bell {% if node.active and show_list %}icon-white{% endif %}"></i> <i class="icon icon-bell {% if node.is_active_slide and active_type == 'list_of_speakers' %}icon-white{% endif %}"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
<a href="{% model_url node 'edit' %}" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini"> <a href="{{ node|absolute_url:'edit' }}" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% model_url node 'delete' %}" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini"> <a href="{{ node|absolute_url:'delete' }}" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</a> </a>
<a href="{% if node.closed %}{% url 'item_open' node.id %}{% else %}{% url 'item_close' node.id %}{% endif %}" <a href="{% if node.closed %}{% url 'item_open' node.id %}{% else %}{% url 'item_close' node.id %}{% endif %}"
@ -30,10 +30,10 @@
<i class="{% if node.closed %}icon-checked-new{% else %}icon-unchecked-new{% endif %}"></i> <i class="{% if node.closed %}icon-checked-new{% else %}icon-unchecked-new{% endif %}"></i>
</a> </a>
{% if not node.is_leaf_node %} {% if not node.is_leaf_node %}
<a href="{% url 'projector_activate_slide' node.sid 'summary' %}" <a href="{{ node|absolute_url:'projector_summary' }}"
class="activate_link btn btn-mini {% if node.active and summary %}btn-primary{% endif %}" class="activate_link btn btn-mini {% if node.is_active_slide and active_type == 'summary' %}btn-primary{% endif %}"
title="{% trans 'Show summary for this item' %}"> title="{% trans 'Show summary for this item' %}">
<i class="icon-summary {% if node.active and summary %}icon-white{% endif %}"></i> <i class="icon-summary {% if node.is_active_slide and active_type == 'summary' %}icon-white{% endif %}"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -79,7 +79,7 @@
{{ form.self }} {{ form.self }}
{{ form.parent }} {{ form.parent }}
{% endwith %} {% endwith %}
<a href="{% model_url node 'view' %}">{% if node.type == node.ORGANIZATIONAL_ITEM %}<i>[{% endif %}{{ node }}{% if node.type == node.ORGANIZATIONAL_ITEM %}]</i>{% endif %}</a> <a href="{{ node|absolute_url }}">{% if node.type == node.ORGANIZATIONAL_ITEM %}<i>[{% endif %}{{ node }}{% if node.type == node.ORGANIZATIONAL_ITEM %}]</i>{% endif %}</a>
{{ node.get_title_supplement|safe }} {{ node.get_title_supplement|safe }}
</div> </div>
</div> </div>

View File

@ -0,0 +1,8 @@
{% load i18n %}
{% if item.text %}
<h1>{{ item }}</h1>
<span>{{ item.text|safe|linebreaks }}</span>
{% else %}
<div class="item_fullscreen">{{ item }}</div>
{% endif %}

View File

@ -0,0 +1,40 @@
{% load i18n %}
<style type="text/css">
/* List of speakers projector slide */
ul#list_of_speakers {
list-style-type: none;
padding: 0;}
#list_of_speakers li {
font-size: 130%;
line-height: 150%;}
#list_of_speakers .old_speaker {
color: #9FA9B7;}
#list_of_speakers .actual_speaker {
font-weight: bold;
margin-bottom: 0.5em;}
</style>
<h1>{{ title }}</h1>
<h2 style="margin-top: -10px;">
{% trans 'List of speakers' %}
{% if item.speaker_list_closed %}(<span class="closed">{% trans 'closed' %}</span>){% endif %}
</h2>
<p>
{% if list_of_speakers %}
<ul id="list_of_speakers">
{% for speaker_dict in list_of_speakers %}
<li class="{{ speaker_dict.type }}"> {# old_speaker, actual_speaker, coming_speaker #}
{% if speaker_dict.type == 'coming_speaker' %}
{{ speaker_dict.prefix }}.
{% endif %}
{{ speaker_dict.speaker }}
</li>
{% endfor %}
</ul>
{% else %}
<i>{% trans 'The list of speakers is empty.' %}</i>
{% endif %}
</p>

View File

@ -0,0 +1,9 @@
{% load i18n %}
<h1>{% if title %}{{ title }}{% else %}{% trans "Agenda" %}{% endif %}</h1>
<ul class="itemlist">
{% for item in items %}
<li{% if item.closed %} class="closed" {% endif %}>{{ item }}</li>
{% endfor %}
</ul>

View File

@ -83,7 +83,7 @@
<th class="manage">{% trans "Actions" %}</th> <th class="manage">{% trans "Actions" %}</th>
{% endif %} {% endif %}
</tr> </tr>
<tr class="topline{% if active_sid == 'agenda' %} activeline{% endif %}"> <tr class="topline{% if agenda_is_active %} activeline{% endif %}">
<td class="title"> <td class="title">
{% trans "Agenda" %} {% trans "Agenda" %}
</td> </td>
@ -98,9 +98,9 @@
{% if perms.projector.can_manage_projector %} {% if perms.projector.can_manage_projector %}
<span> <span>
<a href="{% url 'projector_activate_slide' 'agenda' %}" <a href="{% url 'projector_activate_slide' 'agenda' %}"
class="activate_link btn {% if active_sid == 'agenda' %}btn-primary{% endif %} btn-mini" class="activate_link btn {% if agenda_is_active %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show agenda' %}"> rel="tooltip" data-original-title="{% trans 'Show agenda' %}">
<i class="icon-facetime-video {% if active_sid == 'agenda' %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if agenda_is_active %}icon-white{% endif %}"></i>
</a> </a>
</span> </span>
{% endif %} {% endif %}

View File

@ -22,10 +22,10 @@
<small class="pull-right"> <small class="pull-right">
<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.projector.can_manage_projector %} {% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' item.sid %}" <a href="{{ item|absolute_url:'projector' }}"
class="activate_link btn btn-mini {% if item.active and not show_list %}btn-primary{% endif %}" class="activate_link btn btn-mini {% if item.is_active and active_type != 'list_of_speakers' %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show item' %}"> rel="tooltip" data-original-title="{% trans 'Show item' %}">
<i class="icon icon-facetime-video {% if item.active and not show_list %}icon-white{% endif %}"></i> <i class="icon icon-facetime-video {% if item.is_active and active_type != 'list_of_speakers' %}icon-white{% endif %}"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
@ -35,8 +35,8 @@
<span class="caret"></span> <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
<li><a href="{% model_url item 'update' %}"><i class="icon-pencil"></i> {% trans 'Edit item' %}</a></li> <li><a href="{{ item|absolute_url:'update' }}"><i class="icon-pencil"></i> {% trans 'Edit item' %}</a></li>
<li><a href="{% model_url item 'delete' %}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li> <li><a href="{{ item|absolute_url:'delete' }}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
@ -68,10 +68,10 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if perms.projector.can_manage_projector %} {% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' item.sid 'show_list_of_speakers' %}" <a href="{{ item|absolute_url:'projector_list_of_speakers' }}"
class="activate_link btn btn-mini {% if item.active and show_list %}btn-primary{% endif %}" class="activate_link btn btn-mini {% if item.is_active and active_type == 'list_of_speakers' %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}"> rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}">
<i class="icon icon-bell {% if item.active and show_list %}icon-white{% endif %}"></i> <i class="icon icon-bell {% if item.is_active and active_type == 'list_of_speakers' %}icon-white{% endif %}"></i>
{% trans 'Show list' %} {% trans 'Show list' %}
</a> </a>
{% endif %} {% endif %}
@ -119,7 +119,7 @@
{% else %} {% else %}
<small class="grey">[{{ speaker_dict.speaker.begin_time }}{% if speaker_dict.type == 'old_speaker' %} {{ speaker_dict.speaker.end_time }}{% endif %}]</small> <small class="grey">[{{ speaker_dict.speaker.begin_time }}{% if speaker_dict.type == 'old_speaker' %} {{ speaker_dict.speaker.end_time }}{% endif %}]</small>
{% endif %} {% endif %}
<a href="{% model_url speaker_dict.speaker %}">{{ speaker_dict.speaker }}</a> <a href="{{ speaker_dict.speaker|absolute_url }}">{{ speaker_dict.speaker }}</a>
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
{% if speaker_dict.type == 'actual_speaker' %} {% if speaker_dict.type == 'actual_speaker' %}
<a href="{% url 'agenda_speaker_end_speach' item.pk %}" class="btn btn-mini btn-danger"><i class="icon-bell icon-white"></i> {% trans 'End speach' %}</a> <a href="{% url 'agenda_speaker_end_speach' item.pk %}" class="btn btn-mini btn-danger"><i class="icon-bell icon-white"></i> {% trans 'End speach' %}</a>
@ -127,7 +127,7 @@
<a href="{% url 'agenda_speaker_speak' item.pk speaker_dict.speaker.person.person_id %}" <a href="{% url 'agenda_speaker_speak' item.pk speaker_dict.speaker.person.person_id %}"
class="btn btn-mini"><i class="icon-bell"></i> {% trans "Begin speach" %}</a> class="btn btn-mini"><i class="icon-bell"></i> {% trans "Begin speach" %}</a>
{% endif %} {% endif %}
<a href="{% model_url speaker_dict.speaker 'delete' %}" <a href="{{ speaker_dict.speaker|absolute_url:'delete' }}"
rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini"> rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</a> </a>

View File

@ -2,49 +2,49 @@
{% load tags %} {% load tags %}
<ul style="line-height: 180%"> <ul style="line-height: 180%">
<li class="{% if agenda.active %}activeline{% endif %}"> <li class="{% if agenda_is_active %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' agenda.key %}" <a href="{% url 'projector_activate_slide' 'agenda' %}"
class="activate_link btn {% if agenda.active %}btn-primary{% endif %} btn-mini" class="activate_link btn {% if agenda_is_active %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}"> rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if agenda.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if agenda_is_active %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% url 'projctor_preview_slide' agenda.key %}" title="{% trans 'Preview' %}" class="icon preview right"> <a href="{% url 'projector_preview' 'agenda' %}" title="{% trans 'Preview' %}" class="icon preview right">
<span></span> <span></span>
</a> </a>
{{ agenda.name }} {% trans "Agenda" %}
</li> </li>
</ul> </ul>
<hr> <hr>
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for item in items %} {% for item in items %}
<li class="{% if item.active %}activeline{% endif %}"> <li class="{% if item.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' item.sid %}" <a href="{{ item|absolute_url:'projector' }}"
class="activate_link btn {% if item.active and not summary and not speakers %}btn-primary{% endif %} btn-mini" class="activate_link btn {% if item.is_active_slide and active_type == 'text' %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show' %}"> rel="tooltip" data-original-title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if item.active and not summary and not speakers %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if item.is_active_slide and active_type == 'text' %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% model_url item 'edit' %}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ item|absolute_url:'edit' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' item.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ item|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
<a href="{% url 'projector_activate_slide' item.sid 'show_list_of_speakers' %}" <a href="{{ item|absolute_url:'projector_list_of_speakers' }}"
class="activate_link btn btn-mini right {% if item.active and speakers %}btn-primary{% endif %}" class="activate_link btn btn-mini right {% if item.is_active_slide and active_type == 'list_of_speakers' %}btn-primary{% endif %}"
rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}"> rel="tooltip" data-original-title="{% trans 'Show list of speakers' %}">
<i class="icon icon-bell {% if item.active and speakers %}icon-white{% endif %}"></i> <i class="icon icon-bell {% if item.is_active_slide and active_type == 'list_of_speakers' %}icon-white{% endif %}"></i>
</a> </a>
{% if not item.is_leaf_node %} {% if not item.is_leaf_node %}
<a href="{% url 'projector_activate_slide' item.sid 'summary' %}" <a href="{{ item|absolute_url:'projector_summary' }}"
class="activate_link btn btn-mini {% if item.active and summary %}btn-primary{% endif %} right" class="activate_link btn btn-mini {% if item.is_active_slide and active_type == 'summary' %}btn-primary{% endif %} right"
rel="tooltip" data-original-title="{% trans 'Show summary for this item' %}"> rel="tooltip" data-original-title="{% trans 'Show summary for this item' %}">
<i class="icon-summary {% if item.active and summary %}icon-white{% endif %}"></i> <i class="icon-summary {% if item.is_active_slide and active_type == 'summary' %}icon-white{% endif %}"></i>
</a> </a>
{% endif %} {% endif %}
{% for p in item.get_ancestors %} {% for p in item.get_ancestors %}
<span class="indentation"></span> <span class="indentation"></span>
{% endfor %} {% endfor %}
<a href="{% model_url item 'view' %}">{% if item.type == item.ORGANIZATIONAL_ITEM %}<i>[{% endif %}{{ item }}{% if item.type == item.ORGANIZATIONAL_ITEM %}]</i>{% endif %}</a> <a href="{{ item|absolute_url }}">{% if item.type == item.ORGANIZATIONAL_ITEM %}<i>[{% endif %}{{ item }}{% if item.type == item.ORGANIZATIONAL_ITEM %}]</i>{% endif %}</a>
{{ item.get_title_supplement|safe }} {{ item.get_title_supplement|safe }}
</li> </li>
{% empty %} {% empty %}

View File

@ -1,19 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% block title %}
{% if title %} {{ title }} {% else %} {% trans "Agenda" %} {% endif %}
{% endblock %}
{% block content %}
<h1>{% if title %}{{ title }}{% else %}{% trans "Agenda" %}{% endif %}</h1>
{% endblock %}
{% block scrollcontent %}
<ul class="itemlist">
{% for item in items %}
<li{% if item.closed %} class="closed" {% endif %}>{{ item }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% block title %}{{ item }}{% endblock %}
{% block content %}
{% if item.text %}
<h1>{{ item }}</h1>
{% else %}
<div class="item_fullscreen">{{ item }}</div>
{% endif %}
{% endblock %}
{% block scrollcontent %}
{% if item.text %}
<span>{{ item.text|safe|linebreaks }}</span>
{% endif %}
{% endblock %}

View File

@ -1,46 +0,0 @@
{% extends 'base-projector.html' %}
{% load i18n %}
{% block title %}{{ item }}{% endblock %}
{% block content %}
<style type="text/css">
/* List of speakers projector slide */
ul#list_of_speakers {
list-style-type: none;
padding: 0;}
#list_of_speakers li {
font-size: 130%;
line-height: 150%;}
#list_of_speakers .old_speaker {
color: #9FA9B7;}
#list_of_speakers .actual_speaker {
font-weight: bold;
margin-bottom: 0.5em;}
</style>
<h1>{{ title }}</h1>
<h2 style="margin-top: -10px;">
{% trans 'List of speakers' %}
{% if item.speaker_list_closed %}(<span class="closed">{% trans 'closed' %}</span>){% endif %}
</h2>
{% endblock %}
{% block scrollcontent %}
<p>
{% if list_of_speakers %}
<ul id="list_of_speakers">
{% for speaker_dict in list_of_speakers %}
<li class="{{ speaker_dict.type }}"> {# old_speaker, actual_speaker, coming_speaker #}
{% if speaker_dict.type == 'coming_speaker' %}
{{ speaker_dict.prefix }}.
{% endif %}
{{ speaker_dict.speaker }}
</li>
{% endfor %}
</ul>
{% else %}
<i>{% trans 'The list of speakers is empty.' %}</i>
{% endif %}
</p>
{% endblock %}

View File

@ -29,8 +29,8 @@ from openslides.utils.views import (
DetailView, FormView, SingleObjectMixin) 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, get_slide_from_sid from openslides.projector.api import get_active_slide, update_projector
from openslides.projector.projector import Widget, SLIDE from openslides.projector.projector import Widget
from .models import Item, Speaker from .models import Item, Speaker
from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm, RelatedItemForm from .forms import ItemOrderForm, ItemForm, AppendSpeakerForm, RelatedItemForm
@ -83,14 +83,21 @@ class Overview(TemplateView):
(duration.days * 24 + duration.seconds / 3600.0), (duration.days * 24 + duration.seconds / 3600.0),
(duration.seconds / 60.0 % 60)) (duration.seconds / 60.0 % 60))
active_slide = get_active_slide()
if active_slide['callback'] == 'agenda':
agenda_active = active_slide.get('pk', 'agenda') == 'agenda'
active_type = active_slide.get('type', 'text')
else:
agenda_is_active = None
active_type = None
context.update({ context.update({
'items': items, 'items': items,
'active_sid': get_active_slide(only_sid=True), 'agenda_is_active': agenda_is_active,
'duration': duration, 'duration': duration,
'start': start, 'start': start,
'end': end, 'end': end,
'summary': config['presentation_argument'] == 'summary', 'active_type': active_type})
'show_list': config['presentation_argument'] == 'show_list_of_speakers'})
return context return context
@transaction.commit_manually @transaction.commit_manually
@ -100,6 +107,7 @@ class Overview(TemplateView):
request, request,
_('You are not authorized to manage the agenda.')) _('You are not authorized to manage the agenda.'))
return self.render_to_response(context) return self.render_to_response(context)
transaction.commit() transaction.commit()
for item in Item.objects.all(): for item in Item.objects.all():
form = ItemOrderForm(request.POST, prefix="i%d" % item.id) form = ItemOrderForm(request.POST, prefix="i%d" % item.id)
@ -121,6 +129,11 @@ class Overview(TemplateView):
# TODO: assure, that it is a valid tree # TODO: assure, that it is a valid tree
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
transaction.commit() transaction.commit()
if get_active_slide()['callback'] == 'agenda':
update_projector()
context = self.get_context_data(**kwargs)
transaction.commit()
return self.render_to_response(context) return self.render_to_response(context)
@ -138,11 +151,16 @@ class AgendaItemView(SingleObjectMixin, FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.object = self.get_object() self.object = self.get_object()
list_of_speakers = self.object.get_list_of_speakers() list_of_speakers = self.object.get_list_of_speakers()
active_slide = get_active_slide()
active_type = active_slide.get('type', None)
kwargs.update({ kwargs.update({
'object': self.object, 'object': self.object,
'list_of_speakers': list_of_speakers, 'list_of_speakers': list_of_speakers,
'is_on_the_list_of_speakers': Speaker.objects.filter(item=self.object, begin_time=None, person=self.request.user).exists(), 'is_on_the_list_of_speakers': Speaker.objects.filter(
'show_list': config['presentation_argument'] == 'show_list_of_speakers', item=self.object, begin_time=None, person=self.request.user).exists(),
'active_type': active_type,
}) })
return super(AgendaItemView, self).get_context_data(**kwargs) return super(AgendaItemView, self).get_context_data(**kwargs)
@ -486,11 +504,14 @@ class CurrentListOfSpeakersView(RedirectView):
""" """
Returns the current Item, or None, if the current Slide is not an Agenda Item. 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) active_slide = get_active_slide()
if not isinstance(slide, Item): if active_slide['callback'] == 'agenda':
return None try:
return Item.objects.get(pk=active_slide.get('pk', None))
except Item.DoesNotExist:
return None
else: else:
return slide return None
def get_redirect_url(self): def get_redirect_url(self):
""" """
@ -583,6 +604,14 @@ def get_widgets(request):
""" """
Returns the agenda widget for the projector tab. Returns the agenda widget for the projector tab.
""" """
active_slide = get_active_slide()
if active_slide['callback'] == 'agenda':
agenda_is_active = active_slide.get('pk', 'agenda') == 'agenda'
active_type = active_slide.get('type', 'text')
else:
agenda_is_active = None
active_type = None
return [ return [
Widget( Widget(
request, request,
@ -590,10 +619,9 @@ def get_widgets(request):
display_name=_('Agenda'), display_name=_('Agenda'),
template='agenda/widget.html', template='agenda/widget.html',
context={ context={
'agenda': SLIDE['agenda'], 'agenda_is_active': agenda_is_active,
'items': Item.objects.all(), 'items': Item.objects.all(),
'summary': config['presentation_argument'] == 'summary', 'active_type': active_type},
'speakers': config['presentation_argument'] == 'show_list_of_speakers'},
permission_required='projector.can_manage_projector', permission_required='projector.can_manage_projector',
default_column=1, default_column=1,
default_weight=20), default_weight=20),

View File

@ -11,3 +11,4 @@
""" """
from . import signals from . import signals
from . import slides

View File

@ -18,8 +18,7 @@ from django.utils.datastructures import SortedDict
from openslides.utils.person import PersonField from openslides.utils.person import PersonField
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import register_slidemodel from openslides.projector.models import SlideMixin
from openslides.projector.projector import SlideMixin
from openslides.poll.models import ( from openslides.poll.models import (
BasePoll, CountInvalid, CountVotesCast, BaseOption, PublishPollMixin, BaseVote) BasePoll, CountInvalid, CountVotesCast, BaseOption, PublishPollMixin, BaseVote)
@ -37,8 +36,9 @@ class AssignmentCandidate(models.Model):
unique_together = ("assignment", "person") unique_together = ("assignment", "person")
class Assignment(models.Model, SlideMixin): class Assignment(SlideMixin, models.Model):
prefix = ugettext_noop('assignment') slide_callback_name = 'assignment'
STATUS = ( STATUS = (
('sea', ugettext_lazy('Searching for candidates')), ('sea', ugettext_lazy('Searching for candidates')),
('vot', ugettext_lazy('Voting')), ('vot', ugettext_lazy('Voting')),
@ -53,6 +53,27 @@ class Assignment(models.Model, SlideMixin):
verbose_name=ugettext_lazy("Comment on the ballot paper")) verbose_name=ugettext_lazy("Comment on the ballot paper"))
status = models.CharField(max_length=3, choices=STATUS, default='sea') status = models.CharField(max_length=3, choices=STATUS, default='sea')
class Meta:
permissions = (
('can_see_assignment', ugettext_noop('Can see assignments')), # TODO: Add plural s to the codestring
('can_nominate_other', ugettext_noop('Can nominate another person')),
('can_nominate_self', ugettext_noop('Can nominate oneself')),
('can_manage_assignment', ugettext_noop('Can manage assignments')), # TODO: Add plural s also to the codestring
)
ordering = ('name',)
def __unicode__(self):
return self.name
def get_absolute_url(self, link='detail'):
if link == 'detail' or link == 'view':
return reverse('assignment_detail', args=[str(self.id)])
if link == 'update' or link == 'update':
return reverse('assignment_update', args=[str(self.id)])
if link == 'delete':
return reverse('assignment_delete', args=[str(self.id)])
return super(Assignment, self).get_absolute_url(link)
def set_status(self, status): def set_status(self, status):
status_dict = dict(self.STATUS) status_dict = dict(self.STATUS)
if status not in status_dict: if status not in status_dict:
@ -206,45 +227,6 @@ class Assignment(models.Model, SlideMixin):
def get_agenda_title_supplement(self): def get_agenda_title_supplement(self):
return '(%s)' % _('Assignment') return '(%s)' % _('Assignment')
def slide(self):
"""
return the slide dict
"""
polls = self.poll_set
data = super(Assignment, self).slide()
data['assignment'] = self
data['title'] = self.name
data['some_polls_available'] = polls.exists()
data['polls'] = polls.filter(published=True)
data['vote_results'] = self.vote_results(only_published=True)
data['assignment_publish_winner_results_only'] = \
config['assignment_publish_winner_results_only']
data['template'] = 'projector/Assignment.html'
return data
def get_absolute_url(self, link='detail'):
if link == 'detail' or link == 'view':
return reverse('assignment_detail', args=[str(self.id)])
if link == 'update' or link == 'edit':
return reverse('assignment_update', args=[str(self.id)])
if link == 'delete':
return reverse('assignment_delete', args=[str(self.id)])
def __unicode__(self):
return self.name
class Meta:
permissions = (
('can_see_assignment', ugettext_noop('Can see assignments')), # TODO: Add plural s to the codestring
('can_nominate_other', ugettext_noop('Can nominate another person')),
('can_nominate_self', ugettext_noop('Can nominate oneself')),
('can_manage_assignment', ugettext_noop('Can manage assignments')), # TODO: Add plural s also to the codestring
)
ordering = ('name',)
verbose_name = ugettext_noop('Assignment')
register_slidemodel(Assignment)
class AssignmentVote(BaseVote): class AssignmentVote(BaseVote):
option = models.ForeignKey('AssignmentOption') option = models.ForeignKey('AssignmentOption')
@ -294,7 +276,7 @@ class AssignmentPoll(BasePoll, CountInvalid, CountVotesCast, PublishPollMixin):
return self.assignment.poll_set.filter(id__lte=self.id).count() return self.assignment.poll_set.filter(id__lte=self.id).count()
@models.permalink @models.permalink
def get_absolute_url(self, link='view'): def get_absolute_url(self, link='detail'):
if link == 'view' or link == 'detail' or link == 'update': if link == 'view' or link == 'detail' or link == 'update':
return ('assignment_poll_view', [str(self.id)]) return ('assignment_poll_view', [str(self.id)])
if link == 'delete': if link == 'delete':

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.assignment.slides
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Slides for the assignment app.
:copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from django.template.loader import render_to_string
from openslides.config.api import config
from openslides.projector.api import register_slide
from .models import Assignment
def assignment_slide(**kwargs):
"""
Slide for an Assignment
"""
assignment_pk = kwargs.get('pk', None)
try:
assignment = Assignment.objects.get(pk=assignment_pk)
except Assignment.DoesNotExist:
return ''
polls = assignment.poll_set
context = {
'polls': polls.filter(published=True),
'vote_results': assignment.vote_results(only_published=True),
'assignment': assignment}
return render_to_string('assignment/slide.html', context)
register_slide(Assignment.slide_callback_name, assignment_slide)

View File

@ -65,7 +65,7 @@
<ol> <ol>
{% for person in assignment.get_participants %} {% for person in assignment.get_participants %}
<li> <li>
<a href="{{ person|absolute_url:'detail' }}">{{ person }}</a> <a href="{{ person|absolute_url }}">{{ person }}</a>
{% if perms.assignment.can_manage_assignment %} {% if perms.assignment.can_manage_assignment %}
{% if assignment.status == "sea" or assignment.status == "vot" %} {% if assignment.status == "sea" or assignment.status == "vot" %}
<a href="{% url 'assignment_delother' assignment.id person.person_id %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Remove candidate' %}"><i class="icon-remove"></i></a> <a href="{% url 'assignment_delother' assignment.id person.person_id %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Remove candidate' %}"><i class="icon-remove"></i></a>
@ -126,7 +126,7 @@
<ul> <ul>
{% for person in blocked_candidates %} {% for person in blocked_candidates %}
<li> <li>
<a href="{{ person|absolute_url:'detail' }}">{{ person }}</a> <a href="{{ person|absolute_url }}">{{ person }}</a>
<a class="btn btn-mini" href="{% url 'assignment_delother' assignment.id person.person_id %}" <a class="btn btn-mini" href="{% url 'assignment_delother' assignment.id person.person_id %}"
rel="tooltip" data-original-title="{% trans 'Remove candidate' %}"> rel="tooltip" data-original-title="{% trans 'Remove candidate' %}">
<i class="icon-ban-circle"></i> <i class="icon-ban-circle"></i>
@ -168,16 +168,16 @@
<i class="icon-unchecked-new"></i> <i class="icon-unchecked-new"></i>
{% endif %} {% endif %}
</a> </a>
<a href="{{ poll|absolute_url:'update' }}" class="btn btn-mini" <a href="{% url 'assignment_poll_view' poll.id %}" class="btn btn-mini"
rel="tooltip" data-original-title="{% trans 'Edit' %}"><i class="icon-pencil"></i></a> rel="tooltip" data-original-title="{% trans 'Edit' %}"><i class="icon-pencil"></i></a>
<a href="{{ poll|absolute_url:'delete' }}" class="btn btn-mini" <a href="{% url 'assignment_poll_delete' poll.id %}" class="btn btn-mini"
rel="tooltip" data-original-title="{% trans 'Delete' %}"><i class="icon-remove"></i></a> rel="tooltip" data-original-title="{% trans 'Delete' %}"><i class="icon-remove"></i></a>
{% endif %} {% endif %}
</th> </th>
{% endfor %} {% endfor %}
{% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %}
<th class="span1 nobr"> <th class="span1 nobr">
<a href="{% url 'assignment_poll_create' assignment.id %}" class="btn btn-mini"> <a href="{% url 'assignment_gen_poll' assignment.id %}" class="btn btn-mini">
<i class="icon-plus"></i> {% trans 'New ballot' %} <i class="icon-plus"></i> {% trans 'New ballot' %}
</a> </a>
</th> </th>
@ -200,7 +200,7 @@
<a class="election_link" href="{% url 'assignment_user_elected' assignment.id candidate.person_id %}"></a> <a class="election_link" href="{% url 'assignment_user_elected' assignment.id candidate.person_id %}"></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{{ candidate|absolute_url:'detail' }}">{{ candidate }}</a> <a href="{{ candidate|absolute_url }}">{{ candidate }}</a>
</td> </td>
{% for vote in poll_list %} {% for vote in poll_list %}
<td style="white-space:nowrap;"> <td style="white-space:nowrap;">

View File

@ -0,0 +1,110 @@
{% load i18n %}
{% load staticfiles %}
{% load tags %}
{% if assignment.status != "fin" %}
<div id="sidebar">
<div class="box">
<p><b>{% trans "Status" %}:</b><br>
{% trans assignment.get_status_display %}</p>
{% if assignment.status == "sea" or assignment.status == "vot" %}
<p><b>{% trans "Number of available posts" %}:</b><br>
{{ assignment.posts }}</p>
{% endif %}
</div>
</div>
{% endif %}
<h1>{% trans "Election" %}: {{ assignment }}</h1>
{% if not assignment.candidates %}
<p>
<div class="text">{{ assignment.description|linebreaks }}</div>
</p>
{% endif %}
{% if assignment.candidates and assignment.status != "fin" %}
<h3>{% trans "Candidates" %}</h3>
<ol>
{% for candidate in assignment.candidates %}
<li>{{ candidate }} </li>
{% empty %}
<li style="list-style: none outside none;">
<i>{% trans "No candidates available." %}</i>
</li>
{% endfor %}
</ol>
<p><br></p>
{% endif %}
{% if polls.exists %}
<h3>{% trans "Election results" %}</h3>
<table>
<tr>
<th>{% trans "Candidates" %}</th>
{% for poll in polls %}
<th>
<nobr>{{ poll.get_ballot }}. {% trans "ballot" %}</nobr>
</th>
{% endfor %}
</tr>
{% for candidate, poll_list in vote_results.items %}
<tr class="{% cycle 'odd' '' as rowcolors %}">
<td class="candidate{% if candidate in assignment.elected %} elected{% endif %}">
{% if candidate in assignment.elected %}
<a class="elected">
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Candidate is elected' %}">
</a>
{% endif %}
{{ candidate }}
</td>
{% for vote in poll_list %}
<td style="white-space:nowrap;"{% if candidate in assignment.elected %} class="elected"{% endif %}>
{% if not "assignment_publish_winner_results_only"|get_config or candidate in assignment.elected %}
{% if 'Yes' in vote and 'No' in vote and 'Abstain' in vote %}
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ vote.Yes }}<br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ vote.No }}<br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ vote.Abstain }}<br>
{% elif 'Votes' in vote %}
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ vote.Votes }}
{% elif vote == None %}
{% trans 'was not a <br> candidate'%}
{% else %}
&nbsp;
{% endif %}
{% else %}
&nbsp;
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
<tr class="{% cycle rowcolors %}">
<td>{% trans 'Invalid votes' %}</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}">
{{ poll.print_votesinvalid }}
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="total">
<td>
<strong>{% trans 'Votes cast' %}</strong>
</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}">
<strong>{{ poll.print_votescast }}</strong>
{% endif %}
</td>
{% endfor %}
</tr>
</table>
{% endif %}

View File

@ -3,17 +3,17 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for assignment in assignments %} {% for assignment in assignments %}
<li class="{% if assignment.active %}activeline{% endif %}"> <li class="{% if assignment.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' assignment.sid %}" class="activate_link btn {% if assignment.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{{ assignment|absolute_url:'projector' }}" class="activate_link btn {% if assignment.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if assignment.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if assignment.active %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{{ assignment|absolute_url:'update' }}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ assignment|absolute_url:'update' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' assignment.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ assignment|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
<a href="{{ assignment|absolute_url:'detail' }}">{{ assignment }}</a> <a href="{{ assignment|absolute_url }}">{{ assignment }}</a>
</li> </li>
{% empty %} {% empty %}
<li>{% trans 'No assignments available.' %}</li> <li>{% trans 'No assignments available.' %}</li>

View File

@ -1,122 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{{ assignment }}{% endblock %}
{% block header %}
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/assignment.css' %}" />
<script type="text/javascript" src="{% static 'javascript/assignment.js' %}"></script>
{% endblock %}
{% block content %}
{% if assignment.status != "fin" %}
<div id="sidebar">
<div class="box">
<p><b>{% trans "Status" %}:</b><br>
{% trans assignment.get_status_display %}</p>
{% if assignment.status == "sea" or assignment.status == "vot" %}
<p><b>{% trans "Number of available posts" %}:</b><br>
{{ assignment.posts }}</p>
{% endif %}
</div>
</div>
{% endif %}
<h1>{% trans "Election" %}: {{ assignment }}</h1>
{% endblock %}
{% block scrollcontent %}
{% if not assignment.candidates %}
<p>
<div class="text">{{ assignment.description|linebreaks }}</div>
</p>
{% endif %}
{% if assignment.candidates and assignment.status != "fin" %}
<h3>{% trans "Candidates" %}</h3>
<ol>
{% for candidate in assignment.candidates %}
<li>{{ candidate }} </li>
{% empty %}
<li style="list-style: none outside none;">
<i>{% trans "No candidates available." %}</i>
</li>
{% endfor %}
</ol>
<p><br></p>
{% endif %}
{% if polls.exists %}
<h3>{% trans "Election results" %}</h3>
<table>
<tr>
<th>{% trans "Candidates" %}</th>
{% for poll in polls %}
<th>
<nobr>{{ poll.get_ballot }}. {% trans "ballot" %}</nobr>
</th>
{% endfor %}
</tr>
{% for candidate, poll_list in vote_results.items %}
<tr class="{% cycle 'odd' '' as rowcolors %}">
<td class="candidate{% if candidate in assignment.elected %} elected{% endif %}">
{% if candidate in assignment.elected %}
<a class="elected">
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Candidate is elected' %}">
</a>
{% endif %}
{{ candidate }}
</td>
{% for vote in poll_list %}
<td style="white-space:nowrap;"{% if candidate in assignment.elected %} class="elected"{% endif %}>
{% if not assignment_publish_winner_results_only or candidate in assignment.elected %}
{% if 'Yes' in vote and 'No' in vote and 'Abstain' in vote %}
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ vote.Yes }}<br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ vote.No }}<br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ vote.Abstain }}<br>
{% elif 'Votes' in vote %}
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ vote.Votes }}
{% elif vote == None %}
{% trans 'was not a <br> candidate'%}
{% else %}
&nbsp;
{% endif %}
{% else %}
&nbsp;
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
<tr class="{% cycle rowcolors %}">
<td>{% trans 'Invalid votes' %}</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}">
{{ poll.print_votesinvalid }}
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="total">
<td>
<strong>{% trans 'Votes cast' %}</strong>
</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}">
<strong>{{ poll.print_votescast }}</strong>
{% endif %}
</td>
{% endfor %}
</tr>
</table>
{% endif %}
{% endblock %}

View File

@ -30,12 +30,12 @@
<td>{{ mediafile.filetype }}</td> <td>{{ mediafile.filetype }}</td>
<td>{{ mediafile.get_filesize }}</td> <td>{{ mediafile.get_filesize }}</td>
<td>{{ mediafile.timestamp }}</td> <td>{{ mediafile.timestamp }}</td>
<td><a href="{% model_url mediafile.uploader 'view' %}">{{ mediafile.uploader }}</a></td> <td><a href="{{ mediafile.uploader|absolute_url }}">{{ mediafile.uploader }}</a></td>
{% if perms.mediafile.can_manage %} {% if perms.mediafile.can_manage %}
<td> <td>
<span style="width: 1px; white-space: nowrap;"> <span style="width: 1px; white-space: nowrap;">
<a href="{% model_url mediafile 'update' %}" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini"><i class="icon-pencil"></i></a> <a href="{{ mediafile|absolute_url:'update' }}" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini"><i class="icon-pencil"></i></a>
<a href="{% model_url mediafile 'delete' %}" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini"><i class="icon-remove"></i></a> <a href="{{ mediafile|absolute_url:'delete' }}" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini"><i class="icon-remove"></i></a>
</span> </span>
</td> </td>
{% endif %} {% endif %}

View File

@ -29,7 +29,6 @@ from openslides.config.api import config
from openslides.poll.models import ( from openslides.poll.models import (
BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote) BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote)
from openslides.participant.models import User from openslides.participant.models import User
from openslides.projector.api import register_slidemodel
from openslides.projector.models import SlideMixin from openslides.projector.models import SlideMixin
from openslides.agenda.models import Item from openslides.agenda.models import Item
@ -43,9 +42,9 @@ class Motion(SlideMixin, models.Model):
This class is the main entry point to all other classes related to a motion. This class is the main entry point to all other classes related to a motion.
""" """
prefix = ugettext_noop('motion') slide_callback_name = 'motion'
""" """
Prefix for the slide system. Name of the callback for the slide-system.
""" """
active_version = models.ForeignKey('MotionVersion', null=True, active_version = models.ForeignKey('MotionVersion', null=True,
@ -193,6 +192,7 @@ class Motion(SlideMixin, models.Model):
return reverse('motion_edit', args=[str(self.id)]) return reverse('motion_edit', args=[str(self.id)])
if link == 'delete': if link == 'delete':
return reverse('motion_delete', args=[str(self.id)]) return reverse('motion_delete', args=[str(self.id)])
return super(Motion, self).get_absolute_url(link)
def version_data_changed(self, version): def version_data_changed(self, version):
""" """
@ -464,16 +464,6 @@ class Motion(SlideMixin, models.Model):
Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0]) Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0])
self.set_state(new_state) self.set_state(new_state)
def slide(self):
"""
Return the slide dict.
"""
data = super(Motion, self).slide()
data['motion'] = self
data['title'] = self.title
data['template'] = 'projector/Motion.html'
return data
def get_agenda_title(self): def get_agenda_title(self):
""" """
Return a title for the agenda. Return a title for the agenda.

View File

@ -10,7 +10,26 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from openslides.projector.api import register_slidemodel from django.template.loader import render_to_string
from openslides.projector.api import register_slide
from .models import Motion from .models import Motion
register_slidemodel(Motion)
def motion_slide(**kwargs):
"""
Slide for the motion app.
"""
motion_pk = kwargs.get('pk', None)
try:
motion = Motion.objects.get(pk=motion_pk)
except Motion.DoesNotExist:
return ''
context = {
'motion': motion,
'title': motion.title}
return render_to_string('motion/slide.html', context)
register_slide(Motion.slide_callback_name, motion_slide)

View File

@ -42,12 +42,12 @@
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
<!-- edit --> <!-- edit -->
{% if allowed_actions.edit %} {% if allowed_actions.edit %}
<li><a href="{% model_url motion 'edit' %}"><i class="icon-pencil"></i> {% trans 'Edit motion' %}</a></li> <li><a href="{{ motion|absolute_url:'edit' }}"><i class="icon-pencil"></i> {% trans 'Edit motion' %}</a></li>
{% endif %} {% endif %}
{% if perms.motion.can_manage_motion %} {% if perms.motion.can_manage_motion %}
<!-- delete --> <!-- delete -->
{% if allowed_actions.delete %} {% if allowed_actions.delete %}
<li><a href="{% model_url motion 'delete' %}"><i class="icon-remove"></i> {% trans 'Delete motion' %}</a></li> <li><a href="{{ motion|absolute_url:'delete' }}"><i class="icon-remove"></i> {% trans 'Delete motion' %}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
<!-- create agenda item --> <!-- create agenda item -->
@ -68,14 +68,14 @@
<p><span class="label label-warning"> <p><span class="label label-warning">
<i class="icon-warning-sign icon-white"></i> {% trans "This is not the newest version." %} <i class="icon-warning-sign icon-white"></i> {% trans "This is not the newest version." %}
</span> </span>
<a href="{% model_url last_version %}" class="btn btn-small">{% trans "Go to the newest version" %} <a href="{{ last_version|absolute_url }}" class="btn btn-small">{% trans "Go to the newest version" %}
(# {{ last_version.version_number }})</a></p> (# {{ last_version.version_number }})</a></p>
{% endif %} {% endif %}
{% if version.version_number != active_version.version_number %} {% if version.version_number != active_version.version_number %}
<p><span class="label label-warning"> <p><span class="label label-warning">
<i class="icon-warning-sign icon-white"></i> {% trans "This version is not authorized." %} <i class="icon-warning-sign icon-white"></i> {% trans "This version is not authorized." %}
</span> </span>
<a href="{% model_url active_version %}" class="btn btn-small">{% trans "Go to the authorized version" %} <a href="{{ active_version|absolute_url }}" class="btn btn-small">{% trans "Go to the authorized version" %}
(# {{ active_version.version_number }})</a></p> (# {{ active_version.version_number }})</a></p>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
@ -123,11 +123,11 @@
<input type="radio" value="{{ version.version_number }}" name="rev2"> <input type="radio" value="{{ version.version_number }}" name="rev2">
</td> </td>
<td> <td>
<a href="{% model_url version %}" title="{% trans 'Show' %}" class="btn btn-mini"> <a href="{{ version|absolute_url }}" title="{% trans 'Show' %}" class="btn btn-mini">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
{% if version != motion.active_version %} {% if version != motion.active_version %}
<a href="{% model_url version 'delete' %}" title="{% trans 'Delete' %}" class="btn btn-mini"> <a href="{{ version|absolute_url:'delete' }}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</a> </a>
{% endif %} {% endif %}
@ -166,7 +166,7 @@
<!-- Submitter --> <!-- Submitter -->
<h5>{% trans "Submitter" %}:</h5> <h5>{% trans "Submitter" %}:</h5>
{% for submitter in motion.submitter.all %} {% for submitter in motion.submitter.all %}
<a href="{% model_url submitter.person 'view' %}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %} <a href="{{ submitter.person|absolute_url }}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
<!-- Supporters --> <!-- Supporters -->
@ -177,7 +177,7 @@
{% else %} {% else %}
<ol> <ol>
{% for supporter in motion.supporter.all %} {% for supporter in motion.supporter.all %}
<li><a href="{% model_url supporter.person 'view' %}">{{ supporter }}</a></li> <li><a href="{{ supporter.person|absolute_url }}">{{ supporter }}</a></li>
{% endfor %} {% endfor %}
</ol> </ol>
{% endif %} {% endif %}
@ -196,8 +196,8 @@
{% if perms.motion.can_manage_motion or poll.has_votes %} {% if perms.motion.can_manage_motion or poll.has_votes %}
<li><b>{{ poll.poll_number|ordinal|safe }} {% trans "vote" %}</b> <li><b>{{ poll.poll_number|ordinal|safe }} {% trans "vote" %}</b>
{% if perms.motion.can_manage_motion %} {% if perms.motion.can_manage_motion %}
<a class="btn btn-mini" href="{% model_url poll 'edit' %}" title="{% trans 'Edit Vote' %}"><i class="icon-pencil"></i></a> <a class="btn btn-mini" href="{{ poll|absolute_url:'edit' }}" title="{% trans 'Edit Vote' %}"><i class="icon-pencil"></i></a>
<a class="btn btn-mini" href="{% model_url poll 'delete' %}" title="{% trans 'Delete Vote' %}"><i class="icon-remove"></i></a> <a class="btn btn-mini" href="{{ poll|absolute_url:'delete' }}" title="{% trans 'Delete Vote' %}"><i class="icon-remove"></i></a>
{% endif %} {% endif %}
<br> <br>
{% if poll.has_votes %} {% if poll.has_votes %}

View File

@ -0,0 +1,69 @@
{% load i18n %}
{% load staticfiles %}
<div id="sidebar">
<div class="box">
<!-- Status -->
<p><b>{% trans "Status" %}:</b><br>
{% trans motion.state.name %}
</p>
<!-- poll results -->
{% with motion.polls.all as polls %}
{% if polls.exists and polls.0.has_votes %}
<p><b>{% trans "Poll result" %}:</b>
{% for poll in polls %}
{% if poll.has_votes %}
{% if polls|length > 1 %}
<p>{{ forloop.counter }}. {% trans "Vote" %}:</p>
{% endif %}
{% with poll.get_options.0 as option %}
<div class="results">
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ option.Yes }} <br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ option.No }} <br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ option.Abstain }}<br>
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}"> {{ poll.print_votesinvalid }}<br>
<hr>
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
</div>
{% endwith %}
{% else %}
{% if poll|length == 1 %}
<i>{% trans "No poll results available." %}</i>
{% endif %}
{% endif %}
{% endfor %}
</p>
{% endif %}
{% endwith %}
<!-- Submitter -->
<p><b>{% trans "Submitter" %}:</b><br>
{% for submitter in motion.submitter.all %}
{{ submitter.person }}{% if not forloop.last %},<br> {% endif %}
{% endfor %}
</p>
</div>
</div>
<h1>
{% if motion.identifier %}
{% trans "Motion" %} {{ motion.identifier }}
{% else %}
{% trans "Motion" %} [---]
{% endif %}
<small>
{% if motion.last_version.version_number > 1 %}({% trans 'Version' %} {{ motion.active_version.version_number }}){% endif %}
</small>
</h1>
<b>{{ motion.active_version.title }}</b>
<hr>
<p>
<div class="text">{{ motion.active_version.text|safe }}</div>
{% if motion.active_version.reason %}
<br>
<div class="reason"><p><b>{% trans "Reason" %}:</b></p>
{{ motion.active_version.reason|safe }}</div>
{% endif %}
</p>

View File

@ -3,17 +3,17 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for motion in motions %} {% for motion in motions %}
<li class="{% if motion.active %}activeline{% endif %}"> <li class="{% if motion.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' motion.sid %}" class="activate_link btn {% if motion.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{{ motion|absolute_url:'projector' }}" class="activate_link btn {% if motion.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if motion.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if motion.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% model_url motion 'edit' %}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ motion|absolute_url:'edit' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' motion.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ motion|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
<a href="{% model_url motion %}"> <a href="{{ motion|absolute_url }}">
{{ motion.identifier|add:' | '|default:'' }} {{ motion.identifier|add:' | '|default:'' }}
{{ motion }} {{ motion }}
</a> </a>

View File

@ -1,76 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{{ block.super }} - {% trans "Motion" %} {{ motion.identifier }}{% endblock %}
{% block content %}
<div id="sidebar">
<div class="box">
<!-- Status -->
<p><b>{% trans "Status" %}:</b><br>
{% trans motion.state.name %}
</p>
<!-- poll results -->
{% with motion.polls.all as polls %}
{% if polls.exists and polls.0.has_votes %}
<p><b>{% trans "Poll result" %}:</b>
{% for poll in polls %}
{% if poll.has_votes %}
{% if polls|length > 1 %}
<p>{{forloop.counter}}. {% trans "Vote" %}:</p>
{% endif %}
{% with poll.get_options.0 as option %}
<div class="results">
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ option.Yes }} <br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ option.No }} <br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ option.Abstain }}<br>
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}"> {{ poll.print_votesinvalid }}<br>
<hr>
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
</div>
{% endwith %}
{% else %}
{% if poll|length == 1 %}
<i>{% trans "No poll results available." %}</i>
{% endif %}
{% endif %}
{% endfor %}
</p>
{% endif %}
{% endwith %}
<!-- Submitter -->
<p><b>{% trans "Submitter" %}:</b><br>
{% for submitter in motion.submitter.all %}
{{ submitter.person }}{% if not forloop.last %},<br> {% endif %}
{% endfor %}
</p>
</div>
</div>
<h1>
{% if motion.identifier %}
{% trans "Motion" %} {{ motion.identifier }}
{% else %}
{% trans "Motion" %} [---]
{% endif %}
<small>
{% if motion.last_version.version_number > 1 %}({% trans 'Version' %} {{ motion.active_version.version_number }}){% endif %}
</small>
</h1>
<b>{{ motion.active_version.title }}</b>
<hr>
{% endblock %}
{% block scrollcontent %}
<p>
<div class="text">{{ motion.active_version.text|safe }}</div>
{% if motion.active_version.reason %}
<br>
<div class="reason"><p><b>{% trans "Reason" %}:</b></p>
{{ motion.active_version.reason|safe }}</div>
{% endif %}
</p>
{% endblock %}

View File

@ -30,7 +30,7 @@ from openslides.utils.template import Tab
from openslides.utils.utils import html_strong, htmldiff from openslides.utils.utils import html_strong, htmldiff
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.projector.api import get_active_slide from openslides.projector.api import get_active_slide
from openslides.projector.projector import Widget, SLIDE from openslides.projector.projector import Widget
from openslides.config.api import config from openslides.config.api import config
from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView

View File

@ -13,6 +13,7 @@
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from . import signals from . import signals
from . import slides
NAME = ugettext_noop('Participant') NAME = ugettext_noop('Participant')

View File

@ -10,6 +10,7 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User as DjangoUser, Group as DjangoGroup, Permission from django.contrib.auth.models import User as DjangoUser, Group as DjangoGroup, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
@ -20,13 +21,13 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.utils.person import PersonMixin, Person from openslides.utils.person import PersonMixin, Person
from openslides.utils.person.signals import receive_persons from openslides.utils.person.signals import receive_persons
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import register_slidemodel from openslides.projector.models import SlideMixin
from openslides.projector.projector import SlideMixin
class User(PersonMixin, Person, SlideMixin, DjangoUser): class User(SlideMixin, PersonMixin, Person, DjangoUser):
prefix = 'user' # This is for the slides slide_callback_name = 'user'
person_prefix = 'user' person_prefix = 'user'
GENDER_CHOICES = ( GENDER_CHOICES = (
('male', ugettext_lazy('Male')), ('male', ugettext_lazy('Male')),
('female', ugettext_lazy('Female')), ('female', ugettext_lazy('Female')),
@ -55,6 +56,30 @@ class User(PersonMixin, Person, SlideMixin, DjangoUser):
max_length=100, blank=True, default='', max_length=100, blank=True, default='',
verbose_name=ugettext_lazy("Default password")) verbose_name=ugettext_lazy("Default password"))
class Meta:
permissions = (
('can_see_participant', ugettext_noop('Can see participants')),
('can_manage_participant', ugettext_noop('Can manage participants')),
)
ordering = ('last_name',)
def __unicode__(self):
if self.name_suffix:
return u"%s (%s)" % (self.clean_name, self.name_suffix)
return u"%s" % self.clean_name
def get_absolute_url(self, link='detail'):
"""
Return the URL to the user.
"""
if link == 'detail' or link == 'view':
return reverse('user_view', args=[str(self.id)])
if link == 'edit':
return reverse('user_edit', args=[str(self.id)])
if link == 'delete':
return reverse('user_delete', args=[str(self.id)])
return super(User, self).get_absolute_url(link)
@property @property
def clean_name(self): def clean_name(self):
if self.title: if self.title:
@ -86,50 +111,9 @@ class User(PersonMixin, Person, SlideMixin, DjangoUser):
return self.first_name.lower() return self.first_name.lower()
return self.last_name.lower() return self.last_name.lower()
@models.permalink
def get_absolute_url(self, link='detail'):
"""
Return the URL to this user.
link can be: class Group(SlideMixin, PersonMixin, Person, DjangoGroup):
* detail slide_callback_name = 'group'
* edit
* delete
"""
if link == 'detail' or link == 'view':
return ('user_view', [str(self.id)])
if link == 'edit':
return ('user_edit', [str(self.id)])
if link == 'delete':
return ('user_delete', [str(self.id)])
def __unicode__(self):
if self.name_suffix:
return u"%s (%s)" % (self.clean_name, self.name_suffix)
return u"%s" % self.clean_name
class Meta:
# Rename permissions
permissions = (
('can_see_participant', ugettext_noop('Can see participants')), # TODO: Add plural s to the codestring
('can_manage_participant', ugettext_noop('Can manage participants')), # TODO: Add plural s to the codestring
)
ordering = ('last_name',)
def slide(self):
"""
Returns a map with the data for the slides.
"""
return {
'shown_user': self,
'title': self.clean_name,
'template': 'projector/UserSlide.html'}
register_slidemodel(User)
class Group(PersonMixin, Person, SlideMixin, DjangoGroup):
prefix = 'group' # This is for the slides
person_prefix = 'group' person_prefix = 'group'
django_group = models.OneToOneField(DjangoGroup, editable=False, parent_link=True) django_group = models.OneToOneField(DjangoGroup, editable=False, parent_link=True)
@ -138,39 +122,23 @@ class Group(PersonMixin, Person, SlideMixin, DjangoGroup):
help_text=ugettext_lazy('For example as submitter of a motion.')) help_text=ugettext_lazy('For example as submitter of a motion.'))
description = models.TextField(blank=True, verbose_name=ugettext_lazy("Description")) description = models.TextField(blank=True, verbose_name=ugettext_lazy("Description"))
@models.permalink class Meta:
def get_absolute_url(self, link='detail'): ordering = ('name',)
"""
Return the URL to this user group.
link can be:
* view
* edit
* delete
"""
if link == 'detail' or link == 'view':
return ('user_group_view', [str(self.id)])
if link == 'edit':
return ('user_group_edit', [str(self.id)])
if link == 'delete':
return ('user_group_delete', [str(self.id)])
def __unicode__(self): def __unicode__(self):
return unicode(self.name) return unicode(self.name)
class Meta: def get_absolute_url(self, link='detail'):
ordering = ('name',)
def slide(self):
""" """
Returns a map with the data for the slides. Return the URL to the user group.
""" """
return { if link == 'detail' or link == 'view':
'group': self, return reverse('user_group_view', args=[str(self.id)])
'title': self.name, if link == 'update' or link == 'edit':
'template': 'projector/GroupSlide.html'} return reverse('user_group_edit', args=[str(self.id)])
if link == 'delete':
register_slidemodel(Group) return reverse('user_group_delete', args=[str(self.id)])
return super(Group, self).get_absolute_url(link)
class UsersAndGroupsToPersons(object): class UsersAndGroupsToPersons(object):

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.motion.slides
~~~~~~~~~~~~~~~~~~~~~~~~
Defines the slides for the User app.
:copyright: (c) 20112013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from django.template.loader import render_to_string
from openslides.projector.api import register_slide
from .models import User, Group
def user_slide(**kwargs):
"""
Slide for the user model.
"""
user_pk = kwargs.get('pk', None)
try:
user = User.objects.get(pk=user_pk)
except User.DoesNotExist:
return ''
context = {'shown_user': user}
return render_to_string('participant/user_slide.html', context)
register_slide(User.slide_callback_name, user_slide)
def group_slide(**kwargs):
"""
Slide for the group model.
"""
group_pk = kwargs.get('pk', None)
try:
group = Group.objects.get(pk=group_pk)
except Group.DoesNotExist:
return ''
context = {'group': group}
return render_to_string('participant/group_slide.html', context)
register_slide(Group.slide_callback_name, group_slide)

View File

@ -38,7 +38,7 @@
<ol> <ol>
{% for member in group_members %} {% for member in group_members %}
<li><a href="{% model_url member 'view' %}">{{ member }}</a></li> <li><a href="{{ member|absolute_url }}">{{ member }}</a></li>
{% empty %} {% empty %}
<p>{% trans "No members available." %}</p> <p>{% trans "No members available." %}</p>
{% endfor %} {% endfor %}

View File

@ -40,7 +40,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{% model_url group 'view' %}">{% trans group.name %}</a> <a href="{{ group|absolute_url }}">{% trans group.name %}</a>
</td> </td>
<td> <td>
<span style="width: 1px; white-space: nowrap;"> <span style="width: 1px; white-space: nowrap;">

View File

@ -0,0 +1,7 @@
{% load i18n %}
<div class="item_fullscreen">{% trans group.name %}
<span>
<p><i>{{ group.user_set.all.count }} {% trans "participants" %}</i></p>
</span>
</div>

View File

@ -3,17 +3,17 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for group in groups %} {% for group in groups %}
<li class="{% if group.active %}activeline{% endif %}"> <li class="{% if group.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' group.sid %}" class="activate_link btn {% if group.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{{ group|absolute_url:'projector' }}" class="activate_link btn {% if group.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if group.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if group.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% model_url group 'edit' %}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ group|absolute_url:'update' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' group.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ group|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
<a href="{% model_url group 'view' %}">{% trans group.name %}</a> <a href="{{ group|absolute_url }}">{% trans group.name %}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -0,0 +1,15 @@
{% load i18n %}
<div class="item_fullscreen">{{ shown_user }}
<span>
{% if shown_user.committee %}
<p>{{ shown_user.committee }}</p>
{% endif %}
<p>
{% if shown_user.groups.all %}
{% for group in shown_user.groups.all %}
{% trans group.name %}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}
</p>
</span>
</div>

View File

@ -1,19 +1,18 @@
{% load i18n %} {% load i18n %}
{% load tags %} {% load tags %}
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for user in users %} {% for user in users %}
<li class="{% if user.active %}activeline{% endif %}"> <li class="{% if user.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' user.sid %}" class="activate_link btn {% if user.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{{ user|absolute_url:'projector' }}" class="activate_link btn {% if user.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if user.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if user.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% model_url user 'edit' %}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ user|absolute_url:'edit' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' user.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ user|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
<a href="{% model_url user 'view' %}">{{ user }}</a> <a href="{{ user|absolute_url }}">{{ user }}</a>
</li> </li>
{% empty %} {% empty %}
<li>{% trans 'No participants available.' %}</li> <li>{% trans 'No participants available.' %}</li>

View File

@ -1,13 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="item_fullscreen">{% trans group.name %}
<span>
<p><i>{{ group.user_set.all.count }} {% trans "participants" %}</i></p>
</span>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="item_fullscreen">{{ shown_user }}
<span>
{% if shown_user.committee %}
<p>{{ shown_user.committee }}</p>
{% endif %}
<p>
{% if shown_user.groups.all %}
{% for group in shown_user.groups.all %}
{% trans group.name %}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}
</p>
</span>
</div>
{% endblock %}

View File

@ -43,11 +43,12 @@ from openslides.utils.views import (
RedirectView, SingleObjectMixin, ListView, QuestionMixin, DetailView) RedirectView, SingleObjectMixin, ListView, QuestionMixin, DetailView)
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.projector import Widget from openslides.projector.projector import Widget
from openslides.participant.api import gen_username, gen_password, import_users
from openslides.participant.forms import ( from .api import gen_username, gen_password, import_users
from .forms import (
UserCreateForm, UserUpdateForm, UsersettingsForm, UserCreateForm, UserUpdateForm, UsersettingsForm,
UserImportForm, GroupForm) UserImportForm, GroupForm)
from openslides.participant.models import User, Group, get_protected_perm from .models import User, Group, get_protected_perm
class UserOverview(ListView): class UserOverview(ListView):

View File

@ -10,4 +10,4 @@
: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

@ -10,114 +10,146 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
import json
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.template.loader import render_to_string
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.projector import SLIDE, Slide from openslides.utils.tornado_webserver import ProjectorSocketHandler
from .signals import projector_overlays
def split_sid(sid): slide_callback = {}
"""
A dictonary where the key is the name of a slide, and the value is a
callable object which returns the html code for a slide.
"""
def update_projector():
""" """
Slit a SID in the model-part and in the model-id Sends the data to the clients, who listen to the projector.
""" """
# TODO: only send necessary html
ProjectorSocketHandler.send_updates({'content': get_projector_content()})
def update_projector_overlay(overlay):
"""
Update one overlay on the projector.
Checks if the overlay is activated and updates it in this case.
'overlay' has to be an overlay object, the name of a ovleray or None.
If 'overlay' is None, then all overlays are updated.
"""
if isinstance(overlay, basestring):
overlay = get_overlays()[overlay]
if overlay is None:
overlays = [overlay for overlay in get_overlays().values()]
else:
overlays = [overlay]
overlay_dict = {}
for overlay in overlays:
if overlay.is_active():
overlay_dict[overlay.name] = {
'html': overlay.get_projector_html(),
'javascript': overlay.get_javascript()}
else:
overlay_dict[overlay.name] = None
ProjectorSocketHandler.send_updates({'overlays': overlay_dict})
def get_projector_content(slide_dict=None):
"""
Returns the HTML-Content block of the projector.
"""
if slide_dict is None:
slide_dict = config['projector_active_slide'].copy()
callback = slide_dict.pop('callback', None)
try: try:
data = sid.split('-') return slide_callback[callback](**slide_dict)
except AttributeError:
return None
if len(data) == 2:
model = data[0]
id = data[1]
return (model, id)
if len(data) == 1:
try:
return (SLIDE[data[0]].key, None)
except KeyError:
return None
return None
def get_slide_from_sid(sid, element=False):
"""
Return the Slide for an given sid.
If element== False, return the slide-dict,
else, return the object.
"""
try:
key, id = split_sid(sid)
except TypeError:
return None
if id is not None:
try:
object = SLIDE[key].model.objects.get(pk=id)
except SLIDE[key].model.DoesNotExist:
return None
if element:
return object
return object.slide()
try:
return SLIDE[key].func()
except KeyError: except KeyError:
return None return default_slide()
def get_active_slide(only_sid=False): def default_slide():
""" """
Returns the active slide. If no slide is active, or it can not find an Item, Returns the HTML Code for the default slide.
return None
if only_sid is True, returns only the id of this item. Returns None if not
Item is active.
""" """
sid = config["presentation"] return render_to_string('projector/default_slide.html')
if only_sid:
return sid
return get_slide_from_sid(sid)
def set_active_slide(sid, argument=None): def get_overlays():
"""
Returns all overlay objects.
The returned value is a dictonary with the name of the overlay as key, and
the overlay object as value.
"""
overlays = {}
for receiver, overlay in projector_overlays.send(sender='get_overlays'):
overlays[overlay.name] = overlay
return overlays
def get_projector_overlays():
"""
Returns the HTML code for all active overlays.
"""
overlays = [{'name': key, 'html': overlay.get_projector_html()}
for key, overlay in get_overlays().items()
if overlay.is_active()]
return render_to_string('projector/all_overlays.html', {'overlays': overlays})
def get_projector_overlays_js():
"""
Returns JS-Code for the overlays.
The retuned value is a list of json objects.
"""
js = []
for key, overlay in get_overlays().items():
if overlay.is_active():
overlay_js = overlay.get_javascript()
if overlay_js:
js.append(json.dumps(overlay_js))
return js
def register_slide(name, callback):
"""
Register a function as slide callback.
"""
slide_callback[name] = callback
def set_active_slide(callback, kwargs={}):
""" """
Set the active Slide. Set the active Slide.
callback: The name of the slide callback.
kwargs: Keyword arguments for the slide callback.
""" """
config["presentation"] = sid kwargs.update(callback=callback)
config['presentation_argument'] = argument config['projector_active_slide'] = kwargs
clear_projector_cache() update_projector()
update_projector_overlay(None)
def clear_projector_cache(): def get_active_slide():
cache.delete('projector_content')
cache.delete('projector_scrollcontent')
cache.delete('projector_data')
def register_slidemodel(model, model_name=None, control_template=None, weight=0):
""" """
Register a Model as a slide. Returns the dictonary, witch defindes the active slide.
""" """
# TODO: control_template should never be None return config['projector_active_slide']
if model_name is None:
model_name = model.prefix
category = model.__module__.split('.')[0]
SLIDE[model_name] = Slide(model_slide=True, model=model, category=category,
key=model.prefix, model_name=model_name,
control_template=control_template, weight=weight)
def register_slidefunc(key, func, control_template=None, weight=0, name=''):
"""
Register a function for as a slide.
"""
if control_template is None:
control_template = 'projector/default_control_slidefunc.html'
category = func.__module__.split('.')[0]
SLIDE[key] = Slide(model_slide=False, func=func, category=category,
key=key, control_template=control_template, weight=weight,
name=name,)
def get_all_widgets(request, session=False): def get_all_widgets(request, session=False):
@ -135,9 +167,11 @@ def get_all_widgets(request, session=False):
except ImportError: except ImportError:
continue continue
try: try:
module_widgets = mod.get_widgets(request) mod_get_widgets = mod.get_widgets
except AttributeError: except AttributeError:
continue continue
else:
module_widgets = mod_get_widgets(request)
all_module_widgets.extend(module_widgets) all_module_widgets.extend(module_widgets)
all_module_widgets.sort(key=lambda widget: widget.default_weight) all_module_widgets.sort(key=lambda widget: widget.default_weight)
session_widgets = request.session.get('widgets', {}) session_widgets = request.session.get('widgets', {})

View File

@ -13,34 +13,92 @@
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from openslides.projector.api import register_slidemodel from openslides.utils.utils import int_or_none
from openslides.projector.projector import SlideMixin
class ProjectorSlide(models.Model, SlideMixin): class SlideMixin(object):
"""
A Mixin for a Django-Model, for making the model a slide.
"""
slide_callback_name = None
"""
Name of the callback to render the model as slide.
"""
def save(self, *args, **kwargs):
"""
Updates the projector, if 'self' is the active slide.
"""
from openslides.projector.api import update_projector
super(SlideMixin, self).save(*args, **kwargs)
if self.is_active_slide():
update_projector()
def delete(self, *args, **kwargs):
from openslides.projector.api import update_projector
super(SlideMixin, self).delete(*args, **kwargs)
if self.is_active_slide():
update_projector()
def get_slide_callback_name(self):
"""
Returns the name of the slide callback, which is used to render the slide.
"""
if self.slide_callback_name is None:
raise ImproperlyConfigured(
"SlideMixin requires either a definition of 'slide_callback_name'"
" or an implementation of 'get_slide_callback_name()'")
else:
return self.slide_callback_name
def get_absolute_url(self, link='projector'):
"""
Return the url to activate the slide, if link == 'projector'
"""
if link == 'projector':
url_name = 'projector_activate_slide'
elif link == 'projector_preview':
url_name = 'projector_preview'
if link in ('projector', 'projector_preview'):
return '%s?pk=%d' % (
reverse(url_name,
args=[self.get_slide_callback_name()]),
self.pk)
return super(SlideMixin, self).get_absolute_url(link)
def is_active_slide(self):
"""
Return True, if the the slide is the active slide.
"""
from openslides.projector.api import get_active_slide
active_slide = get_active_slide()
pk = int_or_none(active_slide.get('pk', None))
return (active_slide['callback'] == self.get_slide_callback_name() and
self.pk == pk)
class ProjectorSlide(SlideMixin, models.Model):
""" """
Model for Slides, only for the projector. Also called custom slides. Model for Slides, only for the projector. Also called custom slides.
""" """
prefix = 'ProjectorSlide' slide_callback_name = 'projector_slide'
title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title")) title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title"))
text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Text")) text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Text"))
weight = models.IntegerField(default=0, verbose_name=ugettext_lazy("Weight")) weight = models.IntegerField(default=0, verbose_name=ugettext_lazy("Weight"))
def slide(self): def get_absolute_url(self, link='update'):
return { if link == 'edit' or link == 'update':
'slide': self, return reverse('customslide_edit', args=[str(self.pk)])
'title': self.title,
'template': 'projector/ProjectorSlide.html',
}
@models.permalink
def get_absolute_url(self, link='edit'):
if link == 'edit':
return ('customslide_edit', [str(self.id)])
if link == 'delete': if link == 'delete':
return ('customslide_delete', [str(self.id)]) return reverse('customslide_delete', args=[str(self.pk)])
return super(ProjectorSlide, self).get_absolute_url(link)
def __unicode__(self): def __unicode__(self):
return self.title return self.title
@ -51,6 +109,3 @@ class ProjectorSlide(models.Model, SlideMixin):
('can_see_projector', ugettext_noop("Can see the projector")), ('can_see_projector', ugettext_noop("Can see the projector")),
('can_see_dashboard', ugettext_noop("Can see the dashboard")), ('can_see_dashboard', ugettext_noop("Can see the dashboard")),
) )
register_slidemodel(ProjectorSlide, control_template='projector/control_customslide.html')

View File

@ -20,97 +20,6 @@ from django.template import RequestContext
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
SLIDE = {}
class SlideMixin(object):
"""
A Mixin for a Django-Model, for making the model a slide.
"""
def slide(self):
"""
Return a map with all Data for the Slide.
"""
return {
'slide': self,
'title': 'dummy-title',
'template': 'projector/default.html',
}
@property
def sid(self):
"""
Return the sid from this Slide
"""
return "%s-%d" % (self.prefix, self.id)
@property
def active(self):
"""
Return True, if the the slide is the active slide.
"""
if self.id is None:
return False
from openslides.projector.api import get_active_slide
return get_active_slide(only_sid=True) == self.sid
def set_active(self):
"""
Appoint this item as the active slide.
"""
from openslides.projector.api import set_active_slide
set_active_slide(self.sid)
def save(self, *args, **kwargs):
if self.active:
from api import clear_projector_cache
clear_projector_cache()
return super(SlideMixin, self).save(*args, **kwargs)
class Slide(object):
"""
Represents a Slide for the projector. Can be a modelinstanz, or a function.
"""
def __init__(self, model_slide=False, func=None, model=None, category=None,
key=None, model_name='', control_template='', weight=0, name=''):
"""
model_slide: Boolean if the value is a Model.
func: The function to call. Only if modelslide is False.
model: The model. Only if modelslide is True.
model_name: The name shown for the model.
category: The category to show this Slide.
key: the key in the slide object to find itself.
"""
self.model_slide = model_slide
self.func = func
self.model = model
self.model_name = model_name
self.category = category
self.key = key
self.control_template = control_template
self.weight = weight
self.name = name
@property
def active(self):
"""
Return True if the Slide is active, else: False.
"""
from api import get_active_slide
return get_active_slide(True) == self.key
def get_items(self):
"""
If the Slide is a Slide from a Model, return all Objects.
"""
try:
return self.model.objects.all()
except AttributeError:
return 'No Model'
class Widget(object): class Widget(object):
""" """
Class for a Widget for the Projector-Tab. Class for a Widget for the Projector-Tab.
@ -158,23 +67,52 @@ class Overlay(object):
Represents an overlay which can be seen on the projector. Represents an overlay which can be seen on the projector.
""" """
def __init__(self, name, get_widget_html, get_projector_html): def __init__(self, name, get_widget_html, get_projector_html,
get_javascript=None, allways_active=False):
self.name = name self.name = name
self.widget_html_callback = get_widget_html self.widget_html_callback = get_widget_html
self.projector_html_callback = get_projector_html self.projector_html_callback = get_projector_html
self.javascript_callback = get_javascript
self.allways_active = allways_active
def get_widget_html(self): def get_widget_html(self):
return self.widget_html_callback() value = None
if self.widget_html_callback is not None:
value = self.widget_html_callback()
return value
def get_projector_html(self): def get_projector_html(self):
try: return self.get_html_wrapper(self.projector_html_callback())
return self._projector_html
except AttributeError: def get_javascript(self):
self._projector_html = self.projector_html_callback() if self.javascript_callback is None:
return self.get_projector_html() value = {}
else:
value = self.javascript_callback()
return value
def get_html_wrapper(self, inner_html):
full_html = ''
if inner_html is not None:
full_html = '<div id="overlay_%s">%s</div>' % (self.name, inner_html)
return full_html
def is_active(self): def is_active(self):
return self.name in config['projector_active_overlays'] return self.allways_active or self.name in config['projector_active_overlays']
def set_active(self, active):
"""
Publish or depublish the ovleray on the projector.
publish, if active is true,
depublish, if active is false.
"""
active_overlays = set(config['projector_active_overlays'])
if active:
active_overlays.add(self.name)
else:
active_overlays.discard(self.name)
config['projector_active_overlays'] = list(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

@ -15,13 +15,12 @@ from django.dispatch import Signal, receiver
from django import forms from django import forms
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.contrib.staticfiles.templatetags.staticfiles import static
from openslides.config.api import config from openslides.config.api import config, ConfigVariable, ConfigPage
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
from openslides.config.api import ConfigVariable, ConfigPage
from .projector import Overlay from .projector import Overlay
from .api import clear_projector_cache
projector_overlays = Signal(providing_args=['request']) projector_overlays = Signal(providing_args=['request'])
@ -34,13 +33,13 @@ def setup_projector_config_variables(sender, **kwargs):
config page. config page.
""" """
presentation = ConfigVariable( projector = ConfigVariable(
name='presentation', name='projector_active_slide',
default_value='') default_value={'callback': None})
"""
presentation_argument = ConfigVariable( The active slide. The config-value is a dictonary with at least the entrie
name='presentation_argument', 'callback'.
default_value=None) """
projector_message = ConfigVariable( projector_message = ConfigVariable(
name='projector_message', name='projector_message',
@ -76,7 +75,7 @@ def setup_projector_config_variables(sender, **kwargs):
return ConfigPage( return ConfigPage(
title='No title here', url='bar', required_permission=None, variables=( title='No title here', url='bar', required_permission=None, variables=(
presentation, presentation_argument, projector_message, projector, projector_message,
countdown_time, countdown_start_stamp, countdown_pause_stamp, countdown_time, countdown_start_stamp, countdown_pause_stamp,
countdown_state, bigger, up, projector_active_overlays)) countdown_state, bigger, up, projector_active_overlays))
@ -97,30 +96,32 @@ def countdown(sender, **kwargs):
'countdown_time': config['countdown_time'], 'countdown_time': config['countdown_time'],
'countdown_state': config['countdown_state']} 'countdown_state': config['countdown_state']}
context.update(csrf(request)) context.update(csrf(request))
return render_to_string('projector/overlay_countdown_widget.html', context) return render_to_string('projector/overlay_countdown_widget.html',
context)
def get_projector_js():
"""
Returns JavaScript for the projector
"""
start = int(config['countdown_start_stamp'])
duration = int(config['countdown_time'])
pause = int(config['countdown_pause_stamp'])
state = config['countdown_state']
return {
'load_file': static('javascript/countdown.js'),
'projector_countdown_start': start,
'projector_countdown_duration': duration,
'projector_countdown_pause': pause,
'projector_countdown_state': state}
def get_projector_html(): def get_projector_html():
""" """
Returns an html-code to show on the projector. Returns an html-code to show on the projector.
""" """
start = config['countdown_start_stamp'] return render_to_string('projector/overlay_countdown_projector.html')
duration = config['countdown_time']
pause = config['countdown_pause_stamp']
if config['countdown_state'] == 'active':
seconds = max(0, int(start + duration - time()))
elif config['countdown_state'] == 'paused':
seconds = max(0, int(start + duration - pause))
elif config['countdown_state'] == 'inactive':
seconds = max(0, int(duration))
else:
seconds = 0
if seconds == 0:
config['countdown_state'] = 'expired'
clear_projector_cache()
return render_to_string('projector/overlay_countdown_projector.html',
{'seconds': '%02d:%02d' % (seconds / 60, seconds % 60)})
return Overlay(name, get_widget_html, get_projector_html) return Overlay(name, get_widget_html, get_projector_html, get_projector_js)
@receiver(projector_overlays, dispatch_uid="projector_message") @receiver(projector_overlays, dispatch_uid="projector_message")
@ -148,3 +149,26 @@ def projector_message(sender, **kwargs):
return None return None
return Overlay(name, get_widget_html, get_projector_html) return Overlay(name, get_widget_html, get_projector_html)
@receiver(projector_overlays, dispatch_uid="projector_clock")
def projector_clock(sender, **kwargs):
"""
Receiver to show the clock on the projector.
"""
name = 'projector_clock'
def get_projector_html():
"""
Returns the html-code for the clock.
"""
return render_to_string('projector/overlay_clock_projector.html')
def get_projector_js():
"""
Returns JavaScript for the projector
"""
return {'load_file': static('javascript/clock.js')}
return Overlay(name, None, get_projector_html, get_projector_js,
allways_active=True)

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.projector.slides
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Slides for the projector app.
:copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from django.template.loader import render_to_string
from openslides.config.api import config
from openslides.projector.api import register_slide
from .models import ProjectorSlide
def projector_slide(**kwargs):
"""
Return the html code for a custom slide.
"""
slide_pk = kwargs.get('pk', None)
try:
slide = ProjectorSlide.objects.get(pk=slide_pk)
except ProjectorSlide.DoesNotExist:
slide = None
context = {'slide': slide}
return render_to_string('projector/slide_projectorslide.html', context)
register_slide('projector_slide', projector_slide)

View File

@ -0,0 +1,8 @@
function update_clock() {
var currentTime = new Date();
var currentHours = currentTime.getHours();
var currentMinutes = currentTime.getMinutes();
$('#currentTime').html(currentHours + ':' + currentMinutes);
setTimeout('update_clock()', 200);
}
update_clock();

View File

@ -0,0 +1,25 @@
function update_countdown() {
var time = new Date().getTime() / 1000;
var seconds;
var start = projector.projector_countdown_start;
var duration = projector.projector_countdown_duration;
var pause = projector.projector_countdown_pause;
switch (projector.projector_countdown_state) {
case 'active':
seconds = start + duration - time;
break;
case 'paused':
seconds = start + duration - pause;
break;
case 'inactive':
seconds = duration;
break;
}
if (seconds !== undefined) {
seconds = Math.max(0, Math.floor(seconds));
$('#overlay_countdown_inner').html(seconds);
}
setTimeout('update_countdown()', 200);
}
update_countdown();

View File

@ -1,62 +1,67 @@
/** /**
* OpenSlides projector functions * OpenSlides projector functions
* *
* :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. * :copyright: 20112013 by OpenSlides team, see AUTHORS.
* :license: GNU GPL, see LICENSE for more details. * :license: GNU GPL, see LICENSE for more details.
*/ */
content_hash = null; $(document).ready(function() {
if ($('#content.reload').length > 0) {
updater.start();
}
});
function presentation_reload() { var projector = {
if ($('#config > #ajax').html() == 'on') { _loaded_files: {},
$.ajax({
type: 'GET', load_file: function(src) {
url: '/projector/', if (projector._loaded_files[src] === undefined) {
dataType: 'json', projector._loaded_files[src] = document.createElement('script');
success: function(data) { projector._loaded_files[src].setAttribute("type","text/javascript");
$('#currentTime').removeClass('ajax_error'); projector._loaded_files[src].setAttribute("src", src);
var new_content_hash = data['content_hash']; $('head').append(projector._loaded_files[src]);
if (new_content_hash != content_hash) { }
$('#content').html(data.content); },
content_hash = new_content_hash;
update_data: function(data) {
$.each(data, function (key, value) {
if (key === 'load_file')
projector.load_file(value);
else
projector[key] = value;
});
}
};
var updater = {
socket: null,
start: function() {
var url = "ws://" + location.host + "/projector/socket/";
updater.socket = new WebSocket(url);
updater.socket.onmessage = function(event) {
updater.updateProjector(JSON.parse(event.data));
}
updater.socket.onclose = function() {
setTimeout('updater.start()', 5000);
}
},
updateProjector: function(data) {
$('#content').html(data.content);
var overlays = data.overlays;
$.each(overlays, function (key, value) {
var overlay = $('#overlays #overlay_' + key)
if (!value)
overlay.remove();
else {
if (overlay.length) {
overlay.html(value.html)
} else {
$('#overlays').append(value.html);
} }
$('#scrollcontent').html(data.scrollcontent); projector.update_data(value.javascript);
document.title = data.title;
$('#currentTime').html(data.time);
$('#content').clearQueue();
// content font-size
$('#content').animate({'font-size': data.bigger + '%'}, 200);
$('#content #sidebar').css({'font-size': '18px'}, 0);
$('#scrollcontent').animate({'font-size': data.bigger + '%'}, 100);
// content position
$('#scrollcontent').animate({'margin-top': data.up + 'em'}, 100);
// overlays
$('#overlays div').remove();
$.each(data['overlays'], function (index, overlay){
$('#overlays').append('<div id="overlay_' + overlay['name'] + '">' + overlay['html'] + '</div>');
});
setTimeout("presentation_reload()", 1000);
},
error: function () {
$('#currentTime').addClass('ajax_error');
setTimeout("presentation_reload()", 1000);
} }
}); });
} }
} };
function switchajax() {
if ($('#config > #ajax').html() == 'on') {
$('#config > #ajax').html('off');
$('#ajaxswitcher').html('<a href="#" onClick="switchajax()">Ajax Anschalten</a>');
} else {
$('#config > #ajax').html('on');
$('#ajaxswitcher').html('<a href="#" onClick="switchajax()">Ajax Ausschalten</a>');
}
}
$(document).ready(function() {
switchajax();
switchajax();
presentation_reload();
});

View File

@ -1,4 +1,5 @@
{% load tags %} {% load tags %}
{% load i18n %}
{% load staticfiles %} {% load staticfiles %}
<!DOCTYPE html> <!DOCTYPE html>
@ -6,47 +7,28 @@
<head> <head>
<link type="text/css" rel="stylesheet" href="{% static 'styles/projector.css' %}"> <link type="text/css" rel="stylesheet" href="{% static 'styles/projector.css' %}">
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}" type="image/png" /> <link rel="shortcut icon" href="{% static 'img/favicon.png' %}" type="image/png" />
<script type="text/javascript" src="{% static 'javascript/jquery.min.js' %}"></script> <title>{{ 'event_name'|get_config }} {% trans 'Projector' %}</title>
<script type="text/javascript" src="{% static 'javascript/projector.js' %}"></script>
<title>{% block title %}{% get_config 'event_name' %}{% endblock %}</title>
{% block header %}
{% endblock %}
</head> </head>
<body> <body>
<div id="config" style="display:none;">
<div id="ajax">{{ ajax }}</div>
</div>
<div id="ajaxswitcher"></div>
<div id="header"> <div id="header">
<div id="logo"><img src="{% static 'img/logo-projector.png' %}"></div> <div id="logo"><img src="{% static 'img/logo-projector.png' %}"></div>
<div class="event_name">{% get_config 'event_name' %}</div> <div class="event_name">{% get_config 'event_name' %}</div>
<div class="event_description">{% get_config 'event_description' %}</div> <div class="event_description">{% get_config 'event_description' %}</div>
</div> </div>
<div id="currentTime">
{% now "H:i" %}
</div>
<div id="overlays"> <div id="overlays">
{% for overlay in overlays %} {{ overlays }}
<div id="overlay_{{ overlay.name }}">
{{ overlay.html|safe }}
</div>
{% endfor %}
</div> </div>
<div id="contentwrapper"> <div id="content" {% if reload %}class="reload"{% endif %}>
<div id="content"> {{ content }}
{% block content %}
{% endblock %}
</div>
<div id="scrollcontentcontainer">
<div id="scrollcontent">
{% block scrollcontent %}
{% endblock %}
</div>
</div>
</div> </div>
<script type="text/javascript" src="{% static 'javascript/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'javascript/projector.js' %}"></script>
<script type="text/javascript">
{% for js in overlay_js %}
projector.update_data({{ js|safe }});
{% endfor %}
</script>
</body> </body>
</html> </html>

View File

@ -1,19 +0,0 @@
{% extends "base-projector.html" %}
{% load i18n %}
{% block title %}{{ slide.title }}{% endblock %}
{% block content %}
{% if slide.text %}
<h1>{{ slide.title }}</h1>
{% else %}
<div class="item_fullscreen">{{ slide.title }}</div>
{% endif %}
{% endblock %}
{% block scrollcontent %}
{% if slide.text %}
<span>{{ slide.text|safe|linebreaks }}</span>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,3 @@
{% for overlay in overlays %}
{{ overlay.html|safe }}
{% endfor %}

View File

@ -3,7 +3,7 @@
<ul style="line-height: 180%"> <ul style="line-height: 180%">
<li class="{% if welcomepage_is_active %}activeline{% endif %}"> <li class="{% if welcomepage_is_active %}activeline{% endif %}">
<a href="{% url 'projector_activate_welcomepage' %}" class="activate_link btn {% if welcomepage_is_active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{% url 'projector_activate_slide' 'default' %}" class="activate_link btn {% if welcomepage_is_active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if welcomepage_is_active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if welcomepage_is_active %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% url 'projctor_preview_welcomepage' %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{% url 'projctor_preview_welcomepage' %}" title="{% trans 'Preview' %}" class="btn btn-mini right">
@ -15,18 +15,18 @@
<hr> <hr>
<ul style="line-height: 180%"> <ul style="line-height: 180%">
{% for slide in slides %} {% for slide in slides %}
<li class="{% if slide.active %}activeline{% endif %}"> <li class="{% if slide.is_active_slide %}activeline{% endif %}">
<a href="{% url 'projector_activate_slide' slide.sid %}" class="activate_link btn {% if slide.active %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}"> <a href="{{ slide|absolute_url:'projector' }}" class="activate_link btn {% if slide.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if slide.active %}icon-white{% endif %}"></i> <i class="icon-facetime-video {% if slide.is_active_slide %}icon-white{% endif %}"></i>
</a>&nbsp; </a>&nbsp;
<a href="{% url 'customslide_edit' slide.id %}">{{ slide }}</a> <a href="{{ slide|absolute_url:'update' }}">{{ slide }}</a>
<a href="{% url 'customslide_delete' slide.id %}" title="{% trans 'Delete' %}" class="btn btn-mini right"> <a href="{{ slide|absolute_url:'delete' }}" title="{% trans 'Delete' %}" class="btn btn-mini right">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</a> </a>
<a href="{% url 'customslide_edit' slide.id %}" title="{% trans 'Edit' %}" class="btn btn-mini right"> <a href="{{ slide|absolute_url:'update' }}" title="{% trans 'Edit' %}" class="btn btn-mini right">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a href="{% url 'projctor_preview_slide' slide.sid %}" title="{% trans 'Preview' %}" class="btn btn-mini right"> <a href="{{ slide|absolute_url:'projector_preview' }}" title="{% trans 'Preview' %}" class="btn btn-mini right">
<i class="icon-search"></i> <i class="icon-search"></i>
</a> </a>
</li> </li>

View File

@ -1,8 +0,0 @@
{% extends "base-projector.html" %}
{% load tags %}
{% load i18n %}
{% block content %}
<div class="item_fullscreen">{% get_config 'welcome_title' %}</div>
{% endblock %}

View File

@ -0,0 +1,4 @@
{% load tags %}
<div class="item_fullscreen">{% get_config 'welcome_title' %}</div>

View File

@ -0,0 +1 @@
<div id="currentTime"></div>

View File

@ -13,5 +13,4 @@
</style> </style>
<div id="overlay_countdown_inner"> <div id="overlay_countdown_inner">
{{ seconds }}
</div> </div>

View File

@ -0,0 +1,12 @@
{% load i18n %}
{% if slide.text %}
<h1>{{ slide.title }}</h1>
{% else %}
<div class="item_fullscreen">{{ slide.title }}</div>
{% endif %}
{% if slide.text %}
<span>{{ slide.text|safe|linebreaks }}</span>
{% endif %}

View File

@ -21,19 +21,23 @@ from openslides.projector.views import (DashboardView, ActivateView,
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', url(r'^$',
Projector.as_view(), Projector.as_view(),
{'sid': None},
name='projector_show', name='projector_show',
), ),
url(r'^preview/$', url(r'^preview/$',
Projector.as_view(), Projector.as_view(),
{'sid': None}, {'callback': None},
name='projctor_preview_welcomepage', name='projctor_preview_welcomepage',
), ),
url(r'^preview/(?P<sid>[^/]*)/$', url(r'^preview/(?P<callback>[^/]*)/$',
Projector.as_view(), Projector.as_view(),
name='projctor_preview_slide', name='projector_preview',
),
url(r'^activate/(?P<callback>[^/]*)/$',
ActivateView.as_view(),
name='projector_activate_slide',
), ),
url(r'^dashboard/$', url(r'^dashboard/$',
@ -41,22 +45,6 @@ urlpatterns = patterns('',
name='dashboard', name='dashboard',
), ),
url(r'^activate/$',
ActivateView.as_view(),
{'sid': None},
name='projector_activate_welcomepage',
),
url(r'^activate/(?P<sid>[^/]*)/$',
ActivateView.as_view(),
name='projector_activate_slide',
),
url(r'^activate/(?P<sid>[^/]*)/(?P<argument>[^/]*)/$',
ActivateView.as_view(),
name='projector_activate_slide',
),
url(r'^widgets/$', url(r'^widgets/$',
SelectWidgetsView.as_view(), SelectWidgetsView.as_view(),
name='projector_select_widgets', name='projector_select_widgets',

View File

@ -28,15 +28,16 @@ from openslides.utils.views import (
TemplateView, RedirectView, CreateView, UpdateView, DeleteView, AjaxMixin) TemplateView, RedirectView, CreateView, UpdateView, DeleteView, AjaxMixin)
from openslides.config.api import config from openslides.config.api import config
from .api import ( from .api import (
get_active_slide, set_active_slide, get_slide_from_sid, get_all_widgets, get_projector_content, get_projector_overlays, get_all_widgets,
clear_projector_cache) set_active_slide, update_projector, get_active_slide, update_projector_overlay,
get_overlays, get_projector_overlays_js)
from .forms import SelectWidgetsForm from .forms import SelectWidgetsForm
from .models import ProjectorSlide from .models import ProjectorSlide
from .projector import Widget from .projector import Widget
from .signals import projector_overlays from .signals import projector_overlays
class DashboardView(TemplateView, AjaxMixin): class DashboardView(AjaxMixin, TemplateView):
""" """
Overview over all possible slides, the overlays and a liveview. Overview over all possible slides, the overlays and a liveview.
""" """
@ -50,96 +51,33 @@ class DashboardView(TemplateView, AjaxMixin):
return context return context
class Projector(TemplateView, AjaxMixin): class Projector(TemplateView):
""" """
The Projector-Page. The Projector-Page.
""" """
permission_required = 'projector.can_see_projector' permission_required = 'projector.can_see_projector'
template_name = 'projector.html'
@property
def data(self):
try:
return self._data
except AttributeError:
pass
sid = self.kwargs['sid']
if sid is None:
try:
data = get_active_slide()
except AttributeError: # TODO: It has to be an Slide.DoesNotExist
data = None
ajax = 'on'
active_sid = get_active_slide(True)
else:
data = get_slide_from_sid(sid)
ajax = 'off'
if data is None:
data = {
'title': config['event_name'],
'template': 'projector/default.html',
}
data['ajax'] = ajax
# Projector overlays
data['overlays'] = []
# Do not show overlays on slide preview
if self.kwargs['sid'] is None:
for receiver, overlay in projector_overlays.send(sender=self):
if overlay.show_on_projector():
data['overlays'].append({'name': overlay.name,
'html': overlay.get_projector_html()})
self._data = data
return data
def get_template_names(self):
return [self.data['template']]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(Projector, self).get_context_data(**kwargs) slide_dict = dict(self.request.GET.items())
context.update(self.data) callback = self.kwargs.get('callback', None)
return context if callback:
slide_dict['callback'] = callback
def get_ajax_context(self, **kwargs): if not slide_dict:
content = cache.get('projector_content') kwargs.update({
if not content: 'content': get_projector_content(),
content = render_block_to_string( 'overlays': get_projector_overlays(),
self.get_template_names()[0], 'overlay_js': get_projector_overlays_js(),
'content', self.data) 'reload': True})
cache.set('projector_content', content, 1)
scrollcontent = cache.get('projector_scrollcontent') # For the Preview
if not scrollcontent: else:
scrollcontent = render_block_to_string( kwargs.update({
self.get_template_names()[0], 'content': get_projector_content(slide_dict),
'scrollcontent', self.data) 'reload': False})
cache.set('projector_scrollcontent', scrollcontent, 1)
# TODO: do not call the hole data-methode, if we only need some vars return super(Projector, self).get_context_data(**kwargs)
data = cache.get('projector_data')
if not data:
data = self.data
cache.set('projector_data', data)
context = super(Projector, self).get_ajax_context(**kwargs)
content_hash = hash(content)
context.update({
'content': content,
'scrollcontent': scrollcontent,
'time': datetime.now().strftime('%H:%M'),
'overlays': data['overlays'],
'title': data['title'],
'bigger': config['bigger'],
'up': config['up'],
'content_hash': content_hash,
})
return context
def get(self, request, *args, **kwargs):
if request.is_ajax():
return self.ajax_get(request, *args, **kwargs)
return super(Projector, self).get(request, *args, **kwargs)
class ActivateView(RedirectView): class ActivateView(RedirectView):
@ -151,10 +89,7 @@ class ActivateView(RedirectView):
allow_ajax = True allow_ajax = True
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
try: set_active_slide(kwargs['callback'], kwargs=dict(request.GET.items()))
set_active_slide(kwargs['sid'], kwargs['argument'])
except KeyError:
set_active_slide(kwargs['sid'])
config['up'] = 0 config['up'] = 0
config['bigger'] = 100 config['bigger'] = 100
@ -267,9 +202,9 @@ class CountdownEdit(RedirectView):
pass pass
except AttributeError: except AttributeError:
pass pass
update_projector_overlay('projector_countdown')
def get_ajax_context(self, **kwargs): def get_ajax_context(self, **kwargs):
clear_projector_cache()
return { return {
'state': config['countdown_state'], 'state': config['countdown_state'],
'countdown_time': config['countdown_time'], 'countdown_time': config['countdown_time'],
@ -289,9 +224,9 @@ class OverlayMessageView(RedirectView):
config['projector_message'] = request.POST['message_text'] config['projector_message'] = request.POST['message_text']
elif 'message-clean' in request.POST: elif 'message-clean' in request.POST:
config['projector_message'] = '' config['projector_message'] = ''
update_projector_overlay('projector_message')
def get_ajax_context(self, **kwargs): def get_ajax_context(self, **kwargs):
clear_projector_cache()
return { return {
'overlay_message': config['projector_message'], 'overlay_message': config['projector_message'],
} }
@ -306,21 +241,20 @@ class ActivateOverlay(RedirectView):
permission_required = 'projector.can_manage_projector' permission_required = 'projector.can_manage_projector'
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
self.name = kwargs['name'] overlay = get_overlays()[kwargs['name']]
active_overlays = config['projector_active_overlays'] self.name = overlay.name
if kwargs['activate']: if kwargs['activate']:
if self.name not in active_overlays: if not overlay.is_active():
active_overlays.append(self.name) overlay.set_active(True)
config['projector_active_overlays'] = active_overlays update_projector_overlay(overlay)
self.active = True self.active = True
elif not kwargs['activate']: else:
if self.name in active_overlays: if overlay.is_active():
active_overlays.remove(self.name) overlay.set_active(False)
config['projector_active_overlays'] = active_overlays update_projector_overlay(overlay)
self.active = False self.active = False
def get_ajax_context(self, **kwargs): def get_ajax_context(self, **kwargs):
clear_projector_cache()
return {'active': self.active, 'name': self.name} return {'active': self.active, 'name': self.name}
@ -399,7 +333,8 @@ def get_widgets(request):
# Overlay widget # Overlay widget
overlays = [] overlays = []
for receiver, overlay in projector_overlays.send(sender='overlay_widget', request=request): for receiver, overlay in projector_overlays.send(sender='overlay_widget', request=request):
overlays.append(overlay) if overlay.widget_html_callback is not None:
overlays.append(overlay)
context = {'overlays': overlays} context = {'overlays': overlays}
context.update(csrf(request)) context.update(csrf(request))
widgets.append(Widget( widgets.append(Widget(
@ -413,6 +348,7 @@ def get_widgets(request):
context=context)) context=context))
# Custom slide widget # Custom slide widget
welcomepage_is_active = get_active_slide().get('callback', 'default') == 'default'
widgets.append(Widget( widgets.append(Widget(
request, request,
name='custom_slide', name='custom_slide',
@ -420,7 +356,7 @@ def get_widgets(request):
template='projector/custom_slide_widget.html', template='projector/custom_slide_widget.html',
context={ context={
'slides': ProjectorSlide.objects.all().order_by('weight'), 'slides': ProjectorSlide.objects.all().order_by('weight'),
'welcomepage_is_active': not bool(config["presentation"])}, 'welcomepage_is_active': welcomepage_is_active},
permission_required='projector.can_manage_projector', permission_required='projector.can_manage_projector',
default_column=2, default_column=2,
default_weight=30)) default_weight=30))

View File

@ -8,15 +8,15 @@
:license: GNU GPL, see LICENSE for more details. :license: GNU GPL, see LICENSE for more details.
""" """
import sys
import posixpath import posixpath
from urllib import unquote from urllib import unquote
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import FallbackHandler, Application, StaticFileHandler from tornado.web import FallbackHandler, Application, StaticFileHandler
from tornado.websocket import WebSocketHandler
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.options import options, parse_command_line from tornado.options import parse_command_line
from django.core.handlers.wsgi import WSGIHandler as Django_WSGIHandler from django.core.handlers.wsgi import WSGIHandler as Django_WSGIHandler
from django.conf import settings from django.conf import settings
@ -38,6 +38,25 @@ class DjangoStaticFileHandler(StaticFileHandler):
return super(DjangoStaticFileHandler, self).get(absolute_path, include_body) return super(DjangoStaticFileHandler, self).get(absolute_path, include_body)
class ProjectorSocketHandler(WebSocketHandler):
waiters = set()
## def allow_draft76(self):
## # for iOS 5.0 Safari
## return True
def open(self):
ProjectorSocketHandler.waiters.add(self)
def on_close(self):
ProjectorSocketHandler.waiters.remove(self)
@classmethod
def send_updates(cls, slide):
for waiter in cls.waiters:
waiter.write_message(slide)
def run_tornado(addr, port, reload=False): def run_tornado(addr, port, reload=False):
# Don't try to read the command line args from openslides # Don't try to read the command line args from openslides
parse_command_line(args=[]) parse_command_line(args=[])
@ -54,7 +73,9 @@ def run_tornado(addr, port, reload=False):
tornado_app = Application([ tornado_app = Application([
(r"%s(.*)" % settings.STATIC_URL, DjangoStaticFileHandler), (r"%s(.*)" % settings.STATIC_URL, DjangoStaticFileHandler),
(r'%s(.*)' % settings.MEDIA_URL, StaticFileHandler, {'path': settings.MEDIA_ROOT}), (r'%s(.*)' % settings.MEDIA_URL, StaticFileHandler, {'path': settings.MEDIA_ROOT}),
('.*', FallbackHandler, dict(fallback=app))], debug=reload) (r'/projector/socket/', ProjectorSocketHandler),
('.*', FallbackHandler, dict(fallback=app))
], debug=reload)
server = HTTPServer(tornado_app) server = HTTPServer(tornado_app)
server.listen(port=port, server.listen(port=port,

View File

@ -141,3 +141,10 @@ def htmldiff(text1, text2):
diff = difflib.HtmlDiff(wrapcolumn=60) diff = difflib.HtmlDiff(wrapcolumn=60)
return diff.make_table(text1.splitlines(), text2.splitlines()) return diff.make_table(text1.splitlines(), text2.splitlines())
def int_or_none(object):
try:
return int(object)
except TypeError:
return None

View File

@ -480,6 +480,7 @@ def send_register_tab(sender, request, context, **kwargs):
extra_stylefiles = context['extra_stylefiles'] extra_stylefiles = context['extra_stylefiles']
else: else:
extra_stylefiles = [] extra_stylefiles = []
# TODO: Do not go over the filesystem by any request
for app in settings.INSTALLED_APPS: for app in settings.INSTALLED_APPS:
try: try:
mod = import_module(app + '.views') mod = import_module(app + '.views')

View File

@ -1,16 +1,11 @@
from django.db import models from django.db import models
from openslides.projector.projector import SlideMixin
from openslides.projector.api import register_slidemodel
class ReleatedItem(SlideMixin, models.Model):
prefix = 'releateditem'
class RelatedItem(models.Model):
name = models.CharField(max_length='255') name = models.CharField(max_length='255')
class Meta: class Meta:
verbose_name = 'Releated Item CHFNGEJ5634DJ34F' verbose_name = 'Related Item CHFNGEJ5634DJ34F'
def get_agenda_title(self): def get_agenda_title(self):
return self.name return self.name
@ -22,11 +17,5 @@ class ReleatedItem(SlideMixin, models.Model):
return '/absolute-url-here/' return '/absolute-url-here/'
class BadReleatedItem(SlideMixin, models.Model): class BadRelatedItem(models.Model):
prefix = 'badreleateditem'
name = models.CharField(max_length='255') name = models.CharField(max_length='255')
register_slidemodel(ReleatedItem)
register_slidemodel(BadReleatedItem)

View File

@ -14,6 +14,7 @@ from openslides.utils.test import TestCase
from openslides.participant.models import User, Group from openslides.participant.models import User, Group
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import set_active_slide
class ListOfSpeakerModelTests(TestCase): class ListOfSpeakerModelTests(TestCase):
@ -214,36 +215,30 @@ class SpeakerListOpenView(SpeakerViewTestCase):
class GlobalListOfSpeakersLinks(SpeakerViewTestCase): class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
def test_global_redirect_url(self): def test_global_redirect_url(self):
self.assertFalse(config['presentation'])
response = self.speaker1_client.get('/agenda/list_of_speakers/') response = self.speaker1_client.get('/agenda/list_of_speakers/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
response = self.admin_client.get('/projector/activate/item-1/') set_active_slide('agenda', {'pk': 1})
self.assertEqual(config['presentation'], 'item-1')
response = self.speaker1_client.get('/agenda/list_of_speakers/') response = self.speaker1_client.get('/agenda/list_of_speakers/')
self.assertRedirects(response, '/agenda/1/') self.assertRedirects(response, '/agenda/1/')
def test_global_add_url(self): def test_global_add_url(self):
self.assertFalse(config['presentation'])
response = self.speaker1_client.get('/agenda/list_of_speakers/add/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
response = self.admin_client.get('/projector/activate/item-1/') set_active_slide('agenda', {'pk': 1})
self.assertEqual(config['presentation'], 'item-1')
response = self.speaker1_client.get('/agenda/list_of_speakers/add/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertRedirects(response, '/agenda/1/') self.assertRedirects(response, '/agenda/1/')
self.assertEqual(Speaker.objects.get(item__pk='1').person, self.speaker1) self.assertEqual(Speaker.objects.get(item__pk='1').person, self.speaker1)
def test_global_next_speaker_url(self): def test_global_next_speaker_url(self):
self.assertFalse(config['presentation'])
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
response = self.admin_client.get('/projector/activate/item-1/') set_active_slide('agenda', {'pk': 1})
self.assertEqual(config['presentation'], 'item-1')
response = self.admin_client.get('/agenda/list_of_speakers/next/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'The list of speakers is empty.') self.assertMessage(response, 'The list of speakers is empty.')
@ -255,13 +250,11 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is not None) self.assertTrue(Speaker.objects.get(item__pk='1').begin_time is not None)
def test_global_end_speach_url(self): def test_global_end_speach_url(self):
self.assertFalse(config['presentation'])
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.')
response = self.admin_client.get('/projector/activate/item-1/') set_active_slide('agenda', {'pk': 1})
self.assertEqual(config['presentation'], 'item-1')
response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no one speaking at the moment.') self.assertMessage(response, 'There is no one speaking at the moment.')

View File

@ -14,12 +14,12 @@ from django.test.client import Client
from django.db.models.query import EmptyQuerySet from django.db.models.query import EmptyQuerySet
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
from openslides.projector.api import get_active_slide from openslides.projector.api import get_active_slide, set_active_slide
from openslides.participant.models import User from openslides.participant.models import User
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.agenda.slides import agenda_show from openslides.agenda.slides import agenda_slide
from .models import ReleatedItem, BadReleatedItem # TODO: Rename releated to related from .models import RelatedItem, BadRelatedItem
class ItemTest(TestCase): class ItemTest(TestCase):
@ -28,8 +28,8 @@ class ItemTest(TestCase):
self.item2 = Item.objects.create(title='item2') self.item2 = Item.objects.create(title='item2')
self.item3 = Item.objects.create(title='item1A', parent=self.item1) self.item3 = Item.objects.create(title='item1A', parent=self.item1)
self.item4 = Item.objects.create(title='item1Aa', parent=self.item3) self.item4 = Item.objects.create(title='item1Aa', parent=self.item3)
self.releated = ReleatedItem.objects.create(name='ekdfjen458gj1siek45nv') self.related = RelatedItem.objects.create(name='ekdfjen458gj1siek45nv')
self.item5 = Item.objects.create(title='item5', content_object=self.releated) self.item5 = Item.objects.create(title='item5', content_object=self.related)
def testClosed(self): def testClosed(self):
self.assertFalse(self.item1.closed) self.assertFalse(self.item1.closed)
@ -78,24 +78,18 @@ class ItemTest(TestCase):
self.assertEqual(self.item1.get_absolute_url('edit'), '/agenda/1/edit/') self.assertEqual(self.item1.get_absolute_url('edit'), '/agenda/1/edit/')
self.assertEqual(self.item1.get_absolute_url('delete'), '/agenda/1/del/') self.assertEqual(self.item1.get_absolute_url('delete'), '/agenda/1/del/')
def test_agenda_slide(self): def test_related_item(self):
data = agenda_show() self.assertEqual(self.item5.get_title(), self.related.name)
self.assertEqual(list(data['items']), list(Item.objects.all().filter(parent=None)))
self.assertEqual(data['template'], 'projector/AgendaSummary.html')
self.assertEqual(data['title'], 'Agenda')
def test_releated_item(self):
self.assertEqual(self.item5.get_title(), self.releated.name)
self.assertEqual(self.item5.get_title_supplement(), 'test item') self.assertEqual(self.item5.get_title_supplement(), 'test item')
self.assertEqual(self.item5.content_type.name, 'Releated Item CHFNGEJ5634DJ34F') self.assertEqual(self.item5.content_type.name, 'Related Item CHFNGEJ5634DJ34F')
def test_deleted_releated_item(self): def test_deleted_related_item(self):
self.releated.delete() self.related.delete()
self.assertFalse(ReleatedItem.objects.all().exists()) self.assertFalse(RelatedItem.objects.all().exists())
self.assertEqual(Item.objects.get(pk=self.item5.pk).title, '< Item for deleted slide (ekdfjen458gj1siek45nv) >') self.assertEqual(Item.objects.get(pk=self.item5.pk).title, '< Item for deleted slide (ekdfjen458gj1siek45nv) >')
def test_bad_releated_item(self): def test_bad_related_item(self):
bad = BadReleatedItem.objects.create(name='dhfne94irkgl2047fzvb') bad = BadRelatedItem.objects.create(name='dhfne94irkgl2047fzvb')
item = Item.objects.create(title='item_jghfndzrh46w738kdmc', content_object=bad) item = Item.objects.create(title='item_jghfndzrh46w738kdmc', content_object=bad)
self.assertRaisesMessage( self.assertRaisesMessage(
NotImplementedError, NotImplementedError,
@ -138,20 +132,6 @@ class ViewTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context['items']), len(Item.objects.all())) self.assertEqual(len(response.context['items']), len(Item.objects.all()))
def testActivate(self):
c = self.adminClient
response = c.get('/projector/activate/%s/' % self.item1.sid)
self.assertEqual(response.status_code, 302)
self.assertTrue(self.item1.active)
self.assertFalse(self.item2.active)
response = c.get('/projector/activate/%s/' % 'agenda')
self.assertEqual(response.status_code, 302)
self.assertFalse(self.item2.active)
self.assertFalse(self.item1.active)
self.assertEqual(get_active_slide(only_sid=True), 'agenda')
def testClose(self): def testClose(self):
c = self.adminClient c = self.adminClient