Merge pull request #1243 from normanjaeckel/ListOfSpeakersView

New projector view with the current list of speakers.
This commit is contained in:
Norman Jäckel 2014-04-22 20:21:01 +02:00
commit b3d615ddce
15 changed files with 190 additions and 45 deletions

View File

@ -9,6 +9,7 @@ Version 1.6 (unreleased)
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14] [https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
Agenda: Agenda:
- New projector view with the current list of speakers.
- Added CSV import. - Added CSV import.
Assignment: Assignment:
- Coupled assignment candidates with list of speakers. - Coupled assignment candidates with list of speakers.

View File

@ -137,6 +137,12 @@ Redner. Die Einblendung erscheint nur auf Folien von Tagesordnungspunkten.
:scale-latex: 80 :scale-latex: 80
:alt: Projektor-Ansicht mit Rednerlisten-Overlay :alt: Projektor-Ansicht mit Rednerlisten-Overlay
Schließlich haben Sie die Möglichkeit, die Rednerliste des jeweiligen
Tagesordnungspunktes auf einem gesonderten Projektor anzeigen zu lassen.
Klicken Sie auf der Tagesordnungsseite oben rechts auf auf den
Glocken-Button |bell| und legen Sie die Seite im Vollbildmodus auf einen
eigenen Projektor oder Bildschirm.
Die Rednerliste verwalten Die Rednerliste verwalten
------------------------- -------------------------

View File

@ -101,8 +101,8 @@ def agenda_list_of_speakers(sender, **kwargs):
if slide is None or isinstance(slide, Item): if slide is None or isinstance(slide, Item):
item = slide item = slide
else: else:
# TODO: If there are more the one items, use the first one in the # TODO: If there is more than one item, use the first one in the
# mptt tree that is not closed # mptt tree that is not closed.
try: try:
item = Item.objects.filter( item = Item.objects.filter(
content_type=ContentType.objects.get_for_model(slide), content_type=ContentType.objects.get_for_model(slide),

View File

@ -0,0 +1,14 @@
/*
* JavaScript functions for agenda CurrentListOfSpeakersProjectorView
*/
function reloadListOfSpeakers() {
$.ajax({
url: '',
success: function (data) {
updater.updateProjector(data);
setTimeout('reloadListOfSpeakers()', 2000);
},
dataType: 'json'
});
}

View File

@ -0,0 +1,4 @@
{% extends 'projector.html' %}
{% load i18n %}
{% block title %}{% trans 'List of speakers' %} {{ block.super }}{% endblock %}

View File

@ -39,6 +39,10 @@
<a href="{% url 'item_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a> <a href="{% url 'item_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a>
{% endif %} {% endif %}
<a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a> <a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a>
{% if perms.core.can_see_projector %}
<a href="{% url 'agenda_current_list_of_speakers_projector' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Current list of speakers' %}">
<i class="icon-bell"></i> {% trans 'List of speakers' %}</a>
{% endif %}
</small> </small>
</h1> </h1>

View File

@ -89,6 +89,10 @@ urlpatterns = patterns(
views.CurrentListOfSpeakersView.as_view(end_speach=True), views.CurrentListOfSpeakersView.as_view(end_speach=True),
name='agenda_end_speach_on_current_list_of_speakers'), name='agenda_end_speach_on_current_list_of_speakers'),
url(r'^list_of_speakers/projector/$',
views.CurrentListOfSpeakersProjectorView.as_view(),
name='agenda_current_list_of_speakers_projector'),
url(r'^csv_import/$', url(r'^csv_import/$',
views.ItemCSVImportView.as_view(), views.ItemCSVImportView.as_view(),
name='item_csv_import')) name='item_csv_import'))

View File

@ -2,21 +2,33 @@
# TODO: Rename all views and template names # TODO: Rename all views and template names
from datetime import datetime, timedelta from datetime import datetime, timedelta
from json import dumps
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.db.models import Model from django.db.models import Model
from django.template.loader import render_to_string
from django.utils.datastructures import SortedDict
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from openslides.config.api import config from openslides.config.api import config
from openslides.projector.api import get_active_slide, update_projector from openslides.projector.api import (
get_active_object,
get_active_slide,
get_projector_overlays_js,
get_overlays,
update_projector)
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import ( from openslides.utils.views import (
AjaxMixin,
CreateView, CreateView,
CSVImportView, CSVImportView,
DeleteView, DeleteView,
@ -630,6 +642,108 @@ class CurrentListOfSpeakersView(RedirectView):
return reverse('item_view', args=[item.pk]) return reverse('item_view', args=[item.pk])
class CurrentListOfSpeakersProjectorView(AjaxMixin, TemplateView):
"""
View with the current list of speakers depending on the active slide.
Usefule for the projector.
"""
template_name = 'agenda/current_list_of_speakers_projector.html'
def get(self, request, *args, **kwargs):
"""
Returns response object depending on request type (ajax or normal).
"""
if request.is_ajax():
value = self.ajax_get(request, *args, **kwargs)
else:
value = super(CurrentListOfSpeakersProjectorView, self).get(request, *args, **kwargs)
return value
def get_item(self):
"""
Returns the item of the current slide is an agenda item slide or a
slide of a related model else returns None.
"""
slide_object = get_active_object()
if slide_object is None or isinstance(slide_object, Item):
item = slide_object
else:
# TODO: If there is more than one item, use the first one in the
# mptt tree that is not closed.
try:
item = Item.objects.filter(
content_type=ContentType.objects.get_for_model(slide_object),
object_id=slide_object.pk)[0]
except IndexError:
item = None
return item
def get_content(self):
"""
Returns the content of this slide.
"""
item = self.get_item()
if item is None:
content = mark_safe('<h1>%s</h1><i>%s</i>\n' % (_('List of speakers'), _('Not available.')))
else:
content_dict = {
'title': item.get_title(),
'item': item,
'list_of_speakers': item.get_list_of_speakers(
old_speakers_count=config['agenda_show_last_speakers'])}
content = render_to_string('agenda/item_slide_list_of_speaker.html', content_dict)
return content
def get_overlays_and_overlay_js(self):
"""
Returns the overlays and their JavaScript for this slide as a
two-tuple. The overlay 'agenda_speaker' is always excluded.
The required JavaScript fot this view is inserted.
"""
overlays = get_overlays(only_active=True)
overlays.pop('agenda_speaker', None)
overlay_js = get_projector_overlays_js(as_json=True)
# Note: The JavaScript content of overlay 'agenda_speaker' is not
# excluded because this overlay has no such content at the moment.
extra_js = SortedDict()
extra_js['load_file'] = static('js/agenda_current_list_of_speakers_projector.js')
extra_js['call'] = 'reloadListOfSpeakers();'
extra_js = dumps(extra_js)
overlay_js.append(extra_js)
return overlays, overlay_js
def get_context_data(self, **context):
"""
Returns the context for the projector template. Contains the content
of this slide.
"""
overlays, overlay_js = self.get_overlays_and_overlay_js()
return super(CurrentListOfSpeakersProjectorView, self).get_context_data(
content=self.get_content(),
overlays=overlays,
overlay_js=overlay_js,
**context)
def get_ajax_context(self, **context):
"""
Returns the context including the slide content for ajax response. The
overlay 'agenda_speaker' is always excluded.
"""
overlay_dict = {}
for overlay in get_overlays().values():
if overlay.is_active() and overlay.name != 'agenda_speaker':
overlay_dict[overlay.name] = {
'html': overlay.get_projector_html(),
'javascript': overlay.get_javascript()}
else:
overlay_dict[overlay.name] = None
return super(CurrentListOfSpeakersProjectorView, self).get_ajax_context(
content=self.get_content(),
overlays=overlay_dict,
**context)
class ItemCSVImportView(CSVImportView): class ItemCSVImportView(CSVImportView):
""" """
Imports agenda items from an uploaded csv file. Imports agenda items from an uploaded csv file.

View File

@ -104,29 +104,22 @@ def default_slide():
return render_to_string('projector/default_slide.html') return render_to_string('projector/default_slide.html')
def get_overlays(): def get_overlays(only_active=False):
""" """
Returns all overlay objects. Returns all overlay objects.
If only_active is True, returns only active overlays.
The returned value is a dictonary with the name of the overlay as key, and The returned value is a dictonary with the name of the overlay as key, and
the overlay object as value. the overlay object as value.
""" """
overlays = {} overlays = {}
for receiver, overlay in projector_overlays.send(sender='get_overlays'): for receiver, overlay in projector_overlays.send(sender='get_overlays'):
overlays[overlay.name] = overlay if not only_active or overlay.is_active():
overlays[overlay.name] = overlay
return overlays 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(as_json=False): def get_projector_overlays_js(as_json=False):
""" """
Returns JS-Code for the active overlays. Returns JS-Code for the active overlays.

View File

@ -8,8 +8,7 @@
<link href="{% static 'css/bootstrap.min.css' %}" type="text/css" rel="stylesheet"> <link href="{% static 'css/bootstrap.min.css' %}" type="text/css" rel="stylesheet">
<link href="{% static 'css/projector.css' %}" type="text/css" rel="stylesheet"> <link href="{% static 'css/projector.css' %}" type="text/css" rel="stylesheet">
<link href="{% static 'img/favicon.png' %}" type="image/png" rel="shortcut icon"> <link href="{% static 'img/favicon.png' %}" type="image/png" rel="shortcut icon">
<title>{% trans 'Projector' %} {{ 'event_name'|get_config }}</title> <title>{% block title %}{% trans 'Projector' %} {{ 'event_name'|get_config }}{% endblock %}</title>
<script type="text/javascript" src="{% static 'js/jquery/jquery.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/jquery/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/sockjs-0.3.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/sockjs-0.3.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/projector.js' %}"></script> <script type="text/javascript" src="{% static 'js/projector.js' %}"></script>
@ -59,7 +58,9 @@
</div> </div>
<div id="overlays"> <div id="overlays">
{{ overlays }} {% for overlay in overlays.values %}
{{ overlay.get_projector_html|safe }}
{% endfor %}
</div> </div>
<div id="content" {% if reload %}class="reload"{% endif %}> <div id="content" {% if reload %}class="reload"{% endif %}>

View File

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

View File

@ -6,7 +6,7 @@ from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.views import RedirectView, TemplateView from openslides.utils.views import RedirectView, TemplateView
from .api import (call_on_projector, get_active_slide, from .api import (call_on_projector, get_active_slide,
get_overlays, get_projector_content, get_projector_overlays, get_overlays, get_projector_content,
get_projector_overlays_js, reset_countdown, set_active_slide, get_projector_overlays_js, reset_countdown, set_active_slide,
start_countdown, stop_countdown, update_projector_overlay) start_countdown, stop_countdown, update_projector_overlay)
@ -24,7 +24,7 @@ class ProjectorView(TemplateView):
if callback is None: if callback is None:
kwargs.update({ kwargs.update({
'content': get_projector_content(), 'content': get_projector_content(),
'overlays': get_projector_overlays(), 'overlays': get_overlays(only_active=True),
'overlay_js': get_projector_overlays_js(as_json=True), 'overlay_js': get_projector_overlays_js(as_json=True),
'reload': True, 'reload': True,
'calls': config['projector_js_cache']}) 'calls': config['projector_js_cache']})

View File

@ -314,3 +314,23 @@ class TestOverlay(TestCase):
value = agenda_list_of_speakers(sender='test').get_projector_html() value = agenda_list_of_speakers(sender='test').get_projector_html()
self.assertEqual(value, '') self.assertEqual(value, '')
class TestCurrentListOfSpeakersOnProjectorView(SpeakerViewTestCase):
"""
Test the view with the current list of speakers depending on the actual
slide.
"""
def test_get_none(self):
response = self.admin_client.get('/agenda/list_of_speakers/projector/')
self.assertContains(response, 'List of speakers</h1><i>Not available')
def test_get_normal(self):
self.item1.title = 'title_gupooDee8ahahnaxoo2a'
self.item1.save()
Speaker.objects.add(self.speaker1, self.item1)
config['projector_active_slide'] = {'callback': 'agenda', 'pk': self.item1.pk}
response = self.admin_client.get('/agenda/list_of_speakers/projector/')
self.assertContains(response, 'List of speakers')
self.assertContains(response, 'title_gupooDee8ahahnaxoo2a')
self.assertContains(response, 'speaker1')

View File

@ -91,30 +91,17 @@ class ApiFunctions(TestCase):
mock_overlay = MagicMock() mock_overlay = MagicMock()
mock_overlay.name = 'mock_overlay' mock_overlay.name = 'mock_overlay'
mock_projector_overlays.send.return_value = ((None, mock_overlay), ) mock_projector_overlays.send.return_value = ((None, mock_overlay), )
value = projector_api.get_overlays() value = projector_api.get_overlays()
self.assertEqual(value, {'mock_overlay': mock_overlay}) self.assertEqual(value, {'mock_overlay': mock_overlay})
@patch('openslides.projector.api.render_to_string') @patch('openslides.projector.api.projector_overlays')
@patch('openslides.projector.api.get_overlays') def test_get_overlays_inactive(self, mock_projector_overlays):
def test_get_projector_overlays(self, mock_get_overlays, mock_render_to_string):
mock_overlay = MagicMock() mock_overlay = MagicMock()
mock_overlay.get_projector_html.return_value = 'some html' mock_overlay.name = 'mock_overlay_2'
mock_get_overlays.return_value = {'overlay_name': mock_overlay}
# Test with inactive overlay
mock_overlay.is_active.return_value = False mock_overlay.is_active.return_value = False
projector_api.get_projector_overlays() mock_projector_overlays.send.return_value = ((None, mock_overlay), )
mock_render_to_string.assert_called_with( value = projector_api.get_overlays(only_active=True)
'projector/all_overlays.html', self.assertNotEqual(value, {'mock_overlay_2': mock_overlay})
{'overlays': []})
# Test with active overlay
mock_overlay.is_active.return_value = True
projector_api.get_projector_overlays()
mock_render_to_string.assert_Called_with(
'projector/all_overlays.html',
{'overlays': [{'name': 'overlay_name', 'html': 'some html'}]})
@patch('openslides.projector.api.get_overlays') @patch('openslides.projector.api.get_overlays')
def test_get_projector_overlays_js(self, mock_get_overlays): def test_get_projector_overlays_js(self, mock_get_overlays):

View File

@ -13,9 +13,9 @@ class ProjectorViewTest(TestCase):
rf = RequestFactory() rf = RequestFactory()
@patch('openslides.projector.views.get_projector_overlays_js') @patch('openslides.projector.views.get_projector_overlays_js')
@patch('openslides.projector.views.get_projector_overlays') @patch('openslides.projector.views.get_overlays')
@patch('openslides.projector.views.get_projector_content') @patch('openslides.projector.views.get_projector_content')
def test_get(self, mock_get_projector_content, mock_get_projector_overlays, def test_get(self, mock_get_projector_content, mock_get_overlays,
mock_get_projector_overlays_js): mock_get_projector_overlays_js):
view = views.ProjectorView() view = views.ProjectorView()
view.request = self.rf.get('/') view.request = self.rf.get('/')
@ -34,7 +34,7 @@ class ProjectorViewTest(TestCase):
with patch('openslides.projector.views.config', mock_config): with patch('openslides.projector.views.config', mock_config):
context = view.get_context_data() context = view.get_context_data()
mock_get_projector_content.assert_called_with() mock_get_projector_content.assert_called_with()
mock_get_projector_overlays.assert_called_with() mock_get_overlays.assert_called_with(only_active=True)
mock_get_projector_overlays_js.assert_called_with(as_json=True) mock_get_projector_overlays_js.assert_called_with(as_json=True)
self.assertTrue(context['reload']) self.assertTrue(context['reload'])
self.assertEqual(context['calls'], 'js_cache') self.assertEqual(context['calls'], 'js_cache')