Pull request #41: Feature/slides
Merge in FRAG/50ohm.de from feature/slides to main * commit 'e5e96f5689d0a1323b5d3b03884f24eae7781d38': (26 commits) feat: create build directory if not existing refactor: relative import feat: update tests feat: use standard render method for rendering slides refactor: clean up code refactor: format file with ruff feat: remove background color attributes refactor: use dictionary unpacking refactor: use splat operator for class args feat: add configurable paths with pathlib feat: freeze new requirements refactor: removed uncessessary classes from slide renderer refactor: fixed bugs due to merge fix: fixed a wrong merge feature: build slide index feature: last slide with information feature: added tests for slides feature: finished slide building process fix: removed acceptance test result refactor: gitignore ...
This commit is contained in:
@@ -162,6 +162,7 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
test/acceptanceTest.html
|
||||
test/acceptanceTestSlides.html
|
||||
.DS_Store
|
||||
.envrc
|
||||
config/config.json
|
||||
|
||||
@@ -7,7 +7,7 @@ conf = config.Config()
|
||||
api = directus.DirectusAPI(conf.question_base_url, conf.question_access_token)
|
||||
content_api = directus.DirectusAPI(conf.content_base_url, conf.content_access_token)
|
||||
dl = download.Download(api, content_api, conf)
|
||||
bd = build.Build()
|
||||
bd = build.Build(conf)
|
||||
|
||||
# Download the text files:
|
||||
dl.download_edition("N")
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
# Erstellen von Folien
|
||||
|
||||
Das Erstellen von Folien ist direkt im Directus möglich. Wir verwenden reveal.js als Framework, um die Inhalte darzustellen.
|
||||
|
||||
## Directus
|
||||
|
||||
Im Directus gibt es zu jeder Sektion ein Feld _Slide_ in dem vereinfacht die Inhalte von Folien eingegeben werden können.
|
||||
|
||||
## Unterstützte Formate
|
||||
|
||||
### Folientrennung
|
||||
|
||||
Jede Folie (außer die erste) beginnt mit `---`. Dadurch wird eine neue Folie erzeugt.
|
||||
|
||||
#### Spezielle Folien
|
||||
|
||||
Reveal.js erlaubt es, Folien bestimmte Attribute, wie beispielsweise die Hintergrundfarbe zu übermitteln. Das kann direkt beim Erstellen einer Folie angegeben werden.
|
||||
|
||||
``` MarkDown
|
||||
--- data-background="#FF0000"
|
||||
```
|
||||
|
||||
Dadurch wird die Folie in knallrot gefärbt. Das sollte nur in Ausnahmen verwendet werden.
|
||||
|
||||
Alternativ werden drei Begriffe ausgewertet, die bereits auf der Webseitenansicht zum Einsatz kommen.
|
||||
|
||||
``` MarkDown
|
||||
--- unit
|
||||
```
|
||||
|
||||
Wird verwendet, um eine neue Einheit einzuführen.
|
||||
Färbt die Folie hellgrün ein.
|
||||
|
||||
``` MarkDown
|
||||
--- attention
|
||||
```
|
||||
|
||||
Wird verwendet, um die Aufmerksamkeit zu erregen. Beispielsweise bei Hinweisen auf die Gültigkeit bei Inkrafttreten der AfuV.
|
||||
Färbt die Folie hellblau ein.
|
||||
|
||||
``` attention
|
||||
--- danger
|
||||
```
|
||||
|
||||
Wird verwendet, um auf Gefahren hinzuweisen. Beispielsweise beim Umgang mit elektrischem Strom.
|
||||
Färbt die Folie hellrot ein.
|
||||
|
||||
### Überschriften
|
||||
|
||||
Überschriften werden mit dem `#`-Zeichen eingeleitet. Dabei gibt die Häufigkeit des Zeichens die Ebene der Überschrift an.
|
||||
|
||||
``` MarkDown
|
||||
# Überschrift 1
|
||||
## Überschrift 2
|
||||
### Überschrift 3
|
||||
```
|
||||
|
||||
Die maximale Ebene ist 6.
|
||||
|
||||
### Listen
|
||||
|
||||
Häufig sind auf Folien Listen zu sehen. Diese lassen sich mit `*` einleiten.
|
||||
|
||||
``` MarkDown
|
||||
* Listenpunkt
|
||||
* Noch ein Listenpunkt
|
||||
```
|
||||
|
||||
TODO: Verschachtelte Listen sind noch nicht möglich
|
||||
|
||||
### Tabellen
|
||||
|
||||
Tabellen werden mit Zeichen `|` zur Unterteilung der Spalten gesetzt. Die erste Spalte ist eine Kopfspalte. In ihr kann angegeben werden, ob der Inhalt der Spalte linksbündig (`l`), rechtsbündig (`r`) oder mittig (`c`) ausgerichtet sein soll.
|
||||
|
||||
``` Markdown
|
||||
| l: Wert | l: Bereich | l: Bedeutung | l: Englisch |
|
||||
| R | 1 - 5 | Lesbarkeit | Readability |
|
||||
| S | 1 - 9 | Signalstärke | Signal Strength |
|
||||
| T | 1 - 9 | Tonqualität | Tone |
|
||||
[table:n_rst:Die Bestandteile des RST-Rapports]
|
||||
```
|
||||
|
||||
Die Tabelle muss mit `[table:referenz:Beschreibung]` abgeschlossen werden. Dadurch wird intern eine Referenz gesetzt, auf die verwiesen werden kann.
|
||||
|
||||
### Textauszeichnung
|
||||
|
||||
Es kann einfache Textauszeichnung, wie _hervorgehoben_ oder <u>unterstrichen</u> verwendet werden.
|
||||
|
||||
``` MarkDown
|
||||
Es kann einfache Textauszeichnung, wie *hervorgehoben* oder <u>unterstrichen</u> verwendet werden.
|
||||
```
|
||||
|
||||
### Bilder
|
||||
|
||||
Bilder werden mit einer Referenz auf das Bild in der Datenbank eingefügt.
|
||||
|
||||
``` MarkDown
|
||||
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
|
||||
```
|
||||
|
||||
Es kann `photo` oder `picture` verwendet werden, je nach Datenbanktabelle in der das Bild abgelegt ist. Als nächstes wird die Nummer des Bildes angegeben. Darauf folgt eine Referenz für Verweise. Und abschließend die Beschreibung, die unter dem Bild angezeigt wird. Die Bilder werden pro Dokument durchnummeriert.
|
||||
|
||||
### Fragen aus dem Prüfungskatalog
|
||||
|
||||
Fragen aus dem Fragenkatalog können durch eine Referenz auf die Fragennummer eingefügt werden.
|
||||
|
||||
``` Markdown
|
||||
[question:2482]
|
||||
```
|
||||
|
||||
Fragen sollten auf einer eigenen Folie angezeigt werden, da sie in der Regel sehr groß sind.
|
||||
|
||||
``` Markdown
|
||||
---
|
||||
[question:2482]
|
||||
---
|
||||
```
|
||||
|
||||
Die Antworten werden beim Erstellen der Folien in eine zufällige Reihenfolge gebracht, sodass nicht wie im Prüfungskatalog die erste Antwort die richtige ist.
|
||||
|
||||
### QSO
|
||||
|
||||
Ein beispielhaftes QSO kann folgendermaßen hinzugefügt werden:
|
||||
|
||||
``` Markdown
|
||||
<qso>
|
||||
Ist diese Frequenz frei? DL1PZ
|
||||
> *(keine Antwort)*
|
||||
Ist diese Frequenz frei? DL1PZ
|
||||
> *(keine Antwort)*
|
||||
CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
|
||||
> DL1PZ hier ist DL9MJ bitte kommen
|
||||
</qso>
|
||||
```
|
||||
|
||||
Jede Zeile ist eine Sprechblase. Die einzelnen Sprechblasen werden nacheinander aufgedeckt. Zeilen, die mit `>` beginnen, werden als Teil der Gegenstelle angezeigt.
|
||||
|
||||
### Einblendeeffekt
|
||||
|
||||
Soll ein Teil der Folie erst später aufgedeckt werden, kann dieser in `<fragment>` eingebettet werden.
|
||||
|
||||
``` MarkDown
|
||||
<fragment>
|
||||
Dieser Text erscheint erst später.
|
||||
</fragment>
|
||||
```
|
||||
|
||||
Mehrere `<fragment>`-Abschnitte nacheinander sorgen dafür, dass die Inhalte Schritt für Schritt aufgedeckt werden.
|
||||
|
||||
### Notizen
|
||||
|
||||
Es ist möglich, Notizen für den oder die Vortragende hinzuzufügen. Die sind nur in der "Speaker-Ansicht" (Taste 's' drücken) sichtbar.
|
||||
|
||||
Sinnvoll sind kurze Hinweise auf die Erklärungen, die in der Webseitenversion gegeben werden oder Hintergrundinformationen. Die brauchen nicht auf die Folien. Und kurze Hinweise sollten für Funkamateure ausreichend sein, um die Notiz zu verstehen und den Inhalt zu vermitteln.
|
||||
|
||||
``` MarkDown
|
||||
<note>
|
||||
Die Notiz wird nur in der Speaker-Ansicht angezeigt
|
||||
* Listen sind auch möglich
|
||||
* Am besten kurz halten
|
||||
* Der Platz ist begrenzt
|
||||
</note>
|
||||
```
|
||||
|
||||
### Formeln
|
||||
|
||||
Es können Formeln im LaTeX-Stil angegeben werden. Diese werden in `$` gesetzt.
|
||||
|
||||
``` MarkDown
|
||||
$\lambda[[\text{m}]] = \dfrac{300}{f[[\text{MHz}]]} = \dfrac{300}{145,3 \ \text{MHz}} \approx 2,06 \ \text{m}$
|
||||
```
|
||||
|
||||
Abweichend von LaTeX müssen die rechteckigen Klammern gedoppelt werden.
|
||||
|
||||
### Links
|
||||
|
||||
Es können Links eingefügt werden.
|
||||
|
||||
``` MarkDown
|
||||
[Link zum DARC](https://darc.de/)
|
||||
```
|
||||
|
||||
### Zwei Spalten
|
||||
|
||||
Manchmal ist es sinnvoll, zwei Spalten zu verwenden. Beispielsweise bei einem Bild mit Beschreibung. Dazu gibt es die beiden Tags `<left>` und `<right>`.
|
||||
|
||||
``` MarkDown
|
||||
<left>
|
||||
[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF]
|
||||
</left>
|
||||
<right>
|
||||
* Funkstationen verwenden Rufzeichen, um sich zu identifizieren
|
||||
* Folge von Buchstaben und Ziffern
|
||||
* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen
|
||||
</right>
|
||||
```
|
||||
|
||||
### Webseite interaktiv einbetten
|
||||
|
||||
Es kann eine ganze Webseite interaktiv eingebettet werden, sodass darin agiert werden kann. Zusätzlich sind die Navigationspfeile und der Speaker-View erhalten.
|
||||
|
||||
``` MarkDown
|
||||
--- data-background-iframe="https://f5len.org/tools/locator/" data-background-interactive
|
||||
|
||||
<note>
|
||||
* Es wird eine interaktive Karte mit Maidenhead-Locator gezeigt.
|
||||
</note>
|
||||
```
|
||||
|
||||
Zusätzlich eine Speaker-Note einzufügen ist sinnvoll, um keine leere Slide zu erzeugen und den Speaker darauf hinzuweisen, dass die Webseite nur vom Hauptbildschirm bedient werden kann.
|
||||
|
||||
### Kleinere Schrift
|
||||
|
||||
In ganz seltenen Ausnahmen kann eine kleinere Schrift verwendet werden. Das kann dann angewandt werden, wenn ein oder zwei Zeilen einer Tabelle nicht mehr auf die Folie passen.
|
||||
|
||||
``` MarkDown
|
||||
--- style="font-size: smaller;"
|
||||
|
||||
* Längere
|
||||
* Liste
|
||||
```
|
||||
|
||||
### Direkter Übergang
|
||||
|
||||
Ohne verschieben der Folie zur nächsten Schalten. Das kann als Effekt genutzt werden, damit auf der Folie ein Austauscheffekt stattfindet.
|
||||
|
||||
```MarkDown
|
||||
---
|
||||
## Folie
|
||||
Mit Text
|
||||
--- data-transition="none"
|
||||
## Folie
|
||||
Mit anderem Text
|
||||
```
|
||||
+1
-1
@@ -42,4 +42,4 @@ select = [
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = ["--color=yes"]
|
||||
markers = ["html", "latex"]
|
||||
markers = ["html", "latex", "slide"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from mistletoe import Document, HtmlRenderer
|
||||
|
||||
@@ -30,12 +28,8 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
ref_id = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question_renderer=None,
|
||||
picture_handler=None,
|
||||
photo_handler=None,
|
||||
include_handler=None
|
||||
) :
|
||||
self, *extras, question_renderer=None, picture_handler=None, photo_handler=None, include_handler=None, **kwargs
|
||||
):
|
||||
super().__init__(
|
||||
Dash,
|
||||
BlockComment,
|
||||
@@ -53,8 +47,11 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
Photo,
|
||||
Table,
|
||||
Qso,
|
||||
Include
|
||||
Include,
|
||||
*extras,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.question_renderer = question_renderer
|
||||
self.picture_handler = picture_handler
|
||||
self.photo_handler = photo_handler
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||
|
||||
from .slide_break import SlideBreak
|
||||
|
||||
|
||||
class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
SlideBreak,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def render_slide_break(self, token):
|
||||
inner = self.render_inner(token)
|
||||
|
||||
if token.attribute is None:
|
||||
return f"<section>\n{inner}\n</section>\n"
|
||||
else:
|
||||
return f"<section {token.attribute}>\n{inner}\n</section>\n"
|
||||
|
||||
def render_qso(self, token):
|
||||
qso = '<div class="qso r-fit-text">\n'
|
||||
for child in token.children:
|
||||
direction = "other" if child.received else "own"
|
||||
fade = "fragment fade-left" if child.received else "fragment fade-right"
|
||||
qso += f'<div class="qso_{direction} {fade}">{self.render_inner(child)}</div>\n'
|
||||
qso += "</div>\n"
|
||||
return qso
|
||||
|
||||
def render_tag(self, token):
|
||||
if token.tagtype == "fragment":
|
||||
return f'<div class="fragment">\n{self.render_inner(token)}\n</div>\n'
|
||||
if token.tagtype == "left":
|
||||
return f'<div id="left">\n{self.render_inner(token)}\n</div>\n'
|
||||
if token.tagtype == "right":
|
||||
return f'<div id="right">\n{self.render_inner(token)}\n</div>\n'
|
||||
elif token.tagtype == "note":
|
||||
return f'<aside class="notes">\n{self.render_inner(token)}\n</aside>\n'
|
||||
|
||||
return "" # Ignore other tags in slide context anyway in tokenizer
|
||||
@@ -0,0 +1,33 @@
|
||||
import re
|
||||
|
||||
from mistletoe.block_token import BlockToken, tokenize
|
||||
|
||||
|
||||
class SlideBreak(BlockToken):
|
||||
@staticmethod
|
||||
def start(line):
|
||||
return re.match(r"^---.*$", line)
|
||||
|
||||
@classmethod
|
||||
def read(cls, lines):
|
||||
child_lines = []
|
||||
first_line = next(lines) # Consume the line that started the slide break
|
||||
|
||||
match = re.match(r"^---(.*)$", first_line)
|
||||
attribute = match.group(1).strip() if match.group(1) else None
|
||||
|
||||
# We have to work with peak here, since a normal for loop would already
|
||||
# consume the next line, which might be a `---` line that marks the
|
||||
# beginning of a new slide as well
|
||||
next_line = lines.peek()
|
||||
while next_line is not None:
|
||||
if cls.start(next_line):
|
||||
break
|
||||
else:
|
||||
child_lines.append(next(lines))
|
||||
next_line = lines.peek()
|
||||
|
||||
return tokenize(child_lines), attribute
|
||||
|
||||
def __init__(self, match):
|
||||
self.children, self.attribute = match
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from mistletoe.block_token import BlockToken, tokenize
|
||||
|
||||
captures = [
|
||||
# Website:
|
||||
"margin",
|
||||
"indepth",
|
||||
"webmargin",
|
||||
@@ -18,6 +19,11 @@ captures = [
|
||||
"webonly",
|
||||
"latexonly",
|
||||
"wordorigin",
|
||||
# Slides:
|
||||
"note",
|
||||
"fragment",
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+15
-7
@@ -1,12 +1,20 @@
|
||||
beautifulsoup4==4.13.4
|
||||
coverage==7.6.9
|
||||
iniconfig==2.0.0
|
||||
certifi==2025.4.26
|
||||
charset-normalizer==3.4.2
|
||||
coverage==7.9.1
|
||||
idna==3.10
|
||||
iniconfig==2.1.0
|
||||
jinja2==3.1.6
|
||||
joblib==1.5.1
|
||||
markupsafe==3.0.2
|
||||
mistletoe==1.4.0
|
||||
packaging==24.2
|
||||
pluggy==1.5.0
|
||||
pytest==8.3.4
|
||||
pytest-cov==6.0.0
|
||||
packaging==25.0
|
||||
pluggy==1.6.0
|
||||
pygments==2.19.1
|
||||
pytest==8.4.0
|
||||
pytest-cov==6.2.1
|
||||
requests==2.32.3
|
||||
soupsieve==2.7
|
||||
typing-extensions==4.13.2
|
||||
tqdm==4.67.1
|
||||
typing-extensions==4.14.0
|
||||
urllib3==2.4.0
|
||||
|
||||
+135
-71
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
@@ -8,23 +8,31 @@ from mistletoe import Document
|
||||
from tqdm import tqdm
|
||||
|
||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
class Build:
|
||||
def __init__(self):
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
|
||||
memory = Memory("./cache", verbose=0)
|
||||
self.env = Environment(loader=FileSystemLoader("templates/html"))
|
||||
self.env = Environment(loader=FileSystemLoader("templates/"))
|
||||
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
|
||||
self.questions = self.__parse_katalog()
|
||||
|
||||
# Decorate the method with memory.cache
|
||||
self.__build_question = memory.cache(self.__build_question)
|
||||
self.__build_chapter = memory.cache(self.__build_chapter)
|
||||
self.__build_section = memory.cache(self.__build_section)
|
||||
self.__build_page = memory.cache(self.__build_page)
|
||||
|
||||
self.__build_question_slide = memory.cache(self.__build_question_slide)
|
||||
self.__build_chapter = memory.cache(self.__build_chapter)
|
||||
self.__build_section = memory.cache(self.__build_section)
|
||||
self.__build_page = memory.cache(self.__build_page)
|
||||
self.__build_chapter_slidedeck = memory.cache(self.__build_chapter_slidedeck)
|
||||
|
||||
def __parse_katalog(self):
|
||||
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
|
||||
fragenkatalog = json.load(fragenkatalog_file)
|
||||
with self.config.p_fragenkatalog.open() as file:
|
||||
fragenkatalog = json.load(file)
|
||||
|
||||
questions = {}
|
||||
|
||||
@@ -41,13 +49,13 @@ class Build:
|
||||
return questions
|
||||
|
||||
# cached
|
||||
def __build_question(self, input):
|
||||
def __build_question(self, input, template_file="html/question.html"):
|
||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||
|
||||
question_template = self.env.get_template("question.html")
|
||||
question_template = self.env.get_template(template_file)
|
||||
|
||||
with open("data/metadata.json") as metadata_file:
|
||||
metadata = json.load(metadata_file)
|
||||
with (self.config.p_data / "metadata.json").open() as file:
|
||||
metadata = json.load(file)
|
||||
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
|
||||
|
||||
question = self.questions[number]
|
||||
@@ -88,32 +96,29 @@ class Build:
|
||||
answer_pictures=answer_pictures,
|
||||
)
|
||||
|
||||
def __build_question_slide(self, input):
|
||||
return self.__build_question(input, template_file="slide/question.html")
|
||||
|
||||
# cached
|
||||
def __build_page(self, content, course_wrapper=False, sidebar=None) :
|
||||
page_template = self.env.get_template("page.html")
|
||||
return page_template.render(
|
||||
content=content,
|
||||
course_wrapper=course_wrapper,
|
||||
sidebar=sidebar
|
||||
)
|
||||
|
||||
def __build_page(self, content, course_wrapper=False, sidebar=None):
|
||||
page_template = self.env.get_template("html/page.html")
|
||||
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
|
||||
|
||||
def __picture_handler(self, id):
|
||||
os.makedirs("build/pictures", exist_ok=True)
|
||||
src = f"data/pictures/{id}.svg"
|
||||
dst = f"build/pictures/{id}.svg"
|
||||
shutil.copyfile(src, dst)
|
||||
self.config.p_build_pictures.mkdir(parents=True, exist_ok=True)
|
||||
file = f"{id}.svg"
|
||||
shutil.copyfile(self.config.p_data_pictures / file, self.config.p_build_pictures / file)
|
||||
|
||||
def __photo_handler(self, id):
|
||||
os.makedirs("build/photos", exist_ok=True)
|
||||
src = f"data/photos/{id}.jpg"
|
||||
dst = f"build/photos/{id}.jpg"
|
||||
shutil.copyfile(src, dst)
|
||||
self.config.p_build_photos.mkdir(parents=True, exist_ok=True)
|
||||
file = f"{id}.jpg"
|
||||
shutil.copyfile(self.config.p_data_photos / file, self.config.p_build_photos / file)
|
||||
|
||||
# cached
|
||||
def __build_chapter(self, edition, edition_name, number, chapter, next_chapter=None):
|
||||
chapter_template = self.env.get_template("chapter.html")
|
||||
next_chapter_template = self.env.get_template("next_chapter.html")
|
||||
with open(f'build/{edition}_chapter_{chapter["ident"]}.html', 'w') as file:
|
||||
chapter_template = self.env.get_template("html/chapter.html")
|
||||
next_chapter_template = self.env.get_template("html/next_chapter.html")
|
||||
with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file:
|
||||
result = chapter_template.render(
|
||||
edition=edition,
|
||||
name=edition_name,
|
||||
@@ -123,7 +128,7 @@ class Build:
|
||||
|
||||
if next_chapter is not None:
|
||||
result += next_chapter_template.render(
|
||||
url=f"{edition}_chapter_{next_chapter["ident"]}.html",
|
||||
url=f"{edition}_chapter_{next_chapter['ident']}.html",
|
||||
title=next_chapter["title"],
|
||||
)
|
||||
|
||||
@@ -131,22 +136,23 @@ class Build:
|
||||
file.write(result)
|
||||
|
||||
def __include_handler(self, include):
|
||||
with open("data/includes.json") as file:
|
||||
with (self.config.p_data / "includes.json").open() as file:
|
||||
includes = json.load(file)
|
||||
return includes.get(include)
|
||||
|
||||
# cached
|
||||
def __build_section(self, edition, edition_name, section, chapter, next_section=None, next_chapter=None):
|
||||
section_template = self.env.get_template("section.html")
|
||||
next_section_template = self.env.get_template("next_section.html")
|
||||
next_chapter_template = self.env.get_template("next_chapter.html")
|
||||
with open(f'build/{edition}_{section["ident"]}.html', 'w') as file:
|
||||
|
||||
def __build_section(
|
||||
self, edition, edition_name, section, section_id, chapter, next_section=None, next_chapter=None
|
||||
):
|
||||
section_template = self.env.get_template("html/section.html")
|
||||
next_section_template = self.env.get_template("html/next_section.html")
|
||||
next_chapter_template = self.env.get_template("html/next_chapter.html")
|
||||
with (self.config.p_build / f"{edition}_{section['ident']}.html").open("w") as file:
|
||||
with FiftyOhmHtmlRenderer(
|
||||
self.__build_question,
|
||||
self.__picture_handler,
|
||||
self.__photo_handler,
|
||||
self.__include_handler
|
||||
question_renderer=self.__build_question,
|
||||
picture_handler=self.__picture_handler,
|
||||
photo_handler=self.__photo_handler,
|
||||
include_handler=self.__include_handler,
|
||||
) as renderer:
|
||||
section["content"] = renderer.render(Document(section["content"]))
|
||||
|
||||
@@ -154,56 +160,117 @@ class Build:
|
||||
edition=edition,
|
||||
name=edition_name,
|
||||
section=section,
|
||||
section_id=section_id,
|
||||
chapter=chapter,
|
||||
)
|
||||
|
||||
if next_section is not None:
|
||||
result += next_section_template.render(
|
||||
url=f"{edition}_{next_section["ident"]}.html",
|
||||
url=f"{edition}_{next_section['ident']}.html",
|
||||
title=next_section["title"],
|
||||
)
|
||||
elif next_chapter is not None:
|
||||
result += next_chapter_template.render(
|
||||
url=f"{edition}_chapter_{next_chapter["ident"]}.html",
|
||||
url=f"{edition}_chapter_{next_chapter['ident']}.html",
|
||||
title=next_chapter["title"],
|
||||
)
|
||||
|
||||
result = self.__build_page(result, course_wrapper=True)
|
||||
file.write(result)
|
||||
|
||||
def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter):
|
||||
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
|
||||
slide_template = self.env.get_template("slide/slide.html")
|
||||
help_template = self.env.get_template("slide/help.html")
|
||||
next_template = self.env.get_template("slide/next.html")
|
||||
with FiftyOhmHtmlSlideRenderer(
|
||||
question_renderer=self.__build_question_slide,
|
||||
picture_handler=self.__picture_handler,
|
||||
photo_handler=self.__photo_handler,
|
||||
include_handler=self.__include_handler,
|
||||
) as renderer:
|
||||
result = "<section>\n"
|
||||
result += f'<section data-background="#DAEEFA">\n<h1>{chapter["title"]}</h1>\n</section>\n'
|
||||
result += help_template.render()
|
||||
result += "</section>\n"
|
||||
for section in sections:
|
||||
if not section["slide"].startswith("---"):
|
||||
section["slide"] = "---\n" + section["slide"]
|
||||
tmp = f'<section data-background="#DAEEFA">\n<h1>{section["title"]}</h1>\n</section>\n'
|
||||
tmp += renderer.render(Document(section["slide"]))
|
||||
result += f"<section>{tmp}</section>\n"
|
||||
result += next_template.render(
|
||||
edition=edition,
|
||||
next_chapter=next_chapter,
|
||||
chapter=chapter,
|
||||
)
|
||||
|
||||
result = slide_template.render(content=result)
|
||||
file.write(result)
|
||||
|
||||
def __filter_shuffle_answers(self, seq):
|
||||
answers = []
|
||||
firstrun = True
|
||||
for answer in seq:
|
||||
if firstrun:
|
||||
answers.append({"content": answer, "correct": True})
|
||||
firstrun = False
|
||||
else:
|
||||
answers.append({"content": answer, "correct": False})
|
||||
random.shuffle(answers)
|
||||
return answers
|
||||
|
||||
def __build_book_index(self, book):
|
||||
template = self.env.get_template("course_index.html")
|
||||
with open(f"build/{book['edition']}_course_index.html", "w") as file:
|
||||
template = self.env.get_template("html/course_index.html")
|
||||
with (self.config.p_build / f"{book['edition']}_course_index.html").open("w") as file:
|
||||
result = template.render(
|
||||
book=book,
|
||||
)
|
||||
result = self.__build_page(result)
|
||||
file.write(result)
|
||||
file.write(result)
|
||||
|
||||
def __build_slide_index(self, book):
|
||||
template = self.env.get_template("slide/slide_index.html")
|
||||
with (self.config.p_build / f"{book['edition']}_slide_index.html").open("w") as file:
|
||||
result = template.render(
|
||||
book=book,
|
||||
)
|
||||
result = self.__build_page(result)
|
||||
file.write(result)
|
||||
|
||||
def build_edition(self, edition):
|
||||
self.config.p_build.mkdir(exist_ok=True)
|
||||
|
||||
edition = edition.upper()
|
||||
|
||||
with open(f'data/book_{edition}.json') as f:
|
||||
book = json.load(f)
|
||||
with (self.config.p_data / f"book_{edition}.json").open() as file:
|
||||
book = json.load(file)
|
||||
chapters = book["chapters"]
|
||||
edition_name = book["title"]
|
||||
self.__build_book_index(book)
|
||||
for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"),1):
|
||||
self.__build_slide_index(book)
|
||||
for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
|
||||
next_chapter = chapters[number] if number < len(chapters) else None
|
||||
self.__build_chapter(edition, edition_name, number, chapter, next_chapter)
|
||||
self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter)
|
||||
|
||||
for i, section in enumerate(chapter["sections"],1) :
|
||||
for i, section in enumerate(chapter["sections"], 1):
|
||||
next_section = chapter["sections"][i] if i < len(chapter["sections"]) else None
|
||||
self.__build_section(edition, edition_name, section, chapter, next_section, next_chapter)
|
||||
self.__build_section(edition, edition_name, section, i, chapter, next_section, next_chapter)
|
||||
|
||||
def build_assets(self):
|
||||
shutil.copytree("assets", "build/assets", dirs_exist_ok=True)
|
||||
shutil.copytree(self.config.p_assets, self.config.p_build_assets, dirs_exist_ok=True)
|
||||
|
||||
def __parse_snippets(self):
|
||||
with open("data/snippets.json") as f:
|
||||
snippets = json.load(f)
|
||||
with (self.config.p_data / "snippets.json").open() as file:
|
||||
snippets = json.load(file)
|
||||
|
||||
with FiftyOhmHtmlRenderer(self.__build_question, self.__picture_handler, self.__photo_handler) as renderer:
|
||||
with FiftyOhmHtmlRenderer(
|
||||
question_renderer=self.__build_question,
|
||||
picture_handler=self.__picture_handler,
|
||||
photo_handler=self.__photo_handler,
|
||||
include_handler=self.__include_handler,
|
||||
) as renderer:
|
||||
for key, value in snippets.items():
|
||||
snippets[key] = renderer.render_inner(Document(value))
|
||||
# Remove leading <p> and trailing </p>:
|
||||
@@ -211,35 +278,32 @@ class Build:
|
||||
return snippets
|
||||
|
||||
def __parse_contents(self):
|
||||
with open("data/content.json") as f:
|
||||
contents = json.load(f)
|
||||
with (self.config.p_data / "content.json").open() as file:
|
||||
contents = json.load(file)
|
||||
return contents
|
||||
|
||||
def __build_index(self, snippets):
|
||||
template = self.env.get_template("index.html")
|
||||
template = self.env.get_template("html/index.html")
|
||||
result = template.render({"snippets": snippets})
|
||||
|
||||
with open("build/index.html", "w") as file:
|
||||
with (self.config.p_build / "index.html").open("w") as file:
|
||||
result = self.__build_page(result)
|
||||
|
||||
file.write(result)
|
||||
|
||||
def __build_course_page(self, snippets, template, page):
|
||||
template = self.env.get_template(f"{template}.html")
|
||||
result=template.render({"snippets": snippets})
|
||||
template = self.env.get_template(f"html/{template}.html")
|
||||
result = template.render({"snippets": snippets})
|
||||
|
||||
with open(f"build/{page}.html", "w") as file:
|
||||
with (self.config.p_build / f"{page}.html").open("w") as file:
|
||||
result = self.__build_page(result)
|
||||
file.write(result)
|
||||
|
||||
def __build_html_page(self, contents, page):
|
||||
for content in contents:
|
||||
if content["url_part"] == page:
|
||||
with open(f"build/{page}.html", "w") as file:
|
||||
result = self.__build_page(
|
||||
content=content["content"],
|
||||
sidebar=content["sidebar"]
|
||||
)
|
||||
with (self.config.p_build / f"{page}.html").open("w") as file:
|
||||
result = self.__build_page(content=content["content"], sidebar=content["sidebar"])
|
||||
file.write(result)
|
||||
|
||||
def build_website(self):
|
||||
@@ -250,4 +314,4 @@ class Build:
|
||||
self.__build_course_page(snippets, "kurse-liste", "kurse_vor_ort_liste")
|
||||
self.__build_course_page(snippets, "patenkarte", "patenkarte")
|
||||
self.__build_html_page(contents, "pruefung")
|
||||
self.__build_html_page(contents, "infos")
|
||||
self.__build_html_page(contents, "infos")
|
||||
|
||||
+19
-7
@@ -1,12 +1,10 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Config:
|
||||
env_prefix = "OHM"
|
||||
access_token: str
|
||||
base_url: str
|
||||
no_latex: bool
|
||||
__env_prefix = "OHM"
|
||||
|
||||
def __init__(self):
|
||||
if os.path.isfile("config/config.json"):
|
||||
@@ -19,14 +17,28 @@ class Config:
|
||||
self.content_base_url = self.get_config_value("directus_content_base_url", "https://redaktion.50ohm.de/")
|
||||
self.no_latex = self.get_config_value("no_latex", False)
|
||||
|
||||
self.p_fragenkatalog = Path(self.get_config_value("path_fragenkatalog", "./data/fragenkatalog3b.json"))
|
||||
|
||||
self.p_data = Path("./data")
|
||||
self.p_data_photos = self.p_data / "photos"
|
||||
self.p_data_pictures = self.p_data / "pictures"
|
||||
|
||||
self.p_build = Path("./build")
|
||||
self.p_build_photos = self.p_build / "photos"
|
||||
self.p_build_pictures = self.p_build / "pictures"
|
||||
self.p_build_assets = self.p_build / "assets"
|
||||
|
||||
self.p_assets = Path("./assets")
|
||||
|
||||
def get_config_value(self, key: str, default=None):
|
||||
if key in self.config:
|
||||
return self.config[key]
|
||||
elif f"{self.env_prefix}_{key.upper()}" in os.environ.keys():
|
||||
return os.environ[f"{self.env_prefix}_{key.upper()}"]
|
||||
elif f"{self.__env_prefix}_{key.upper()}" in os.environ.keys():
|
||||
return os.environ[f"{self.__env_prefix}_{key.upper()}"]
|
||||
elif default is not None:
|
||||
return default
|
||||
else:
|
||||
raise Exception(
|
||||
f"ERROR: Required value for '{key}' not found. Add to config.json or env as '${self.env_prefix}_{key.upper()}'"
|
||||
f"ERROR: Required value for '{key}' not found."
|
||||
f"Add to config.json or env as '${self.__env_prefix}_{key.upper()}'"
|
||||
)
|
||||
|
||||
+34
-23
@@ -6,9 +6,13 @@ import shutil
|
||||
from joblib import Memory, Parallel, delayed
|
||||
from tqdm import tqdm
|
||||
|
||||
from api.directus import DirectusAPI
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
class Download:
|
||||
def __init__(self, api, content_api, config):
|
||||
def __init__(self, api: DirectusAPI, content_api: DirectusAPI, config: Config):
|
||||
self.api = api
|
||||
self.config = config
|
||||
self.content_api = content_api
|
||||
@@ -60,7 +64,7 @@ class Download:
|
||||
"edition": edition,
|
||||
}
|
||||
|
||||
with open("data/book_" + edition + ".json", "w", encoding="utf-8") as file:
|
||||
with (self.config.p_data / f"book_{edition}.json").open("w", encoding="utf-8") as file:
|
||||
json.dump(out_edition, file, ensure_ascii=False, indent=4)
|
||||
|
||||
def download_question_metadata(self):
|
||||
@@ -82,26 +86,26 @@ class Download:
|
||||
}
|
||||
|
||||
# Serializing json:
|
||||
with open("data/metadata.json", "w") as f:
|
||||
with (self.config.p_data / "metadata.json").open("w", encoding="utf-8") as f:
|
||||
json.dump(result, f, indent=4)
|
||||
f.close()
|
||||
|
||||
def download_photos(self):
|
||||
self.config.p_data_photos.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
photos = self.api.get("items/Fotos", params={"limit": -1})
|
||||
|
||||
for photo in tqdm(photos, desc="Downloading photos"):
|
||||
data = self.api.get_file("assets/" + photo["photo"])
|
||||
with open(f"./data/photos/{photo['id']}.jpg", "wb") as file:
|
||||
with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file:
|
||||
file.write(data)
|
||||
|
||||
def __symlink_pictures(self):
|
||||
os.chdir("./data/pictures")
|
||||
if not os.path.islink("foto"):
|
||||
os.system("ln -s ../photos foto")
|
||||
os.chdir("../..")
|
||||
|
||||
def download_pictures(self):
|
||||
self.__symlink_pictures()
|
||||
self.config.p_data_pictures.mkdir(parents=True, exist_ok=True)
|
||||
# Symlink photos, so LaTeX can access these assets to render into graphics.
|
||||
foto_link = self.config.p_data_pictures / "foto"
|
||||
if not foto_link.is_symlink():
|
||||
foto_link.symlink_to(self.config.p_data_photos)
|
||||
|
||||
memory = Memory("./cache", verbose=0)
|
||||
|
||||
@@ -115,17 +119,22 @@ class Download:
|
||||
return
|
||||
|
||||
if self.config.no_latex:
|
||||
shutil.copyfile("./assets/images/50ohm_gray.svg", f"./data/pictures/{pid}.svg")
|
||||
shutil.copyfile("./assets/images/50ohm_gray.svg", self.config.p_data_pictures / f"{pid}.svg")
|
||||
else:
|
||||
with open(f"./data/pictures/img/{pid}include.tex", "w") as file:
|
||||
with (self.config.p_data_pictures / f"img/{pid}include.tex").open("w") as file:
|
||||
file.write(picture["latex"])
|
||||
with open(f"./data/pictures/{pid}.tex", "w", encoding="utf-8") as file:
|
||||
|
||||
aux_file = self.config.p_data_photos / f"{pid}.tex"
|
||||
with aux_file.open("w", encoding="utf-8") as file:
|
||||
# Write the auxilary LaTeX file:
|
||||
file.write(f"\\documentclass{{BNetzA-Fragenkatalog}}\\DARCimageOnly{{9cm}}{{{pid}include}}")
|
||||
# Build the picture:
|
||||
os.system(f"latexmk -lualatex -f -interaction=nonstopmode ./data/pictures/{pid}.tex > /dev/null 2>&1")
|
||||
os.system(f"latexmk -lualatex -f -interaction=nonstopmode {aux_file} > /dev/null 2>&1")
|
||||
# Convert the PDF to SVG:
|
||||
os.system(f"pdftocairo -svg data/pictures/{pid}.pdf - > data/pictures/{pid}.svg")
|
||||
os.system(
|
||||
"pdftocairo -svg"
|
||||
f"{self.config.p_data_pictures / f'{pid}.pdf'} - > {self.config.p_data_pictures / f'{pid}.svg'}"
|
||||
)
|
||||
|
||||
pictures = self.api.get("items/pictures", params={"limit": -1})
|
||||
|
||||
@@ -139,20 +148,22 @@ class Download:
|
||||
result = {}
|
||||
for include in tqdm(includes, desc="Downloading includes"):
|
||||
result[include["ident"]] = include["content"]
|
||||
|
||||
with open("data/includes.json", "w") as f:
|
||||
json.dump(result, f, indent=4)
|
||||
|
||||
with (self.config.p_data / "includes.json").open("w", encoding="utf-8") as file:
|
||||
json.dump(result, file, indent=4)
|
||||
|
||||
def download_snippets(self):
|
||||
snippets = {}
|
||||
for snippet in tqdm(self.content_api.get("items/snippet"), desc="Downloading snippets"):
|
||||
snippets[snippet["ident"]] = snippet["content"]
|
||||
with open("data/snippets.json", "w", encoding="utf-8") as file:
|
||||
with (self.config.p_data / "snippets.json").open("w", encoding="utf-8") as file:
|
||||
json.dump(snippets, file, ensure_ascii=False, indent=4)
|
||||
|
||||
def download_content(self):
|
||||
contents = []
|
||||
for content in self.content_api.get("items/content"): #tqdm(self.content_api.get("items/content"), desc="Downloading content"):
|
||||
for content in self.content_api.get(
|
||||
"items/content"
|
||||
): # tqdm(self.content_api.get("items/content"), desc="Downloading content"):
|
||||
contents.append(
|
||||
{
|
||||
"url_part": content["url_part"],
|
||||
@@ -160,6 +171,6 @@ class Download:
|
||||
"sidebar": content["sidebar"],
|
||||
}
|
||||
)
|
||||
|
||||
with open("data/content.json", "w", encoding="utf-8") as file:
|
||||
|
||||
with (self.config.p_data / "content.json").open("w", encoding="utf-8") as file:
|
||||
json.dump(contents, file, ensure_ascii=False, indent=4)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<section>
|
||||
<h3>Navigationshilfe</h3>
|
||||
<p>Diese Navigationshilfe zeigt die ersten Schritte zur Verwendung der Präsention. Sie kann mit ⟶ (Pfeiltaste
|
||||
rechts) übersprungen werden.</p>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Navigation</h4>
|
||||
<div id="left">
|
||||
<p>Zwischen den Folien und Abschnitten kann man mittels der Pfeiltasten hin- und herspringen, dazu kann man auch
|
||||
die Pfeiltasten am Computer nutzen.</p>
|
||||
<img src="assets/images/slides/navigation.png" alt="Navigationspfeile für die Präsentation" />
|
||||
</div>
|
||||
<div id="right">
|
||||
<ul class="list">
|
||||
<li>Pfeil runter und hoch: Nächste / Vorherige Folie</li>
|
||||
<li>Pfeil rechts und links: Nächster / Vorheriger Abschnitt</li>
|
||||
<li>Leertaste oder „n“: Der Reihe nach alle Elemente in Folien aufdecken oder zur nächsten Folie blättern
|
||||
</li>
|
||||
<li>Shift-Leertaste oder „p“: Der Reihe nach Elemente rückwärts zudecken oder zur vorherigen Folie blättern
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Weitere Funktionen</h4>
|
||||
<p>Mit ein paar Tastenkürzeln können weitere Funktionen aufgerufen werden. Die wichtigsten sind:</p>
|
||||
<dl>
|
||||
<dt>F1</dt>
|
||||
<dd>Help / Hilfe</dd>
|
||||
<dt>o</dt>
|
||||
<dd>Overview / Übersicht aller Folien</dd>
|
||||
<dt>s</dt>
|
||||
<dd>Speaker View / Referentenansicht</dd>
|
||||
<dt>f</dt>
|
||||
<dd>Full Screen / Vollbildmodus</dd>
|
||||
<dt>b</dt>
|
||||
<dd>Break, Black, Pause / Ausblenden der Präsentation</dd>
|
||||
<dt>Alt-Click</dt>
|
||||
<dd>In die Folie hin- oder herauszoomen</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Übersicht</h4>
|
||||
<div id="left">
|
||||
<p>Die Präsentation ist zweidimensional aufgebaut. Dadurch sind in Spalten die einzelnen Abschnitte eines
|
||||
Kapitels und in den Reihen die Folien zu den Abschnitten.</p>
|
||||
<p>Tippt man ein „o“ ein, bekommt man eine Übersicht über alle Folien des jeweiligen Kapitels. Das hilft
|
||||
sich zunächst einen Überblick zu verschaffen oder sich zu orientieren, wenn man das Gefühlt hat sich
|
||||
„verlaufen“ zu haben. Die Navigation erfolgt über die Pfeiltasten.</p>
|
||||
</div>
|
||||
<div id="right">
|
||||
<img src="assets/images/slides/overview.png" alt="Übersicht aller Folien" />
|
||||
<p>Durch Anklicken einer Folie wird diese präsentiert.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Referentenansicht</h4>
|
||||
<div id="left">
|
||||
<img src="assets/images/slides/notes.png" alt="Referentenansicht" />
|
||||
<p>Tippt man ein „s“ ein, bekommt man ein neues Fenster, die Referentenansicht.</p>
|
||||
<p>Indem man „Layout“ auswählt, kann man zwischen verschieden Anordnungen der Elemente auswählen.</p>
|
||||
</div>
|
||||
<div id="right">
|
||||
<p>Die Referentenansicht bietet folgende Elemente:</p>
|
||||
<ul class="list">
|
||||
<li>Links sieht man die aktuelle Folie</li>
|
||||
<li>Rechts oben sieht man die nächste Folie</li>
|
||||
<li>Rechts in der Mitte Hilfsmittel zur Zeiteinteilung</li>
|
||||
<li>Rechts unten, die „Notizen für den Vortragenden“</li>
|
||||
<li>Unten die Pfeile zur Navigation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h3>Praxistipps zur Referentenansicht</h3>
|
||||
<ul class="list">
|
||||
<li>Wenn man mit einem Projektor arbeitet, stellt man im Betriebssystem die Nutzung von 2 Monitoren ein: Die
|
||||
Referentenansicht wird dann zum Beispiel auf dem Laptop angezeigt, während die Teilnehmer die
|
||||
Präsentation angezeigt bekommen.</li>
|
||||
<li>Bei einer Online-Präsentation, wie beispielsweise auf <a href="https://treff.darc.de/">TREFF.darc.de</a>
|
||||
präsentiert man den Browser-Tab und navigiert im „Speaker View“ Fenster.</li>
|
||||
<li>Die Referentenansicht bezieht sich immer auf ein Kapitel. Am Ende des Kapitels muss sie geschlossen
|
||||
werden, um im neuen Kapitel eine neue Referentenansicht zu öffnen.</li>
|
||||
<li>Um mit dem Mauszeiger etwas zu markieren oder den Zoom zu verwenden, muss mit der Maus auf den
|
||||
Bildschirm mit der Präsentation gewechselt werden.</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Vollbild</h4>
|
||||
<p>Tippt man ein „f“ ein, wird die aktuelle Folie im Vollbild angezeigt. Mit „Esc“ kann man diesen wieder
|
||||
verlassen.</p>
|
||||
<p>Das ist insbesondere für den Bildschirm mit der Präsentation für das Publikum praktisch.</li>
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Ausblenden</h4>
|
||||
<p>Tippt man ein „b“ ein, wird die Präsentation ausgeblendet.</p>
|
||||
<p>Sie kann wie folgte wieder eingeblendet werden:</p>
|
||||
<ul class="list">
|
||||
<li>Durch klicken in das Fenster.</li>
|
||||
<li>Durch nochmaliges Drücken von „b“.</li>
|
||||
<li>Durch klicken der Schaltfläche „Resume presentation:</li>
|
||||
</ul>
|
||||
<img src="assets/images/slides/resume.png" alt="Schaltfläche für Resume Presentation" />
|
||||
</section>
|
||||
<section style="font-size: smaller;">
|
||||
<h4>Zoom</h4>
|
||||
<p>Bei gedrückter Alt-Taste und einem Mausklick in der Präsentation wird in diesen Teil hineingezoomt. Das ist
|
||||
praktisch, um Details von Schaltungen hervorzuheben. Durh einen nochmaligen Mausklick zusammen mit Alt wird
|
||||
wieder herausgezoomt.</p>
|
||||
<p>Das Zoomen funktioniert nur im ausgewählten Fenster. Die Referentenansicht ist hier nicht mit dem
|
||||
Präsenationsansicht gesynct.</p>
|
||||
</section>
|
||||
@@ -0,0 +1,15 @@
|
||||
<section data-background="#DAEEFA">
|
||||
<h2>Fragen?</h2>
|
||||
<hr />
|
||||
<h3>Links zu diesem Foliensatz</h3>
|
||||
<ul>
|
||||
{% if next_chapter is defined -%}
|
||||
<li><a href="{{ edition }}_slide_{{ next_chapter["ident"] }}.html">Weiter zum nächsten Kapitel: {{
|
||||
next_chapter["title"] }}</a></li>
|
||||
{% endif -%}
|
||||
<li><a href="{{ edition }}_chapter_{{ chapter["ident"] }}.html">Zum Kapitel auf 50Ohm.de</a></li>
|
||||
<li><a href="{{ edition }}_slide_index.html">Zur Kapitelübersicht aller Foliensätze</a></li>
|
||||
<li><a href="?print-pdf">Diesen Foliensatz zum Ausdrucken öffnen</a></li>
|
||||
<li><a href="?print-pdf&showNotes=true">Diesen Foliensatz mit Notizen zum Ausdrucken öffnen</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,24 @@
|
||||
{% set answer_label = ["A", "B", "C", "D"] -%}
|
||||
{% if picture_question or (answer_pictures[0] and layout in ["1", "9", None]) -%}
|
||||
{# OHM-13: Fix layout for 4x times picture in row to 2x2 field #}
|
||||
<div class="question r-fit-text layout-8">
|
||||
{% else -%}
|
||||
<div class="question r-fit-text layout-{{ layout }}">
|
||||
{% endif -%}
|
||||
<div class="question-text">{{ number }}: {{ question }}</div>
|
||||
{% if picture_question -%}
|
||||
<div class="question-picture"><img class="r-stretch" src="pictures/{{ picture_question }}.svg"></div>
|
||||
{% endif -%}
|
||||
<div class="answers">
|
||||
{% if answers[0] -%}
|
||||
{% for answer in answers|shuffle_answers -%}
|
||||
<p{% if not answer.correct %} class="fragment semi-fade-out" data-fragment-index="1" {% endif -%}><b>{{ answer_label[loop.index - 1] }}:</b> {{ answer.content }}</p>
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
{% if answer_pictures[0] -%}
|
||||
{% for answer in answer_pictures|shuffle_answers -%}
|
||||
<div class="question-picture{% if not answer.correct %} fragment semi-fade-out{% endif -%}" {% if not answer.correct -%} data-fragment-index="1" {% endif -%}><b>{{ answer_label[loop.index - 1] }}:</b><img class="r-stretch" src="pictures/{{ answer.content }}.svg"></div>
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DARC Ausbildungsplattform</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="assets/reveal.js/dist/reset.css">
|
||||
<link rel="stylesheet" href="assets/reveal.js/dist/reveal.css">
|
||||
<link rel="stylesheet" href="assets/style-slide.css">
|
||||
<!-- Theme used for syntax highlighting of code -->
|
||||
<link rel="stylesheet" href="assets/reveal.js/plugin/highlight/monokai.css">
|
||||
<!-- Chart plugin -->
|
||||
<script src="assets/reveal.js-plugins/chart/plugin.js"></script>
|
||||
<script src="assets/chartjs/4.4.1/chart.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<!-- Any section element inside of this container is displayed as a slide -->
|
||||
<div class="slides">
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
<script src="assets/reveal.js/dist/reveal.js"></script>
|
||||
<script src="assets/reveal.js/plugin/zoom/zoom.js"></script>
|
||||
<script src="assets/reveal.js/plugin/notes/notes.js"></script>
|
||||
<script src="assets/reveal.js/plugin/search/search.js"></script>
|
||||
<script src="assets/reveal.js/plugin/markdown/markdown.js"></script>
|
||||
<script src="assets/reveal.js/plugin/highlight/highlight.js"></script>
|
||||
<script src="assets/reveal.js/plugin/math/math.js"></script>
|
||||
<script>
|
||||
|
||||
// Also available as an ES module, see:
|
||||
// https://revealjs.com/initialization/
|
||||
Reveal.initialize({
|
||||
controls: true,
|
||||
progress: true,
|
||||
slideNumber: 'c/t',
|
||||
showSlideNumber: 'speaker',
|
||||
center: true,
|
||||
hash: true,
|
||||
transition: 'slide',
|
||||
showNotes: false,
|
||||
pdfSeparateFragments: false,
|
||||
|
||||
// Learn about plugins: https://revealjs.com/plugins/
|
||||
plugins: [ RevealZoom, RevealNotes, RevealSearch, RevealMarkdown, RevealHighlight, RevealMath.KaTeX, RevealChart ]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<h1>Folien: {{ book["title"] }}</h1>
|
||||
<p>{{ book["abstract"] }}</p>
|
||||
<div class="chapter-toc d-grid gap-2">
|
||||
{% for chapter in book["chapters"] %}
|
||||
<a class="btn btn-outline-primary btn-labeled" href="{{ book["edition"] }}_slide_{{ chapter["ident"] }}.html">
|
||||
<span class="btn-label">{{loop.index}}</span>{{ chapter["title"]}}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
|
||||
# Akzeptanztest
|
||||
|
||||
--- style="font-size: 4pt;"
|
||||
|
||||
## Kleinere Schrift
|
||||
* Längere
|
||||
* Liste
|
||||
|
||||
---
|
||||
|
||||
## Folie mit direktem Übergang
|
||||
Mit Text
|
||||
|
||||
--- data-transition="none"
|
||||
|
||||
## Folie mit direktem Übergang
|
||||
Mit anderem Text
|
||||
|
||||
---
|
||||
|
||||
## Externer Link und Formel
|
||||
|
||||
[Link zum DARC](https://darc.de/)
|
||||
|
||||
$\lambda[[\text{m}]] = \dfrac{300}{f[[\text{MHz}]]} = \dfrac{300}{145,3 \ \text{MHz}} \approx 2,06 \ \text{m}$
|
||||
|
||||
---
|
||||
|
||||
## Notes Test
|
||||
|
||||
Diese Folie hat Notizen
|
||||
|
||||
<note>
|
||||
Die Notiz wird nur in der Speaker-Ansicht angezeigt
|
||||
* Listen sind auch möglich
|
||||
* Am besten <u>kurz</u> halten
|
||||
* Der Platz ist *begrenzt*
|
||||
</note>
|
||||
|
||||
---
|
||||
|
||||
## Fragment Test
|
||||
|
||||
Dies ist ein Text der direkt da ist
|
||||
|
||||
<fragment>
|
||||
Dieser Text erscheint erst später.
|
||||
</fragment>
|
||||
|
||||
---
|
||||
|
||||
## Slide mit QSO
|
||||
|
||||
<qso>
|
||||
Ist diese Frequenz frei? DL1PZ
|
||||
> *(keine Antwort)*
|
||||
Ist diese Frequenz frei? DL1PZ
|
||||
> *(keine Antwort)*
|
||||
CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
|
||||
> DL1PZ hier ist DL9MJ bitte kommen
|
||||
</qso>
|
||||
|
||||
---
|
||||
|
||||
## Slide mit Picture
|
||||
|
||||
[picture:123:p_test:testbild]
|
||||
|
||||
---
|
||||
|
||||
## Slide mit Photo
|
||||
|
||||
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
|
||||
|
||||
---
|
||||
[question:2482]
|
||||
---
|
||||
|
||||
## Slide Tabelle
|
||||
|
||||
| l: Wert | l: Bereich | l: Bedeutung | l: Englisch |
|
||||
| R | 1 - 5 | Lesbarkeit | Readability |
|
||||
| S | 1 - 9 | Signalstärke | Signal Strength |
|
||||
| T | 1 - 9 | Tonqualität | Tone |
|
||||
[table:n_rst:Die Bestandteile des RST-Rapports]
|
||||
|
||||
---
|
||||
|
||||
## Slide Mit Markdown
|
||||
|
||||
Das ist ein *Text*
|
||||
|
||||
* Das ist ein Bullet
|
||||
* Das ist noch ein Bullet
|
||||
|
||||
Hier wird <u>text</u> unterstrichen
|
||||
|
||||
--- unit
|
||||
|
||||
## Slide Unit
|
||||
|
||||
Slide C
|
||||
|
||||
--- data-background="#FF00FF"
|
||||
|
||||
## Slide PINK
|
||||
|
||||
PINK
|
||||
|
||||
## Iframe als Hintergrund
|
||||
|
||||
--- data-background-iframe="https://f5len.org/tools/locator/" data-background-interactive
|
||||
|
||||
---
|
||||
|
||||
## Double Column
|
||||
|
||||
<left>
|
||||
[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF]
|
||||
</left>
|
||||
<right>
|
||||
* Funkstationen verwenden Rufzeichen, um sich zu identifizieren
|
||||
* Folge von Buchstaben und Ziffern
|
||||
* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen
|
||||
</right>
|
||||
|
||||
---
|
||||
|
||||
## Folie mit direktem Übergang
|
||||
Mit Text
|
||||
|
||||
--- data-transition="none"
|
||||
|
||||
## Folie mit direktem Übergang
|
||||
Mit anderem Text ... keine ahnung ob das je ging?
|
||||
@@ -0,0 +1,98 @@
|
||||
import json
|
||||
import random
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from mistletoe import Document
|
||||
|
||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||
|
||||
|
||||
def parse_katalog():
|
||||
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
|
||||
fragenkatalog = json.load(fragenkatalog_file)
|
||||
|
||||
questions = {}
|
||||
|
||||
for exampart in fragenkatalog["sections"]:
|
||||
for chapter in exampart["sections"]:
|
||||
if "questions" in chapter:
|
||||
for question in chapter["questions"]:
|
||||
questions[question["number"]] = question
|
||||
if "sections" in chapter:
|
||||
for section in chapter["sections"]:
|
||||
for question in section["questions"]:
|
||||
questions[question["number"]] = question
|
||||
|
||||
return questions
|
||||
|
||||
|
||||
def filter_shuffle_answers(seq):
|
||||
try:
|
||||
answers = []
|
||||
firstrun = True
|
||||
for answer in seq:
|
||||
if firstrun:
|
||||
answers.append({"content": answer, "correct": True})
|
||||
firstrun = False
|
||||
else:
|
||||
answers.append({"content": answer, "correct": False})
|
||||
random.shuffle(answers)
|
||||
return answers
|
||||
except Exception:
|
||||
return seq
|
||||
|
||||
|
||||
def question_stub(input):
|
||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||
with open("data/metadata.json") as metadata_file:
|
||||
metadata = json.load(metadata_file)
|
||||
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
|
||||
|
||||
question = questions[number]
|
||||
metadata = metadata[f"{input}"]
|
||||
|
||||
if "answer_a" in question:
|
||||
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
||||
else:
|
||||
answers = []
|
||||
|
||||
if metadata["picture_a"] != "":
|
||||
answer_pictures = [
|
||||
metadata["picture_a"],
|
||||
metadata["picture_b"],
|
||||
metadata["picture_c"],
|
||||
metadata["picture_d"],
|
||||
]
|
||||
else:
|
||||
answer_pictures = []
|
||||
|
||||
if "picture_question" in question:
|
||||
picture_question = metadata["picture_question"]
|
||||
else:
|
||||
picture_question = ""
|
||||
|
||||
return question_template.render(
|
||||
question=question["question"],
|
||||
number=number,
|
||||
layout=metadata["layout"],
|
||||
picture_question=picture_question,
|
||||
answers=answers,
|
||||
answer_pictures=answer_pictures,
|
||||
)
|
||||
|
||||
|
||||
env = Environment(loader=FileSystemLoader("templates/"))
|
||||
env.filters["shuffle_answers"] = filter_shuffle_answers
|
||||
question_template = env.get_template("slide/question.html")
|
||||
slide_template = env.get_template("slide/slide.html")
|
||||
questions = parse_katalog()
|
||||
|
||||
|
||||
def test_html_slides(capsys):
|
||||
with capsys.disabled():
|
||||
with open("test/acceptanceTestSlides.md") as file:
|
||||
content = file.read()
|
||||
with FiftyOhmHtmlSlideRenderer(question_renderer=question_stub) as renderer:
|
||||
output = renderer.render(Document(content))
|
||||
with open("test/acceptanceTestSlides.html", "w") as output_file:
|
||||
output_file.write(output)
|
||||
+12
-1
@@ -2,10 +2,11 @@ import mistletoe
|
||||
import pytest
|
||||
|
||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||
|
||||
|
||||
@pytest.mark.html
|
||||
def test_table_html():
|
||||
def test_qso_html():
|
||||
assertions = {
|
||||
"<qso>\nfoo\n</qso>": FiftyOhmHtmlRenderer.render_tag_helper("qso", '<div class="qso_own">foo</div>\n', 1, 0)
|
||||
+ "\n",
|
||||
@@ -21,3 +22,13 @@ def test_table_html():
|
||||
|
||||
for assertion in assertions:
|
||||
assert mistletoe.markdown(assertion, FiftyOhmHtmlRenderer) == assertions[assertion]
|
||||
|
||||
@pytest.mark.slide
|
||||
def test_qso_slide():
|
||||
assertions = {
|
||||
"<qso>\nfoo\n> bar\n</qso>":
|
||||
'<div class="qso r-fit-text">\n<div class="qso_own fragment fade-right">foo</div>\n<div class="qso_other fragment fade-left">bar</div>\n</div>\n\n',
|
||||
}
|
||||
|
||||
for assertion in assertions:
|
||||
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_question_html():
|
||||
def test_function(input):
|
||||
return f"{input}"
|
||||
|
||||
with FiftyOhmHtmlRenderer(test_function) as renderer:
|
||||
with FiftyOhmHtmlRenderer(question_renderer=test_function) as renderer:
|
||||
for assertion in assertions:
|
||||
assert renderer.render(Document(assertion)) == assertions[assertion]
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_question_translation_html():
|
||||
metadata = json.load(file)
|
||||
return metadata[f"{input}"].get("number")
|
||||
|
||||
with FiftyOhmHtmlRenderer(test_function) as renderer:
|
||||
with FiftyOhmHtmlRenderer(question_renderer=test_function) as renderer:
|
||||
for assertion in assertions:
|
||||
assert renderer.render(Document(assertion)) == assertions[assertion]
|
||||
|
||||
|
||||
+30
-1
@@ -2,6 +2,7 @@ import mistletoe
|
||||
import pytest
|
||||
|
||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||
from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer
|
||||
|
||||
|
||||
@@ -29,7 +30,8 @@ def test_tag_html():
|
||||
+ "\n",
|
||||
"<unit>\nFoo\n</unit>": FiftyOhmHtmlRenderer.render_tag_helper("unit", "<p>Foo</p>", 1, 0) + "\n",
|
||||
"<danger>\nFoo\n</danger>": FiftyOhmHtmlRenderer.render_tag_helper("danger", "<p>Foo</p>", 1, 0) + "\n",
|
||||
"<wordorigin>\nFoo\n</wordorigin>": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "<p>Foo</p>", 1, 0) + "\n",
|
||||
"<wordorigin>\nFoo\n</wordorigin>": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "<p>Foo</p>", 1, 0)
|
||||
+ "\n",
|
||||
"<webonly>\nFoo\n</webonly>": "<p>Foo</p>\n",
|
||||
"<latexonly>\nFoo\n</latexonly>": "",
|
||||
}
|
||||
@@ -103,3 +105,30 @@ def test_thematic_break_and_tag_latex():
|
||||
|
||||
for assertion in assertions:
|
||||
assert mistletoe.markdown(assertion, FiftyOhmLaTeXRenderer) == assertions[assertion]
|
||||
|
||||
|
||||
@pytest.mark.slide
|
||||
def test_tag_slide():
|
||||
assertions = {
|
||||
"<note>\nFoo\n</note>": '<aside class="notes">\n<p>Foo</p>\n</aside>\n\n',
|
||||
"<fragment>\nFoo\n</fragment>": '<div class="fragment">\n<p>Foo</p>\n</div>\n\n',
|
||||
"<left>\nFoo\n</left>": '<div id="left">\n<p>Foo</p>\n</div>\n\n',
|
||||
"<right>\nFoo\n</right>": '<div id="right">\n<p>Foo</p>\n</div>\n\n',
|
||||
}
|
||||
|
||||
for assertion in assertions:
|
||||
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]
|
||||
|
||||
|
||||
@pytest.mark.slide
|
||||
def test_slide_break():
|
||||
assertions = {
|
||||
"---\na\n": "<section>\n<p>a</p>\n</section>\n\n",
|
||||
"---\na\n---\nb\n": "<section>\n<p>a</p>\n</section>\n\n<section>\n<p>b</p>\n</section>\n\n",
|
||||
"--- foo\na\n": "<section foo>\n<p>a</p>\n</section>\n\n",
|
||||
"--- foo bar\na\n": "<section foo bar>\n<p>a</p>\n</section>\n\n",
|
||||
"--- foo bar \na\n": "<section foo bar>\n<p>a</p>\n</section>\n\n",
|
||||
}
|
||||
|
||||
for assertion in assertions:
|
||||
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]
|
||||
|
||||
Reference in New Issue
Block a user