added possibility to show pdfs on the projector

This commit is contained in:
Max Brauer 2013-10-04 11:38:39 +02:00
parent 95ea800958
commit bd332bd3d5
20 changed files with 46777 additions and 26 deletions

View File

@ -250,6 +250,8 @@ OpenSlides uses the following projects or parts of them:
* `Django haystack <http://haystacksearch.org>`_, License: BSD
* `pdf.js <http://mozilla.github.io/pdf.js/>`_, License: Apache License v2.0
* `Pillow <https://github.com/python-imaging/Pillow/>`_, License: Standard
PIL License

View File

@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import slides # noqa

View File

@ -12,16 +12,20 @@
import mimetypes
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.projector.models import SlideMixin
from openslides.utils.person.models import PersonField
class Mediafile(models.Model):
class Mediafile(SlideMixin, models.Model):
"""
Class for uploaded files which can be delivered under a certain url.
"""
slide_callback_name = 'mediafile'
PRESENTABLE_FILE_TYPES = ['application/pdf']
mediafile = models.FileField(upload_to='file', verbose_name=ugettext_lazy("File"))
"""
@ -41,6 +45,12 @@ class Mediafile(models.Model):
filetype = models.CharField(max_length=255, editable=False)
"""A string used to show the type of the file."""
is_presentable = models.BooleanField(
default=False,
verbose_name=ugettext_lazy("Is Presentable"),
help_text=ugettext_lazy("If checked, this file can be presented on the projector. "
"Currently, this is only possible for PDFs."))
class Meta:
"""
Meta class for the mediafile model.
@ -67,15 +77,16 @@ class Mediafile(models.Model):
self.filetype = ugettext_noop('unknown')
return super(Mediafile, self).save(*args, **kwargs)
@models.permalink
def get_absolute_url(self, link='update'):
"""
Returns the URL to a mediafile. The link can be 'update' or 'delete'.
Returns the URL to a mediafile. The link can be 'projector',
'update' or 'delete'.
"""
if link == 'update' or link == 'edit': # 'edit' ist only used until utils/views.py is fixed
return ('mediafile_update', [str(self.id)])
return reverse('mediafile_update', kwargs={'pk': str(self.id)})
if link == 'delete':
return ('mediafile_delete', [str(self.id)])
return reverse('mediafile_delete', kwargs={'pk': str(self.id)})
return super(Mediafile, self).get_absolute_url(link)
def get_filesize(self):
"""

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.template.loader import render_to_string
from openslides.config.api import config
from openslides.projector.api import register_slide
from .models import Mediafile
def mediafile_presentation_as_slide(**kwargs):
"""
Return the html code for a presentation of a Mediafile.
At the moment, only the presentation of pdfs is supported.
"""
file_pk = kwargs.get('pk', None)
page_num = kwargs.get('page_num', 1)
try:
pdf = Mediafile.objects.get(
pk=file_pk,
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
is_presentable=True)
except Mediafile.DoesNotExist:
# TODO what doing, if a wrong pk is given?
pdf = None
context = {'pdf': pdf, 'page_num': page_num,
'fullscreen': config['pdf_fullscreen']}
return render_to_string('mediafile/presentation_slide.html', context)
register_slide('mediafile', mediafile_presentation_as_slide)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
var pdf = PDFJS.getDocument(projector['pdf_url']);
projector['load_pdf_page'] = function(page) {
projector['pdf_page_num'] = page;
pdf.then(function(pdf) {
pdf.getPage(page).then(set_convas_size);
});
};
projector['load_pdf'] = function(data) {
projector['pdf_url'] = data['url'];
projector['pdf_page_num'] = data['page_num'];
pdf = PDFJS.getDocument(projector['pdf_url']);
projector['load_pdf_page'](projector['pdf_page_num']);
};
projector['toggle_fullscreen'] = function(fullscreen) {
projector['pdf_fullscreen'] = fullscreen;
content = $('#content');
presentation = $('#presentation');
footer = $('#footer');
body = $('body');
if (fullscreen) {
content.addClass('fullscreen');
presentation.addClass('fullscreen');
footer.addClass('black');
body.addClass('black');
} else {
content.removeClass('fullscreen');
presentation.removeClass('fullscreen');
footer.removeClass('black');
body.removeClass('black');
}
$(window).resize();
};
function scale_to_height(page) {
return page.getViewport(window.innerHeight / page.getViewport(1.0).height);
}
function scale_to_width(page) {
return page.getViewport(window.innerWidth / page.getViewport(1.0).width);
}
function get_correct_viewport(page, canvas) {
if(window.innerWidth > window.innerHeight) {
viewport = scale_to_height(page);
if (viewport.width > window.innerWidth) {
viewport = scale_to_width(page);
canvas.height = viewport.height;
canvas.width = window.innerWidth;
} else {
canvas.height = window.innerHeight;
canvas.width = viewport.width;
}
} else {
viewport = scale_to_width(page);
if (viewport.height > window.innerHeight) {
viewport = scale_to_height(page);
canvas.height = window.innerHeight;
canvas.width = viewport.width;
} else {
canvas.height = viewport.height;
canvas.width = window.innerWidth;
}
}
return viewport;
}
function set_convas_size(page) {
var canvas = document.getElementById('presentation');
var context = canvas.getContext('2d');
if (projector['pdf_fullscreen']) {
viewport = get_correct_viewport(page, canvas);
} else {
viewport = page.getViewport(window.innerWidth / page.getViewport(1.0).width);
canvas.height = viewport.height;
canvas.width = window.innerWidth;
}
page.render({canvasContext: context, viewport: viewport});
}
$(document).ready(function () {
$(window).resize(function() {
projector['load_pdf_page'](projector['pdf_page_num']);
});
if (projector['pdf_fullscreen']) {
if (!$('#content').hasClass('fullscreen')) {
$('#content').addClass('fullscreen');
$('#footer').addClass('black');
$('body').addClass('black');
}
}
$(window).resize();
});

View File

@ -36,6 +36,12 @@
<span style="width: 1px; white-space: nowrap;">
<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="{{ mediafile|absolute_url:'delete' }}" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini"><i class="icon-remove"></i></a>
{% if mediafile.is_presentable %}{% if mediafile.filetype in mediafile.PRESENTABLE_FILE_TYPES %}
<a href="{{ mediafile|absolute_url:'projector' }}" class="activate_link choose-pdf btn {% if mediafile.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if mediafile.is_active_slide %}icon-white{% endif %}">
</i>
</a>
{% endif %}{% endif %}
</span>
</td>
{% endif %}

View File

@ -0,0 +1,45 @@
{% load i18n %}
{% load tags %}
<form action="{% url 'target_pdf_page' %}" method="GET" class="set-page-form">
<div class="input-prepend" style="margin-bottom:0;">
<a class="btn go-first-page">
<i class="icon-fast-backward"></i>
</a>
<a class="btn pdf-page-ctl" href="{% url 'prev_pdf_page' %}">
<i class="icon-backward"></i>
</a>
<a class="btn pdf-page-ctl" href="{% url 'next_pdf_page' %}">
<i class="icon-forward"></i>
</a>
</div>
<div class="input-append" style="margin-bottom:0;">
<a class="btn pdf-toggle-fullscreen {%if pdf_fullscreen %}btn-primary{% endif %}" href="{% url 'toggle_fullscreen' %}">
<i class="icon-fullscreen {%if pdf_fullscreen %}icon-white{% endif %}"></i>
</a>
</div>
<div class="input-append input-prepend" style="margin-bottom:0;">
<span class="add-on">Page:</span>
<input id="page_num" name="page_num" type="number" style="width: 22px;" value="{{ current_page }}">
<button type="submit" id="go_to_page" class="btn" style="width: 40px;">
<i class="icon-refresh"></i>
</button>
</div>
</form>
<ul style="line-height: 180%">
{% for pdf in pdfs %}
<li class="{% if pdf.is_active_slide %}activeline{% endif %}">
<a href="{{ pdf|absolute_url:'projector' }}" class="activate_link choose-pdf btn {% if pdf.is_active_slide %}btn-primary{% endif %} btn-mini" title="{% trans 'Show' %}">
<i class="icon-facetime-video {% if pdf.is_active_slide %}icon-white{% endif %}"></i>
</a> {{ pdf }}
</li>
{% empty %}
<li>{% trans 'No PDFs available.' %}</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,15 @@
{% load i18n %}
{% load static %}
<script src="{% static 'javascript/pdf.js' %}" type="text/javascript"></script>
<script type="text/javascript">
projector['pdf_url'] = '{{ pdf.mediafile.url }}';
projector['pdf_page_num'] = {{ page_num }};
projector['pdf_fullscreen'] = {% if fullscreen %}true{% else %}false{% endif %};
PDFJS.workerSrc = "{% static 'javascript/pdf.worker.js' %}";
</script>
<script src="{% static 'javascript/pdf_presenter.js' %}" type="text/javascript"></script>
<div class="canvas-container">
<canvas id="presentation" class="{% if fullscreen %}fullscreen{% endif %}"></canvas>
</div>

View File

@ -31,4 +31,12 @@ urlpatterns = patterns(
url(r'^(?P<pk>\d+)/del/$',
views.MediafileDeleteView.as_view(),
name='mediafile_delete'),
url(r'^pdf/next/$', views.PdfNextView.as_view(), name='next_pdf_page'),
url(r'^pdf/prev/$', views.PdfPreviousView.as_view(), name='prev_pdf_page'),
url(r'^pdf/target_page/$',
views.PdfGoToPageView.as_view(),
name='target_pdf_page'),
url(r'^pdf/toggle_fullscreen/$',
views.PdfToggleFullscreenView.as_view(),
name='toggle_fullscreen')
)

View File

@ -11,10 +11,16 @@
"""
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from openslides.config.api import config
from openslides.projector.api import get_active_slide
from openslides.projector.projector import Widget
from openslides.utils.template import Tab
from openslides.utils.views import CreateView, DeleteView, ListView, UpdateView
from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
UpdateView)
from .forms import MediafileNormalUserCreateForm, MediafileUpdateForm
from .models import Mediafile
@ -94,6 +100,132 @@ class MediafileDeleteView(DeleteView):
return super(MediafileDeleteView, self).on_clicked_yes(*args, **kwargs)
class PdfNavBaseView(AjaxView):
"""
BaseView for the Pdf Ajax Navigation.
"""
def get_ajax_context(self, *args, **kwargs):
return {'current_page': self.active_slide['page_num']}
def load_other_page(self, active_slide):
"""
Tell connected clients to load an other pdf page.
"""
config['projector_active_slide'] = active_slide
ProjectorSocketHandler.send_updates(
{'calls': {'load_pdf_page': active_slide['page_num']}})
class PdfNextView(PdfNavBaseView):
"""
Activate the next Page of a pdf and return the number of the current page.
"""
def get(self, request, *args, **kwargs):
"""
Increment the page number by 1.
If the page number is set in the active slide, we are the value is
incremented by 1. Otherwise, it is the first page and it is set to 2.
"""
self.active_slide = get_active_slide()
if self.active_slide['callback'] == 'mediafile':
if 'page_num' not in self.active_slide:
self.active_slide['page_num'] = 2
else:
self.active_slide['page_num'] += 1
self.load_other_page(self.active_slide)
response = super(PdfNextView, self).get(self, request, *args, **kwargs)
else:
# no Mediafile is active and the JavaScript should not do anything.
response = HttpResponse()
return response
class PdfPreviousView(PdfNavBaseView):
"""
Activate the previous Page of a pdf and return the number of the current page.
"""
def get(self, request, *args, **kwargs):
"""
Decrement the page number by 1.
If the page number is set and it is greater than 1, it is decremented
by 1. Otherwise, it is the first page and nothing happens.
"""
self.active_slide = get_active_slide()
response = None
if self.active_slide['callback'] == 'mediafile':
if 'page_num' in self.active_slide and self.active_slide['page_num'] > 1:
self.active_slide['page_num'] -= 1
self.load_other_page(self.active_slide)
response = super(PdfPreviousView, self).get(self, request, *args, **kwargs)
if not response:
response = HttpResponse()
return response
class PdfGoToPageView(PdfNavBaseView):
"""
Activate the page set in the textfield.
"""
def get(self, request, *args, **kwargs):
target_page = int(request.GET.get('page_num'))
self.active_slide = get_active_slide()
if target_page:
self.active_slide['page_num'] = target_page
self.load_other_page(self.active_slide)
response = super(PdfGoToPageView, self).get(self, request, *args, **kwargs)
else:
response = HttpResponse()
return response
class PdfToggleFullscreenView(RedirectView):
"""
Toggle fullscreen mode for pdf presentations.
"""
allow_ajax = True
url_name = 'dashboard'
def get_ajax_context(self, *args, **kwargs):
config['pdf_fullscreen'] = not config['pdf_fullscreen']
active_slide = get_active_slide()
if active_slide['callback'] == 'mediafile':
ProjectorSocketHandler.send_updates(
{'calls': {'toggle_fullscreen': config['pdf_fullscreen']}})
return {'fullscreen': config['pdf_fullscreen']}
def get_widgets(request):
"""
Return the widgets of the projector app
"""
widgets = []
# PDF-Presentation widget
pdfs = Mediafile.objects.filter(
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
is_presentable=True
)
current_page = get_active_slide().get('page_num', 1)
widgets.append(Widget(
request,
name='presentations',
display_name=_('Presentations'),
template='mediafile/pdfs_widget.html',
context={'pdfs': pdfs, 'current_page': current_page,
'pdf_fullscreen': config['pdf_fullscreen']},
permission_required='projector.can_manage_projector',
default_column=1,
default_weight=75))
return widgets
def register_tab(request):
"""
Inserts a new Tab to the views for files.

View File

@ -73,12 +73,17 @@ def config_variables(sender, **kwargs):
name='projector_active_overlays',
default_value=[])
projector_pdf_fullscreen = ConfigVariable(
name='pdf_fullscreen',
default_value=False)
return ConfigPage(
title='No title here', url='bar', required_permission=None, variables=(
projector, projector_message,
countdown_time, countdown_start_stamp, countdown_pause_stamp,
countdown_state, projector_scale, projector_scroll,
projector_active_overlays, projector_js_cache))
projector_active_overlays, projector_js_cache,
projector_pdf_fullscreen))
@receiver(projector_overlays, dispatch_uid="projector_countdown")

View File

@ -22,7 +22,7 @@ function restoreOrder() {
var colid = value.id;
var cookieName = "cookie-" + colid;
var cookie = $.cookie(cookieName);
if ( cookie == null ) { return; }
if ( cookie === null ) { return; }
var IDs = cookie.split(",");
for (var i = 0, n = IDs.length; i < n; i++ ) {
var widgetID = IDs[i];
@ -81,7 +81,7 @@ $(function() {
$('#countdown_play').show();
$('#countdown_stop').hide();
}
$('#countdown_time').val(data['countdown_time'])
$('#countdown_time').val(data['countdown_time']);
}
});
});
@ -123,6 +123,55 @@ $(function() {
}
});
// control pdf pages
$('.pdf-page-ctl').click(function(event){
event.preventDefault();
var link = $(this);
$.ajax({
type: 'GET',
url: link.attr('href'),
dataType: 'json',
success: function(data) {
if (typeof data.current_page !== 'undefined') {
$('#page_num').val(data.current_page);
}
}
});
});
$('.set-page-form').submit(function() {
$(this).ajaxSubmit();
return false;
});
$('.go-first-page').click(function() {
$('#page_num').val('1');
$('.set-page-form').ajaxSubmit();
});
$('.pdf-toggle-fullscreen').click(function(event){
event.preventDefault();
var link = $(this);
$.ajax({
type: 'GET',
url: link.attr('href'),
dataType: 'json',
success: function(data) {
if(data.fullscreen) {
if (!link.hasClass('btn-primary')) {
link.addClass('btn-primary');
link.find('i').addClass('icon-white');
}
} else {
if (link.hasClass('btn-primary')) {
link.removeClass('btn-primary');
link.find('i').removeClass('icon-white');
}
}
}
});
});
/* comment out this function because '$.browser' has been removed from jquery 1.9, see:
http://blog.jquery.com/2013/01/15/jquery-1-9-final-jquery-2-0-beta-migrate-final-released/
TODO: use jquery migrate to have $.browser support for IE8;

View File

@ -66,6 +66,9 @@ var updater = {
updateProjector: function(data) {
if (data.content) {
$('#content').removeClass('fullscreen');
$('#footer').removeClass('black');
$('body').removeClass('black');
$('#content').html(data.content);
}
if (data.overlays) {

View File

@ -86,12 +86,11 @@ body{
top: 150px;
right: 40px;
z-index: -1;
transition: all 1s;
line-height: normal;
transition-property: margin, font-size;
transition-duration: 1s;
}
#content .scroll {
transition: margin 1s;
}
h1 {
font-size: 2.25em;
margin-bottom: 40px;
@ -193,3 +192,37 @@ tr.total td {
td.elected {
background-color: #BED4DE !important;
}
/* PDF Presentation */
.canvas-container {
width: 100%;
text-align:center;
}
#presentation {
position: relative;
left:-75px;
top: -77px;
}
#presentation.fullscreen {
left: 0;
top: 0;
}
.fullscreen {
z-index: 1000!important;
left: 0!important;
top: 0!important;
right: 0!important;
background-color: #000000;
}
#footer.black {
display: none;
}
body.black {
background-color: #000000;
}

View File

@ -9,6 +9,19 @@
<link href="{% static 'styles/projector.css' %}" type="text/css" rel="stylesheet">
<link href="{% static 'img/favicon.png' %}" type="image/png" rel="shortcut icon">
<title>{% trans 'Projector' %} {{ 'event_name'|get_config }}</title>
<script type="text/javascript" src="{% static 'javascript/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'javascript/sockjs-0.3.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 %}
{% for key, value in calls.items %}
projector.{{ key }}({{ value }});
{% endfor %}
</script>
</head>
<body>
<div id="header">
@ -34,16 +47,5 @@
{% endif %}
</div>
</div>
<script type="text/javascript" src="{% static 'javascript/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'javascript/sockjs-0.3.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 %}
{% for key, value in calls.items %}
projector.{{ key }}({{ value }});
{% endfor %}
</script>
</body>
</html>

View File

@ -18,9 +18,11 @@ from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from openslides.config.api import config
from openslides.utils.tornado_webserver import ProjectorSocketHandler
from openslides.utils.template import Tab
from openslides.utils.views import (AjaxMixin, CreateView, DeleteView,
RedirectView, TemplateView, UpdateView)
from openslides.mediafile.models import Mediafile
from .api import (call_on_projector, get_active_slide, get_all_widgets,
get_overlays, get_projector_content, get_projector_overlays,
@ -84,6 +86,16 @@ class ActivateView(RedirectView):
allow_ajax = True
def pre_redirect(self, request, *args, **kwargs):
if kwargs['callback'] == 'mediafile' and \
get_active_slide()['callback'] == 'mediafile':
# If the current slide is a pdf and the new page is also a slide, we dont have to use
# set_active_slide, because is causes a content reload.
kwargs.update({'page_num': 1, 'pk': request.GET.get('pk')})
url = Mediafile.objects.get(pk=kwargs['pk'], is_presentable=True).mediafile.url
config['projector_active_slide'] = kwargs
ProjectorSocketHandler.send_updates(
{'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}})
else:
set_active_slide(kwargs['callback'], kwargs=dict(request.GET.items()))
config['projector_scroll'] = config.get_default('projector_scroll')
config['projector_scale'] = config.get_default('projector_scale')

View File

@ -46,6 +46,10 @@ $(function () {
}
link.addClass('btn-primary');
link.children('i').addClass('icon-white');
// set page_num to 1 if a pdf is activated
if ( link.hasClass('choose-pdf') ) {
$('#page_num').val(1);
}
}
});
});

View File

@ -322,7 +322,9 @@ legend + .control-group {
.icon-append_to_list_of_speakers {
background-position: -48px -144px;
}
.icon-presentations {
background-position: -264px -48px;
}
/** More glyphicons free icons **/
.status_link .icon-on, .icon-checked-new {
background-image: url("../img/glyphicons_152_check.png");