mirror of
https://github.com/Kunsi/pretalx-plugin-broadcast-tools
synced 2025-04-05 09:44:35 +00:00
Compare commits
151 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5613bf5acb | ||
![]() |
b07745b2ea | ||
![]() |
a05cbfc0d4 | ||
![]() |
1c951c6f65 | ||
![]() |
3af67e5e53 | ||
a3435e518f | |||
ade18a033e | |||
283686feb3 | |||
![]() |
baa8ce043c | ||
![]() |
de0c4721ed | ||
fec30e79ff | |||
![]() |
7fead51cb6 | ||
![]() |
65b8b87c8d | ||
6a6fc0be9e | |||
632c756245 | |||
929e41f0f8 | |||
0cc256d134 | |||
![]() |
3addc8b19e | ||
f63603d6de | |||
07090626e9 | |||
cb09f7c65a | |||
9d92add067 | |||
c0b3bdb55e | |||
f459c6c498 | |||
ed960358a4 | |||
dfa0945632 | |||
6a3b1b309e | |||
9ebcde7ab1 | |||
cd77ee8b91 | |||
73bd7f6c96 | |||
d1d3283c8e | |||
8dde19105d | |||
29db37bca5 | |||
410ca28b79 | |||
90c50c9652 | |||
bad650d5b9 | |||
443c7ce85a | |||
86a6075c30 | |||
620b2fb85e | |||
529f7f1eee | |||
3235a91ff5 | |||
d295ae18c3 | |||
![]() |
18cf1cd77a | ||
![]() |
d14f6e78f3 | ||
0b0df8b600 | |||
![]() |
74078695ed | ||
1a5aaa90fc | |||
513f3ebe75 | |||
![]() |
7c05357067 | ||
![]() |
c9e72db1f7 | ||
![]() |
545bdfb966 | ||
![]() |
6f8c536cb8 | ||
![]() |
d54087b7d0 | ||
![]() |
2e2efbff9d | ||
3917fa65cf | |||
81494bed54 | |||
819e8ea2aa | |||
77148e41f7 | |||
cb60e02671 | |||
25d8be0397 | |||
af4e3408db | |||
dabc8e5443 | |||
341a9c072c | |||
![]() |
98f8374b66 | ||
2b22f6a155 | |||
5335e911b7 | |||
![]() |
210a18f6b3 | ||
3063adcc73 | |||
8f8f8a90e2 | |||
433f316719 | |||
ca8a2e3c65 | |||
85893d9c9c | |||
c6ceedf041 | |||
1ad4c73ed4 | |||
![]() |
57351a4e1a | ||
![]() |
190ce8f222 | ||
3bd74850a5 | |||
0d3d96e96d | |||
d858f8d039 | |||
ab42dade2b | |||
6c058cca1f | |||
4c114d6cee | |||
de73a288b8 | |||
8b7acb10fb | |||
aa91af001c | |||
788273e870 | |||
0486cd44da | |||
46be2a02b5 | |||
3f7649b09a | |||
12864749f9 | |||
fa31e72db1 | |||
498782a962 | |||
c5cb67d969 | |||
737a72f296 | |||
bb55df2723 | |||
e0f47458cd | |||
08998e7535 | |||
6bc110b9fb | |||
04a7a0e3a9 | |||
c9b01acb6e | |||
c1604efb08 | |||
275935e747 | |||
7d5f278536 | |||
5eabb5fa6a | |||
078341e4ed | |||
11afb88602 | |||
97784373c0 | |||
16350548f5 | |||
de8065cf22 | |||
327981eade | |||
2da018775b | |||
d86cd011f3 | |||
dd0f0ad4f0 | |||
8dd9bec525 | |||
d42a2744d0 | |||
f618654ee0 | |||
40245ec86d | |||
d22f7826ac | |||
a272f21498 | |||
280458f6aa | |||
c9bfd7e4aa | |||
191ce772c5 | |||
2b02350e29 | |||
8cdb391dae | |||
b5f18033ba | |||
f41784175f | |||
1f0f928795 | |||
b9d0f42d98 | |||
76f615862f | |||
9634838c0f | |||
ac758f505e | |||
a4ef3f0da3 | |||
f18d59a368 | |||
1de7bcc749 | |||
31b6686279 | |||
c90b7c4fdc | |||
eb142b2483 | |||
bfbce9e98b | |||
5930907aa1 | |||
d03fef4e34 | |||
ddd4bca708 | |||
2e221cbf46 | |||
24acd451ad | |||
25fe8b804e | |||
c4ccb146ef | |||
![]() |
8ebed60b18 | ||
bc7f5414bd | |||
818b474bc4 | |||
![]() |
1c78eefb81 | ||
![]() |
d08e7db9bb | ||
4582e4db37 |
41 changed files with 2661 additions and 392 deletions
112
.github/workflows/style.yml
vendored
112
.github/workflows/style.yml
vendored
|
@ -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') }}
|
||||
|
@ -65,14 +65,14 @@ jobs:
|
|||
name: black
|
||||
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 +89,14 @@ jobs:
|
|||
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') }}
|
||||
|
@ -113,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') }}
|
||||
|
@ -133,34 +133,34 @@ jobs:
|
|||
- name: Run docformatter
|
||||
run: find -name "*.html" | xargs djhtml -c
|
||||
working-directory: .
|
||||
packaging:
|
||||
name: packaging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@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
58
CHANGELOG.md
Normal 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
|
2
LICENSE
2
LICENSE
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
include Makefile
|
||||
include *.md
|
||||
recursive-include img *.png
|
||||
recursive-include pretalx_broadcast_tools *.py
|
||||
recursive-include pretalx_broadcast_tools/locale *
|
||||
|
|
72
README.rst
72
README.rst
|
@ -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
BIN
img/pdf_export.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
BIN
img/room_info.png
Normal file
BIN
img/room_info.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
1
pretalx_broadcast_tools/__init__.py
Normal file
1
pretalx_broadcast_tools/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = "2.4.0"
|
|
@ -1,6 +1,8 @@
|
|||
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"
|
||||
|
@ -15,7 +17,9 @@ class PluginApp(AppConfig):
|
|||
"embedded into your broadcasting software"
|
||||
)
|
||||
visible = True
|
||||
version = "0.2.0"
|
||||
version = __version__
|
||||
category = "FEATURE"
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
from . import tasks # NOQA
|
||||
|
|
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal file
94
pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE
Normal 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.
|
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
BIN
pretalx_broadcast_tools/assets/titilium-web-regular.ttf
Normal file
Binary file not shown.
376
pretalx_broadcast_tools/exporter.py
Normal file
376
pretalx_broadcast_tools/exporter.py
Normal 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(),
|
||||
)
|
|
@ -1,20 +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):
|
||||
lower_thirds_no_talk_info = I18nFormField(
|
||||
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'),
|
||||
label=_('"No talk running" information'),
|
||||
widget=I18nTextInput,
|
||||
required=True,
|
||||
)
|
||||
lower_thirds_info_string = I18nFormField(
|
||||
help_text=_("Will only be shown if there's a talk running."),
|
||||
label=_("info line"),
|
||||
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,
|
||||
)
|
||||
|
|
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal file
274
pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po
Normal 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"
|
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal file
240
pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po
Normal 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 ""
|
|
@ -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)
|
|
@ -1,19 +1,20 @@
|
|||
from django.dispatch import receiver
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pretalx.common.models.settings import hierarkey
|
||||
from pretalx.common.signals import register_data_exporters
|
||||
from pretalx.orga.signals import nav_event_settings
|
||||
|
||||
hierarkey.add_default(
|
||||
"lower_thirds_no_talk_info",
|
||||
"broadcast_tools_lower_thirds_no_talk_info",
|
||||
LazyI18nString.from_gettext(
|
||||
gettext_noop("Sorry, there's currently no talk running")
|
||||
),
|
||||
LazyI18nString,
|
||||
)
|
||||
hierarkey.add_default("lower_thirds_info_string", "", LazyI18nString)
|
||||
hierarkey.add_default("broadcast_tools_lower_thirds_info_string", "", LazyI18nString)
|
||||
|
||||
|
||||
@receiver(nav_event_settings)
|
||||
|
@ -23,7 +24,7 @@ def navbar_info(sender, request, **kwargs):
|
|||
return []
|
||||
return [
|
||||
{
|
||||
"label": _("lower thirds"),
|
||||
"label": _("broadcast tools"),
|
||||
"url": reverse(
|
||||
"plugins:pretalx_broadcast_tools:orga",
|
||||
kwargs={
|
||||
|
@ -34,3 +35,10 @@ def navbar_info(sender, request, **kwargs):
|
|||
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
|
||||
|
|
|
@ -2,35 +2,124 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.2em;
|
||||
color: white;
|
||||
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
#box {
|
||||
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;
|
||||
|
||||
color: white;
|
||||
font-family: "Muli","Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
padding: 15px;
|
||||
|
||||
box-shadow: 5px 5px 10px 0px rgba(50, 50, 50, 0.75);
|
||||
background-color: #3aa57c;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 30px;
|
||||
#broadcast_tools_lower_thirds_title {
|
||||
font-size: 3vh;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#speaker {
|
||||
font-size: 20px;
|
||||
#broadcast_tools_lower_thirds_speaker {
|
||||
font-size: 2vh;
|
||||
}
|
||||
|
||||
#info_line {
|
||||
font-size: 16px;
|
||||
#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;
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -1,82 +0,0 @@
|
|||
schedule = null;
|
||||
room_name = null;
|
||||
event_info = 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 || !event_info) {
|
||||
console.warn("There's no schedule or no event info 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(event_info['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('../event.json', function(data) {
|
||||
event_info = data;
|
||||
|
||||
$('#box').css('background-color', data['color']);
|
||||
});
|
||||
$.getJSON('../schedule.json', function(data) {
|
||||
console.info('schedule updated with ' + data['talks'].length + ' talks in ' + data['rooms'].length + ' rooms');
|
||||
|
||||
schedule = data;
|
||||
|
||||
window.setTimeout(update_schedule, 30000);
|
||||
});
|
||||
}
|
||||
update_schedule();
|
78
pretalx_broadcast_tools/tasks.py
Normal file
78
pretalx_broadcast_tools/tasks.py
Normal 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
|
||||
)
|
|
@ -4,18 +4,20 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html" charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>{{ request.event.name }} lower thirds</title>
|
||||
{% compress js %}
|
||||
<script src="{% static "vendored/jquery-3.1.1.js" %}"></script>
|
||||
{% endcompress %}
|
||||
<script src="{% static "pretalx_broadcast_tools/update.js" %}"></script>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -1,40 +1,78 @@
|
|||
{% extends "orga/base.html" %}
|
||||
{% load bootstrap4 %}
|
||||
{% 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 "Set up lower thirds" %}
|
||||
{% translate "Lower Thirds" %}
|
||||
</legend>
|
||||
{% bootstrap_field form.lower_thirds_no_talk_info layout='event' %}
|
||||
{% bootstrap_field form.lower_thirds_info_string layout='event' %}
|
||||
<p>
|
||||
The info line will be shown on the bottom right side of your
|
||||
lower third. If you set it to an empty string, it will automatically
|
||||
hide itself.
|
||||
</p>
|
||||
<p>
|
||||
pretalx will automatically replace some placeholders in your info
|
||||
string.
|
||||
Use <code>{CODE}</code> to embed the talk code (<code>MUX9U3</code>
|
||||
for example). You could use this to directly link to the talk
|
||||
feedback page.
|
||||
Use <code>{EVENT_SLUG}</code> to get the event slug.
|
||||
Use <code>{TALK_SLUG}</code> to get the talk slug.
|
||||
</p>
|
||||
|
||||
{% if request.event.rooms %}
|
||||
<h3>{% trans "room list" %}</h3>
|
||||
<ul>
|
||||
{% for room in request.event.rooms.all %}
|
||||
<li><a href="{% url 'plugins:pretalx_broadcast_tools:lowerthirds' request.event.slug %}#{{ room.name }}">{{ room.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{{ 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">
|
||||
|
@ -45,7 +83,7 @@
|
|||
name="action" value="save"
|
||||
>
|
||||
<i class="fa fa-check"></i>
|
||||
{% trans "Save" %}
|
||||
{% translate "Save" %}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -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"> </p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,27 +1,67 @@
|
|||
from django.urls import re_path
|
||||
from pretalx.event.models.event import SLUG_CHARS
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
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 = [
|
||||
re_path(
|
||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/lower-thirds/$",
|
||||
views.BroadcastToolsLowerThirdsView.as_view(),
|
||||
name="lowerthirds",
|
||||
path(
|
||||
"<slug:event>/p/broadcast-tools/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"event.json",
|
||||
BroadcastToolsEventInfoView.as_view(),
|
||||
name="event_info",
|
||||
),
|
||||
path(
|
||||
"schedule.json",
|
||||
BroadcastToolsScheduleView.as_view(),
|
||||
name="schedule",
|
||||
),
|
||||
path(
|
||||
"lower-thirds/",
|
||||
BroadcastToolsLowerThirdsView.as_view(),
|
||||
name="lowerthirds",
|
||||
),
|
||||
path(
|
||||
"lower-thirds.voctomix.tar.gz",
|
||||
BroadcastToolsLowerThirdsVoctomixDownloadView.as_view(),
|
||||
name="lowerthirds_voctomix_download",
|
||||
),
|
||||
path(
|
||||
"feedback-qr/<talk>.svg",
|
||||
BroadcastToolsFeedbackQrCodeSvg.as_view(),
|
||||
name="feedback_qr_id",
|
||||
),
|
||||
path(
|
||||
"public-qr/<talk>.svg",
|
||||
BroadcastToolsPublicQrCodeSvg.as_view(),
|
||||
name="public_qr_id",
|
||||
),
|
||||
path(
|
||||
"room-info/",
|
||||
BroadcastToolsRoomInfoView.as_view(),
|
||||
name="room_info",
|
||||
),
|
||||
path(
|
||||
"room-timer/",
|
||||
BroadcastToolsRoomTimerView.as_view(),
|
||||
name="room_timer",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/event.json$",
|
||||
views.BroadcastToolsEventInfoView.as_view(),
|
||||
name="event_info",
|
||||
),
|
||||
re_path(
|
||||
f"^(?P<event>[{SLUG_CHARS}]+)/p/broadcast-tools/schedule.json$",
|
||||
views.BroadcastToolsScheduleView.as_view(),
|
||||
name="schedule",
|
||||
),
|
||||
re_path(
|
||||
f"^orga/event/(?P<event>[{SLUG_CHARS}]+)/settings/p/broadcast-tools/$",
|
||||
views.BroadcastToolsOrgaView.as_view(),
|
||||
path(
|
||||
"orga/event/<slug:event>/settings/p/broadcast-tools/",
|
||||
BroadcastToolsOrgaView.as_view(),
|
||||
name="orga",
|
||||
),
|
||||
]
|
||||
|
|
32
pretalx_broadcast_tools/utils/placeholders.py
Normal file
32
pretalx_broadcast_tools/utils/placeholders.py
Normal 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
|
|
@ -1,108 +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 BroadcastToolsSettingsForm
|
||||
|
||||
|
||||
class BroadcastToolsLowerThirdsView(TemplateView):
|
||||
template_name = "pretalx_broadcast_tools/lower_thirds.html"
|
||||
|
||||
|
||||
class BroadcastToolsOrgaView(PermissionRequired, FormView):
|
||||
form_class = BroadcastToolsSettingsForm
|
||||
permission_required = "orga.change_settings"
|
||||
template_name = "pretalx_broadcast_tools/orga.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_object(self):
|
||||
return self.request.event
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
return {
|
||||
"obj": self.request.event,
|
||||
"attribute_name": "settings",
|
||||
"locales": self.request.event.locales,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
|
||||
class BroadcastToolsEventInfoView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
color = self.request.event.primary_color or "#3aa57c"
|
||||
return JsonResponse(
|
||||
{
|
||||
"slug": self.request.event.slug,
|
||||
"name": str(self.request.event.name),
|
||||
"no_talk": str(self.request.event.settings.lower_thirds_no_talk_info),
|
||||
"color": color,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class BroadcastToolsScheduleView(EventPermissionRequired, ScheduleMixin, TemplateView):
|
||||
permission_required = "agenda.view_schedule"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
schedule = ScheduleData(
|
||||
event=self.request.event,
|
||||
schedule=self.schedule,
|
||||
)
|
||||
tz = pytz.timezone(schedule.event.timezone)
|
||||
infoline = str(schedule.event.settings.infoline or "")
|
||||
return JsonResponse(
|
||||
{
|
||||
"rooms": sorted(
|
||||
{
|
||||
str(room["name"])
|
||||
for day in schedule.data
|
||||
for room in day["rooms"]
|
||||
}
|
||||
),
|
||||
"talks": [
|
||||
{
|
||||
"id": talk.submission.id,
|
||||
"start": talk.start.astimezone(tz).isoformat(),
|
||||
"end": (talk.start + dt.timedelta(minutes=talk.duration))
|
||||
.astimezone(tz)
|
||||
.isoformat(),
|
||||
"slug": talk.frab_slug,
|
||||
"title": talk.submission.title,
|
||||
"persons": sorted(
|
||||
{
|
||||
person.get_display_name()
|
||||
for person in talk.submission.speakers.all()
|
||||
}
|
||||
),
|
||||
"track": {
|
||||
"color": talk.submission.track.color,
|
||||
"name": str(talk.submission.track.name),
|
||||
}
|
||||
if talk.submission.track
|
||||
else None,
|
||||
"room": str(room["name"]),
|
||||
"infoline": infoline.format(
|
||||
EVENT_SLUG=str(schedule.event.slug),
|
||||
TALK_SLUG=talk.frab_slug,
|
||||
CODE=talk.submission.code,
|
||||
),
|
||||
}
|
||||
for day in schedule.data
|
||||
for room in day["rooms"]
|
||||
for talk in room["talks"]
|
||||
],
|
||||
},
|
||||
)
|
34
pretalx_broadcast_tools/views/event_info.py
Normal file
34
pretalx_broadcast_tools/views/event_info.py
Normal 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,
|
||||
},
|
||||
)
|
29
pretalx_broadcast_tools/views/orga.py
Normal file
29
pretalx_broadcast_tools/views/orga.py
Normal 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,
|
||||
}
|
30
pretalx_broadcast_tools/views/qr.py
Normal file
30
pretalx_broadcast_tools/views/qr.py
Normal 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")
|
122
pretalx_broadcast_tools/views/schedule.py
Normal file
122
pretalx_broadcast_tools/views/schedule.py
Normal 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),
|
||||
],
|
||||
}
|
||||
)
|
13
pretalx_broadcast_tools/views/static_html.py
Normal file
13
pretalx_broadcast_tools/views/static_html.py
Normal 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"
|
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal file
22
pretalx_broadcast_tools/views/voctomix_export.py
Normal 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
|
41
pyproject.toml
Normal file
41
pyproject.toml
Normal 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*"]
|
46
setup.py
46
setup.py
|
@ -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-broadcast-tools",
|
||||
version="0.2,0",
|
||||
description=(
|
||||
"Some tools which can be used for supporting a broadcasting "
|
||||
"software, for example a 'lower third' page which can be "
|
||||
"embedded into your broadcasting software"
|
||||
),
|
||||
long_description=long_description,
|
||||
url="https://github.com/Kunsi/pretalx-plugin-broadcast-tools",
|
||||
author="kunsi",
|
||||
author_email="git@kunsmann.eu",
|
||||
license="Apache Software License",
|
||||
install_requires=[],
|
||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||
include_package_data=True,
|
||||
cmdclass=cmdclass,
|
||||
entry_points="""
|
||||
[pretalx.plugin]
|
||||
pretalx_broadcast_tools=pretalx_broadcast_tools:PretalxPluginMeta
|
||||
""",
|
||||
)
|
Loading…
Add table
Reference in a new issue