From 54ecfb99b8b5bacbd0f5813a39e59e70d31f166f Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 14:32:40 +0200 Subject: [PATCH 01/26] feature: add slides doku from lars --- doc/Slides.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 doc/Slides.md diff --git a/doc/Slides.md b/doc/Slides.md new file mode 100644 index 0000000..90e3293 --- /dev/null +++ b/doc/Slides.md @@ -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 unterstrichen verwendet werden. + +``` MarkDown +Es kann einfache Textauszeichnung, wie *hervorgehoben* oder unterstrichen 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 + +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 + +``` + +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 `` eingebettet werden. + +``` MarkDown + +Dieser Text erscheint erst später. + +``` + +Mehrere ``-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 + +Die Notiz wird nur in der Speaker-Ansicht angezeigt +* Listen sind auch möglich +* Am besten kurz halten +* Der Platz ist begrenzt + +``` + +### 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 `` und ``. + +``` MarkDown + +[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF] + + +* Funkstationen verwenden Rufzeichen, um sich zu identifizieren +* Folge von Buchstaben und Ziffern +* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen + +``` + +### 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 + + +* Es wird eine interaktive Karte mit Maidenhead-Locator gezeigt. + +``` + +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 +``` From 44189e881639b30c147d4dd529c8b85e8e0db6fe Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 14:35:32 +0200 Subject: [PATCH 02/26] feature: first slide rendering and acceptance test based on html renderer --- renderer/fifty_ohm_html_renderer.py | 12 +- renderer/fifty_ohm_html_slide_renderer.py | 35 +++++ renderer/slide_break.py | 35 +++++ src/build.py | 6 +- templates/slide/question.html | 24 ++++ test/acceptanceTestSlides.html | 129 +++++++++++++++++ test/acceptanceTestSlides.md | 44 ++++++ test/test_html.py | 2 +- test/test_html_slides.py | 160 ++++++++++++++++++++++ test/test_question.py | 4 +- 10 files changed, 444 insertions(+), 7 deletions(-) create mode 100644 renderer/fifty_ohm_html_slide_renderer.py create mode 100644 renderer/slide_break.py create mode 100644 templates/slide/question.html create mode 100644 test/acceptanceTestSlides.html create mode 100644 test/acceptanceTestSlides.md create mode 100644 test/test_html_slides.py diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index fcc1329..72f929a 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -1,5 +1,7 @@ +from itertools import chain + from jinja2 import Environment, FileSystemLoader from mistletoe import Document, HtmlRenderer @@ -28,8 +30,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): section_url = "section.html" ref_id = 0 - def __init__(self, question_renderer=None, picture_handler=None, photo_handler=None): - super().__init__( + def __init__(self, *extras, question_renderer=None, picture_handler=None, photo_handler=None): + + final_extras = chain(extras,( Dash, BlockComment, Quote, @@ -46,7 +49,10 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): Photo, Table, Qso, - ) + )) + + super().__init__(*final_extras) + self.question_renderer = question_renderer self.picture_handler = picture_handler self.photo_handler = photo_handler diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py new file mode 100644 index 0000000..54aeb4e --- /dev/null +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -0,0 +1,35 @@ +from mistletoe import Document + +from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer + +from .slide_break import SlideBreak + + +class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): + + def __init__(self, question_renderer=None, picture_handler=None, photo_handler=None): + super().__init__( + SlideBreak, + question_renderer=question_renderer, + picture_handler=picture_handler, + photo_handler=photo_handler, + ) + + def render_wrapper(self, content): + content = "---\n" + content + return super().render(Document(content)) + + def render_slide_break(self, token): + inner = self.render_inner(token) + + if token.attribute is None: + return f'
\n{inner}\n
\n' + if token.attribute == "attention": + return f'
\n{inner}\n
\n' + elif token.attribute == "danger": + return f'
\n{inner}\n
\n' + elif token.attribute == "unit": + return f'
\n{inner}\n
\n' + else: + return f'
\n{inner}\n
\n' + diff --git a/renderer/slide_break.py b/renderer/slide_break.py new file mode 100644 index 0000000..dadfe08 --- /dev/null +++ b/renderer/slide_break.py @@ -0,0 +1,35 @@ + +# ^\s*---\s*(.*)\s*$ +import re + +from mistletoe.block_token import BlockToken, tokenize + + +class SlideBreak(BlockToken): + @staticmethod + def start(line): + return re.match(r"^\s*---.*$", line) + + @classmethod + def read(cls, lines): + child_lines = [] + first_line = next(lines) # Consume the line that started the slide break + + match = re.match(r"^\s*---\s*(.*)\s*.*$", first_line) + attribute = match.group(1) 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 re.match(r"^\s*---.*$", 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 diff --git a/src/build.py b/src/build.py index f5aa932..e726b13 100644 --- a/src/build.py +++ b/src/build.py @@ -135,7 +135,11 @@ class Build: next_chapter_template = self.env.get_template("next_chapter.html") with open(f'build/{edition}_{section["ident"]}.html', 'w') as 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 + ) as renderer: section["content"] = renderer.render(Document(section["content"])) result = section_template.render( diff --git a/templates/slide/question.html b/templates/slide/question.html new file mode 100644 index 0000000..78e782b --- /dev/null +++ b/templates/slide/question.html @@ -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 #} +
+{% else -%} +
+{% endif -%} +
{{ number }}: {{ question }}
+ {% if picture_question -%} +
+ {% endif -%} +
+ {% if answers[0] -%} + {% for answer in answers|shuffle_answers -%} + {{ answer_label[loop.index - 1] }}: {{ answer.content }}

+ {% endfor -%} + {% endif -%} + {% if answer_pictures[0] -%} + {% for answer in answer_pictures|shuffle_answers -%} +
{{ answer_label[loop.index - 1] }}:
+ {% endfor -%} + {% endif -%} +
+
diff --git a/test/acceptanceTestSlides.html b/test/acceptanceTestSlides.html new file mode 100644 index 0000000..cf78af5 --- /dev/null +++ b/test/acceptanceTestSlides.html @@ -0,0 +1,129 @@ + + + + DARC Ausbildungsplattform + + + + + + + + + + + + + + + + + +
+
+
+

Slide mit Picture

+

+ +
Abbildung TODO: testbild
+
+

+
+ +
+

Slide mit Photo

+

+ +
Abbildung TODO: Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt
+
+

+
+ +
+ +
+
NB201: Welches Bauteil wird durch das Schaltzeichen symbolisiert?
+
+
+

A: Widerstand

+

B: Batterie

+

C: Diode

+

D: Kondensator

+
+
+
+ +
+

Slide Tabelle

+ + + + + + + + + + + + + + + + + + + + + + + + +
WertBereichBedeutungEnglisch
R1 – 5LesbarkeitReadability
S1 – 9SignalstärkeSignal Strength
T1 – 9TonqualitätTone
Die Bestandteile des RST-Rapports
+
+ +
+

Slide Mit Markdown

Das ist ein Text

    +
  • Das ist ein Bullet
  • +
  • Das ist noch ein Bullet
  • +

Hier wird text unterstrichen

+
+ +
+

Slide Unit

Slide C

+
+ +
+

Slide PINK

PINK

+
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/test/acceptanceTestSlides.md b/test/acceptanceTestSlides.md new file mode 100644 index 0000000..18290a3 --- /dev/null +++ b/test/acceptanceTestSlides.md @@ -0,0 +1,44 @@ +## 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 text unterstrichen + +--- unit + +## Slide Unit + +Slide C + +--- data-background="#FF00FF" + +## Slide PINK + +PINK \ No newline at end of file diff --git a/test/test_html.py b/test/test_html.py index 2d3844c..ec20f45 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -74,7 +74,7 @@ def test_html(capsys): with capsys.disabled(): with open("test/acceptanceTest.md") as file: content = file.read() - with FiftyOhmHtmlRenderer(question_stub) as renderer: + with FiftyOhmHtmlRenderer(question_renderer=question_stub) as renderer: output = renderer.render(Document(content)) with open("test/acceptanceTest.html", "w") as output_file: pretty = BeautifulSoup(output, "html.parser").prettify() diff --git a/test/test_html_slides.py b/test/test_html_slides.py new file mode 100644 index 0000000..f7a9dbd --- /dev/null +++ b/test/test_html_slides.py @@ -0,0 +1,160 @@ +import json +import random + +from jinja2 import Environment, FileSystemLoader + +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 + + +env = Environment(loader=FileSystemLoader("templates/")) +env.filters["shuffle_answers"] = filter_shuffle_answers +question_template = env.get_template("slide/question.html") +questions = parse_katalog() + + +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, + ) + + +def print_head(): + return """ + + + DARC Ausbildungsplattform + + + + + + + + + + + + + + + + + +
+
+""" + + # + # + # + +def print_tail(): + return """
+
+ + + + + + + + + """ + +def test_html_slides(capsys): + with capsys.disabled(): + with open("test/acceptanceTestSlides.md") as file: + content = file.read() + with FiftyOhmHtmlSlideRenderer(question_stub) as renderer: + output = "" + output = print_head() + output += renderer.render_wrapper(content) + output += print_tail() + with open("test/acceptanceTestSlides.html", "w") as output_file: + output_file.write(output) diff --git a/test/test_question.py b/test/test_question.py index d45d9ca..526677d 100644 --- a/test/test_question.py +++ b/test/test_question.py @@ -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] From 83eeef6c7c6c27acc21d3ddc67632fbe2a4a37b1 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 14:58:06 +0200 Subject: [PATCH 03/26] feature: support qso in slides --- renderer/fifty_ohm_html_slide_renderer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 54aeb4e..1c9e98e 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -1,7 +1,9 @@ +from jinja2 import Environment, FileSystemLoader from mistletoe import Document from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer +from .qso import Qso from .slide_break import SlideBreak @@ -10,10 +12,12 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): def __init__(self, question_renderer=None, picture_handler=None, photo_handler=None): super().__init__( SlideBreak, + Qso, question_renderer=question_renderer, picture_handler=picture_handler, photo_handler=photo_handler, ) + self.env = Environment(loader=FileSystemLoader("templates/slide")) def render_wrapper(self, content): content = "---\n" + content @@ -33,3 +37,11 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): else: return f'
\n{inner}\n
\n' + def render_qso(self, token): + qso = "" + 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'
{self.render_inner(child)}
\n' + return qso + From 43e544558e27428b76ce2e29df67720751894565 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 14:58:38 +0200 Subject: [PATCH 04/26] feature: support fragments in slides --- renderer/tag.py | 1 + 1 file changed, 1 insertion(+) diff --git a/renderer/tag.py b/renderer/tag.py index 460d16a..376d867 100644 --- a/renderer/tag.py +++ b/renderer/tag.py @@ -18,6 +18,7 @@ captures = [ "webonly", "latexonly", "wordorigin", + "fragment", ] From ae433d4fc7f432a6cfdbd273d43cfdf89f2d1fae Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 14:59:11 +0200 Subject: [PATCH 05/26] feature: add qso and fragments to test --- test/acceptanceTestSlides.md | 21 +++++++++++++++++++++ test/test_html_slides.py | 1 - 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/acceptanceTestSlides.md b/test/acceptanceTestSlides.md index 18290a3..0302de7 100644 --- a/test/acceptanceTestSlides.md +++ b/test/acceptanceTestSlides.md @@ -1,3 +1,24 @@ +## Fragment Test + +Dies ist ein Text der direkt da ist + + +Dieser Text erscheint erst später. + + +--- + +## Slide mit 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 + + ## Slide mit Picture [picture:123:p_test:testbild] diff --git a/test/test_html_slides.py b/test/test_html_slides.py index f7a9dbd..86fadda 100644 --- a/test/test_html_slides.py +++ b/test/test_html_slides.py @@ -84,7 +84,6 @@ def question_stub(input): answer_pictures=answer_pictures, ) - def print_head(): return """ From 77eb1f1c39ab88a0d6537927e43affa85eddadc1 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 15:53:10 +0200 Subject: [PATCH 06/26] feature: tags for slides --- renderer/fifty_ohm_html_slide_renderer.py | 20 ++++++- renderer/slide_break.py | 1 - renderer/tag.py | 5 ++ test/acceptanceTestSlides.md | 72 ++++++++++++++++++++++- test/test_html_slides.py | 3 +- 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 1c9e98e..d5fc61e 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -5,6 +5,7 @@ from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer from .qso import Qso from .slide_break import SlideBreak +from .tag import Tag class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): @@ -13,6 +14,7 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): super().__init__( SlideBreak, Qso, + Tag, question_renderer=question_renderer, picture_handler=picture_handler, photo_handler=photo_handler, @@ -38,10 +40,26 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): return f'
\n{inner}\n
\n' def render_qso(self, token): - qso = "" + qso = '
\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'
{self.render_inner(child)}
\n' + qso += '
\n' return qso + def render_note(self, token): + return f'\n' + + def render_tag(self, token): + if token.tagtype == "fragment": + return f'
\n{self.render_inner(token)}\n
\n' + if token.tagtype == "left": + return f'
\n{self.render_inner(token)}\n
\n' + if token.tagtype == "right": + return f'\n' + elif token.tagtype == "note": + return f'\n' + else: + return "" # Ignore other tags in slide context + diff --git a/renderer/slide_break.py b/renderer/slide_break.py index dadfe08..84bde86 100644 --- a/renderer/slide_break.py +++ b/renderer/slide_break.py @@ -1,5 +1,4 @@ -# ^\s*---\s*(.*)\s*$ import re from mistletoe.block_token import BlockToken, tokenize diff --git a/renderer/tag.py b/renderer/tag.py index 376d867..803385d 100644 --- a/renderer/tag.py +++ b/renderer/tag.py @@ -3,6 +3,7 @@ import re from mistletoe.block_token import BlockToken, tokenize captures = [ + # Website: "margin", "indepth", "webmargin", @@ -18,7 +19,11 @@ captures = [ "webonly", "latexonly", "wordorigin", + # Slides: + "note", "fragment", + "left", + "right", ] diff --git a/test/acceptanceTestSlides.md b/test/acceptanceTestSlides.md index 0302de7..c24e6c5 100644 --- a/test/acceptanceTestSlides.md +++ b/test/acceptanceTestSlides.md @@ -1,3 +1,44 @@ +# 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 + + +Die Notiz wird nur in der Speaker-Ansicht angezeigt +* Listen sind auch möglich +* Am besten kurz halten +* Der Platz ist *begrenzt* + + +--- + ## Fragment Test Dies ist ein Text der direkt da ist @@ -19,6 +60,8 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört. > DL1PZ hier ist DL9MJ bitte kommen +--- + ## Slide mit Picture [picture:123:p_test:testbild] @@ -62,4 +105,31 @@ Slide C ## Slide PINK -PINK \ No newline at end of file +PINK + +## Iframe als Hintergrund + +--- data-background-iframe="https://f5len.org/tools/locator/" data-background-interactive + +--- + +## Double Column + + +[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF] + + +* Funkstationen verwenden Rufzeichen, um sich zu identifizieren +* Folge von Buchstaben und Ziffern +* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen + + +--- + +## Folie mit direktem Übergang +Mit Text + +--- data-transition="none" + +## Folie mit direktem Übergang +Mit anderem Text ... keine ahnung ob das je ging? \ No newline at end of file diff --git a/test/test_html_slides.py b/test/test_html_slides.py index 86fadda..06362b0 100644 --- a/test/test_html_slides.py +++ b/test/test_html_slides.py @@ -85,7 +85,8 @@ def question_stub(input): ) def print_head(): - return """ + return """ + DARC Ausbildungsplattform From 04e9eabbbb07bbeee32f2ca726a781436c65461c Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 15:54:16 +0200 Subject: [PATCH 07/26] refactor: gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b9ff1d4..b5796f8 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,7 @@ cython_debug/ #.idea/ test/acceptanceTest.html +test/acceptanceTestSlides.html .DS_Store .envrc config/config.json From dcfeb78bfa13928d55c2dcae3829a9facf656c43 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 15:55:24 +0200 Subject: [PATCH 08/26] fix: removed acceptance test result which was accidently committed --- test/acceptanceTestSlides.html | 129 --------------------------------- 1 file changed, 129 deletions(-) delete mode 100644 test/acceptanceTestSlides.html diff --git a/test/acceptanceTestSlides.html b/test/acceptanceTestSlides.html deleted file mode 100644 index cf78af5..0000000 --- a/test/acceptanceTestSlides.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - DARC Ausbildungsplattform - - - - - - - - - - - - - - - - - -
-
-
-

Slide mit Picture

-

- -
Abbildung TODO: testbild
-
-

-
- -
-

Slide mit Photo

-

- -
Abbildung TODO: Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt
-
-

-
- -
- -
-
NB201: Welches Bauteil wird durch das Schaltzeichen symbolisiert?
-
-
-

A: Widerstand

-

B: Batterie

-

C: Diode

-

D: Kondensator

-
-
-
- -
-

Slide Tabelle

- - - - - - - - - - - - - - - - - - - - - - - - -
WertBereichBedeutungEnglisch
R1 – 5LesbarkeitReadability
S1 – 9SignalstärkeSignal Strength
T1 – 9TonqualitätTone
Die Bestandteile des RST-Rapports
-
- -
-

Slide Mit Markdown

Das ist ein Text

    -
  • Das ist ein Bullet
  • -
  • Das ist noch ein Bullet
  • -

Hier wird text unterstrichen

-
- -
-

Slide Unit

Slide C

-
- -
-

Slide PINK

PINK

-
- -
-
- - - - - - - - - \ No newline at end of file From 8a5a55371df5cba6251156a3a4dce4d5ef4e9357 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 20:34:54 +0200 Subject: [PATCH 09/26] feature: finished slide building process --- src/build.py | 75 ++++++++++++++++++----- templates/slide/help.html | 112 ++++++++++++++++++++++++++++++++++ templates/slide/question.html | 4 +- templates/slide/slide.html | 56 +++++++++++++++++ test/test_html_slides.py | 81 +++--------------------- 5 files changed, 236 insertions(+), 92 deletions(-) create mode 100644 templates/slide/help.html create mode 100644 templates/slide/slide.html diff --git a/src/build.py b/src/build.py index e726b13..2db2fed 100644 --- a/src/build.py +++ b/src/build.py @@ -1,5 +1,6 @@ import json import os +import random import shutil from bs4 import BeautifulSoup @@ -9,19 +10,23 @@ 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 class Build: def __init__(self): 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 = memory.cache(self.__build_question) + 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: @@ -42,10 +47,10 @@ 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) @@ -89,9 +94,12 @@ 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) : - page_template = self.env.get_template("page.html") + page_template = self.env.get_template("html/page.html") return page_template.render(content=content) def __picture_handler(self, id): @@ -108,8 +116,8 @@ class Build: # 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") + chapter_template = self.env.get_template("html/chapter.html") + next_chapter_template = self.env.get_template("html/next_chapter.html") with open(f'build/{edition}_chapter_{chapter["ident"]}.html', 'w') as file: result = chapter_template.render( edition=edition, @@ -129,10 +137,10 @@ class Build: file.write(result) # 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") + 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 open(f'build/{edition}_{section["ident"]}.html', 'w') as file: with FiftyOhmHtmlRenderer( @@ -146,6 +154,7 @@ class Build: edition=edition, name=edition_name, section=section, + section_id=section_id, chapter=chapter, ) @@ -164,8 +173,41 @@ class Build: result = BeautifulSoup(result, "html.parser").prettify() file.write(result) + def __build_chapter_slidedeck(self, edition, chapter, sections): + with open(f'build/{edition}_slide_{chapter["ident"]}.html', 'w') as file: + slide_template = self.env.get_template("slide/slide.html") + help_template = self.env.get_template("slide/help.html") + with FiftyOhmHtmlSlideRenderer( + question_renderer=self.__build_question_slide, + picture_handler=self.__picture_handler, + photo_handler=self.__photo_handler + ) as renderer: + result = "
\n" + result += f'
\n

{chapter["title"]}

\n
\n' + result += help_template.render() + result += "
\n" + for section in sections: + tmp = f'
\n

{section["title"]}

\n
\n' + tmp += renderer.render_wrapper(section["slide"]) + result += f"
{tmp}
\n" + + 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") + template = self.env.get_template("html/course_index.html") with open(f"build/{book['edition']}_course_index.html", "w") as file: result = template.render( book=book, @@ -185,10 +227,11 @@ class Build: 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"]) 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) \ No newline at end of file diff --git a/templates/slide/help.html b/templates/slide/help.html new file mode 100644 index 0000000..036c33a --- /dev/null +++ b/templates/slide/help.html @@ -0,0 +1,112 @@ +
+

Navigationshilfe

+

Diese Navigationshilfe zeigt die ersten Schritte zur Verwendung der Präsention. Sie kann mit ⟶ (Pfeiltaste + rechts) übersprungen werden.

+
+
+

Navigation

+
+

Zwischen den Folien und Abschnitten kann man mittels der Pfeiltasten hin- und herspringen, dazu kann man auch + die Pfeiltasten am Computer nutzen.

+ Navigationspfeile für die Präsentation +
+ +
+
+

Weitere Funktionen

+

Mit ein paar Tastenkürzeln können weitere Funktionen aufgerufen werden. Die wichtigsten sind:

+
+
F1
+
Help / Hilfe
+
o
+
Overview / Übersicht aller Folien
+
s
+
Speaker View / Referentenansicht
+
f
+
Full Screen / Vollbildmodus
+
b
+
Break, Black, Pause / Ausblenden der Präsentation
+
Alt-Click
+
In die Folie hin- oder herauszoomen
+
+
+
+

Übersicht

+
+

Die Präsentation ist zweidimensional aufgebaut. Dadurch sind in Spalten die einzelnen Abschnitte eines + Kapitels und in den Reihen die Folien zu den Abschnitten.

+

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.

+
+ +
+
+

Referentenansicht

+
+ Referentenansicht +

Tippt man ein „s“ ein, bekommt man ein neues Fenster, die Referentenansicht.

+

Indem man „Layout“ auswählt, kann man zwischen verschieden Anordnungen der Elemente auswählen.

+
+ +
+
+

Praxistipps zur Referentenansicht

+
    +
  • 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.
  • +
  • Bei einer Online-Präsentation, wie beispielsweise auf TREFF.darc.de + präsentiert man den Browser-Tab und navigiert im „Speaker View“ Fenster.
  • +
  • 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.
  • +
  • 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.
  • +
+
+
+

Vollbild

+

Tippt man ein „f“ ein, wird die aktuelle Folie im Vollbild angezeigt. Mit „Esc“ kann man diesen wieder + verlassen.

+

Das ist insbesondere für den Bildschirm mit der Präsentation für das Publikum praktisch. +

+
+

Ausblenden

+

Tippt man ein „b“ ein, wird die Präsentation ausgeblendet.

+

Sie kann wie folgte wieder eingeblendet werden:

+
    +
  • Durch klicken in das Fenster.
  • +
  • Durch nochmaliges Drücken von „b“.
  • +
  • Durch klicken der Schaltfläche „Resume presentation:
  • +
+ Schaltfläche für Resume Presentation +
+
+

Zoom

+

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.

+

Das Zoomen funktioniert nur im ausgewählten Fenster. Die Referentenansicht ist hier nicht mit dem + Präsenationsansicht gesynct.

+
diff --git a/templates/slide/question.html b/templates/slide/question.html index 78e782b..ac15882 100644 --- a/templates/slide/question.html +++ b/templates/slide/question.html @@ -7,7 +7,7 @@ {% endif -%}
{{ number }}: {{ question }}
{% if picture_question -%} -
+
{% endif -%}
{% if answers[0] -%} @@ -17,7 +17,7 @@ {% endif -%} {% if answer_pictures[0] -%} {% for answer in answer_pictures|shuffle_answers -%} -
{{ answer_label[loop.index - 1] }}:
+
{{ answer_label[loop.index - 1] }}:
{% endfor -%} {% endif -%}
diff --git a/templates/slide/slide.html b/templates/slide/slide.html new file mode 100644 index 0000000..c102b17 --- /dev/null +++ b/templates/slide/slide.html @@ -0,0 +1,56 @@ + + + + + + DARC Ausbildungsplattform + + + + + + + + + + + + + + + + +
+ +
+ {{ content }} +
+ + + + + + + + + + + \ No newline at end of file diff --git a/test/test_html_slides.py b/test/test_html_slides.py index 06362b0..18c6f5f 100644 --- a/test/test_html_slides.py +++ b/test/test_html_slides.py @@ -39,13 +39,6 @@ def filter_shuffle_answers(seq): except Exception: return seq - -env = Environment(loader=FileSystemLoader("templates/")) -env.filters["shuffle_answers"] = filter_shuffle_answers -question_template = env.get_template("slide/question.html") -questions = parse_katalog() - - def question_stub(input): """Combines the original question dataset from BNetzA with our internal metadata""" with open("data/metadata.json") as metadata_file: @@ -84,77 +77,17 @@ def question_stub(input): answer_pictures=answer_pictures, ) -def print_head(): - return """ - - - - DARC Ausbildungsplattform - - - - - - - - - - - - - - - - - -
-
-""" - - # - # - # - -def print_tail(): - return """
-
- - - - - - - - - """ +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_stub) as renderer: - output = "" - output = print_head() - output += renderer.render_wrapper(content) - output += print_tail() + with FiftyOhmHtmlSlideRenderer(question_renderer=question_stub) as renderer: + output = renderer.render_wrapper(content) with open("test/acceptanceTestSlides.html", "w") as output_file: output_file.write(output) From 283b6c8456cc2e4261edceb347775db0a34ee322 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 22:03:47 +0200 Subject: [PATCH 10/26] feature: added tests for slides --- pyproject.toml | 2 +- renderer/fifty_ohm_html_slide_renderer.py | 7 ++---- test/test_qso.py | 13 ++++++++++- test/test_tag.py | 28 +++++++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9f34b5..c1d7ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,4 +42,4 @@ select = [ [tool.pytest.ini_options] addopts = ["--color=yes"] -markers = ["html", "latex"] +markers = ["html", "latex", "slide"] diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index d5fc61e..138c182 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -48,9 +48,6 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): qso += '
\n' return qso - def render_note(self, token): - return f'\n' - def render_tag(self, token): if token.tagtype == "fragment": return f'
\n{self.render_inner(token)}\n
\n' @@ -60,6 +57,6 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): return f'\n' elif token.tagtype == "note": return f'\n' - else: - return "" # Ignore other tags in slide context + + return "" # Ignore other tags in slide context anyway in tokenizer diff --git a/test/test_qso.py b/test/test_qso.py index 5495c7b..231c70c 100644 --- a/test/test_qso.py +++ b/test/test_qso.py @@ -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 = { "\nfoo\n": FiftyOhmHtmlRenderer.render_tag_helper("qso", '
foo
\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 = { + "\nfoo\n> bar\n": + '
\n
foo
\n
bar
\n
\n\n', + } + + for assertion in assertions: + assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion] diff --git a/test/test_tag.py b/test/test_tag.py index 55d4fbf..7be0c03 100644 --- a/test/test_tag.py +++ b/test/test_tag.py @@ -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 @@ -103,3 +104,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 = { + "\nFoo\n": '\n\n', + "\nFoo\n": '
\n

Foo

\n
\n\n', + "\nFoo\n": '
\n

Foo

\n
\n\n', + "\nFoo\n": '\n\n', + } + + for assertion in assertions: + assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion] + +@pytest.mark.slide +def test_slide_break(): + assertions = { + "---\na\n" : "
\n

a

\n
\n\n", + "---\na\n---\nb\n" : "
\n

a

\n
\n\n
\n

b

\n
\n\n", + "--- attention\na\n" : "
\n

a

\n
\n\n", + "--- unit\na\n" : "
\n

a

\n
\n\n", + "--- danger\na\n" : "
\n

a

\n
\n\n", + "--- foo\na\n" : "
\n

a

\n
\n\n", + } + + for assertion in assertions: + assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion] From 42cfe5407d14e66be90ce7330e00b2a4cf330320 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 22:27:51 +0200 Subject: [PATCH 11/26] feature: last slide with information --- src/build.py | 10 ++++++++-- templates/slide/next.html | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 templates/slide/next.html diff --git a/src/build.py b/src/build.py index 2db2fed..d45887b 100644 --- a/src/build.py +++ b/src/build.py @@ -173,10 +173,11 @@ class Build: result = BeautifulSoup(result, "html.parser").prettify() file.write(result) - def __build_chapter_slidedeck(self, edition, chapter, sections): + def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter): with open(f'build/{edition}_slide_{chapter["ident"]}.html', '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, @@ -190,6 +191,11 @@ class Build: tmp = f'
\n

{section["title"]}

\n
\n' tmp += renderer.render_wrapper(section["slide"]) result += f"
{tmp}
\n" + result += next_template.render( + edition=edition, + next_chapter=next_chapter, + chapter=chapter, + ) result = slide_template.render(content=result) file.write(result) @@ -227,7 +233,7 @@ class Build: 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"]) + self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter) for i, section in enumerate(chapter["sections"],1) : next_section = chapter["sections"][i] if i < len(chapter["sections"]) else None diff --git a/templates/slide/next.html b/templates/slide/next.html new file mode 100644 index 0000000..bd55881 --- /dev/null +++ b/templates/slide/next.html @@ -0,0 +1,15 @@ +
+

Fragen?

+
+

Links zu diesem Foliensatz

+ +
From fe7d0f722a4f30c3cb32302c574ddf4619015139 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Sun, 1 Jun 2025 22:44:33 +0200 Subject: [PATCH 12/26] feature: build slide index --- src/build.py | 11 +++++++++++ templates/slide/slide_index.html | 9 +++++++++ 2 files changed, 20 insertions(+) create mode 100644 templates/slide/slide_index.html diff --git a/src/build.py b/src/build.py index d45887b..e85beee 100644 --- a/src/build.py +++ b/src/build.py @@ -221,6 +221,16 @@ class Build: result = self.__build_page(result) result = BeautifulSoup(result, "html.parser").prettify() file.write(result) + + def __build_slide_index(self, book): + template = self.env.get_template("slide/slide_index.html") + with open(f"build/{book['edition']}_slide_index.html", "w") as file: + result = template.render( + book=book, + ) + result = self.__build_page(result) + result = BeautifulSoup(result, "html.parser").prettify() + file.write(result) def build_edition(self, edition): edition = edition.upper() @@ -230,6 +240,7 @@ class Build: chapters = book["chapters"] edition_name = book["title"] self.__build_book_index(book) + 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) diff --git a/templates/slide/slide_index.html b/templates/slide/slide_index.html new file mode 100644 index 0000000..e52dd96 --- /dev/null +++ b/templates/slide/slide_index.html @@ -0,0 +1,9 @@ +

Folien: {{ book["title"] }}

+

{{ book["abstract"] }}

+
+ {% for chapter in book["chapters"] %} + + {{loop.index}}{{ chapter["title"]}} + + {% endfor %} +
\ No newline at end of file From 857afc094db82cf54c12092b0e68735e5ac37318 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Mon, 2 Jun 2025 22:27:42 +0200 Subject: [PATCH 13/26] fix: fixed a wrong merge --- src/build.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/build.py b/src/build.py index b98187d..ea9364a 100644 --- a/src/build.py +++ b/src/build.py @@ -283,7 +283,11 @@ class Build: with open("data/snippets.json") as f: snippets = json.load(f) - 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 + ) as renderer: for key, value in snippets.items(): snippets[key] = renderer.render_inner(Document(value)) # Remove leading

and trailing

: @@ -296,7 +300,7 @@ class Build: 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: @@ -305,7 +309,7 @@ class Build: file.write(result) def __build_course_page(self, snippets, template, page): - template = self.env.get_template(f"{template}.html") + template = self.env.get_template(f"html/{template}.html") result=template.render({"snippets": snippets}) with open(f"build/{page}.html", "w") as file: From 9ec9b03d99e48c35975f734270b11e5ca78515e9 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Mon, 2 Jun 2025 22:55:48 +0200 Subject: [PATCH 14/26] refactor: fixed bugs due to merge --- renderer/fifty_ohm_html_slide_renderer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 138c182..09c0cb7 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -1,4 +1,3 @@ -from jinja2 import Environment, FileSystemLoader from mistletoe import Document from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer @@ -10,7 +9,13 @@ from .tag import Tag class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): - def __init__(self, question_renderer=None, picture_handler=None, photo_handler=None): + def __init__( + self, + question_renderer=None, + picture_handler=None, + photo_handler=None, + include_handler=None + ): super().__init__( SlideBreak, Qso, @@ -18,8 +23,8 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): question_renderer=question_renderer, picture_handler=picture_handler, photo_handler=photo_handler, + include_handler=include_handler, ) - self.env = Environment(loader=FileSystemLoader("templates/slide")) def render_wrapper(self, content): content = "---\n" + content From 4252f969907feeab553488bef8dcce052a285a0d Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Mon, 2 Jun 2025 23:02:24 +0200 Subject: [PATCH 15/26] refactor: removed uncessessary classes from slide renderer --- renderer/fifty_ohm_html_slide_renderer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 09c0cb7..daaf937 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -2,9 +2,7 @@ from mistletoe import Document from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer -from .qso import Qso from .slide_break import SlideBreak -from .tag import Tag class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): @@ -18,8 +16,6 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): ): super().__init__( SlideBreak, - Qso, - Tag, question_renderer=question_renderer, picture_handler=picture_handler, photo_handler=photo_handler, From b11e843f7de3809c8dec6c8ec5747f007a27b446 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 18 Jun 2025 02:09:24 +0200 Subject: [PATCH 16/26] feat: freeze new requirements --- requirements.txt | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2166ff2..5245afd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 96499db3d42b30c92b0d55e68dd190991fc8a38c Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 18 Jun 2025 02:10:08 +0200 Subject: [PATCH 17/26] feat: add configurable paths with pathlib --- build.py | 2 +- src/build.py | 120 +++++++++++++++++++++++------------------------- src/config.py | 26 ++++++++--- src/download.py | 56 ++++++++++++---------- 4 files changed, 110 insertions(+), 94 deletions(-) diff --git a/build.py b/build.py index f86c6e7..e5143dc 100644 --- a/build.py +++ b/build.py @@ -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") diff --git a/src/build.py b/src/build.py index d915ce9..9e0d9a9 100644 --- a/src/build.py +++ b/src/build.py @@ -1,5 +1,4 @@ import json -import os import random import shutil @@ -10,26 +9,29 @@ from tqdm import tqdm from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer +from src.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/")) 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_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_question = memory.cache(self.__build_question) + 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 = {} @@ -51,8 +53,8 @@ class Build: 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] @@ -97,31 +99,25 @@ class Build: return self.__build_question(input, template_file="slide/question.html") # cached - def __build_page(self, content, course_wrapper=False, sidebar=None) : + 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 - ) - + 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("html/chapter.html") next_chapter_template = self.env.get_template("html/next_chapter.html") - with open(f'build/{edition}_chapter_{chapter["ident"]}.html', 'w') as file: + with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file: result = chapter_template.render( edition=edition, name=edition_name, @@ -131,7 +127,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"], ) @@ -139,22 +135,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, section_id, chapter, next_section=None, next_chapter=None): + 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 open(f'build/{edition}_{section["ident"]}.html', 'w') as file: - + with (self.config.p_build / f"{edition}_{section['ident']}.html").open("w") as file: with FiftyOhmHtmlRenderer( question_renderer=self.__build_question, picture_handler=self.__picture_handler, photo_handler=self.__photo_handler, - include_handler=self.__include_handler + include_handler=self.__include_handler, ) as renderer: section["content"] = renderer.render(Document(section["content"])) @@ -168,12 +165,12 @@ class Build: 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"], ) @@ -181,7 +178,7 @@ class Build: file.write(result) def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter): - with open(f'build/{edition}_slide_{chapter["ident"]}.html', 'w') as file: + 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") @@ -189,7 +186,7 @@ class Build: question_renderer=self.__build_question_slide, picture_handler=self.__picture_handler, photo_handler=self.__photo_handler, - include_handler=self.__include_handler + include_handler=self.__include_handler, ) as renderer: result = "
\n" result += f'
\n

{chapter["title"]}

\n
\n' @@ -204,7 +201,7 @@ class Build: next_chapter=next_chapter, chapter=chapter, ) - + result = slide_template.render(content=result) file.write(result) @@ -219,55 +216,55 @@ class Build: answers.append({"content": answer, "correct": False}) random.shuffle(answers) return answers - + def __build_book_index(self, book): template = self.env.get_template("html/course_index.html") - with open(f"build/{book['edition']}_course_index.html", "w") as file: + 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 open(f"build/{book['edition']}_slide_index.html", "w") as file: + 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) + file.write(result) def build_edition(self, edition): 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) self.__build_slide_index(book) - for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"),1): + 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, 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( question_renderer=self.__build_question, picture_handler=self.__picture_handler, photo_handler=self.__photo_handler, - include_handler=self.__include_handler + include_handler=self.__include_handler, ) as renderer: for key, value in snippets.items(): snippets[key] = renderer.render_inner(Document(value)) @@ -276,35 +273,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("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"html/{template}.html") - result=template.render({"snippets": snippets}) + 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): @@ -315,4 +309,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") \ No newline at end of file + self.__build_html_page(contents, "infos") diff --git a/src/config.py b/src/config.py index ef1f6c2..fe381b9 100644 --- a/src/config.py +++ b/src/config.py @@ -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()}'" ) diff --git a/src/download.py b/src/download.py index 9295825..3b0165c 100644 --- a/src/download.py +++ b/src/download.py @@ -6,9 +6,12 @@ import shutil from joblib import Memory, Parallel, delayed from tqdm import tqdm +from api.directus import DirectusAPI +from src.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 +63,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 +85,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 +118,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 +147,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 +170,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) From 95974aa85ddbfcd938c70f1e17fc64a91c8f7709 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 14:56:28 +0200 Subject: [PATCH 18/26] refactor: use splat operator for class args --- renderer/fifty_ohm_html_renderer.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index c2e9c1e..c61e5a9 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -1,7 +1,3 @@ - - -from itertools import chain - from jinja2 import Environment, FileSystemLoader from mistletoe import Document, HtmlRenderer @@ -32,15 +28,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): ref_id = 0 def __init__( - self, - *extras, - question_renderer=None, - picture_handler=None, - photo_handler=None, - include_handler=None - ): - - final_extras = chain(extras,( + self, *extras, question_renderer=None, picture_handler=None, photo_handler=None, include_handler=None, **kwargs + ): + super().__init__( Dash, BlockComment, Quote, @@ -58,9 +48,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): Table, Qso, Include, - )) - - super().__init__(*final_extras) + *extras, + **kwargs, + ) self.question_renderer = question_renderer self.picture_handler = picture_handler From 6282fc61c0d956f438df35de92a8c090225ff9a9 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 14:58:52 +0200 Subject: [PATCH 19/26] refactor: use dictionary unpacking --- renderer/fifty_ohm_html_slide_renderer.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index daaf937..2078e2e 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -6,20 +6,10 @@ from .slide_break import SlideBreak class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): - - def __init__( - self, - question_renderer=None, - picture_handler=None, - photo_handler=None, - include_handler=None - ): + def __init__(self, **kwargs): super().__init__( SlideBreak, - question_renderer=question_renderer, - picture_handler=picture_handler, - photo_handler=photo_handler, - include_handler=include_handler, + **kwargs, ) def render_wrapper(self, content): From 77435b2354a2dcb5184e6e3f474dbfd59790cac8 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 14:59:34 +0200 Subject: [PATCH 20/26] feat: remove background color attributes --- renderer/fifty_ohm_html_slide_renderer.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 2078e2e..3fd4be2 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -20,15 +20,9 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): inner = self.render_inner(token) if token.attribute is None: - return f'
\n{inner}\n
\n' - if token.attribute == "attention": - return f'
\n{inner}\n
\n' - elif token.attribute == "danger": - return f'
\n{inner}\n
\n' - elif token.attribute == "unit": - return f'
\n{inner}\n
\n' + return f"
\n{inner}\n
\n" else: - return f'
\n{inner}\n
\n' + return f"
\n{inner}\n
\n" def render_qso(self, token): qso = '
\n' From 68b1abf7e8b5cfd32ed37425588692ab847fe1d8 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 14:59:51 +0200 Subject: [PATCH 21/26] refactor: format file with ruff --- renderer/fifty_ohm_html_slide_renderer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 3fd4be2..3a8e69b 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -30,7 +30,7 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): direction = "other" if child.received else "own" fade = "fragment fade-left" if child.received else "fragment fade-right" qso += f'
{self.render_inner(child)}
\n' - qso += '
\n' + qso += "
\n" return qso def render_tag(self, token): @@ -42,6 +42,5 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): return f'\n' elif token.tagtype == "note": return f'\n' - - return "" # Ignore other tags in slide context anyway in tokenizer + return "" # Ignore other tags in slide context anyway in tokenizer From c13ae4c9a23f1046977a64e1eddc334ad16e20d0 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 15:00:04 +0200 Subject: [PATCH 22/26] refactor: clean up code --- renderer/slide_break.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/renderer/slide_break.py b/renderer/slide_break.py index 84bde86..f8f4c04 100644 --- a/renderer/slide_break.py +++ b/renderer/slide_break.py @@ -1,4 +1,3 @@ - import re from mistletoe.block_token import BlockToken, tokenize @@ -7,23 +6,23 @@ from mistletoe.block_token import BlockToken, tokenize class SlideBreak(BlockToken): @staticmethod def start(line): - return re.match(r"^\s*---.*$", 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 + first_line = next(lines) # Consume the line that started the slide break - match = re.match(r"^\s*---\s*(.*)\s*.*$", first_line) - attribute = match.group(1) if match.group(1) else None + 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 re.match(r"^\s*---.*$", next_line) : - break + if cls.start(next_line): + break else: child_lines.append(next(lines)) next_line = lines.peek() From ac90cd60020b3815897e3b91fae502dcf0e60350 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 15:00:43 +0200 Subject: [PATCH 23/26] feat: use standard render method for rendering slides --- renderer/fifty_ohm_html_slide_renderer.py | 6 ------ src/build.py | 4 +++- test/acceptanceTestSlides.md | 2 ++ test/test_html_slides.py | 7 ++++++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py index 3a8e69b..8ded848 100644 --- a/renderer/fifty_ohm_html_slide_renderer.py +++ b/renderer/fifty_ohm_html_slide_renderer.py @@ -1,5 +1,3 @@ -from mistletoe import Document - from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer from .slide_break import SlideBreak @@ -11,10 +9,6 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer): SlideBreak, **kwargs, ) - - def render_wrapper(self, content): - content = "---\n" + content - return super().render(Document(content)) def render_slide_break(self, token): inner = self.render_inner(token) diff --git a/src/build.py b/src/build.py index 9e0d9a9..465784e 100644 --- a/src/build.py +++ b/src/build.py @@ -193,8 +193,10 @@ class Build: result += help_template.render() result += "\n" for section in sections: + if not section["slide"].startswith("---"): + section["slide"] = "---\n" + section["slide"] tmp = f'
\n

{section["title"]}

\n
\n' - tmp += renderer.render_wrapper(section["slide"]) + tmp += renderer.render(Document(section["slide"])) result += f"
{tmp}
\n" result += next_template.render( edition=edition, diff --git a/test/acceptanceTestSlides.md b/test/acceptanceTestSlides.md index c24e6c5..dca7dea 100644 --- a/test/acceptanceTestSlides.md +++ b/test/acceptanceTestSlides.md @@ -1,3 +1,5 @@ +--- + # Akzeptanztest --- style="font-size: 4pt;" diff --git a/test/test_html_slides.py b/test/test_html_slides.py index 18c6f5f..291e528 100644 --- a/test/test_html_slides.py +++ b/test/test_html_slides.py @@ -2,6 +2,7 @@ import json import random from jinja2 import Environment, FileSystemLoader +from mistletoe import Document from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer @@ -24,6 +25,7 @@ def parse_katalog(): return questions + def filter_shuffle_answers(seq): try: answers = [] @@ -39,6 +41,7 @@ def filter_shuffle_answers(seq): 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: @@ -77,17 +80,19 @@ def question_stub(input): 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_wrapper(content) + output = renderer.render(Document(content)) with open("test/acceptanceTestSlides.html", "w") as output_file: output_file.write(output) From 399baf7401124db0678f64231751cdbd057b454d Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 15:01:13 +0200 Subject: [PATCH 24/26] feat: update tests --- test/test_tag.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/test_tag.py b/test/test_tag.py index 7be0c03..4af938d 100644 --- a/test/test_tag.py +++ b/test/test_tag.py @@ -30,7 +30,8 @@ def test_tag_html(): + "\n", "\nFoo\n": FiftyOhmHtmlRenderer.render_tag_helper("unit", "

Foo

", 1, 0) + "\n", "\nFoo\n": FiftyOhmHtmlRenderer.render_tag_helper("danger", "

Foo

", 1, 0) + "\n", - "\nFoo\n": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "

Foo

", 1, 0) + "\n", + "\nFoo\n": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "

Foo

", 1, 0) + + "\n", "\nFoo\n": "

Foo

\n", "\nFoo\n": "", } @@ -118,15 +119,15 @@ def test_tag_slide(): for assertion in assertions: assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion] + @pytest.mark.slide def test_slide_break(): assertions = { - "---\na\n" : "
\n

a

\n
\n\n", - "---\na\n---\nb\n" : "
\n

a

\n
\n\n
\n

b

\n
\n\n", - "--- attention\na\n" : "
\n

a

\n
\n\n", - "--- unit\na\n" : "
\n

a

\n
\n\n", - "--- danger\na\n" : "
\n

a

\n
\n\n", - "--- foo\na\n" : "
\n

a

\n
\n\n", + "---\na\n": "
\n

a

\n
\n\n", + "---\na\n---\nb\n": "
\n

a

\n
\n\n
\n

b

\n
\n\n", + "--- foo\na\n": "
\n

a

\n
\n\n", + "--- foo bar\na\n": "
\n

a

\n
\n\n", + "--- foo bar \na\n": "
\n

a

\n
\n\n", } for assertion in assertions: From 8171205abf842988b9785d15e141c165185ebd8c Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 15:01:26 +0200 Subject: [PATCH 25/26] refactor: relative import --- src/build.py | 3 ++- src/download.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/build.py b/src/build.py index 465784e..d5891ad 100644 --- a/src/build.py +++ b/src/build.py @@ -9,7 +9,8 @@ from tqdm import tqdm from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer -from src.config import Config + +from .config import Config class Build: diff --git a/src/download.py b/src/download.py index 3b0165c..9459417 100644 --- a/src/download.py +++ b/src/download.py @@ -7,7 +7,8 @@ from joblib import Memory, Parallel, delayed from tqdm import tqdm from api.directus import DirectusAPI -from src.config import Config + +from .config import Config class Download: From 4b06baa837e2d710dbe3ed9a18a33f083a0c7acb Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Thu, 19 Jun 2025 15:01:36 +0200 Subject: [PATCH 26/26] feat: create build directory if not existing --- src/build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/build.py b/src/build.py index d5891ad..dd5b144 100644 --- a/src/build.py +++ b/src/build.py @@ -239,6 +239,8 @@ class Build: file.write(result) def build_edition(self, edition): + self.config.p_build.mkdir(exist_ok=True) + edition = edition.upper() with (self.config.p_data / f"book_{edition}.json").open() as file: