1
0
Fork 0
mirror of https://github.com/Kunsi/pretalx-plugin-broadcast-tools synced 2024-06-18 11:56:21 +00:00

Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

31 changed files with 392 additions and 938 deletions

19
.github/workflows/release-on-tag.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Create release when creating a tag
on:
create:
tags:
- '*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false

View file

@ -18,14 +18,14 @@ jobs:
name: isort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.12
uses: actions/setup-python@v5
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.12
- uses: actions/cache@v4
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -41,14 +41,14 @@ jobs:
name: flake8
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.12
uses: actions/setup-python@v5
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.12
- uses: actions/cache@v4
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -65,14 +65,14 @@ jobs:
name: black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.12
uses: actions/setup-python@v5
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.12
- uses: actions/cache@v4
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -89,14 +89,14 @@ jobs:
name: docformatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.12
uses: actions/setup-python@v5
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.12
- uses: actions/cache@v4
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -113,14 +113,14 @@ jobs:
name: djhtml
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.12
uses: actions/setup-python@v5
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.12
- uses: actions/cache@v4
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -133,34 +133,34 @@ jobs:
- name: Run docformatter
run: find -name "*.html" | xargs djhtml -c
working-directory: .
# packaging:
# name: packaging
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: install gettext
# run: sudo apt install gettext
# - name: Set up Python 3.12
# uses: actions/setup-python@v5
# with:
# python-version: 3.12
# - uses: actions/cache@v4
# with:
# path: ~/.cache/pip
# key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
# restore-keys: |
# ${{ runner.os }}-pip-
# - name: Install pretalx
# run: pip3 install pretalx
# - name: Install Dependencies
# run: pip3 install twine check-manifest -Ue .
# - name: Run check-manifest
# run: check-manifest .
# working-directory: .
# - name: Build package
# run: python setup.py sdist
# working-directory: .
# - name: Check package
# run: twine check dist/*
# working-directory: .
#
packaging:
name: packaging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install pretalx
run: pip3 install pretalx
- name: Install Dependencies
run: pip3 install twine check-manifest -Ue .
- name: Run check-manifest
run: check-manifest .
working-directory: .
- name: Build package
run: python setup.py sdist
working-directory: .
- name: Check package
run: twine check dist/*
working-directory: .

View file

@ -1,57 +1,3 @@
# 2.2.0
* add plugin category (#16)
* add placeholders `{TRACK_NAME}` and `{TRACK_NAME_COLOURED}`
# 2.1.0
* fixed installation procedure
* add some more information to the json outputs to be able to be compatible
with [scheduled-plugin-pretalx-broadcast-tools](https://github.com/Kunsi/scheduled-plugin-pretalx-broadcast-tools)
(a plugin for [info-beamer hosted](https://info-beamer.com/))
# 2.0.1 (no longer available due to bugs during installation)
* fixes to support pretalx 2023.1.0
* use non-deprecated gettext call
* safe timezone handling
* usage of pyproject.toml
# 2.0.0
* room info page can now show more content on the lower half of the view
* **BREAKING:** The option to select which content should be shown
is now a ChoiceField, the old setting will be ignored.
* **BREAKING:** lower thirds now use css selectors using the same rules
as the other css selectors
* `#l3box` is now `#broadcast_tools_lower_thirds_box`
* `#l3info_line` is now `#broadcast_tools_lower_thirds_infoline`
* `#l3speaker` is now `#broadcast_tools_lower_thirds_speaker`
* `#l3title` is now `#broadcast_tools_lower_thirds_title`
* `.lower3rd` is now `broadcast_tools_lower_thirds`
# 1.1.0
* add a "room info" page to show conference attendees the currently running talk
* fix more compatibility issues with pretalx 2.3.x
# 1.0.4
* fix compatibility with pretalx 2.3.x
* always localize text using the selected default event locale
# 1.0.3
* fix a bug where questions could not be sorted
# 1.0.2
* fix compatibility issue with pretalx 2.3.1
# 1.0.1
* fix version identifier in setup.py
# 1.0.0
* PDF export: contains talk details, notes and answers to questions

View file

@ -1,5 +1,4 @@
include Makefile
include *.md
recursive-include img *.png
recursive-include pretalx_broadcast_tools *.py
recursive-include pretalx_broadcast_tools/locale *

View file

@ -1,54 +1,30 @@
Pretalx: Broadcast Tools (and more)
===================================
Lower Thirds
==========================
This is a plugin for `pretalx`_.
This adds the following features to your pretalx instance:
* Lower Thirds ("Bauchbinden") for using with something like OBS
* a "room info" screen, if you want to show information about the
currently running talk outside the room
* a pdf export containing information about a talk, so video helpers
can have easy access to the needed information
Screenshots
-----------
The first two screenshots show the talk "Compatible static software" by
"Stephanie Fisher", which was generated by the `create_test_event` command.
The event color is `#EA652D`, a bright orange. The track color is `#857EB0`,
a light purple. The last screenshot shows "Multi-layered encompassing
paradigm" by "Michael Rodriguez".
This plugin allows you to add configurable lower thirds ("Bauchbinden"
in German) to your pretalx instance. Most likely this will be used in
(for example) a Browser Source inside `OBS Studio`_.
.. image:: img/lower_thirds.png
:width: 400
:alt: Screenshot of "lower thirds" view. The box is located in the
bottom quarter of the screen, taking about half the screen width.
The box is mostly colored in the event color, with a small strip
showing the track color at the bottom. Inside the box the talk
title is shown in large text on top, the speaker name below that.
On the bottom right of the box the configured info line is shown.
:width: 400
:alt: Screenshot of the lower third output. There's currently a talk running.
.. image:: img/room_info.png
:width: 400
:alt: Screenshot of the "room info" view. The whole screen is coloured
in the track color. On top of the screen you see the room name
in small font, below that the talk title in large letters. Below
that there's the speaker name listed. On the remainder of the
screen you see a large QR code linking to the talk detail page
in the schedule. The URL is also shown in plain-text below the
QR code.
The colours will be automatically determined from the event and track
colours set inside pretalx.
.. image:: img/pdf_export.png
:width: 400
:alt: Screenshot of the first half of a pdf page. On the top you see
a large text "DO NOT RECORD - DO NOT STREAM", because the speaker
selected "Do not record" in the CfP. Below that you find
information like event- and room-name, date and time, talk title,
checkboxes for each speaker, length of the talk, talk abstract.
Also you can find answers to select questions and notes entered
inside pretalx on the pdf export. In this screenshot most info
has been extended by adding lots of "lorem ipsum" text.
You can also add a configurable third line to the lower thirds, for
example to hint your audience to vote for the talks. To make this easier,
the plugin will automatically replace some placeholders inside the text,
so you can have individual text for all talks.
.. image:: img/orga_view.png
:width: 400
:alt: Screenshot of the orga view
Inside the orga view, you can also configure the text snippet that's
shown if there's no talk running currently.
Development setup
-----------------
@ -72,7 +48,7 @@ Development setup
License
-------
Copyright 2021-2023 Franziska 'kunsi' Kunsmann
Copyright 2021 Franziska 'kuns' Kunsmann
Released under the terms of the Apache License 2.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
img/orga_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View file

@ -15,8 +15,7 @@ class PluginApp(AppConfig):
"embedded into your broadcasting software"
)
visible = True
version = "2.2.0"
category = "FEATURE"
version = "0.3.0"
def ready(self):
from . import signals # NOQA

View file

@ -17,8 +17,6 @@ from reportlab.platypus import (
TableStyle,
)
from .utils.placeholders import placeholders
A4_WIDTH, A4_HEIGHT = A4
PAGE_PADDING = 10 * mm
@ -79,14 +77,6 @@ class PDFInfoPage(Flowable):
self._add(Spacer(1, PAGE_PADDING / 2))
def draw(self):
if hasattr(self.talk, "local_start"):
talk_start = self.talk.local_start
else:
talk_start = self.talk.start.astimezone(self.event.tz)
if hasattr(self.talk, "local_end"):
talk_end = self.talk.local_end
else:
talk_end = self.talk.end.astimezone(self.event.tz)
# add some information horizontally to the side of the page
self.canv.saveState()
self.canv.rotate(90)
@ -97,13 +87,11 @@ class PDFInfoPage(Flowable):
" | ".join(
[
self.talk.submission.code,
self.talk.submission.submission_type.name.localize(
self.event.locale
),
self.event.name.localize(self.event.locale),
talk_start.isoformat(),
str(self.talk.submission.submission_type.name),
str(self.event.name),
self.talk.local_start.isoformat(),
f"Day {self.day['index']}",
self.room["name"].localize(self.event.locale),
str(self.room["name"]),
f"Schedule {self.schedule.version}",
],
),
@ -121,10 +109,9 @@ class PDFInfoPage(Flowable):
Paragraph(
" | ".join(
[
self.event.name.localize(self.event.locale),
self.room["name"].localize(self.event.locale),
talk_start.strftime("%F"),
f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}',
str(self.event.name),
str(self.room["name"]),
self.talk.local_start.strftime("%F %T"),
],
),
style=self.style["Meta"],
@ -178,17 +165,6 @@ class PDFInfoPage(Flowable):
)
)
if self.event.settings.broadcast_tools_pdf_additional_content:
self._space()
self._add(
Paragraph(
self.event.settings.broadcast_tools_pdf_additional_content.format(
**placeholders(self.schedule, self.talk)
),
style=self.style["Meta"],
)
)
if self.talk.submission.answers and self._questions:
self._space()
self._add(
@ -197,11 +173,11 @@ class PDFInfoPage(Flowable):
style=self.style["Heading"],
)
)
for answer in self.talk.submission.answers.order_by("question"):
for answer in sorted(self.talk.submission.answers.all()):
if answer.question.id not in self._questions:
continue
self._question_text(
answer.question.question.localize(self.event.locale),
str(answer.question.question),
answer.answer,
style=self.style["Question"],
)

View file

@ -1,4 +1,4 @@
from django.forms import BooleanField, CharField, ChoiceField, Textarea
from django.forms import BooleanField, CharField
from django.utils.translation import gettext_lazy as _
from hierarkey.forms import HierarkeyForm
from i18nfield.forms import I18nFormField, I18nFormMixin, I18nTextInput
@ -9,47 +9,16 @@ class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
help_text=_(
"Will be shown as talk title if there's currently no talk running."
),
label=_('"No talk running" information'),
label=_('"no talk running" information'),
widget=I18nTextInput,
required=True,
)
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."
),
label=_("Info line"),
help_text=_("Will only be shown if there's a talk running."),
label=_("info line"),
required=False,
widget=I18nTextInput,
)
broadcast_tools_room_info_lower_content = ChoiceField(
choices=(
("", "No lower content"),
("public_qr", "QR code linking to the 'talk detail' page"),
(
"feedback_qr",
"QR code linking to the feedback page of the currently running talk",
),
("talk_image", "session image uploaded by the speaker(s)"),
),
help_text=_(
"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."
),
label=_("lower content"),
required=False,
)
broadcast_tools_room_info_show_next_talk = BooleanField(
help_text=_(
"If no talk is running in the room, show the time and title "
"of the next talk in the room."
),
label=_("Show next talk"),
required=False,
)
broadcast_tools_pdf_show_internal_notes = BooleanField(
help_text=_(
"If checked, the value of the 'internal notes' field in a "
@ -74,13 +43,3 @@ class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
label=_("Questions to include"),
required=False,
)
broadcast_tools_pdf_additional_content = CharField(
help_text=_(
"Additional content to print onto the PDF export. "
"Will get printed as-is. You may use the place holders "
"mentioned below."
),
label=_("Additional text"),
required=False,
widget=Textarea,
)

View file

@ -1,7 +1,7 @@
from django.dispatch import receiver
from django.urls import resolve, reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
from django.utils.translation import ugettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretalx.common.models.settings import hierarkey
from pretalx.common.signals import register_data_exporters

View file

@ -2,80 +2,35 @@
margin: 0;
padding: 0;
line-height: 1.2em;
color: white;
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
body {
background-color: black;
}
#broadcast_tools_room_info {
display: flex;
flex-flow: column;
height: 100vh;
overflow: hidden;
}
#broadcast_tools_room_info_header, #broadcast_tools_room_info_qr {
padding: 2em;
text-align: center;
}
#broadcast_tools_room_info_roomname {
font-size: 2em;
margin-bottom: 0.5em;
font-weight: bold;
}
#broadcast_tools_room_info_title {
font-size: 6em;
margin-bottom: 0.2em;
font-weight: bold;
}
#broadcast_tools_room_info_speaker {
font-size: 3em;
font-weight: normal;
}
#broadcast_tools_room_info_qr {
flex: 1;
}
#broadcast_tools_room_info_qr img {
background-color: white;
height: calc(100% - 2em);
}
#broadcast_tools_room_info_qr p {
margin-top: 1em;
}
#broadcast_tools_lower_thirds_box {
#l3box {
width: 1020px;
position: absolute;
bottom: 80px;
left: 50%;
margin-left: -510px;
color: white;
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
padding: 15px;
box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);
background-color: #3aa57c;
}
#broadcast_tools_lower_thirds_title {
#l3title {
font-size: 30px;
font-weight: 500;
margin-bottom: 15px;
}
#broadcast_tools_lower_thirds_speaker {
#l3speaker {
font-size: 20px;
}
#broadcast_tools_lower_thirds_infoline {
#l3info_line {
font-size: 16px;
text-align: right;
}

View file

@ -1,106 +0,0 @@
schedule = null;
event_info = null;
function get_current_talk(max_offset) {
room_name = get_room_name();
if (!room_name) {
return null;
}
for (let offset = 0; offset <= max_offset; offset++) {
time_start = new Date(Date.now() + offset*60000).getTime();
time_end = new Date(Date.now() - offset*60000).getTime();
for (talk_i in schedule['talks']) {
talk = schedule['talks'][talk_i]
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
// not in this room
continue;
}
talk_start = new Date(talk['start']).getTime();
talk_end = new Date(talk['end']).getTime();
if (talk_start < time_start && talk_end > time_end) {
return talk;
}
}
}
return null;
}
function get_next_talk() {
room_name = get_room_name();
if (!room_name) {
return null;
}
time_start = new Date(Date.now()).getTime();
for (talk_i in schedule['talks']) {
talk = schedule['talks'][talk_i]
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
// not in this room
continue;
}
talk_start = new Date(talk['start']).getTime();
if (talk_start > time_start) {
return talk;
}
}
return null;
}
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;
}
function format_time_from_pretalx(from_pretalx) {
d = new Date(from_pretalx);
h = d.getHours();
m = d.getMinutes();
if (h < 10) {
h = '0' + h;
}
if (m < 10) {
m = '0' + m;
}
return h + ':' + m;
}
function update_schedule() {
$.getJSON('../event.json', function(data) {
event_info = data;
});
$.getJSON('../schedule.json', function(data) {
if ('error' in data) {
console.error(data['error']);
} else {
console.info('schedule updated with ' + data['talks'].length + ' talks in ' + data['rooms'].length + ' rooms');
}
schedule = data;
window.setTimeout(update_schedule, 30000);
});
}
update_schedule();

View file

@ -1,47 +0,0 @@
function update_lower_third() {
room_name = get_room_name();
if (!event_info) {
console.warn("Waiting for event info ...");
return
}
$('#broadcast_tools_lower_thirds_box').css('background-color', event_info['color']);
if (!schedule) {
$('#broadcast_tools_lower_thirds_title').text('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('');
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('');
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']);
} else {
$('#broadcast_tools_lower_thirds_title').text(event_info['no_talk']);
$('#broadcast_tools_lower_thirds_speaker').text('');
$('#broadcast_tools_lower_thirds_infoline').text('');
}
if (current_talk && current_talk['track']) {
$('#broadcast_tools_lower_thirds_box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
} else {
$('#broadcast_tools_lower_thirds_box').css('border-bottom', 'none');
}
}
window.setInterval(update_lower_third, 1000);

View file

@ -1,79 +0,0 @@
$(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();
if (!event_info) {
console.warn("Waiting for event info ...");
return
}
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']);
return
}
if (!schedule) {
$('#broadcast_tools_room_info_speaker').text('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('');
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('');
return
}
current_talk = get_current_talk(15);
next_talk = get_next_talk();
if (current_talk) {
if (event_info['room-info']['lower_info'] == 'feedback_qr') {
qr_info = '<img src="' + current_talk['urls']['feedback_qr'] + '" alt="Feedback QR Code"><p>Leave Feedback by scanning the code or visiting ' + current_talk['urls']['feedback'] + '</p>';
} else if (event_info['room-info']['lower_info'] == 'public_qr') {
qr_info = '<img src="' + current_talk['urls']['public_qr'] + '" alt="QR Code linking to URL below"><p>' + current_talk['urls']['public'] + '</p>';
} else if (event_info['room-info']['lower_info'] == 'talk_image' && current_talk['image_url']) {
qr_info = '<img src="' + current_talk['image_url'] + '" alt="Talk image">';
} else {
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);
} else {
$('#broadcast_tools_room_info_roomname').text(event_info['name']);
$('#broadcast_tools_room_info_title').text(room_name);
$('#broadcast_tools_room_info_qr').text('');
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']);
} else {
$('#broadcast_tools_room_info_speaker').text('');
}
}
if (current_talk && current_talk['track']) {
$('#broadcast_tools_room_info').css('background-color', current_talk['track']['color']);
} else {
$('#broadcast_tools_room_info').css('background-color', event_info['color']);
}
}
window.setInterval(update_room_info, 1000);

View file

@ -0,0 +1,87 @@
schedule = null;
room_name = null;
event_info = null;
$(function() {
$('#l3speaker').text('Content will appear soon.');
});
function update_lower_third() {
current_time = new Date(Date.now()).getTime()
try {
hash = decodeURIComponent(window.location.hash.substring(1));
room_name = hash;
} catch (e) {
console.error(e);
return
}
if (!event_info) {
console.warn("There's no event info yet, exiting");
return
}
if (!schedule) {
$('#l3title').text('Waiting for schedule ...')
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
$('#l3title').text('Error')
$('#l3speaker').text('Invalid room_name. Valid names: ' + schedule['rooms'].join(', '));
return
}
current_talk = null;
for (talk_i in schedule['talks']) {
talk = schedule['talks'][talk_i]
if (schedule['rooms'].length > 1 && talk['room'] != room_name) {
// not in this room
continue;
}
talk_start = new Date(talk['start']).getTime();
talk_end = new Date(talk['end']).getTime();
if (talk_start < current_time && talk_end > current_time) {
current_talk = talk;
}
}
if (current_talk) {
$('#l3title').text(current_talk['title']);
$('#l3speaker').text(current_talk['persons'].join(', '));
$('#l3info_line').text(current_talk['infoline']);
} else {
$('#l3title').text(event_info['no_talk']);
$('#l3speaker').text('');
$('#l3info_line').text('');
}
if (current_talk && current_talk['track']) {
$('#l3box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
} else {
$('#l3box').css('border-bottom', 'none');
}
}
window.setInterval(update_lower_third, 1000);
function update_schedule() {
$.getJSON('../event.json', function(data) {
event_info = data;
$('#l3box').css('background-color', data['color']);
});
$.getJSON('../schedule.json', function(data) {
console.info('schedule updated with ' + data['talks'].length + ' talks in ' + data['rooms'].length + ' rooms');
schedule = data;
window.setTimeout(update_schedule, 30000);
});
}
update_schedule();

View file

@ -4,23 +4,21 @@
<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 }} 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>
<script src="{% static "pretalx_broadcast_tools/update.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_lower_thirds">
<div id="broadcast_tools_lower_thirds_box">
<p id="broadcast_tools_lower_thirds_title">Loading ...</p>
<p id="broadcast_tools_lower_thirds_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</p>
<p id="broadcast_tools_lower_thirds_infoline"></p>
<body class="lower3rd">
<div id="l3box">
<p id="l3title">Loading ...</p>
<p id="l3speaker">Content should appear soon. If not, please verify you have Javascript enabled.</p>
<p id="l3info_line"></p>
</div>
</body>
</html>

View file

@ -6,53 +6,9 @@
<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 %}
<p>
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>
<dt><code>{EVENT_SLUG}</code></dt>
<dd>The event slug (<code>{{ request.event.slug }}</code>)</dd>
<dt><code>{FEEDBACK_URL}</code></dt>
<dd>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>
<dt><code>{TALK_URL}</code></dt>
<dd>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>
</dl>
<fieldset>
<legend>
{% translate "Lower thirds" %}
{% translate "Set up 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' %}
@ -61,13 +17,24 @@
lower third. If you set it to an empty string, it will automatically
hide itself.
</p>
</fieldset>
<fieldset>
<legend>
{% 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' %}
<p>
pretalx will automatically replace some placeholders in your info
string.
Use <code>{CODE}</code> to embed the talk code (<code>MUX9U3</code>
for example). You could use this to directly link to the talk
feedback page.
Use <code>{EVENT_SLUG}</code> to get the event slug.
Use <code>{TALK_SLUG}</code> to get the talk slug.
</p>
{% if request.event.rooms %}
<h3>{% trans "room list" %}</h3>
<ul>
{% for room in request.event.rooms.all %}
<li><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.name }}">{{ room.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
</fieldset>
<fieldset>
<legend>
@ -76,7 +43,6 @@
{% 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' %}
</fieldset>
<fieldset>
<div class="submit-group panel">

View file

@ -1,27 +0,0 @@
{% 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 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" %}" />
{% 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_info">
<div id="broadcast_tools_room_info_header">
<h1 id="broadcast_tools_room_info_roomname"></h1>
<h2 id="broadcast_tools_room_info_title">Loading ...</h2>
<h3 id="broadcast_tools_room_info_speaker">Content should appear soon. If not, please verify you have Javascript enabled.</h3>
</div>
<div id="broadcast_tools_room_info_qr"></div>
</body>
</html>

View file

@ -1,46 +1,27 @@
from django.urls import re_path
from pretalx.event.models.event import SLUG_CHARS
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 . import views
urlpatterns = [
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/lower-thirds/$",
views.BroadcastToolsLowerThirdsView.as_view(),
name="lowerthirds",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/event.json$",
BroadcastToolsEventInfoView.as_view(),
views.BroadcastToolsEventInfoView.as_view(),
name="event_info",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/schedule.json$",
BroadcastToolsScheduleView.as_view(),
views.BroadcastToolsScheduleView.as_view(),
name="schedule",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/lower-thirds/$",
BroadcastToolsLowerThirdsView.as_view(),
name="lowerthirds",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/feedback-qr/(?P<talk>[0-9]+).svg$",
BroadcastToolsFeedbackQrCodeSvg.as_view(),
name="feedback_qr_id",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/public-qr/(?P<talk>[0-9]+).svg$",
BroadcastToolsPublicQrCodeSvg.as_view(),
name="public_qr_id",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/room-info/$",
BroadcastToolsRoomInfoView.as_view(),
name="room_info",
),
re_path(
f"^orga/event/(?P<event>[{SLUG_CHARS}]+)/settings/p/broadcast-tools/$",
BroadcastToolsOrgaView.as_view(),
views.BroadcastToolsOrgaView.as_view(),
name="orga",
),
]

View file

@ -1,32 +0,0 @@
from django.conf import settings
def placeholders(schedule, 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),
"FEEDBACK_URL": "{}{}".format(
schedule.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,
talk.submission.urls.public,
),
"TRACK_NAME": track_name,
}
if talk.submission.track and supports_html_colour:
result["TRACK_NAME_COLOURED"] = '<span style="color: {}">{}</span>'.format(
talk.submission.track.color, track_name
)
else:
result["TRACK_NAME_COLOURED"] = track_name
# for the americans
result["TRACK_NAME_COLORED"] = result["TRACK_NAME_COLOURED"]
return result

View file

@ -0,0 +1,108 @@
import datetime as dt
import pytz
from django.http import JsonResponse
from django.views.generic import FormView
from django.views.generic.base import TemplateView
from pretalx.agenda.views.schedule import ScheduleMixin
from pretalx.common.mixins.views import EventPermissionRequired, PermissionRequired
from pretalx.schedule.exporters import ScheduleData
from .forms import BroadcastToolsSettingsForm
class BroadcastToolsLowerThirdsView(TemplateView):
template_name = "pretalx_broadcast_tools/lower_thirds.html"
class BroadcastToolsOrgaView(PermissionRequired, FormView):
form_class = BroadcastToolsSettingsForm
permission_required = "orga.change_settings"
template_name = "pretalx_broadcast_tools/orga.html"
def get_success_url(self):
return self.request.path
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_object(self):
return self.request.event
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
return {
"obj": self.request.event,
"attribute_name": "settings",
"locales": self.request.event.locales,
**kwargs,
}
class BroadcastToolsEventInfoView(TemplateView):
def get(self, request, *args, **kwargs):
color = self.request.event.primary_color or "#3aa57c"
return JsonResponse(
{
"slug": self.request.event.slug,
"name": str(self.request.event.name),
"no_talk": str(self.request.event.settings.broadcast_tools_lower_thirds_no_talk_info),
"color": color,
},
)
class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, TemplateView):
permission_required = "agenda.view_schedule"
def get(self, request, *args, **kwargs):
schedule = ScheduleData(
event=self.request.event,
schedule=self.schedule,
)
tz = pytz.timezone(schedule.event.timezone)
infoline = str(schedule.event.settings.broadcast_tools_lower_thirds_info_string or "")
return JsonResponse(
{
"rooms": sorted(
{
str(room["name"])
for day in schedule.data
for room in day["rooms"]
}
),
"talks": [
{
"id": talk.submission.id,
"start": talk.start.astimezone(tz).isoformat(),
"end": (talk.start + dt.timedelta(minutes=talk.duration))
.astimezone(tz)
.isoformat(),
"slug": talk.frab_slug,
"title": talk.submission.title,
"persons": sorted(
{
person.get_display_name()
for person in talk.submission.speakers.all()
}
),
"track": {
"color": talk.submission.track.color,
"name": str(talk.submission.track.name),
}
if talk.submission.track
else None,
"room": str(room["name"]),
"infoline": infoline.format(
EVENT_SLUG=str(schedule.event.slug),
TALK_SLUG=talk.frab_slug,
CODE=talk.submission.code,
),
}
for day in schedule.data
for room in day["rooms"]
for talk in room["talks"]
],
},
)

View file

@ -1,30 +0,0 @@
from django.http import JsonResponse
from django.views import View
class BroadcastToolsEventInfoView(View):
def get(self, request, *args, **kwargs):
color = self.request.event.primary_color or "#3aa57c"
return JsonResponse(
{
"color": color,
"name": self.request.event.name.localize(self.request.event.locale),
"no_talk": str(
self.request.event.settings.broadcast_tools_lower_thirds_no_talk_info
),
"room-info": {
"lower_info": self.request.event.settings.broadcast_tools_room_info_lower_content
or "",
"show_next_talk": (
True
if self.request.event.settings.broadcast_tools_room_info_show_next_talk
else False
),
},
"slug": self.request.event.slug,
"start": self.request.event.date_from.isoformat(),
"end": self.request.event.date_to.isoformat(),
"timezone": str(self.request.event.tz),
"locale": self.request.event.locale,
},
)

View file

@ -1,37 +0,0 @@
from django.views.generic import FormView
from pretalx.common.mixins.views import PermissionRequired
from ..forms import BroadcastToolsSettingsForm
class BroadcastToolsOrgaView(PermissionRequired, FormView):
form_class = BroadcastToolsSettingsForm
permission_required = "orga.change_settings"
template_name = "pretalx_broadcast_tools/orga.html"
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)
def get_object(self):
return self.request.event
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
return {
"obj": self.request.event,
"attribute_name": "settings",
"locales": self.request.event.locales,
**kwargs,
}

View file

@ -1,30 +0,0 @@
from xml.etree import ElementTree as ET
import qrcode
import qrcode.image.svg
from django.conf import settings
from django.http import HttpResponse
from django.utils.safestring import mark_safe
from django.views import View
class BroadcastToolsFeedbackQrCodeSvg(View):
def get(self, request, *args, **kwargs):
talk = self.request.event.submissions.filter(id=kwargs["talk"]).first()
domain = request.event.custom_domain or settings.SITE_URL
image = qrcode.make(
f"{domain}{talk.urls.feedback}", image_factory=qrcode.image.svg.SvgImage
)
svg_data = mark_safe(ET.tostring(image.get_image()).decode())
return HttpResponse(svg_data, content_type="image/svg+xml")
class BroadcastToolsPublicQrCodeSvg(View):
def get(self, request, *args, **kwargs):
talk = self.request.event.submissions.filter(id=kwargs["talk"]).first()
domain = request.event.custom_domain or settings.SITE_URL
image = qrcode.make(
f"{domain}{talk.urls.public}", image_factory=qrcode.image.svg.SvgImage
)
svg_data = mark_safe(ET.tostring(image.get_image()).decode())
return HttpResponse(svg_data, content_type="image/svg+xml")

View file

@ -1,122 +0,0 @@
import datetime as dt
from django.conf import settings
from django.http import JsonResponse
from django.urls import reverse
from django.views import View
from pretalx.agenda.views.schedule import ScheduleMixin
from pretalx.common.mixins.views import EventPermissionRequired
from pretalx.schedule.exporters import ScheduleData
from ..utils.placeholders import placeholders
class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, View):
permission_required = "agenda.view_schedule"
def get(self, request, *args, **kwargs):
schedule = ScheduleData(
event=self.request.event,
schedule=self.schedule,
)
infoline = str(
schedule.event.settings.broadcast_tools_lower_thirds_info_string or ""
)
try:
return JsonResponse(
{
"rooms": sorted(
{
room["name"].localize(schedule.event.locale)
for day in schedule.data
for room in day["rooms"]
}
),
"talks": [
{
"id": talk.submission.id,
"start": talk.start.astimezone(
schedule.event.tz
).isoformat(),
"start_ts": int(talk.start.timestamp()),
"end": (talk.start + dt.timedelta(minutes=talk.duration))
.astimezone(schedule.event.tz)
.isoformat(),
"end_ts": int(
(
talk.start + dt.timedelta(minutes=talk.duration)
).timestamp()
),
"slug": talk.frab_slug,
"title": talk.submission.title,
"persons": [
person.get_display_name()
for person in talk.submission.speakers.all()
],
"track": (
{
"color": talk.submission.track.color,
"name": str(talk.submission.track.name),
}
if talk.submission.track
else None
),
"room": room["name"].localize(schedule.event.locale),
"infoline": infoline.format(
**placeholders(
schedule, talk, supports_html_colour=True
)
),
"image_url": talk.submission.image_url,
"locale": talk.submission.content_locale,
"do_not_record": talk.submission.do_not_record,
"abstract": talk.submission.abstract,
"urls": {
"feedback": "{}{}".format(
schedule.event.custom_domain or settings.SITE_URL,
talk.submission.urls.feedback,
),
"feedback_qr": reverse(
"plugins:pretalx_broadcast_tools:feedback_qr_id",
kwargs={
"event": schedule.event.slug,
"talk": talk.submission.id,
},
),
"public": "{}{}".format(
schedule.event.custom_domain or settings.SITE_URL,
talk.submission.urls.public,
),
"public_qr": reverse(
"plugins:pretalx_broadcast_tools:public_qr_id",
kwargs={
"event": schedule.event.slug,
"talk": talk.submission.id,
},
),
},
}
for day in schedule.data
for room in day["rooms"]
for talk in room["talks"]
],
},
)
except KeyError as e:
key = str(e)[1:-1]
return JsonResponse(
{
"error": [
f"Could not find value for placeholder {{{key}}} in info line.",
f"If you want to use {{{key}}} without evaluating it, please use as follows: {{{{{key}}}}}",
],
}
)
except Exception as e:
return JsonResponse(
{
"error": [
repr(e),
],
}
)

View file

@ -1,9 +0,0 @@
from django.views.generic.base import TemplateView
class BroadcastToolsLowerThirdsView(TemplateView):
template_name = "pretalx_broadcast_tools/lower_thirds.html"
class BroadcastToolsRoomInfoView(TemplateView):
template_name = "pretalx_broadcast_tools/room_info.html"

View file

@ -1,39 +0,0 @@
[project]
name = "pretalx-broadcast-tools"
version = "2.2.0"
description = """
Some tools which can be used for supporting a broadcasting software.
This currently includes a generator for PDF printouts, a 'lower thirds'
endpoint, and a full-screen webpage showing information about the
currently running talk.
"""
readme = "README.rst"
license = {text = "Apache Software License"}
keywords = ["pretalx"]
authors = [
{name = "Franziska Kunsmann", email = "git@kunsmann.eu"},
]
maintainers = [
{name = "Franziska Kunsmann", email = "git@kunsmann.eu"},
]
dependencies = [
]
[project.entry-points."pretalx.plugin"]
pretalx_broadcast_tools = "pretalx_broadcast_tools:PretalxPluginMeta"
[build-system]
requires = [
"setuptools",
]
[project.urls]
homepage = "https://github.com/Kunsi/pretalx-plugin-broadcast-tools"
repository = "https://github.com/Kunsi/pretalx-plugin-broadcast-tools.git"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
include = ["pretalx*"]

View file

@ -1,3 +1,46 @@
from setuptools import setup
import os
from distutils.command.build import build
setup()
from django.core import management
from setuptools import find_packages, setup
try:
with open(
os.path.join(os.path.dirname(__file__), "README.rst"), encoding="utf-8"
) as f:
long_description = f.read()
except FileNotFoundError:
long_description = ""
class CustomBuild(build):
def run(self):
management.call_command("compilemessages", verbosity=1)
build.run(self)
cmdclass = {"build": CustomBuild}
setup(
name="pretalx-plugin-broadcast-tools",
version="0.3,0",
description=(
"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"
),
long_description=long_description,
url="https://github.com/Kunsi/pretalx-plugin-broadcast-tools",
author="kunsi",
author_email="git@kunsmann.eu",
license="Apache Software License",
install_requires=[],
packages=find_packages(exclude=["tests", "tests.*"]),
include_package_data=True,
cmdclass=cmdclass,
entry_points="""
[pretalx.plugin]
pretalx_broadcast_tools=pretalx_broadcast_tools:PretalxPluginMeta
""",
)