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

Compare commits

...

165 commits
0.1.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
1a5aaa90fc
bump version to 2.2.1 2024-08-20 12:26:13 +02:00
513f3ebe75
Merge pull request #22 from pretalx/main
Compatibility with pretalx v2024.2.x
2024-08-09 19:06:38 +02:00
Tobias Kunze
7c05357067 Fix imports 2024-08-09 11:12:16 +02:00
Tobias Kunze
c9e72db1f7 Fix urls.py 2024-08-09 11:12:11 +02:00
Tobias Kunze
545bdfb966 Code style 2024-08-09 11:12:02 +02:00
Tobias Kunze
6f8c536cb8 Build wheels 2024-08-09 11:04:13 +02:00
Tobias Kunze
d54087b7d0 Use dynamic version 2024-08-09 11:04:09 +02:00
Tobias Kunze
2e2efbff9d Remove duplicate project setup file 2024-08-09 11:03:26 +02:00
3917fa65cf
some more code style 2024-03-16 11:37:26 +01:00
81494bed54
update github actions some more 2024-03-16 11:36:41 +01:00
819e8ea2aa
update github actions to use python 3.12 2024-03-16 11:29:10 +01:00
77148e41f7 release 2.2.0 2023-10-23 08:34:53 +02:00
cb60e02671
github workflows: remove apparently broken packaging check 2023-10-10 16:28:38 +02:00
25d8be0397
apparently, you need a boilerplate setup.py to publish to pypi 2023-10-10 16:16:15 +02:00
af4e3408db
improve placeholder explainations 2023-10-10 16:05:20 +02:00
dabc8e5443
add TRACK_NAME and TRACK_NAME_COLOURED placeholders 2023-10-10 16:00:47 +02:00
341a9c072c
Merge pull request #16 from rixx/category
Add plugin category
2023-10-03 13:08:54 +02:00
Tobias Kunze
98f8374b66 Add pluginc category 2023-10-03 11:22:40 +02:00
2b22f6a155
release 2.1.0 2023-09-30 13:38:31 +02:00
5335e911b7
Merge pull request #15 from rixx/pyproject-toml
Fix pyproject.toml install
2023-09-30 13:21:38 +02:00
Tobias Kunze
210a18f6b3 Fix pyproject.toml install 2023-09-30 13:15:24 +02:00
3063adcc73
move views into dedicated files 2023-09-30 12:41:47 +02:00
8f8f8a90e2
add more information to the json outputs 2023-09-30 12:35:37 +02:00
433f316719 release 2.0.1 2023-08-23 08:13:46 +02:00
ca8a2e3c65 make the linter happy 2023-08-23 08:11:19 +02:00
85893d9c9c migrate to pyproject.toml 2023-08-23 08:08:38 +02:00
c6ceedf041
Merge pull request #13 from rixx/timezone
Safe timezone handling
2023-06-19 10:35:01 +02:00
1ad4c73ed4
Merge pull request #14 from rixx/ugett
Use non-deprecated gettext call
2023-06-19 10:34:10 +02:00
Tobias Kunze
57351a4e1a Use non-deprecated gettext call 2023-06-03 23:07:34 +02:00
Tobias Kunze
190ce8f222 Safe timezone handling 2023-06-03 22:07:13 +02:00
3bd74850a5
Release 2.0.0 2023-03-29 11:15:59 +02:00
0d3d96e96d
fix screenshot descriptions 2023-03-29 11:14:02 +02:00
d858f8d039
add some screenshots to the README 2023-03-29 11:10:31 +02:00
ab42dade2b
lower thirds: change css selectors to be similar to the others 2023-03-29 10:33:07 +02:00
6c058cca1f
add changes to CHANGELOG 2023-03-29 10:23:04 +02:00
4c114d6cee
fix license info in README 2023-03-29 10:16:52 +02:00
de73a288b8
clean up urls and unused views 2023-03-29 09:52:44 +02:00
8b7acb10fb use dedicated function to get pretty-printed time from pretalx 2023-03-01 14:07:54 +01:00
aa91af001c orga: ChoiceField cannot be required if '' is a valid choice 2023-03-01 13:58:00 +01:00
788273e870 room info: add option to show next talk if no talk is running 2023-03-01 13:55:54 +01:00
0486cd44da orga view: fix deleted fiel 2023-03-01 11:49:14 +01:00
46be2a02b5 make the linter happy
Und täglich grüßt das Murmeltier ...
2023-03-01 11:34:34 +01:00
3f7649b09a remove githubs "release on tag" action 2023-03-01 11:33:46 +01:00
12864749f9 room info: add option to hide qr code or show the submission image 2023-03-01 11:29:56 +01:00
fa31e72db1
make the linter happy
once again we have a release which contains formatting errors, wheeee!
2023-02-28 19:21:16 +01:00
498782a962
release 1.1.0 2023-02-28 19:20:43 +01:00
c5cb67d969
update README 2023-02-28 19:19:47 +01:00
737a72f296
add room name to room info page 2023-02-28 19:11:10 +01:00
bb55df2723
add option to switch between feedback and public qr code 2023-02-28 19:10:18 +01:00
e0f47458cd
add endpoint to get qr code linking to talk detail page 2023-02-28 18:49:29 +01:00
08998e7535
re-add colorizing of lower thirds box 2023-02-28 15:55:32 +01:00
6bc110b9fb
version 1.1.0-beta1 2023-02-28 15:41:52 +01:00
04a7a0e3a9
github workflows: only create release for stable releases 2023-02-28 15:41:11 +01:00
c9b01acb6e
show room name and event name if there are no talks scheduled 2023-02-28 15:37:27 +01:00
c1604efb08
disable pinch-zooming for lower thirds and room info endpoints 2023-02-28 13:39:07 +01:00
275935e747
fix url generation 2023-02-28 13:16:30 +01:00
7d5f278536
room info: look up to 15 minutes into the past and into the future 2023-02-28 11:17:14 +01:00
5eabb5fa6a
linters, of ffs 2023-02-28 10:34:12 +01:00
078341e4ed
make the linter happy 2023-02-28 10:32:38 +01:00
11afb88602
add TALK_URL and FEEDBACK_URL to the list of placeholders 2023-02-28 10:19:27 +01:00
97784373c0
room info: add small helper text to hint people into giving feedback 2023-02-28 10:14:26 +01:00
16350548f5
room-info: show "backstage" if no room name was supplied 2023-02-28 09:54:22 +01:00
de8065cf22
add "room info" url to show on room info screens 2023-02-28 09:51:07 +01:00
327981eade
do not sort speaker names in schedule 2023-02-28 08:38:16 +01:00
2da018775b
add url to get a qr code which leads to the feedback page for a specific talk 2023-02-28 08:27:01 +01:00
d86cd011f3
add talk end time to pdf export 2022-12-21 10:41:22 +01:00
dd0f0ad4f0
make the linter happy 2022-11-22 19:21:52 +01:00
8dd9bec525
use generic View instead of TemplateView when not rendering templates 2022-11-22 19:19:33 +01:00
d42a2744d0
lower_thirds: always use event locale when localizing text 2022-11-22 19:19:11 +01:00
f618654ee0
exporter: always use event locale when localizing text 2022-11-22 19:17:56 +01:00
40245ec86d
fix indentation in settings html 2022-11-15 13:57:56 +01:00
d22f7826ac
some typography 2022-11-15 13:45:51 +01:00
a272f21498
add option to include additional custom content on pdf export 2022-11-15 13:43:17 +01:00
280458f6aa
fix my nickname 2022-11-08 20:13:12 +01:00
c9bfd7e4aa
forgot about the linter once again 2022-11-08 20:04:28 +01:00
191ce772c5
lower thirds: look up to five minutes into the future and into the past 2022-11-08 20:00:04 +01:00
2b02350e29
add some error handling for lower thirds placeholders 2022-11-08 19:43:17 +01:00
8cdb391dae
exporter: remove unneeded talk_end variable 2022-11-07 06:47:44 +01:00
b5f18033ba
Version 1.0.3 2022-11-07 06:45:21 +01:00
f41784175f
fix questions sorting 2022-11-07 06:42:43 +01:00
1f0f928795
fix ' vs " in exporter.py 2022-11-07 06:36:22 +01:00
b9d0f42d98
Release 1.0.2 2022-11-07 06:34:00 +01:00
76f615862f
fix compatibility to pretalx 2.3.1 2022-11-07 06:32:37 +01:00
9634838c0f
fix version identifier in setup.py and apps.py 2022-11-07 04:45:25 +01:00
ac758f505e
make the linter happy 2022-11-07 04:27:00 +01:00
a4ef3f0da3
add github workflow for automatic releases 2022-11-07 04:24:06 +01:00
f18d59a368
add CHANGELOG.md 2022-11-07 04:22:27 +01:00
1de7bcc749
rename 'lower_thirds_' fields to 'broadcast_tools_lower_thirds_' 2022-11-07 04:21:53 +01:00
31b6686279
Merge pull request #10 from Kunsi/kunsi-feature-pdf-export
add feature "export pdf page for each talk happening"
2022-11-07 04:13:32 +01:00
c90b7c4fdc
add headings to questions and notes sections in pdf export 2022-11-07 04:10:17 +01:00
eb142b2483
minor style fixes 2022-11-07 04:00:54 +01:00
bfbce9e98b
add option to be able to choose which questions to include in pdf export 2022-11-07 04:00:41 +01:00
5930907aa1 pdf export: add all answered questions to page 2022-10-29 06:01:54 +02:00
d03fef4e34
fix exporter dispatch uid 2022-10-22 22:17:15 +02:00
ddd4bca708
pdf export: make "show internal notes" and "ignore do_not_record" configurable 2022-10-22 22:16:01 +02:00
2e221cbf46
style improvements for pdf export 2022-10-22 19:46:41 +02:00
24acd451ad
code style improvements in pdf exporter 2022-10-22 18:40:05 +02:00
25fe8b804e
add feature "export pdf page for each talk happening" 2022-10-22 18:28:31 +02:00
c4ccb146ef
release 0.3.0 2021-12-25 09:54:35 +01:00
Daniel Havlik
8ebed60b18
fix info line 2021-12-25 09:38:20 +01:00
bc7f5414bd
show in frontend if we're waiting for a schedule update 2021-12-24 21:22:40 +01:00
818b474bc4
Merge pull request #7 from dhavlik/main
load custom css from event settings, slightly change css selectors
2021-12-24 21:20:53 +01:00
Daniel Havlik
1c78eefb81 repair selectors in js 2021-12-24 21:15:14 +01:00
Daniel Havlik
d08e7db9bb load custom css from event settings, slightly change css selectors 2021-12-24 20:50:28 +01:00
4582e4db37
fix name in settings sidebar 2021-11-22 20:53:07 +01:00
a55d9ec97d
release 0.2.0 2021-11-22 20:51:35 +01:00
2e745f5fe3
orga template: fix missing </fieldset> 2021-11-22 12:12:06 +01:00
213db3f640
rename plugin to 'pretalx_broadcast_tools' 2021-11-22 12:09:01 +01:00
7c5e58023c
code style, ffs 2021-11-22 10:26:49 +01:00
4973446da8
move event info to dedicated json file, also load event color from json 2021-11-22 10:14:25 +01:00
f1e61ad655
release 0.1.2 2021-11-21 13:35:20 +01:00
5f47157b86
more style fixes 2021-11-21 13:30:31 +01:00
6d58ba7feb
adjust ci settings to match pretalx settings 2021-11-21 13:30:12 +01:00
a425caa211
Merge pull request #3 from rixx/i18n
Add i18n-support
2021-11-21 13:16:54 +01:00
Tobias Kunze
07474c6976 Add i18n support 2021-11-21 13:07:19 +01:00
7107150736
Merge pull request #5 from rixx/fallback-info
Don't display "None" when no infoline is configured
2021-11-21 10:12:18 +01:00
71330a7ecc
Merge pull request #4 from rixx/primary-color
Show content for events without primary color
2021-11-21 10:12:04 +01:00
Tobias Kunze
2f312b3509 Don't display "None" when no infoline is configured 2021-11-21 10:06:45 +01:00
Tobias Kunze
0066e301f6 Show content for events without primary color
Currently, this turns into white-on-white.
2021-11-21 09:56:04 +01:00
53 changed files with 2842 additions and 513 deletions

View file

@ -18,14 +18,14 @@ jobs:
name: isort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.8
uses: actions/setup-python@v1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.8
- uses: actions/cache@v1
python-version: 3.12
- uses: actions/cache@v4
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@v2
- uses: actions/checkout@v4
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.8
uses: actions/setup-python@v1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.8
- uses: actions/cache@v1
python-version: 3.12
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -61,18 +61,42 @@ jobs:
- name: Run flake8
run: flake8 .
working-directory: .
black:
name: black
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 black -Ue .
- name: Run black
run: black --check .
working-directory: .
docformatter:
name: docformatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.8
uses: actions/setup-python@v1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.8
- uses: actions/cache@v1
python-version: 3.12
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -89,14 +113,14 @@ jobs:
name: djhtml
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: install gettext
run: sudo apt install gettext
- name: Set up Python 3.8
uses: actions/setup-python@v1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.8
- uses: actions/cache@v1
python-version: 3.12
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
@ -109,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@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: .
# 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: .
#

58
CHANGELOG.md Normal file
View file

@ -0,0 +1,58 @@
# 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
* Lower Thirds: containing talk details to embed in a stream or recording

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,7 @@
include Makefile
include *.md
recursive-include img *.png
recursive-include pretalx_lower_thirds *.py
recursive-include pretalx_lower_thirds/locale *
recursive-include pretalx_lower_thirds/static *
recursive-include pretalx_lower_thirds/templates *
recursive-include pretalx_broadcast_tools *.py
recursive-include pretalx_broadcast_tools/locale *
recursive-include pretalx_broadcast_tools/static *
recursive-include pretalx_broadcast_tools/templates *

View file

@ -1,30 +1,58 @@
Lower Thirds
==========================
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 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`_.
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".
.. image:: img/lower_thirds.png
:width: 400
:alt: Screenshot of the lower third output. There's currently a talk running.
: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.
The colours will be automatically determined from the event and track
colours set inside pretalx.
.. 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.
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.
.. 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.
Development setup
-----------------
@ -35,7 +63,7 @@ Development setup
3. Activate the virtual environment you use for pretalx development.
4. Execute ``python setup.py develop`` within this directory to register
4. Execute ``python -m pip install -e .`` within this directory to register
this application with pretalx's plugin registry.
5. Execute ``make`` within this directory to compile translations.
@ -48,7 +76,7 @@ Development setup
License
-------
Copyright 2021 Franziska 'kuns' Kunsmann
Copyright 2021-2023 Franziska 'kunsi' Kunsmann
Released under the terms of the Apache License 2.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

BIN
img/pdf_export.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
img/room_info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -0,0 +1 @@
__version__ = "2.4.0"

View file

@ -0,0 +1,25 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy
from pretalx_broadcast_tools import __version__
class PluginApp(AppConfig):
name = "pretalx_broadcast_tools"
verbose_name = "Broadcasting Tools"
class PretalxPluginMeta:
name = gettext_lazy("Broadcasting Tools")
author = "kunsi"
description = gettext_lazy(
"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"
)
visible = True
version = __version__
category = "FEATURE"
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

@ -0,0 +1,376 @@
from tempfile import NamedTemporaryFile
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
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.platypus import (
Flowable,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from pretalx_broadcast_tools.utils.placeholders import placeholders
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__()
self.event = event
self.schedule = schedule
self.day = fahrplan_day
self.room = room_details
self.talk = talk
self.style = style
self.y_position = PAGE_PADDING
@property
def _questions(self):
return {
int(i.strip())
for i in (
self.event.settings.broadcast_tools_pdf_questions_to_include or ""
).split(",")
if i
}
def _add(self, item, gap=2):
_, height = item.wrapOn(
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
)
self.y_position += height + gap * mm
item.drawOn(self.canv, PAGE_PADDING, -self.y_position)
def _checkbox_text(self, text, **kwargs):
item = Paragraph(text, **kwargs)
_, height = item.wrapOn(
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
)
self.y_position += height + 2 * mm
item.drawOn(self.canv, PAGE_PADDING + 1.3 * height, -self.y_position)
self.canv.rect(PAGE_PADDING, -self.y_position, height * 0.8, height * 0.8)
def _question_text(self, question, answer, **kwargs):
item = Paragraph(question, **kwargs)
_, height = item.wrapOn(
self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
)
self.y_position += height + 2 * mm
item.drawOn(self.canv, PAGE_PADDING, -self.y_position)
item = Paragraph(answer, **kwargs)
_, height = item.wrapOn(
self.canv, A4_WIDTH - 3 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING
)
self.y_position += height + 2 * mm
item.drawOn(self.canv, 2 * PAGE_PADDING, -self.y_position)
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
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)
self.canv.setFont("Helvetica", 12)
self.canv.drawString(
-(A4_HEIGHT - (PAGE_PADDING / 3)),
-(PAGE_PADDING / 3),
" | ".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(),
f"Day {self.day['index']}",
self.room["name"].localize(self.event.locale),
f"Schedule {self.schedule.version}",
],
),
)
self.canv.restoreState()
if self.talk.submission.do_not_record:
self._add(
Paragraph(
self._localize(_("DO NOT RECORD - DO NOT STREAM")),
style=self.style["Warning"],
),
gap=0,
)
self._space()
self._add(
Paragraph(
" | ".join(
[
self._localize(self.event.name),
self._localize(self.room["name"]),
talk_start.strftime("%F"),
f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}',
],
),
style=self.style["Meta"],
)
)
self._add(
Paragraph(self.talk.submission.title, style=self.style["Title"]), gap=0
)
self._space()
for spk in self.talk.submission.speakers.all():
self._checkbox_text(
spk.get_display_name(),
style=self.style["Speaker"],
)
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(
[
(
self._localize(_("Duration")),
self._localize(_("Language")),
self._localize(_("Type")),
self._localize(_("Code")),
),
(
self.talk.export_duration,
self.talk.submission.content_locale,
self.talk.submission.submission_type.name,
self.talk.submission.code,
),
],
colWidths=30 * mm,
style=TableStyle(
[
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
("BOX", (0, 0), (-1, -1), 0.25, colors.black),
]
),
)
)
if self.talk.submission.abstract:
self._add(
Paragraph(
self.talk.submission.abstract,
style=self.style["Abstract"],
)
)
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.event, self.talk)
),
style=self.style["Meta"],
)
)
if self.talk.submission.answers and self._questions:
self._space()
self._add(
Paragraph(
self._localize(_("Questions")),
style=self.style["Heading"],
)
)
for answer in self.talk.submission.answers.order_by("question__position"):
if answer.question.id not in self._questions:
continue
self._question_text(
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(
self._localize(_("Notes")),
style=self.style["Heading"],
)
)
for line in self.talk.submission.notes.splitlines():
line = line.strip()
if not line:
continue
self._add(
Paragraph(
line,
style=self.style["Notes"],
)
)
if (
self.talk.submission.internal_notes
and self.event.settings.broadcast_tools_pdf_show_internal_notes
):
self._space()
self._add(
Paragraph(
self._localize(_("Internal Notes")),
style=self.style["Heading"],
)
)
for line in self.talk.submission.internal_notes.splitlines():
line = line.strip()
if not line:
continue
self._add(
Paragraph(
line,
style=self.style["Notes"],
)
)
class PDFExporter(ScheduleData):
identifier = "broadcast_pdf"
verbose_name = "Broadcast Tools PDF"
public = False
show_qrcode = False
icon = "fa-file-pdf"
def _add_pages(self, doc):
style = self._style()
pages = []
for fahrplan_day in self.data:
for room_details in fahrplan_day["rooms"]:
for talk in room_details["talks"]:
if (
talk.submission.do_not_record
and self.event.settings.broadcast_tools_pdf_ignore_do_not_record
):
continue
pages.append(
PDFInfoPage(
self.event,
self.schedule,
fahrplan_day,
room_details,
talk,
style,
)
)
pages.append(PageBreak())
return pages
def _style(self):
stylesheet = StyleSheet1()
stylesheet.add(
ParagraphStyle(name="Normal", fontName="Helvetica", fontSize=12, leading=14)
)
stylesheet.add(
ParagraphStyle(
name="Title", fontName="Helvetica-Bold", fontSize=20, leading=24
)
)
stylesheet.add(
ParagraphStyle(
name="Speaker", fontName="Helvetica-Oblique", fontSize=12, leading=14
)
)
stylesheet.add(
ParagraphStyle(name="Meta", fontName="Helvetica", fontSize=14, leading=16)
)
stylesheet.add(
ParagraphStyle(
name="Heading", fontName="Helvetica-Bold", fontSize=14, leading=16
)
)
stylesheet.add(
ParagraphStyle(
name="Question", fontName="Helvetica", fontSize=12, leading=14
)
)
stylesheet.add(
ParagraphStyle(
name="Abstract", fontName="Helvetica-Oblique", fontSize=10, leading=12
)
)
stylesheet.add(
ParagraphStyle(name="Notes", fontName="Helvetica", fontSize=12, leading=14)
)
stylesheet.add(
ParagraphStyle(
name="Warning",
fontName="Helvetica-Bold",
fontSize=20,
leading=24,
alignment=TA_CENTER,
textColor=colors.red,
)
)
return stylesheet
def render(self, *args, **kwargs):
with NamedTemporaryFile(suffix=".pdf") as f:
doc = SimpleDocTemplate(
f.name,
pagesize=A4,
rightMargin=0,
leftMargin=0,
topMargin=0,
bottomMargin=0,
)
doc.build(self._add_pages(doc))
f.seek(0)
return (
f"{self.event.slug}_broadcast_tools_{self.schedule.version}.pdf",
"application/pdf",
f.read(),
)

View file

@ -0,0 +1,96 @@
from django.forms import BooleanField, CharField, ChoiceField, Textarea
from django.utils.translation import gettext_lazy as _
from hierarkey.forms import HierarkeyForm
from i18nfield.forms import I18nFormField, I18nFormMixin, I18nTextInput
class BroadcastToolsSettingsForm(I18nFormMixin, HierarkeyForm):
broadcast_tools_lower_thirds_no_talk_info = I18nFormField(
help_text=_(
"Will be shown as talk title if there's currently no talk running."
),
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. 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=(
("", "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 "
"submission will get added to the pdf export."
),
label=_("Show internal notes in pdf export"),
required=False,
)
broadcast_tools_pdf_ignore_do_not_record = BooleanField(
help_text=_(
"If checked, 'do not record' talks will not generate a page "
"in the pdf export."
),
label=_("Ignore 'do not record' talks when generating pdf"),
required=False,
)
broadcast_tools_pdf_questions_to_include = CharField(
help_text=_(
"Comma-Separated list of question ids to include in pdf export. "
"If empty, no questions will get added."
),
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

@ -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

@ -0,0 +1,44 @@
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 i18nfield.strings import LazyI18nString
from pretalx.common.models.settings import hierarkey
from pretalx.common.signals import register_data_exporters
from pretalx.orga.signals import nav_event_settings
hierarkey.add_default(
"broadcast_tools_lower_thirds_no_talk_info",
LazyI18nString.from_gettext(
gettext_noop("Sorry, there's currently no talk running")
),
LazyI18nString,
)
hierarkey.add_default("broadcast_tools_lower_thirds_info_string", "", LazyI18nString)
@receiver(nav_event_settings)
def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info)
if not request.user.has_perm("orga.change_settings", request.event):
return []
return [
{
"label": _("broadcast tools"),
"url": reverse(
"plugins:pretalx_broadcast_tools:orga",
kwargs={
"event": request.event.slug,
},
),
"active": url.namespace == "plugins:pretalx_broadcast_tools"
and url.url_name == "orga",
}
]
@receiver(register_data_exporters, dispatch_uid="exporter_broadcast_pdf")
def register_data_exporter(sender, **kwargs):
from .exporter import PDFExporter
return PDFExporter

View file

@ -0,0 +1,125 @@
* {
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;
}
/* room info *********************************************/
#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: 5vh;
margin-bottom: 0.5em;
font-weight: bold;
}
#broadcast_tools_room_info_title {
font-size: 8vh;
margin-bottom: 0.2em;
font-weight: bold;
}
#broadcast_tools_room_info_speaker {
font-size: 6vh;
font-weight: normal;
}
#broadcast_tools_room_info_qr {
flex: 1;
}
#broadcast_tools_room_info_qr img {
background-color: white;
height: calc(100% - 2vh);
}
#broadcast_tools_room_info_qr p {
margin-top: 1em;
}
/* lower thirds ******************************************/
#broadcast_tools_lower_thirds_box {
width: 1020px;
position: absolute;
bottom: 80px;
left: 50%;
margin-left: -510px;
padding: 15px;
box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);
background-color: #3aa57c;
}
#broadcast_tools_lower_thirds_title {
font-size: 3vh;
font-weight: 500;
margin-bottom: 15px;
}
#broadcast_tools_lower_thirds_speaker {
font-size: 2vh;
}
#broadcast_tools_lower_thirds_infoline {
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

@ -0,0 +1,130 @@
schedule = null;
event_info = null;
req = {};
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() {
try {
hash = decodeURIComponent(window.location.hash.substring(1));
} catch (e) {
console.error(e);
}
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) {
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 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() {
xhr_get('../event.json', function(text) {
console.debug("events: " + text);
event_info = JSON.parse(text);
});
xhr_get('../schedule.json', function(text) {
console.debug("schedule: " + text);
data = JSON.parse(text);
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

@ -0,0 +1,52 @@
function update_lower_third() {
room_name = get_room_name();
if (!event_info) {
console.warn("Waiting for event info ...");
return
}
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) {
title.innerHTML = 'Waiting for schedule ...';
return
}
if ('error' in schedule) {
title.innerHTML = 'Error';
speaker.innerHTML = schedule['error'].join('<br>');
infoline.innerHTML = '';
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
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) {
title.innerHTML = current_talk['title'];
speaker.innerHTML = current_talk['persons'].join(', ');
infoline.innerHTML = current_talk['infoline'];
} else {
title.innerHTML = event_info['no_talk'];
speaker.innerHTML = '';
infoline.innerHTML = '';
}
if (current_talk && current_talk['track']) {
box.style.borderBottom = '10px solid ' + current_talk['track']['color'];
} else {
box.style.borderBottom = 'none';
}
}
window.setInterval(update_lower_third, 1000);

View file

@ -0,0 +1,81 @@
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_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) {
roomname.innerHTML = event_info['name'];
title.innerHTML = 'Backstage';
speaker.innerHTML = '';
qr.innerHTML = '';
box.style.backgroundColor = event_info['color'];
return
}
if (!schedule) {
speaker.innerHTML = 'Waiting for schedule ...';
return
}
if ('error' in schedule) {
title.innerHTML = 'Error';
speaker.innerHTML = schedule['error'].join('<br>');
qr.innerHTML = '';
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
title.innerHTML = 'Error';
speaker.innerHTML = 'Invalid room_name. Valid names: ' + schedule['rooms'].join(', ');
qr.innerHTML = '';
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 = '';
}
roomname.innerHTML = room_name;
title.innerHTML = current_talk['title'];
speaker.innerHTML = current_talk['persons'].join(', ');
qr.innerHTML = qr_info;
} else {
roomname.innerHTML = event_info['name'];
title.innerHTML = room_name;
qr.innerHTML = '';
if (next_talk && event_info['room-info']['show_next_talk']) {
speaker.innerHTML = format_time_from_pretalx(next_talk['start']) + ' ' + next_talk['title'];
} else {
speaker.innerHTML = '';
}
}
if (current_talk && current_talk['track']) {
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 {
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

@ -0,0 +1,23 @@
{% 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 }} lower thirds</title>
<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" %}" />
{% 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>
</div>
</body>
</html>

View file

@ -0,0 +1,92 @@
{% extends "orga/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<h2>{% translate "broadcasting tools" %}</h2>
<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>{% 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>{% translate "The event slug" %} (<code>{{ request.event.slug }}</code>)</dd>
<dt><code>{FEEDBACK_URL}</code></dt>
<dd>{% translate "URL to the talk feedback page." %}</dd>
<dt><code>{TALK_SLUG}</code></dt>
<dd>{% translate "The talk slug" %} (<code>{{ request.event.slug }}-1-my-super-great-talk</code>)</dd>
<dt><code>{TALK_URL}</code></dt>
<dd>{% translate "URL to the talk detail page." %}</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" %}
</legend>
{{ 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" %}
</legend>
{{ 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" %}
</legend>
{{ 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">
<span></span>
<span class="d-flex flex-row-reverse">
<button
type="submit" class="btn btn-success btn-lg"
name="action" value="save"
>
<i class="fa fa-check"></i>
{% translate "Save" %}
</button>
</span>
</div>
</fieldset>
</form>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% 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>
<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

@ -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

@ -0,0 +1,67 @@
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,
BroadcastToolsRoomTimerView,
)
from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView
urlpatterns = [
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",
),
],
),
),
path(
"orga/event/<slug:event>/settings/p/broadcast-tools/",
BroadcastToolsOrgaView.as_view(),
name="orga",
),
]

View file

@ -0,0 +1,32 @@
from django.conf import settings
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(event.slug),
"FEEDBACK_URL": "{}{}".format(
event.custom_domain or settings.SITE_URL,
talk.submission.urls.feedback,
),
"TALK_SLUG": talk.frab_slug,
"TALK_URL": "{}{}".format(
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,34 @@
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
),
},
"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(),
"timezone": str(self.request.event.tz),
"locale": self.request.event.locale,
},
)

View file

@ -0,0 +1,29 @@
from django.views.generic import FormView
from pretalx.common.views.mixins 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 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

@ -0,0 +1,30 @@
from xml.etree import ElementTree
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(ElementTree.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(ElementTree.tostring(image.get_image()).decode())
return HttpResponse(svg_data, content_type="image/svg+xml")

View file

@ -0,0 +1,122 @@
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.views.mixins 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.event, 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

@ -0,0 +1,13 @@
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"
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

View file

@ -1,21 +0,0 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy
class PluginApp(AppConfig):
name = "pretalx_lower_thirds"
verbose_name = "Lower Thirds"
class PretalxPluginMeta:
name = gettext_lazy("Lower Thirds")
author = "kunsi"
description = gettext_lazy(
"Creates lower thirds from your current schedule. Will show "
"speaker names and talk title using the configured track and "
"event colours."
)
visible = True
version = "0.0.0"
def ready(self):
from . import signals # NOQA

View file

@ -1,23 +0,0 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from hierarkey.forms import HierarkeyForm
class LowerThirdsSettingsForm(HierarkeyForm):
lower_thirds_no_talk_info = forms.CharField(
help_text=_(
"Will be shown as talk title if there's currently no talk "
"running."
),
initial="Sorry, there's currently no talk running",
label=_('"no talk running" information'),
required=True,
)
lower_thirds_info_string = forms.CharField(
help_text=_(
"Will only be shown if there's a talk running."
),
initial="",
label=_("info line"),
required=False,
)

View file

@ -1,24 +0,0 @@
from django.dispatch import receiver
from django.urls import resolve, reverse
from django.utils.translation import ugettext_lazy as _
from pretalx.orga.signals import nav_event_settings
@receiver(nav_event_settings)
def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info)
if not request.user.has_perm("orga.change_settings", request.event):
return []
return [
{
"label": _("lower thirds"),
"url": reverse(
"plugins:pretalx_lower_thirds:orga",
kwargs={
"event": request.event.slug,
},
),
"active": url.namespace == "plugins:pretalx_lower_thirds"
and url.url_name == "orga",
}
]

View file

@ -1,35 +0,0 @@
* {
margin: 0;
padding: 0;
line-height: 1.2em;
}
#box {
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);
}
#title {
font-size: 30px;
font-weight: 500;
margin-bottom: 15px;
}
#speaker {
font-size: 20px;
}
#info_line {
font-size: 16px;
text-align: right;
}

View file

@ -1,76 +0,0 @@
schedule = null;
room_name = null;
$(function() {
$('#speaker').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 (!schedule) {
console.warn("There's no schedule yet, exiting ...");
return
}
if (schedule['rooms'].length > 1 && !schedule['rooms'].includes(room_name)) {
$('#title').text('Error')
$('#speaker').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) {
$('#title').text(current_talk['title']);
$('#speaker').text(current_talk['persons'].join(', '));
$('#info_line').text(current_talk['infoline']);
} else {
$('#title').text(schedule['conference']['no_talk']);
$('#speaker').text('');
$('#info_line').text('');
}
if (current_talk && current_talk['track']) {
$('#box').css('border-bottom', '10px solid ' + current_talk['track']['color']);
} else {
$('#box').css('border-bottom', 'none');
}
}
window.setInterval(update_lower_third, 1000);
function update_schedule() {
$.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

@ -1,26 +0,0 @@
{% load static %}
{% load compress %}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
<title>{{ request.event.name }} lower thirds</title>
{% compress js %}
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
{% endcompress %}
<script src="{% static "pretalx_lower_thirds/update.js" %}"></script>
<link rel="stylesheet" href="{% static "pretalx_lower_thirds/frontend.css" %}" />
<style type="text/css">
#box {
background-color: {{ request.event.primary_color }};
}
</style>
</head>
<body>
<div id="box">
<p id="title">Loading ...</p>
<p id="speaker">Content should appear soon. If not, please verify you have Javascript enabled.</p>
<p id="info_line"></p>
</div>
</body>
</html>

View file

@ -1,47 +0,0 @@
{% extends "orga/base.html" %}
{% load bootstrap4 %}
{% load i18n %}
{% block content %}
<h2>{% trans "Set up lower thirds" %}</h2>
<form method="post">
{% csrf_token %}
{% bootstrap_form form 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>
<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_lower_thirds:lowerthirds' request.event.slug %}#{{ room.name }}">{{ room.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<div class="submit-group panel">
<span></span>
<span class="d-flex flex-row-reverse">
<button
type="submit" class="btn btn-success btn-lg"
name="action" value="save"
>
<i class="fa fa-check"></i>
{% trans "Save" %}
</button>
</span>
</div>
</form>
{% endblock %}

View file

@ -1,22 +0,0 @@
from django.urls import re_path
from pretalx.event.models.event import SLUG_CHARS
from . import views
urlpatterns = [
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/lower-thirds/$",
views.LowerThirdsView.as_view(),
name="lowerthirds",
),
re_path(
f"^(?P<event>[{SLUG_CHARS}]+)/p/lower-thirds/schedule.json$",
views.ScheduleView.as_view(),
name="schedule",
),
re_path(
f"^orga/event/(?P<event>[{SLUG_CHARS}]+)/p/lower-thirds/$",
views.LowerThirdsOrgaView.as_view(),
name="orga",
),
]

View file

@ -1,107 +0,0 @@
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 LowerThirdsSettingsForm
class LowerThirdsView(TemplateView):
template_name = "pretalx_lower_thirds/lower_thirds.html"
class LowerThirdsOrgaView(PermissionRequired, FormView):
form_class = LowerThirdsSettingsForm
permission_required = "orga.change_settings"
template_name = "pretalx_lower_thirds/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",
**kwargs,
}
class ScheduleView(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)
return JsonResponse(
{
"conference": {
"slug": schedule.event.slug,
"name": str(schedule.event.name),
"no_talk": str(
schedule.event.settings.lower_thirds_no_talk_info),
},
"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": str(
schedule.event.settings.lower_thirds_info_string
).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"]
],
},
json_dumps_params={
"indent": 4,
},
)

41
pyproject.toml Normal file
View file

@ -0,0 +1,41 @@
[project]
name = "pretalx-broadcast-tools"
dynamic = ["version"]
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]
build-backend = "setuptools.build_meta"
requires = ["setuptools", "wheel"]
[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.dynamic]
version = {attr = "pretalx_broadcast_tools.__version__"}
[tool.setuptools.packages.find]
include = ["pretalx*"]

View file

@ -1,10 +1,22 @@
[isort]
balanced_wrapping = True
combine_as_imports = True
default_section = THIRDPARTY
include_trailing_comma = True
known_third_party = pretalx
line_length = 80
multi_line_output = 5
not_skip = __init__.py
multi_line_output = 3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
skip = migrations,setup.py
default_section = THIRDPARTY
known_third_party = pretalx
[tool:pytest]
DJANGO_SETTINGS_MODULE=pretalx.common.settings.test_settings
[check-manifest]
ignore =
LICENSE
README.rst
tests
tests/*
[flake8]
max-line-length = 120

View file

@ -1,46 +0,0 @@
import os
from distutils.command.build import build
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-lower-thirds",
version="0.1.1",
description=(
"Creates lower thirds from your current schedule. Will show "
"speaker names and talk title using the configured track and "
"event colours."
),
long_description=long_description,
url="https://git.franzi.business/kunsi/pretalx-plugin-lower-thirds",
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_lower_thirds=pretalx_lower_thirds:PretalxPluginMeta
""",
)