1
0
Fork 0
mirror of https://github.com/Kunsi/pretalx-plugin-broadcast-tools synced 2025-04-04 23:34:36 +00:00

Compare commits

...

46 commits
2.2.1 ... main

Author SHA1 Message Date
Franziska Kunsmann
5613bf5acb Partially revert "adjust sizing for room timer"
This reverts commit b07745b2ea.

Turns out if we have bigger font size longer talk titles no longer fit.
2025-03-13 18:16:16 +01:00
Franziska Kunsmann
b07745b2ea adjust sizing for room timer 2025-03-12 13:59:45 +01:00
Franziska Kunsmann
a05cbfc0d4 fix design for "talk has ended" 2025-03-10 05:38:44 +01:00
Franziska Kunsmann
1c951c6f65 bigger font for timer, please 2025-03-10 05:26:43 +01:00
Franziska Kunsmann
3af67e5e53 room timer: use more standard timer layout, properly show length bigger than one hour 2025-03-10 05:23:23 +01:00
a3435e518f
Fix "X min remaining" for events longer than 59min 2025-03-09 21:27:25 +01:00
ade18a033e
UX: show current time if no talk is currently running 2025-03-09 21:26:44 +01:00
283686feb3
Merge pull request #27 from pretalx-translations/weblate-pretalx-plugin-broadcast-tools-pretalx-plugin-broadcast-tools
Translations update from Translate pretalx
2025-02-07 18:30:17 +01:00
Harrissou Sant-anna
baa8ce043c Translated using Weblate (French)
Currently translated at 16.3% (8 of 49 strings)

Translation: pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools
Translate-URL: http://translate.pretalx.com/projects/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/fr/
2025-02-05 12:56:04 +00:00
Franziska Kunsmann
de0c4721ed room_timer: it is confusing for speakers if the timer does not move every second 2024-12-03 10:36:58 +01:00
fec30e79ff
Merge pull request #26 from pretalx-translations/weblate-pretalx-plugin-broadcast-tools-pretalx-plugin-broadcast-tools
Translations update from Translate pretalx
2024-11-14 19:30:13 +01:00
Franziska
7fead51cb6 Translated using Weblate (German)
Currently translated at 96.1% (50 of 52 strings)

Translation: pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools
Translate-URL: http://translate.pretalx.com/projects/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/de/
2024-11-13 05:41:49 +00:00
Franziska
65b8b87c8d Translated using Weblate (German)
Currently translated at 94.1% (48 of 51 strings)

Translation: pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools
Translate-URL: http://translate.pretalx.com/projects/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/de/
2024-11-13 05:38:52 +00:00
6a6fc0be9e
fix case issues for some translations 2024-11-13 06:38:13 +01:00
632c756245
add translation status to README 2024-11-13 06:23:26 +01:00
929e41f0f8
release 2.4.0 2024-11-05 07:32:49 +01:00
0cc256d134
update translations 2024-11-05 07:30:44 +01:00
Franziska Kunsmann
3addc8b19e room_timer: address some design suggestions 2024-11-04 15:53:59 +01:00
f63603d6de
export_voctomix_lower_thirds: no submission for talk means this is a break 2024-11-03 15:05:20 +01:00
07090626e9
room_timer: show we're on break 2024-11-03 14:49:00 +01:00
cb09f7c65a
blank out time left display if talk has not started yet 2024-11-03 14:47:44 +01:00
9d92add067
add basic "room timer" page 2024-11-03 14:45:37 +01:00
c0b3bdb55e
add downloadable lower thirds images to be used in voctomix 2024-11-03 13:46:47 +01:00
f459c6c498
LICENSE: more years pleae 2024-11-03 13:25:10 +01:00
ed960358a4
placeholders: what we actually want is schedule.event here 2024-11-03 12:56:24 +01:00
dfa0945632
move room detection to room uuid 2024-10-31 21:46:55 +01:00
6a3b1b309e
replace re_path() with include() and path() 2024-10-31 21:23:17 +01:00
9ebcde7ab1
adjust font sizes in css 2024-10-31 21:14:35 +01:00
cd77ee8b91
add track name to PDF export 2024-10-31 21:14:16 +01:00
73bd7f6c96
bump version 2024-10-31 20:46:38 +01:00
d1d3283c8e
make the linters happy
I wonder if someday i'll remember this before doing a release.
2024-10-31 20:45:17 +01:00
8dde19105d
fix translation 2024-10-31 20:21:55 +01:00
29db37bca5
fix 404 in pdf exporter 2024-10-31 20:21:26 +01:00
410ca28b79
fix all the javascript i broke 2024-10-31 20:04:23 +01:00
90c50c9652
add german translation 2024-10-31 19:00:39 +01:00
bad650d5b9
exporter: use schedule version for pdf file name, not timestamp 2024-10-31 19:00:09 +01:00
443c7ce85a
add translation to pdf export 2024-10-31 18:13:53 +01:00
86a6075c30
ensure we also get "per speaker" answers in pdf output 2024-10-31 18:05:51 +01:00
620b2fb85e
remove jquery altogether 2024-10-12 19:24:35 +02:00
529f7f1eee
Merge pull request #24 from pretalx/main
Fix overlooked jquery link + remove bootstrap4
2024-10-12 19:23:44 +02:00
3235a91ff5
room_info: colour screen according to next talk if enabled 2024-10-10 16:59:37 +02:00
d295ae18c3
replace jQuery with plain JS 2024-10-10 16:58:12 +02:00
Tobias Kunze
18cf1cd77a Remove django-bootstrap4 2024-10-10 02:04:48 +02:00
Tobias Kunze
d14f6e78f3 Fix overlooked jquery link 2024-10-10 02:04:48 +02:00
0b0df8b600
Merge pull request #23 from pretalx/main
Use jQuery from django-formset-js
2024-10-07 20:06:49 +02:00
Tobias Kunze
74078695ed Use jQuery from django-formset-js 2024-10-07 18:04:12 +02:00
29 changed files with 1507 additions and 182 deletions

View file

@ -1,4 +1,4 @@
Copyright 2021 Franziska 'kunsi' Kunsmann
Copyright 2021-2024 Franziska 'kunsi' Kunsmann
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,6 +1,10 @@
Pretalx: Broadcast Tools (and more)
===================================
.. image:: http://translate.pretalx.com/widget/pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/svg-badge.svg
:alt: Translation status
:target: http://translate.pretalx.com/engage/pretalx-plugin-broadcast-tools/
This is a plugin for `pretalx`_.
This adds the following features to your pretalx instance:

View file

@ -1 +1 @@
__version__ = "2.2.1"
__version__ = "2.4.0"

View file

@ -22,3 +22,4 @@ class PluginApp(AppConfig):
def ready(self):
from . import signals # NOQA
from . import tasks # NOQA

View file

@ -0,0 +1,94 @@
Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students
of MA course of Visual design. Some rights reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,6 +1,7 @@
from tempfile import NamedTemporaryFile
from django.utils.timezone import now
from django.utils.translation import gettext_noop
from i18nfield.strings import LazyI18nString
from pretalx.schedule.exporters import ScheduleData
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
@ -23,6 +24,10 @@ A4_WIDTH, A4_HEIGHT = A4
PAGE_PADDING = 10 * mm
def _(text):
return LazyI18nString.from_gettext(gettext_noop(text))
class PDFInfoPage(Flowable):
def __init__(self, event, schedule, fahrplan_day, room_details, talk, style):
super().__init__()
@ -38,9 +43,9 @@ class PDFInfoPage(Flowable):
def _questions(self):
return {
int(i.strip())
for i in self.event.settings.broadcast_tools_pdf_questions_to_include.split(
","
)
for i in (
self.event.settings.broadcast_tools_pdf_questions_to_include or ""
).split(",")
if i
}
@ -78,6 +83,9 @@ class PDFInfoPage(Flowable):
def _space(self):
self._add(Spacer(1, PAGE_PADDING / 2))
def _localize(self, translation):
return translation.localize(self.event.locale)
def draw(self):
if hasattr(self.talk, "local_start"):
talk_start = self.talk.local_start
@ -112,7 +120,10 @@ class PDFInfoPage(Flowable):
if self.talk.submission.do_not_record:
self._add(
Paragraph("DO NOT RECORD - DO NOT STREAM", style=self.style["Warning"]),
Paragraph(
self._localize(_("DO NOT RECORD - DO NOT STREAM")),
style=self.style["Warning"],
),
gap=0,
)
self._space()
@ -121,8 +132,8 @@ class PDFInfoPage(Flowable):
Paragraph(
" | ".join(
[
self.event.name.localize(self.event.locale),
self.room["name"].localize(self.event.locale),
self._localize(self.event.name),
self._localize(self.room["name"]),
talk_start.strftime("%F"),
f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}',
],
@ -142,14 +153,21 @@ class PDFInfoPage(Flowable):
)
self._space()
if self.talk.submission.track:
self._add(
Paragraph(
f"{self._localize(_('Track:'))} {self._localize(self.talk.submission.track.name)}",
style=self.style["Meta"],
)
)
self._add(
Table(
[
(
"Duration",
"Language",
"Type",
"Code",
self._localize(_("Duration")),
self._localize(_("Language")),
self._localize(_("Type")),
self._localize(_("Code")),
),
(
self.talk.export_duration,
@ -183,7 +201,7 @@ class PDFInfoPage(Flowable):
self._add(
Paragraph(
self.event.settings.broadcast_tools_pdf_additional_content.format(
**placeholders(self.schedule, self.talk)
**placeholders(self.schedule.event, self.talk)
),
style=self.style["Meta"],
)
@ -193,24 +211,35 @@ class PDFInfoPage(Flowable):
self._space()
self._add(
Paragraph(
"Questions",
self._localize(_("Questions")),
style=self.style["Heading"],
)
)
for answer in self.talk.submission.answers.order_by("question"):
for answer in self.talk.submission.answers.order_by("question__position"):
if answer.question.id not in self._questions:
continue
self._question_text(
answer.question.question.localize(self.event.locale),
self._localize(answer.question.question),
answer.answer,
style=self.style["Question"],
)
for spk in self.talk.submission.speakers.all():
for answer in spk.answers.order_by("question__position"):
if answer.question.id not in self._questions:
continue
self._question_text(
f"{self._localize(answer.question.question)} ({spk.get_display_name()})",
answer.answer,
style=self.style["Question"],
)
if self.talk.submission.notes:
self._space()
self._add(
Paragraph(
"Notes",
self._localize(_("Notes")),
style=self.style["Heading"],
)
)
@ -232,7 +261,7 @@ class PDFInfoPage(Flowable):
self._space()
self._add(
Paragraph(
"Internal Notes",
self._localize(_("Internal Notes")),
style=self.style["Heading"],
)
)
@ -339,10 +368,9 @@ class PDFExporter(ScheduleData):
)
doc.build(self._add_pages(doc))
f.seek(0)
timestamp = now().strftime("%Y-%m-%d-%H%M")
return (
f"{self.event.slug}_broadcast_tools_{timestamp}.pdf",
f"{self.event.slug}_broadcast_tools_{self.schedule.version}.pdf",
"application/pdf",
f.read(),
)

View file

@ -16,12 +16,22 @@ class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
broadcast_tools_lower_thirds_info_string = I18nFormField(
help_text=_(
"Will only be shown if there's a talk running. You may use "
"the place holders mentioned below."
"the place holders mentioned below. The info line will be shown "
"on the bottom right side of the lower third. Setting this to an "
"empty string will hide the line entirely."
),
label=_("Info line"),
required=False,
widget=I18nTextInput,
)
broadcast_tools_lower_thirds_export_voctomix = BooleanField(
help_text=_(
"If checked, pretalx will periodically generate voctomix-compatible "
"lower thirds images and make them available as <code>.tar.gz</code>."
),
label=_("Generate voctomix lower thirds"),
required=False,
)
broadcast_tools_room_info_lower_content = ChoiceField(
choices=(

View file

@ -0,0 +1,274 @@
# pretalx-broadcast-tools
# Copyright (C) 2024 Franziska 'kunsi' Kunsmann
# This file is distributed under the same license as the pretalx-broadcast-tools package.
#
msgid ""
msgstr ""
"Project-Id-Version: 2.4.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
"PO-Revision-Date: 2024-11-13 05:41+0000\n"
"Last-Translator: Franziska <pretalx@kunsmann.eu>\n"
"Language-Team: German <http://translate.pretalx.com/projects/"
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/de/>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.6.2\n"
#: apps.py:12
msgid "Broadcasting Tools"
msgstr ""
#: apps.py:15
msgid ""
"Some tools which can be used for supporting a broadcasting software, for "
"example a 'lower third' page which can be embedded into your broadcasting "
"software"
msgstr ""
"Einige Helfer, die zur Unterstützung von Broadcasting-Software benutzt "
"werden kann, zum Beispiel eine 'Bauchbinden'-Seite, die in deine "
"Broadcasting-Software eingebunden werden kann"
#: exporter.py:124
msgid "DO NOT RECORD - DO NOT STREAM"
msgstr "NICHT AUFNEHMEN - NICHT STREAMEN"
#: exporter.py:167
msgid "Duration"
msgstr "Dauer"
#: exporter.py:168
msgid "Language"
msgstr "Sprache"
#: exporter.py:169
msgid "Type"
msgstr "Typ"
#: exporter.py:170
msgid "Code"
msgstr "Code"
#: exporter.py:214
msgid "Questions"
msgstr "Fragen"
#: exporter.py:242
msgid "Notes"
msgstr "Notizen"
#: exporter.py:264
msgid "Internal Notes"
msgstr "Interne Notizen"
#: forms.py:10
msgid "Will be shown as talk title if there's currently no talk running."
msgstr "Wird angezeigt, wenn derzeit kein Vortrag läuft."
#: forms.py:12
msgid "\"No talk running\" information"
msgstr "\"Derzeit kein Vortrag\"-Informationen"
#: forms.py:18
msgid ""
"Will only be shown if there's a talk running. You may use the place holders "
"mentioned below. The info line will be shown on the bottom right side of the "
"lower third. Setting this to an empty string will hide the line entirely."
msgstr ""
"Wird nur angezeigt, wenn derzeit ein Vortrag läuft. Du kannst die oben "
"genannten Platzhalter benutzen. Die Info-Zeile wird unten rechts in den "
"Bauchbinden angezeigt. Wenn das Feld leer ist, wird die Zeile automatisch "
"ausgeblendet."
#: forms.py:23
msgid "Info line"
msgstr "Info-Zeile"
#: forms.py:29
msgid ""
"If checked, pretalx will periodically generate voctomix-compatible lower "
"thirds images and make them available as <code>.tar.gz</code>."
msgstr ""
"Wenn aktiviert, wird pretalx automatisch voctomix-kompatible bauchbinden "
"generieren und diese als <code>.tar.gz</code> zur Verfügung stellen."
#: forms.py:32
msgid "Generate voctomix lower thirds"
msgstr "Erzeuge voctomix-Bauchbinden"
#: forms.py:47
msgid ""
"If a talk is running, the room info page will always show the talk title and "
"the list of speakers. The content below is configurable here."
msgstr ""
"Wenn ein Vortrag läuft, zeigt die Raum-Info-Seite immer den Vortrags-Titel "
"und die Liste der Vortragenden an. Der Inhalt unterhalb dessen ist hier "
"konfigurierbar."
#: forms.py:51
msgid "lower content"
msgstr "Unterer Inhalt"
#: forms.py:56
msgid ""
"If no talk is running in the room, show the time and title of the next talk "
"in the room."
msgstr ""
"Wenn derzeit kein Vortrag läuft, soll die Startzeit und der Titel des "
"nächsten Vortrags angezeigt werden."
#: forms.py:59
msgid "Show next talk"
msgstr "Zeige nächsten Vortrag"
#: forms.py:65
msgid ""
"If checked, the value of the 'internal notes' field in a submission will get "
"added to the pdf export."
msgstr ""
"Wenn aktiviert, wird der Inhalt des Feldes 'Interne Notizen' im PDF-Export "
"angezeigt."
#: forms.py:68
msgid "Show internal notes in pdf export"
msgstr "Zeige interne Notizen im PDF-Export"
#: forms.py:73
msgid ""
"If checked, 'do not record' talks will not generate a page in the pdf export."
msgstr ""
"Wenn aktiviert, werden Vorträge mit 'Zeichnet meinen Vortrag nicht auf'-Flag "
"keine Seite im PDF-Export generieren."
#: forms.py:76
msgid "Ignore 'do not record' talks when generating pdf"
msgstr "Ignoriere 'Zeichnet meinen Vortrag nicht auf'-Vorträge im PDF-Export"
#: forms.py:81
msgid ""
"Comma-Separated list of question ids to include in pdf export. If empty, no "
"questions will get added."
msgstr ""
"Komma-separierte Liste von Fragen, die im PDF-Export eingebunden werden. "
"Falls dieses Feld leer ist, werden keine Fragen im PDF angezeigt."
#: forms.py:84
msgid "Questions to include"
msgstr "Eingebundene Fragen"
#: forms.py:89
msgid ""
"Additional content to print onto the PDF export. Will get printed as-is. You "
"may use the place holders mentioned below."
msgstr ""
"Zusätzlicher Inhalt, der auf dem PDF-Export angezeigt wird. Wird wie hier "
"eingegeben angezeigt. Du kannst die oben genannten Platzhalter benutzen."
#: forms.py:93
msgid "Additional text"
msgstr "Zusätzlicher Text"
#: signals.py:13
msgid "Sorry, there's currently no talk running"
msgstr "Leider läuft aktuell kein Vortrag"
#: signals.py:27
msgid "broadcast tools"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:8
msgid "broadcasting tools"
msgstr "Broadcasting-Tools"
#: templates/pretalx_broadcast_tools/orga.html:13
msgid "room"
msgstr "Raum"
#: templates/pretalx_broadcast_tools/orga.html:14
msgid "Feature"
msgstr "Funktion"
#: templates/pretalx_broadcast_tools/orga.html:21
msgid "Lower Thirds"
msgstr "Bauchbinden"
#: templates/pretalx_broadcast_tools/orga.html:22
#: templates/pretalx_broadcast_tools/orga.html:63
msgid "Room Info"
msgstr "Raum-Information"
#: templates/pretalx_broadcast_tools/orga.html:23
msgid "Room Timer"
msgstr "Raum-Timer"
#: templates/pretalx_broadcast_tools/orga.html:28
msgid "Download voctomix-compatible lower thirds images"
msgstr "Lade voctomix-kompatible Bauchbinden-Bilder herunter"
#: templates/pretalx_broadcast_tools/orga.html:30
msgid "Placeholders"
msgstr "Platzhalter"
#: templates/pretalx_broadcast_tools/orga.html:31
msgid ""
"pretalx will automatically replace some placeholders in your custom content:"
msgstr "pretalx ersetzt automatisch einige Platzhalter in deinen Texten:"
#: templates/pretalx_broadcast_tools/orga.html:34
msgid ""
"talk code (<code>MUX9U3</code> for example) - most useful in combination "
"with pretalx-proposal-redirects or something like that"
msgstr ""
"Vortrags-Slug (zum Beispiel <code>MUX9U3</code>) - am hilfreichsten in "
"Kombination mit pretalx-proposal-redirects oder ähnlichen Plugins"
#: templates/pretalx_broadcast_tools/orga.html:37
msgid "The event slug"
msgstr "Der Event-Slug"
#: templates/pretalx_broadcast_tools/orga.html:40
msgid "URL to the talk feedback page."
msgstr "Adresse der Vortrags-Feedback-Seite"
#: templates/pretalx_broadcast_tools/orga.html:43
msgid "The talk slug"
msgstr "Der Vortrags-Slug"
#: templates/pretalx_broadcast_tools/orga.html:46
msgid "URL to the talk detail page."
msgstr "Adresse der Vortrags-Seite"
#: templates/pretalx_broadcast_tools/orga.html:48
msgid "or"
msgstr "oder"
#: templates/pretalx_broadcast_tools/orga.html:49
msgid "Track name in plain text or coloured using the track colour."
msgstr "Track-Name in einfachem Text oder in der Track-Farbe eingefärbt."
#: templates/pretalx_broadcast_tools/orga.html:52
msgid "Settings"
msgstr "Einstellungen"
#: templates/pretalx_broadcast_tools/orga.html:70
msgid "PDF Export"
msgstr "PDF-Export"
#: templates/pretalx_broadcast_tools/orga.html:86
msgid "Save"
msgstr "Speichern"
#: templates/pretalx_broadcast_tools/orga.html:55
msgid "Lower thirds"
msgstr "Bauchbinden"
#: templates/pretalx_broadcast_tools/orga.html:63
msgid "Room info"
msgstr "Raum-Information"
#: templates/pretalx_broadcast_tools/orga.html:70
msgid "PDF export"
msgstr "PDF-Export"

View file

@ -0,0 +1,240 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-13 06:37+0100\n"
"PO-Revision-Date: 2025-02-05 12:56+0000\n"
"Last-Translator: Harrissou Sant-anna <delazj@gmail.com>\n"
"Language-Team: French <http://translate.pretalx.com/projects/"
"pretalx-plugin-broadcast-tools/pretalx-plugin-broadcast-tools/fr/>\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.6.2\n"
#: apps.py:12
msgid "Broadcasting Tools"
msgstr ""
#: apps.py:15
msgid ""
"Some tools which can be used for supporting a broadcasting software, for "
"example a 'lower third' page which can be embedded into your broadcasting "
"software"
msgstr ""
#: exporter.py:124
msgid "DO NOT RECORD - DO NOT STREAM"
msgstr "NE PAS ENREGISTRER - NE PAS DIFFUSER EN CONTINU"
#: exporter.py:167
msgid "Duration"
msgstr "Durée"
#: exporter.py:168
msgid "Language"
msgstr "Langue"
#: exporter.py:169
msgid "Type"
msgstr "Type"
#: exporter.py:170
msgid "Code"
msgstr "Code"
#: exporter.py:214
msgid "Questions"
msgstr "Questions"
#: exporter.py:242
msgid "Notes"
msgstr "Notes"
#: exporter.py:264
msgid "Internal Notes"
msgstr "Notes internes"
#: forms.py:10
msgid "Will be shown as talk title if there's currently no talk running."
msgstr ""
#: forms.py:12
msgid "\"No talk running\" information"
msgstr ""
#: forms.py:18
msgid ""
"Will only be shown if there's a talk running. You may use the place holders "
"mentioned below. The info line will be shown on the bottom right side of the "
"lower third. Setting this to an empty string will hide the line entirely."
msgstr ""
#: forms.py:23
msgid "Info line"
msgstr ""
#: forms.py:29
msgid ""
"If checked, pretalx will periodically generate voctomix-compatible lower "
"thirds images and make them available as <code>.tar.gz</code>."
msgstr ""
#: forms.py:32
msgid "Generate voctomix lower thirds"
msgstr ""
#: forms.py:47
msgid ""
"If a talk is running, the room info page will always show the talk title and "
"the list of speakers. The content below is configurable here."
msgstr ""
#: forms.py:51
msgid "lower content"
msgstr ""
#: forms.py:56
msgid ""
"If no talk is running in the room, show the time and title of the next talk "
"in the room."
msgstr ""
#: forms.py:59
msgid "Show next talk"
msgstr ""
#: forms.py:65
msgid ""
"If checked, the value of the 'internal notes' field in a submission will get "
"added to the pdf export."
msgstr ""
#: forms.py:68
msgid "Show internal notes in pdf export"
msgstr ""
#: forms.py:73
msgid ""
"If checked, 'do not record' talks will not generate a page in the pdf export."
msgstr ""
#: forms.py:76
msgid "Ignore 'do not record' talks when generating pdf"
msgstr ""
#: forms.py:81
msgid ""
"Comma-Separated list of question ids to include in pdf export. If empty, no "
"questions will get added."
msgstr ""
#: forms.py:84
msgid "Questions to include"
msgstr ""
#: forms.py:89
msgid ""
"Additional content to print onto the PDF export. Will get printed as-is. You "
"may use the place holders mentioned below."
msgstr ""
#: forms.py:93
msgid "Additional text"
msgstr ""
#: signals.py:13
msgid "Sorry, there's currently no talk running"
msgstr ""
#: signals.py:27
msgid "broadcast tools"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:8
msgid "broadcasting tools"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:13
msgid "room"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:14
msgid "Feature"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:21
#: templates/pretalx_broadcast_tools/orga.html:55
msgid "Lower Thirds"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:22
#: templates/pretalx_broadcast_tools/orga.html:63
msgid "Room Info"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:23
msgid "Room Timer"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:28
msgid "Download voctomix-compatible lower thirds images"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:30
msgid "Placeholders"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:31
msgid ""
"pretalx will automatically replace some placeholders in your custom content:"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:34
msgid ""
"talk code (<code>MUX9U3</code> for example) - most useful in combination "
"with pretalx-proposal-redirects or something like that"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:37
msgid "The event slug"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:40
msgid "URL to the talk feedback page."
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:43
msgid "The talk slug"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:46
msgid "URL to the talk detail page."
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:48
msgid "or"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:49
msgid "Track name in plain text or coloured using the track colour."
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:52
msgid "Settings"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:70
msgid "PDF Export"
msgstr ""
#: templates/pretalx_broadcast_tools/orga.html:86
msgid "Save"
msgstr ""

View file

@ -0,0 +1,343 @@
import logging
import tarfile
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django_scopes import scope, scopes_disabled
from PIL import Image, ImageDraw, ImageFont
from pretalx.agenda.management.commands.export_schedule_html import delete_directory
from pretalx.event.models import Event
from pretalx_broadcast_tools.utils.placeholders import placeholders
IMG_WIDTH = 1920 # px
IMG_HEIGHT = 1080 # px
FONT_SIZE_TITLE = 30 # px
FONT_SIZE_SPEAKER = 25 # px
FONT_SIZE_INFOLINE = 18 # px
FONT_FILE = (
Path(__file__).resolve().parent.parent.parent
/ "assets"
/ "titilium-web-regular.ttf"
)
BOX_PADDING = 10 # px
BOX_WIDTH = int(IMG_WIDTH * 0.8)
BOX_BOTTOM = int(IMG_HEIGHT - (IMG_HEIGHT * 0.1))
BOX_LEFT = int((IMG_WIDTH - BOX_WIDTH) / 2)
def get_export_path(event):
return settings.HTMLEXPORT_ROOT / event.slug / "broadcast-tools"
def get_export_targz_path(event):
return get_export_path(event).with_suffix(".voctomix.tar.gz")
def make_targz(generated_files, targz_path):
tmp_name = targz_path.with_suffix(".tmp")
tmp_name.unlink(missing_ok=True)
with tarfile.open(tmp_name, "w:gz") as tar:
for file in generated_files:
tar.add(file, arcname=file.name)
tmp_name.rename(targz_path)
class VoctomixLowerThirdsExporter:
def __init__(self, event, tmp_dir):
self.log = logging.getLogger(event.slug)
self.event = event
self.tmp_dir = tmp_dir
self.exported = set()
if event.primary_color:
self.primary_colour = self._hex2rgb(event.primary_color)
else:
# pretalx.settings.DEFAULT_EVENT_PRIMARY_COLOR
self.primary__color = (58, 165, 124)
self.infoline = event.settings.broadcast_tools_lower_thirds_info_string or ""
self.font_title = ImageFont.truetype(
FONT_FILE,
size=FONT_SIZE_TITLE,
encoding="unic",
)
self.font_speaker = ImageFont.truetype(
FONT_FILE,
size=FONT_SIZE_SPEAKER,
encoding="unic",
)
self.font_infoline = ImageFont.truetype(
FONT_FILE,
size=FONT_SIZE_INFOLINE,
encoding="unic",
)
def _hex2rgb(self, hex_value):
hex_value = hex_value.lstrip("#")
# black insists this should have spaces around the :, but flake8
# complains about spaces around the :, soooooo ....
return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4)) # NOQA
def _fit_text(self, input_text, font, max_width):
words = [i.strip() for i in input_text.split()]
lines = []
line = []
for word in words:
new_line = " ".join([*line, word])
_, _, w, _ = font.getbbox(new_line)
if w > max_width:
# append old line to list of lines, then start new line with
# current word
lines.append(" ".join(line))
line = [word]
elif word.endswith(":"):
lines.append(new_line)
line = []
else:
line.append(word)
if line:
lines.append(" ".join(line))
return lines
def _get_infoline(self, talk):
infoline = self.infoline.localize(self.event.locale).format(
**placeholders(
self.event,
talk,
)
)
_, _, w, _ = self.font_infoline.getbbox(infoline)
return w, infoline
def export_speaker(self, talk, speaker):
img = Image.new(
mode="RGBA",
size=(IMG_WIDTH, IMG_HEIGHT),
color=(0, 0, 0, 0),
)
speaker_text = self._fit_text(
speaker.get_display_name(),
self.font_speaker,
BOX_WIDTH,
)
infoline_width, infoline_text = self._get_infoline(talk)
y_pos = BOX_BOTTOM - BOX_PADDING
if speaker_text:
y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
if infoline_text:
y_pos -= FONT_SIZE_INFOLINE
draw = ImageDraw.Draw(img)
draw.rectangle(
[
(BOX_LEFT - BOX_PADDING, y_pos),
(BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
],
fill=self.primary_colour,
)
if talk.submission.track and talk.submission.track.color:
draw.rectangle(
[
(BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
(
BOX_LEFT + BOX_WIDTH + BOX_PADDING,
BOX_BOTTOM + (BOX_PADDING * 2),
),
],
fill=self._hex2rgb(talk.submission.track.color),
)
for line in speaker_text:
draw.text(
(BOX_LEFT, y_pos),
line,
font=self.font_speaker,
fill=(255, 255, 255),
)
y_pos += FONT_SIZE_SPEAKER
if infoline_text:
draw.text(
(BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
infoline_text,
font=self.font_infoline,
fill=(255, 255, 255),
)
filename = self.tmp_dir / f"event_{talk.submission_id}_person_{speaker.id}.png"
img.save(filename)
self.log.debug(
f"Generated single-speaker image for {speaker.get_display_name()!r} "
"of talk {talk.submission.title!r}, saved as {filename}"
)
return filename
def export_talk(self, talk):
img = Image.new(
mode="RGBA",
size=(IMG_WIDTH, IMG_HEIGHT),
color=(0, 0, 0, 0),
)
title_text = self._fit_text(talk.submission.title, self.font_title, BOX_WIDTH)
speaker_text = self._fit_text(
", ".join(
[person.get_display_name() for person in talk.submission.speakers.all()]
),
self.font_speaker,
BOX_WIDTH,
)
infoline_width, infoline_text = self._get_infoline(talk)
y_pos = BOX_BOTTOM - BOX_PADDING
if title_text:
y_pos -= len(title_text) * FONT_SIZE_TITLE
if speaker_text:
y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER
if title_text and speaker_text:
y_pos -= BOX_PADDING
if infoline_text:
y_pos -= FONT_SIZE_INFOLINE
draw = ImageDraw.Draw(img)
draw.rectangle(
[
(BOX_LEFT - BOX_PADDING, y_pos),
(BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
],
fill=self.primary_colour,
)
if talk.submission.track and talk.submission.track.color:
draw.rectangle(
[
(BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING),
(
BOX_LEFT + BOX_WIDTH + BOX_PADDING,
BOX_BOTTOM + (BOX_PADDING * 2),
),
],
fill=self._hex2rgb(talk.submission.track.color),
)
for line in title_text:
draw.text(
(BOX_LEFT, y_pos),
line,
font=self.font_title,
fill=(255, 255, 255),
)
y_pos += FONT_SIZE_TITLE
if title_text and speaker_text:
y_pos += BOX_PADDING
for line in speaker_text:
draw.text(
(BOX_LEFT, y_pos),
line,
font=self.font_speaker,
fill=(255, 255, 255),
)
y_pos += FONT_SIZE_SPEAKER
if infoline_text:
draw.text(
(BOX_LEFT + BOX_WIDTH - infoline_width, y_pos),
infoline_text,
font=self.font_infoline,
fill=(255, 255, 255),
)
filename = self.tmp_dir / f"event_{talk.submission_id}_persons.png"
img.save(filename)
self.log.debug(
f"Generated image for talk {talk.submission.title!r}, saved as {filename}"
)
return filename
def export(self):
generated_files = set()
if not self.event.current_schedule:
raise CommandError(
f"event {self.event.slug} ({self.event.name}) does not have a schedule to be exported!"
)
self.log.info(
f"Generating voctomix-compatible lower thirds for event {self.event.name}"
)
for talk in self.event.current_schedule.talks.filter(
is_visible=True
).select_related("submission"):
if talk.id in self.exported:
# account for talks that are scheduled multiple times
self.log.warning(
f"Talk {talk.submission.title!r} was already generated, skipping. "
"(Possibly scheduled multiple times?)"
)
continue
if talk.submission is None:
self.log.info(
f"Talk {talk.id} has no associated submission, this is a break. "
"Skipping."
)
continue
self.log.info(f"Generating image(s) for talk {talk.submission.title!r}")
generated_files.add(self.export_talk(talk))
for speaker in talk.submission.speakers.all():
generated_files.add(self.export_speaker(talk, speaker))
self.exported.add(talk.id)
return generated_files
class Command(BaseCommand):
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument("event", type=str)
parser.add_argument("--no-delete-source-files", action="store_true")
def handle(self, *args, **options):
event_slug = options.get("event")
with scopes_disabled():
try:
event = Event.objects.get(slug__iexact=event_slug)
except Event.DoesNotExist:
raise CommandError(f"could not find event with slug {event_slug!r}")
with scope(event=event):
logging.info(f"Exporting {event.name}")
export_dir = get_export_path(event)
targz_path = get_export_targz_path(event)
delete_directory(export_dir)
# for the first export of the conference, the "broadcast-tools"
# directory does not exist
export_dir.mkdir(parents=True)
try:
exporter = VoctomixLowerThirdsExporter(event, export_dir)
generated_files = exporter.export()
make_targz(generated_files, targz_path)
except Exception:
logging.exception(f"Export of {event.name} failed")
else:
logging.info(
f"Export of {event.name} succeeded, export available at {targz_path}"
)
finally:
if not options.get("no_delete_source_files"):
delete_directory(export_dir)

View file

@ -37,7 +37,7 @@ def navbar_info(sender, request, **kwargs):
]
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdfexporter")
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdf")
def register_data_exporter(sender, **kwargs):
from .exporter import PDFExporter

View file

@ -10,6 +10,7 @@ body {
background-color: black;
}
/* room info *********************************************/
#broadcast_tools_room_info {
display: flex;
flex-flow: column;
@ -23,19 +24,19 @@ body {
}
#broadcast_tools_room_info_roomname {
font-size: 2em;
font-size: 5vh;
margin-bottom: 0.5em;
font-weight: bold;
}
#broadcast_tools_room_info_title {
font-size: 6em;
font-size: 8vh;
margin-bottom: 0.2em;
font-weight: bold;
}
#broadcast_tools_room_info_speaker {
font-size: 3em;
font-size: 6vh;
font-weight: normal;
}
@ -45,13 +46,14 @@ body {
#broadcast_tools_room_info_qr img {
background-color: white;
height: calc(100% - 2em);
height: calc(100% - 2vh);
}
#broadcast_tools_room_info_qr p {
margin-top: 1em;
}
/* lower thirds ******************************************/
#broadcast_tools_lower_thirds_box {
width: 1020px;
@ -66,16 +68,58 @@ body {
}
#broadcast_tools_lower_thirds_title {
font-size: 30px;
font-size: 3vh;
font-weight: 500;
margin-bottom: 15px;
}
#broadcast_tools_lower_thirds_speaker {
font-size: 20px;
font-size: 2vh;
}
#broadcast_tools_lower_thirds_infoline {
font-size: 16px;
font-size: 1.8vh;
text-align: right;
}
/* room timer ********************************************/
#broadcast_tools_room_timer_header {
padding: 2em;
text-align: center;
}
#broadcast_tools_room_timer_title {
font-size: 8vh;
margin-bottom: 0.2em;
font-weight: bold;
}
#broadcast_tools_room_timer_speaker {
font-size: 6vh;
font-weight: normal;
}
#broadcast_tools_room_timer_scheduledata {
font-size: 3vh;
font-weight: normal;
}
#broadcast_tools_room_timer_timeleft {
font-size: 6vh;
text-align: center;
}
#broadcast_tools_room_timer_timeleft_timer {
font-size: 35vh;
}
#broadcast_tools_room_timer_progressbar, #broadcast_tools_room_timer_progressbar_bar {
position: fixed;
left: 0;
bottom: 0;
height: 2em;
}
#broadcast_tools_room_timer_progressbar {
right: 0;
}

View file

@ -1,5 +1,6 @@
schedule = null;
event_info = null;
req = {};
function get_current_talk(max_offset) {
room_name = get_room_name();
@ -60,14 +61,16 @@ function get_next_talk() {
}
function get_room_name() {
room_name = null;
try {
hash = decodeURIComponent(window.location.hash.substring(1));
room_name = hash;
} catch (e) {
console.error(e);
}
return room_name;
if (event_info && event_info["rooms"].hasOwnProperty(hash)) {
return event_info["rooms"][hash];
}
// XXX remove fallback when releasing 3.0.0
return hash;
}
function format_time_from_pretalx(from_pretalx) {
@ -87,11 +90,32 @@ function format_time_from_pretalx(from_pretalx) {
return h + ':' + m;
}
function xhr_get(url, callback_func) {
req[url] = new XMLHttpRequest();
req[url].timeout = 10000;
req[url].onreadystatechange = () => {
if (req[url].readyState === 4) {
if (req[url].status != 200) {
return;
}
callback_func(req[url].responseText);
}
};
req[url].open('GET', url);
req[url].setRequestHeader('Accept', 'application/json');
req[url].setRequestHeader("Content-Type", "application/json;charset=UTF-8");
req[url].send();
}
function update_schedule() {
$.getJSON('../event.json', function(data) {
event_info = data;
xhr_get('../event.json', function(text) {
console.debug("events: " + text);
event_info = JSON.parse(text);
});
$.getJSON('../schedule.json', function(data) {
xhr_get('../schedule.json', function(text) {
console.debug("schedule: " + text);
data = JSON.parse(text);
if ('error' in data) {
console.error(data['error']);
} else {

View file

@ -6,42 +6,47 @@ function update_lower_third() {
return
}
$('#broadcast_tools_lower_thirds_box').css('background-color', event_info['color']);
box = document.getElementById('broadcast_tools_lower_thirds_box');
title = document.getElementById('broadcast_tools_lower_thirds_title');
speaker = document.getElementById('broadcast_tools_lower_thirds_speaker');
infoline = document.getElementById('broadcast_tools_lower_thirds_infoline');
box.style.backgroundColor = event_info['color'];
if (!schedule) {
$('#broadcast_tools_lower_thirds_title').text('Waiting for schedule ...')
title.innerHTML = 'Waiting for schedule ...';
return
}
if ('error' in schedule) {
$('#broadcast_tools_lower_thirds_title').text('Error')
$('#broadcast_tools_lower_thirds_speaker').html(schedule['error'].join('<br>'));
$('#broadcast_tools_lower_thirds_infoline').text('');
title.innerHTML = 'Error';
speaker.innerHTML = schedule['error'].join('<br>');
infoline.innerHTML = '';
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
$('#broadcast_tools_lower_thirds_title').text('Error')
$('#broadcast_tools_lower_thirds_speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
$('#broadcast_tools_lower_thirds_infoline').text('');
title.innerHTML = 'Error';
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
infoline.innerHTML = '';
return
}
current_talk = get_current_talk(5);
if (current_talk) {
$('#broadcast_tools_lower_thirds_title').text(current_talk['title']);
$('#broadcast_tools_lower_thirds_speaker').text(current_talk['persons'].join(', '));
$('#broadcast_tools_lower_thirds_infoline').text(current_talk['infoline']);
title.innerHTML = current_talk['title'];
speaker.innerHTML = current_talk['persons'].join(', ');
infoline.innerHTML = current_talk['infoline'];
} else {
$('#broadcast_tools_lower_thirds_title').text(event_info['no_talk']);
$('#broadcast_tools_lower_thirds_speaker').text('');
$('#broadcast_tools_lower_thirds_infoline').text('');
title.innerHTML = event_info['no_talk'];
speaker.innerHTML = '';
infoline.innerHTML = '';
}
if (current_talk && current_talk['track']) {
$('#broadcast_tools_lower_thirds_box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
box.style.borderBottom = '10px solid ' + current_talk['track']['color'];
} else {
$('#broadcast_tools_lower_thirds_box').css('border-bottom', 'none');
box.style.borderBottom = 'none';
}
}
window.setInterval(update_lower_third, 1000);

View file

@ -1,9 +1,3 @@
$(function() {
$('#broadcast_tools_room_info_title').text('Content will appear soon.');
$('#broadcast_tools_room_info_speaker').text('');
$('#broadcast_tools_room_info_qr').text('');
});
function update_room_info() {
room_name = get_room_name();
@ -12,31 +6,37 @@ function update_room_info() {
return
}
box = document.getElementById('broadcast_tools_room_info');
roomname = document.getElementById('broadcast_tools_room_info_roomname');
title = document.getElementById('broadcast_tools_room_info_title');
speaker = document.getElementById('broadcast_tools_room_info_speaker');
qr = document.getElementById('broadcast_tools_room_info_qr');
if (!room_name) {
$('#broadcast_tools_room_info_roomname').text(event_info['name']);
$('#broadcast_tools_room_info_title').text('Backstage');
$('#broadcast_tools_room_info_speaker').text('');
$('#broadcast_tools_room_info_qr').text('');
$('#broadcast_tools_room_info').css('background-color', event_info['color']);
roomname.innerHTML = event_info['name'];
title.innerHTML = 'Backstage';
speaker.innerHTML = '';
qr.innerHTML = '';
box.style.backgroundColor = event_info['color'];
return
}
if (!schedule) {
$('#broadcast_tools_room_info_speaker').text('Waiting for schedule ...')
speaker.innerHTML = 'Waiting for schedule ...';
return
}
if ('error' in schedule) {
$('#broadcast_tools_room_info_title').text('Error')
$('#broadcast_tools_room_info_speaker').html(schedule['error'].join('<br>'));
$('#broadcast_tools_room_info_qr').text('');
title.innerHTML = 'Error';
speaker.innerHTML = schedule['error'].join('<br>');
qr.innerHTML = '';
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
$('#broadcast_tools_room_info_title').text('Error')
$('#broadcast_tools_room_info_speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
$('#broadcast_tools_room_info_qr').text('');
title.innerHTML = 'Error';
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
qr.innerHTML = '';
return
}
@ -54,26 +54,28 @@ function update_room_info() {
qr_info = '';
}
$('#broadcast_tools_room_info_roomname').text(room_name);
$('#broadcast_tools_room_info_title').text(current_talk['title']);
$('#broadcast_tools_room_info_speaker').text(current_talk['persons'].join(', '));
$('#broadcast_tools_room_info_qr').html(qr_info);
roomname.innerHTML = room_name;
title.innerHTML = current_talk['title'];
speaker.innerHTML = current_talk['persons'].join(', ');
qr.innerHTML = qr_info;
} else {
$('#broadcast_tools_room_info_roomname').text(event_info['name']);
$('#broadcast_tools_room_info_title').text(room_name);
$('#broadcast_tools_room_info_qr').text('');
roomname.innerHTML = event_info['name'];
title.innerHTML = room_name;
qr.innerHTML = '';
if (next_talk && event_info['room-info']['show_next_talk']) {
$('#broadcast_tools_room_info_speaker').text(format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title']);
speaker.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
} else {
$('#broadcast_tools_room_info_speaker').text('');
speaker.innerHTML = '';
}
}
if (current_talk && current_talk['track']) {
$('#broadcast_tools_room_info').css('background-color', current_talk['track']['color']);
box.style.backgroundColor = current_talk['track']['color'];
} else if (next_talk && next_talk['track'] && event_info['room-info']['show_next_talk']) {
box.style.backgroundColor = next_talk['track']['color'];
} else {
$('#broadcast_tools_room_info').css('background-color', event_info['color']);
box.style.backgroundColor = event_info['color'];
}
}
window.setInterval(update_room_info, 1000);

View file

@ -0,0 +1,115 @@
function _left_zero_pad(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
function update_room_info() {
room_name = get_room_name();
if (!event_info) {
console.warn("Waiting for event info ...");
return
}
box = document.getElementById('broadcast_tools_room_timer');
header = document.getElementById('broadcast_tools_room_timer_header');
title = document.getElementById('broadcast_tools_room_timer_title');
speaker = document.getElementById('broadcast_tools_room_timer_speaker');
scheduledata = document.getElementById('broadcast_tools_room_timer_scheduledata');
timeleft = document.getElementById('broadcast_tools_room_timer_timeleft_timer');
timehint = document.getElementById('broadcast_tools_room_timer_timeleft_hint');
progressbar= document.getElementById('broadcast_tools_room_timer_progressbar');
progressbar_bar = document.getElementById('broadcast_tools_room_timer_progressbar_bar');
box.style.backgroundColor = event_info['color'];
if (!schedule) {
speaker.innerHTML = 'Waiting for schedule ...';
return
}
if ('error' in schedule) {
title.innerHTML = 'Error';
speaker.innerHTML = schedule['error'].join('<br>');
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
title.innerHTML = 'Error';
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
return
}
current_talk = get_current_talk(60);
next_talk = get_next_talk();
now = new Date();
if (current_talk) {
title.innerHTML = current_talk['title'];
speaker.innerHTML = current_talk['persons'].join(', ');
scheduledata.innerHTML = format_time_from_pretalx(current_talk['start']);
scheduledata.innerHTML += ' - ';
scheduledata.innerHTML += format_time_from_pretalx(current_talk['end']);
scheduled_start = new Date(current_talk['start']);
scheduled_end = new Date(current_talk['end']);
if (scheduled_start > now) {
timeleft.innerHTML = '';
progressbar_bar.style.width = '0';
timehint.innerHTML = '';
} else if (scheduled_end < now) {
timeleft.innerHTML = '0:00';
progressbar_bar.style.width = '100vw';
timehint.innerHTML = 'talk has ended';
} else {
diff = scheduled_end - now;
let diff_s = Math.floor(diff / 1000) % 60;
let diff_m = Math.floor(diff / 1000 / 60) % 60;
let diff_h = Math.floor(diff / 1000 / 60 / 60);
if (diff_h > 0) {
timeleft.innerHTML = diff_h + ":" + _left_zero_pad(diff_m) + ":" + _left_zero_pad(diff_s);
} else {
timeleft.innerHTML = diff_m + ":" + _left_zero_pad(diff_s);
}
total_time = scheduled_end - scheduled_start;
progressbar_bar.style.width = (((diff/total_time)*100)-100)*-1 + 'vw';
timehint.innerHTML = 'left in this talk';
}
if (current_talk['track']) {
header.style.backgroundColor = current_talk['track']['color'];
progressbar.style.borderTop = '2px solid ' + current_talk['track']['color'];
progressbar_bar.style.backgroundColor = current_talk['track']['color'];
} else {
header.style.backgroundColor = null;
progressbar.style.borderTop = '2px solid white';
progressbar_bar.style.backgroundColor = 'white';
}
} else {
progressbar.style.borderTop = 'none';
progressbar_bar.style.width = '0';
speaker.innerHTML = 'Break';
timehint.innerHTML = '';
title.innerHTML = room_name;
timeleft.innerHTML = _left_zero_pad(now.getHours()) + ":" + _left_zero_pad(now.getMinutes()) + ":" + _left_zero_pad(now.getSeconds());
if (next_talk) {
scheduledata.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
if (next_talk['track']) {
header.style.backgroundColor = next_talk['track']['color'];
} else {
header.style.backgroundColor = null;
}
} else {
scheduledata.innerHTML = '';
}
}
}
window.setInterval(update_room_info, 1000);

View file

@ -0,0 +1,78 @@
import logging
from datetime import timedelta
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scope, scopes_disabled
from pretalx.celery_app import app
from pretalx.common.signals import periodic_task
from pretalx.event.models import Event
LOG = logging.getLogger(__name__)
@app.task(name="pretalx_broadcast_tools.export_voctomix_lower_thirds")
def export_voctomix_lower_thirds(*, event_id):
from django.core.management import call_command
with scopes_disabled():
event = Event.objects.filter(pk=event_id).first()
if not event:
LOG.error(f"Could not find event {event_id=} for export")
return
with scope(event=event):
if not event.current_schedule:
LOG.error(f"event {event.slug} does not have schedule, can't export")
return
call_command(
"export_voctomix_lower_thirds",
event.slug,
)
@app.task(name="pretalx_broadcast_tools.periodic_voctomix_export")
def task_periodic_voctomix_export(*, event_slug):
from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
get_export_targz_path,
)
with scopes_disabled():
event = Event.objects.filter(slug=event_slug).first()
with scope(event=event):
if (
not event.settings.broadcast_tools_lower_thirds_export_voctomix
or not event.current_schedule
):
return
targz_path = get_export_targz_path(event)
needs_rebuild = False
last_rebuild = event.cache.get("broadcast_tools_last_voctomix_export")
_now = now()
if not targz_path.exists():
needs_rebuild = True
if not last_rebuild or _now - last_rebuild >= timedelta(hours=1):
needs_rebuild = True
if event.cache.get("broadcast_tools_force_new_voctomix_export"):
needs_rebuild = True
if needs_rebuild:
event.cache.delete("broadcast_tools_force_new_voctomix_export")
event.cache.set("broadcast_tools_last_voctomix_export", _now, None)
export_voctomix_lower_thirds.apply_async(
kwargs={"event_id": event.id}, ignore_result=True
)
@receiver(periodic_task)
def periodic_event_services(sender, **kwargs):
for event in Event.objects.all():
with scope(event=event):
if (event.date_to + timedelta(days=2)) < now().date():
continue
task_periodic_voctomix_export.apply_async(
kwargs={"event_slug": event.slug}, ignore_result=True
)

View file

@ -6,9 +6,6 @@
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{ request.event.name }} lower thirds</title>
{% compress js %}
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
{% endcompress %}
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
<script src="{% static "pretalx_broadcast_tools/lower_thirds.js" %}"></script>
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />

View file

@ -1,82 +1,78 @@
{% extends "orga/base.html" %}
{% load bootstrap4 %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
{% if localized_rooms %}
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th scope="col">{% trans "room list" %}</th>
<th scope="col" colspan="2">Feature</th>
</tr>
</thead>
<tbody>
{% for room in localized_rooms %}
<tr>
<th scope="row">{{ room }}</th>
<td><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room }}">Lower Thirds</a></td>
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_info' request.event.slug %}#{{ room }}">Room Info</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2>{% translate "broadcasting tools" %}</h2>
<p>
pretalx will automatically replace some placeholders in your custom
content:
</p>
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th scope="col">{% translate "room" %}</th>
<th scope="col" colspan="3">{% translate "Feature" %}</th>
</tr>
</thead>
<tbody>
{% for room in request.event.rooms.all %}
<tr>
<th scope="row">{{ room.name }}</th>
<td><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.uuid }}">{% translate "Lower Thirds" %}</a></td>
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_info' request.event.slug %}#{{ room.uuid }}">{% translate "Room Info" %}</a></td>
<td><a href="{% url 'plugins:pretalx_broadcast_tools:room_timer' request.event.slug %}#{{ room.uuid }}">{% translate "Room Timer" %}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<p><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds_voctomix_download' request.event.slug %}">{% translate "Download voctomix-compatible lower thirds images" %}</a></p>
<h2>{% translate "Placeholders" %}</h2>
<p>{% translate "pretalx will automatically replace some placeholders in your custom content:" %}</p>
<dl>
<dt><code>{CODE}</code></dt>
<dd>talk code (<code>MUX9U3</code> for example) - most useful in combination with pretalx-proposal-redirects or something like that</dd>
<dd>{% translate "talk code (<code>MUX9U3</code> for example) - most useful in combination with pretalx-proposal-redirects or something like that" %}</dd>
<dt><code>{EVENT_SLUG}</code></dt>
<dd>The event slug (<code>{{ request.event.slug }}</code>)</dd>
<dd>{% translate "The event slug" %} (<code>{{ request.event.slug }}</code>)</dd>
<dt><code>{FEEDBACK_URL}</code></dt>
<dd>URL to the talk feedback page.</dd>
<dd>{% translate "URL to the talk feedback page." %}</dd>
<dt><code>{TALK_SLUG}</code></dt>
<dd>The talk slug (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
<dd>{% translate "The talk slug" %} (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
<dt><code>{TALK_URL}</code></dt>
<dd>URL to the talk detail page.</dd>
<dd>{% translate "URL to the talk detail page." %}</dd>
<dt><code>{TRACK_NAME}</code> or <code>{TRACK_NAME_COLOURED}</code></dt>
<dd>Track name in plain text or coloured using the track colour.</dd>
<dt><code>{TRACK_NAME}</code> {% translate "or" %} <code>{TRACK_NAME_COLOURED}</code></dt>
<dd>{% translate "Track name in plain text or coloured using the track colour." %}</dd>
</dl>
<h2>{% translate "Settings" %}</h2>
<fieldset>
<legend>
{% translate "Lower thirds" %}
{% translate "Lower Thirds" %}
</legend>
{% bootstrap_field form.broadcast_tools_lower_thirds_no_talk_info layout='event' %}
{% bootstrap_field form.broadcast_tools_lower_thirds_info_string layout='event' %}
<p>
The info line will be shown on the bottom right side of your
lower third. If you set it to an empty string, it will automatically
hide itself.
</p>
{{ form.broadcast_tools_lower_thirds_no_talk_info.as_field_group }}
{{ form.broadcast_tools_lower_thirds_info_string.as_field_group }}
{{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}
</fieldset>
<fieldset>
<legend>
{% translate "Room info" %}
{% translate "Room Info" %}
</legend>
{% bootstrap_field form.broadcast_tools_room_info_lower_content layout='event' %}
{% bootstrap_field form.broadcast_tools_room_info_show_next_talk layout='event' %}
{{ form.broadcast_tools_room_info_lower_content.as_field_group }}
{{ form.broadcast_tools_room_info_show_next_talk.as_field_group }}
</fieldset>
<fieldset>
<legend>
{% translate "PDF export" %}
{% translate "PDF Export" %}
</legend>
{% bootstrap_field form.broadcast_tools_pdf_show_internal_notes layout='event' %}
{% bootstrap_field form.broadcast_tools_pdf_ignore_do_not_record layout='event' %}
{% bootstrap_field form.broadcast_tools_pdf_questions_to_include layout='event' %}
{% bootstrap_field form.broadcast_tools_pdf_additional_content layout='event' %}
{{ form.broadcast_tools_pdf_show_internal_notes.as_field_group }}
{{ form.broadcast_tools_pdf_ignore_do_not_record.as_field_group }}
{{ form.broadcast_tools_pdf_questions_to_include.as_field_group }}
{{ form.broadcast_tools_pdf_additional_content.as_field_group }}
</fieldset>
<fieldset>
<div class="submit-group panel">
@ -87,7 +83,7 @@
name="action" value="save"
>
<i class="fa fa-check"></i>
{% trans "Save" %}
{% translate "Save" %}
</button>
</span>
</div>

View file

@ -6,9 +6,6 @@
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{ request.event.name }} room info</title>
{% compress js %}
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
{% endcompress %}
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
<script src="{% static "pretalx_broadcast_tools/room_info.js" %}"></script>
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />

View file

@ -0,0 +1,30 @@
{% load static %}
{% load compress %}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{ request.event.name }} room timer</title>
<script src="{% static "pretalx_broadcast_tools/generic.js" %}"></script>
<script src="{% static "pretalx_broadcast_tools/room_timer.js" %}"></script>
<link rel="stylesheet" href="{% static "pretalx_broadcast_tools/frontend.css" %}" />
{% if request.event and request.event.custom_css %}
<link rel="stylesheet" type="text/css" href="{{ request.event.custom_css.url }}"/>
{% endif %}
</head>
<body id="broadcast_tools_room_timer">
<div id="broadcast_tools_room_timer_header">
<h2 id="broadcast_tools_room_timer_title">Loading ...</h2>
<h3 id="broadcast_tools_room_timer_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</h3>
<p id="broadcast_tools_room_timer_scheduledata"></p>
</div>
<div id="broadcast_tools_room_timer_timeleft">
<p id="broadcast_tools_room_timer_timeleft_timer"></p>
<p id="broadcast_tools_room_timer_timeleft_hint"></p>
</div>
<div id="broadcast_tools_room_timer_progressbar">
<p id="broadcast_tools_room_timer_progressbar_bar">&nbsp;</p>
</div>
</body>
</html>

View file

@ -1,45 +1,66 @@
from django.urls import re_path
from pretalx.event.models.event import SLUG_REGEX
from django.urls import include, path
from .views.event_info import BroadcastToolsEventInfoView
from .views.orga import BroadcastToolsOrgaView
from .views.qr import BroadcastToolsFeedbackQrCodeSvg, BroadcastToolsPublicQrCodeSvg
from .views.schedule import BroadcastToolsScheduleView
from .views.static_html import BroadcastToolsLowerThirdsView, BroadcastToolsRoomInfoView
from .views.static_html import (
BroadcastToolsLowerThirdsView,
BroadcastToolsRoomInfoView,
BroadcastToolsRoomTimerView,
)
from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView
urlpatterns = [
re_path(
rf"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/event.json$",
BroadcastToolsEventInfoView.as_view(),
name="event_info",
path(
"<slug:event>/p/broadcast-tools/",
include(
[
path(
"event.json",
BroadcastToolsEventInfoView.as_view(),
name="event_info",
),
path(
"schedule.json",
BroadcastToolsScheduleView.as_view(),
name="schedule",
),
path(
"lower-thirds/",
BroadcastToolsLowerThirdsView.as_view(),
name="lowerthirds",
),
path(
"lower-thirds.voctomix.tar.gz",
BroadcastToolsLowerThirdsVoctomixDownloadView.as_view(),
name="lowerthirds_voctomix_download",
),
path(
"feedback-qr/<talk>.svg",
BroadcastToolsFeedbackQrCodeSvg.as_view(),
name="feedback_qr_id",
),
path(
"public-qr/<talk>.svg",
BroadcastToolsPublicQrCodeSvg.as_view(),
name="public_qr_id",
),
path(
"room-info/",
BroadcastToolsRoomInfoView.as_view(),
name="room_info",
),
path(
"room-timer/",
BroadcastToolsRoomTimerView.as_view(),
name="room_timer",
),
],
),
),
re_path(
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/schedule.json$",
BroadcastToolsScheduleView.as_view(),
name="schedule",
),
re_path(
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/lower-thirds/$",
BroadcastToolsLowerThirdsView.as_view(),
name="lowerthirds",
),
re_path(
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/feedback-qr/(?P<talk>[0-9]+).svg$",
BroadcastToolsFeedbackQrCodeSvg.as_view(),
name="feedback_qr_id",
),
re_path(
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/public-qr/(?P<talk>[0-9]+).svg$",
BroadcastToolsPublicQrCodeSvg.as_view(),
name="public_qr_id",
),
re_path(
f"^(?P<event>{SLUG_REGEX})/p/broadcast-tools/room-info/$",
BroadcastToolsRoomInfoView.as_view(),
name="room_info",
),
re_path(
f"^orga/event/(?P<event>{SLUG_REGEX})/settings/p/broadcast-tools/$",
path(
"orga/event/<slug:event>/settings/p/broadcast-tools/",
BroadcastToolsOrgaView.as_view(),
name="orga",
),

View file

@ -1,19 +1,19 @@
from django.conf import settings
def placeholders(schedule, talk, supports_html_colour=False):
def placeholders(event, talk, supports_html_colour=False):
track_name = str(talk.submission.track.name) if talk.submission.track else ""
result = {
"CODE": talk.submission.code,
"EVENT_SLUG": str(schedule.event.slug),
"EVENT_SLUG": str(event.slug),
"FEEDBACK_URL": "{}{}".format(
schedule.event.custom_domain or settings.SITE_URL,
event.custom_domain or settings.SITE_URL,
talk.submission.urls.feedback,
),
"TALK_SLUG": talk.frab_slug,
"TALK_URL": "{}{}".format(
schedule.event.custom_domain or settings.SITE_URL,
event.custom_domain or settings.SITE_URL,
talk.submission.urls.public,
),
"TRACK_NAME": track_name,

View file

@ -21,6 +21,10 @@ class BroadcastToolsEventInfoView(View):
else False
),
},
"rooms": {
str(room.uuid): room.name.localize(self.request.event.locale)
for room in self.request.event.rooms.all()
},
"slug": self.request.event.slug,
"start": self.request.event.date_from.isoformat(),
"end": self.request.event.date_to.isoformat(),

View file

@ -12,14 +12,6 @@ class BroadcastToolsOrgaView(PermissionRequired, FormView):
def get_success_url(self):
return self.request.path
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["localized_rooms"] = [
room.name.localize(self.request.event.locale)
for room in self.request.event.rooms.all()
]
return context
def form_valid(self, form):
form.save()
return super().form_valid(form)

View file

@ -64,7 +64,7 @@ class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, View):
"room": room["name"].localize(schedule.event.locale),
"infoline": infoline.format(
**placeholders(
schedule, talk, supports_html_colour=True
schedule.event, talk, supports_html_colour=True
)
),
"image_url": talk.submission.image_url,

View file

@ -7,3 +7,7 @@ class BroadcastToolsLowerThirdsView(TemplateView):
class BroadcastToolsRoomInfoView(TemplateView):
template_name = "pretalx_broadcast_tools/room_info.html"
class BroadcastToolsRoomTimerView(TemplateView):
template_name = "pretalx_broadcast_tools/room_timer.html"

View file

@ -0,0 +1,22 @@
from django.http import FileResponse, Http404
from django.views import View
from pretalx.common.text.path import safe_filename
from pretalx.common.views.mixins import EventPermissionRequired
from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import (
get_export_targz_path,
)
class BroadcastToolsLowerThirdsVoctomixDownloadView(EventPermissionRequired, View):
permission_required = "agenda.view_schedule"
def get(self, request, *args, **kwargs):
targz_path = get_export_targz_path(self.request.event)
if not targz_path.exists():
raise Http404()
response = FileResponse(open(targz_path, "rb"), as_attachment=True)
response["Content-Disposition"] = (
f"attachment; filename={safe_filename(targz_path.name)}"
)
return response