Merge branch 'main' into siunitx

This commit is contained in:
Matthias Jung
2026-02-08 21:35:47 +01:00
committed by GitHub
20 changed files with 123 additions and 33169 deletions
-4
View File
@@ -167,7 +167,3 @@ test/acceptanceTestSlides.html
.DS_Store
.envrc
config/config.json
data/*
!data/fragenkatalog3b.json
!data/README.txt
+7 -10
View File
@@ -2,28 +2,25 @@
50ohm.de ist die Ausbildungsplattform des DARC e.V.
Dieses Repository enthält den Generator für die Seite [50ohm.de](https://50ohm.de), der mit einer Directus-Datenbank als Quelle arbeitet.
Dieses Repository enthält den Generator für die Seite [50ohm.de](https://50ohm.de), der mit einem andern [Repository](https://github.com/DARC-e-V/50ohm-contents-dl/) als Quelle für die Inhalte arbeitet.
## Überblick
Die Ausbildungsmaterialien von 50ohm.de werden in einem erweiterten Marcdownformat geschrieben, intern als Darcdown bezeichnet. Kern des Generators ist ein Parser, der auf [mistletoe](https://github.com/miyuchina/mistletoe) basiert und um zusätzliche Syntax und Ausgabeformate erweitert wurde.
Die Ausbildungsmaterialien von 50ohm.de werden in einem erweiterten Markdownformat geschrieben, intern als DARCdown bezeichnet. Kern des Generators ist ein Parser, der auf [mistletoe](https://github.com/miyuchina/mistletoe) basiert und um zusätzliche Syntax und Ausgabeformate erweitert wurde.
Die möglichen Ausgabeformate sind:
- HTML: [`fifty_ohm_html_renderer.py`](renderer/fifty_ohm_html_renderer.py)
- LaTeX: [`fifty_ohm_latex_renderer`](renderer/fifty_ohm_latex_renderer.py)
- HTML für Slides mit reveal.js: [`fifty_ohm_html_slide_renderer.py`](renderer/fifty_ohm_html_slide_renderer.py)
Prinzipiell lassen sich diese Generatoren auch losgelöst vom Directus einsetzen. Der zweite Teil des Generators liegt in `src`, unterteilt in die Prozessschritte Herunderladen ([`download.py`](src/download.py)) und Seite Bauen ([`build.py`](src/build.py)).
Prinzipiell lassen sich diese Generatoren auch für andere Quell-Repositories verwenden. Dies ist insbesondere für andere IARU-Verbände interessant, welche auch 50ohm als Ausbildungsseite implementieren wollen. Der eigentliche Teil des Generators liegt in `src`, insbesondere das Skript ([`build.py`](src/build.py)). In dieser Datei finden sich außerdem weitere Begleitfunktionen zum Renderprozess, z.B. das Übersetzen von Fragennummern in Fragen oder das Kopieren von Assets.
Hier werden alle Inhalte aus der Datenbank heruntergeladen und strukturiert in verschiedenen JSON-Dateien abgelegt. Diese dienen im Build-Prozess als Quelle für Abschnitte, Kapitelstruktur und weitere Inhalte.
In der `build.py` finden sich außerdem weitere Begleitfunktionen zum Renderprozess, z.B. das Übersetzen von Fragennummern in Fragen oder das Kopieren von Assets.
## Mitmachen
## Mitmachen und Kontakt
Aktuell wird 50ohm durch ein kleines ehrenamtliches Entwicklerteam beim DARC e.V. betreut und weiterentwickelt. Falls du einen Fehler oder einen Funktionswunsch hast, schreibe gerne ein Issue. Auch über einen PR freuen wir uns!
Wenn du inhaltliche Beiträge hast, kannst du diese über dieses [Repository](https://github.com/DARC-e-V/50ohm-contents-dl) einreichen.
Wenn du inhaltliche Beiträge hast, wende dich bitte an das 50ohm-Autorenteam.
Bei Anfragen bitte die Email-Adresse [[email protected]](mailto:[email protected]) nutzen. Weiterhin findet man viele der Beitragenden im Matrix-Kanal [50ohm Contribution](https://matrix.to/#/#50ohm-contribution:darc.de).
### Python einrichten und Abhängigkeiten installieren
@@ -44,7 +41,7 @@ $ uv pip sync requirements.txt
### Ausführen
Um 50ohm ausführen zu können, wird in den meisten Fällen ein Zugang zu passenden Directus-Instanzen benötigt. Diese können über eine `config/config.json` konfiguriert werden.
Um 50ohm ausführen zu können, muss der Pfad zu einem lokal ausgechecktem Content-Repository in die `config/config.json` eingetragen werden. Ein Beispiel-Content-Repository für deutsche Inhalte findet sich hier: [50ohm-contents-dl](https://github.com/DARC-e-V/50ohm-contents-dl).
Die 50ohm.de-Website wird mit folgendem Befehl vollständig gebaut:
-56
View File
@@ -1,56 +0,0 @@
import json
import requests
class DirectusAPI:
def __init__(self, base_url, access_token):
self.base_url = base_url
self.access_token = access_token
def __get_raw(self, endpoint, params=None):
if params is None:
params = {}
for k in params.keys():
if type(params[k]) is dict:
params[k] = json.dumps(params[k])
url = self.base_url + endpoint
result = requests.get(
url, params=params, headers={"Authorization": f"Bearer {self.access_token}", "Cache-Control": "no-store"}
)
return result
def get(self, endpoint, params=None):
if params is None:
params = {}
result = self.__get_raw(endpoint, params)
try:
result_json = result.json()
if "errors" in result_json.keys():
print(result_json["errors"])
raise Exception("Error communicating with API")
data = result_json["data"]
except Exception as e:
print(f"Error: {e}")
return None
return data
def get_one(self, endpoint, params=None):
if params is None:
params = {}
results = self.get(endpoint, params)
if len(results) == 1:
return results[0]
elif len(results) == 0:
return None
else:
raise Exception("One result expected, but got more than one result")
def get_file(self, endpoint, params=None):
if params is None:
params = {}
result = self.__get_raw(endpoint, params)
data = result.content
if data is None:
raise Exception("No data from API")
return data
+3
View File
@@ -21,8 +21,10 @@ document.addEventListener("DOMContentLoaded", function() {
"\\nano": "\\text{n}",
"\\pico": "\\text{p}",
"\\kilogram": "\\text{kg}",
"\\gram": "\\text{g}",
"\\meter": "\\text{m}",
"\\second": "\\text{s}",
"\\hour": "\\text{h}",
"\\ampere": "\\text{A}",
"\\kelvin": "\\text{K}",
"\\mol": "\\text{mol}",
@@ -49,6 +51,7 @@ document.addEventListener("DOMContentLoaded", function() {
"\\weber": "\\text{Wb}",
"\\tesla": "\\text{T}",
"\\bit": "\\text{b}",
"\\baud": "\\text{Bd}",
"\\degree": "^\\circ",
"\\degreeCelsius": "^\\circ\\text{C}",
};
+1 -22
View File
@@ -1,31 +1,10 @@
import api.directus as directus
import src.build as build
import src.config as config
import src.download as download
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(conf)
# Download the text files:
dl.download_edition("N")
dl.download_edition("E")
dl.download_edition("A")
dl.download_edition("NE")
dl.download_edition("EA")
dl.download_edition("NEA")
# Download additional files:
# dl.download_question_metadata() # (now constant file metadata3b.json)
dl.download_photos()
dl.download_pictures()
dl.download_includes()
dl.download_snippets()
dl.download_content()
# Build Everything:
bd = build.Build(conf)
bd.build_website()
bd.build_edition("N")
bd.build_edition("E")
+1 -5
View File
@@ -1,7 +1,3 @@
{
"directus_question_base_url" : "https://fragenkatalog.darc.de/",
"directus_question_api_key" : "",
"directus_content_base_url" : "https://redaktion.50ohm.de/",
"directus_content_api_key" : "",
"no_latex": true
"content_path" : "/home/your_path_to/50ohm-contents-dl"
}
-57
View File
@@ -1,57 +0,0 @@
Prüfungsfragen zum Erwerb von Amateurfunkprüfungsbescheinigungen
3. Auflage, März 2024
Hinweis:
========
Die Prüfungsfragen werden in maschinenlesbarer Form von der
Bundesnetzagentur für Elektrizität, Gas, Telekommunikation, Post und Eisenbahnen,
Tulpenfeld 4, 53113 Bonn
unter der Datenlizenz DL-DE->BY-2.0 bereitgestellt.
Die bereitgestellten Daten und Metadaten dürfen für die kommerzielle und nicht kommerzielle Nutzung insbesondere
vervielfältigt, ausgedruckt, präsentiert, verändert, bearbeitet sowie an Dritte übermittelt werden;
mit eigenen Daten und Daten Anderer zusammengeführt und zu selbständigen neuen Datensätzen verbunden werden;
in interne und externe Geschäftsprozesse, Produkte und Anwendungen in öffentlichen und nicht öffentlichen
elektronischen Netzwerken eingebunden werden.
Bei der Nutzung ist sicherzustellen, dass folgende Angaben als Quellenvermerk enthalten sind:
"Prüfungsfragen zum Erwerb von Amateurfunkprüfungsbescheinigungen, Bundesnetzagentur, 3. Auflage, März 2024,
(www.bundesnetzagentur.de/amateurfunk), Datenlizenz Deutschland – Namensnennung – Version 2.0
(www.govdata.de/dl-de/by-2-0)"
Veränderungen, Bearbeitungen, neue Gestaltungen oder sonstige Abwandlungen sind im Quellenvermerk mit dem Hinweis
zu versehen, dass die Daten geändert wurden.
Anleitung:
==========
Die maschinenlesbare Version der Prüfungsfragen teilt sich in zwei Teile auf:
1. Die Struktur des Katalogs und die Prüfungsfragen als JSON-Datei
2. Alle Bilder zu den Prüfungsfragen als SVG-Dateien
Das JSON-Schema besteht aus Kapiteln "sections", die jeweils einen Titel
"title" beinhalten. Eine Section kann entweder weitere Unterkapitel "sections"
beinhalten, oder eine Liste von Prüfungsfragen "questions". Eine Prüfungsfrage
beinhaltet folgende Attribute:
- "number" : Die Prüfungsnummer im Fragenkatalog
- "class" : Die Klasse (1:N, 2:E, 3:A)
- "question" : Der Fragenkopf
- "answer_a" : Die richtige Antwort A
- "answer_b" : Ein Distraktor B (falsche Antwort)
- "answer_c" : Ein Distraktor C (falsche Antwort)
- "answer_d" : Ein Distraktor D (falsche Antwort)
- "picture_question" : Das Fragenbild [Optional]
- "picture_a" : Das Antwortbild A [Optional]
- "picture_b" : Das Antwortbild B [Optional]
- "picture_c" : Das Antwortbild C [Optional]
- "picture_d" : Das Antwortbild D [Optional]
Die Prüfungsfragen enthalten LaTeX-Code für die Darstellung der Formeln. Diese
können durch verschiedene Bibliotheken z. B. durch KaTeX (https://katex.org)
angezeigt werden.
File diff suppressed because it is too large Load Diff
-15752
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -10,6 +10,7 @@ dependencies = [
"mistletoe==1.4.0",
"requests==2.32.4",
"tqdm==4.67.1",
"gitpython==3.1.46",
]
[tool.ruff]
+1 -1
View File
@@ -277,7 +277,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
def render_photo_helper(id, ref, text, number, alt_text):
return f"""
<figure class="photo" id="ref_{ref}" name="{ref}">
<img src="photos/{id}.jpg" alt="{alt_text}">
<img src="photos/{id}.png" alt="{alt_text}">
<figcaption>Abbildung {number}: {text}</figcaption>
</figure>
"""
+2 -2
View File
@@ -8,12 +8,12 @@ class Question(BlockToken):
def start(line):
# Search for:
# [question:123]
return re.match(r"^\s*\[question:\d+\]", line)
return re.match(r"^\s*\[question:[\w\d]+\]", line)
@classmethod
def read(cls, lines):
first_line = next(lines)
question_number = re.match(r"^\s*\[question:(\d+)\]", first_line).group(1)
question_number = re.match(r"^\s*\[question:([\w\d]+)\]", first_line).group(1)
return question_number
@classmethod
+1
View File
@@ -17,3 +17,4 @@ ruff==0.12.2
tqdm==4.67.1
ty==0.0.1a13
urllib3==2.5.0
gitpython==3.1.46
+58 -34
View File
@@ -1,9 +1,9 @@
import json
import random
import shutil
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from joblib import Memory
from mistletoe import Document
from tqdm import tqdm
@@ -17,20 +17,12 @@ class Build:
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_chapter_slidedeck = memory.cache(self.__build_chapter_slidedeck)
def __parse_katalog(self):
with self.config.p_fragenkatalog.open() as file:
with self.config.p_data_fragenkatalog.open() as file:
fragenkatalog = json.load(file)
questions = {}
@@ -47,27 +39,28 @@ class Build:
return questions
# cached
def __build_question(self, input, template_file="html/question.html"):
def __build_question(self, number, template_file="html/question.html"):
"""Combines the original question dataset from BNetzA with our internal metadata"""
question_template = self.env.get_template(template_file)
with (self.config.p_data / "metadata3b.json").open() as file:
metadata = json.load(file)
with (self.config.p_data_metadata).open() as file:
metadata_json = json.load(file)
question = None
number = None
if f"{input}" in metadata:
metadata = metadata[f"{input}"]
number = metadata["number"] # Fragennummer z.B. AB123
metadata = None
if number in self.questions:
question = self.questions[number]
if question is None:
if number in metadata_json:
metadata = metadata_json[number]
if question is None or metadata is None:
tqdm.write(
f"\033[31mQuestion #{input} is missing"
+ (f" (but found number: {number})" if number is not None else "")
f"\033[31mQuestion #{number} is missing"
+ (" (Question not in question pool)" if question is None else "")
+ (" (Question not in metadata)" if metadata is None else "")
+ "\033[0m"
)
metadata = {"layout": "not-found", "picture_a": ""}
@@ -124,7 +117,6 @@ class Build:
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("html/page.html")
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
@@ -143,7 +135,7 @@ class Build:
def __photo_handler(self, id):
self.config.p_build_photos.mkdir(parents=True, exist_ok=True)
file = f"{id}.jpg"
file = f"{id}.png"
try:
shutil.copyfile(self.config.p_data_photos / file, self.config.p_build_photos / file)
if (self.config.p_data_photos / f"{id}.txt").exists():
@@ -153,7 +145,6 @@ class Build:
except FileNotFoundError:
tqdm.write(f"\033[31mPhoto #{id} not found\033[0m")
# 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")
@@ -175,11 +166,9 @@ class Build:
file.write(result)
def __include_handler(self, include):
with (self.config.p_data / "includes.json").open() as file:
includes = json.load(file)
return includes.get(include)
with (self.config.p_data_html / f"{include}.html").open() as file:
return file.read()
# cached
def __build_section(
self,
edition,
@@ -337,9 +326,22 @@ class Build:
edition = edition.upper()
with (self.config.p_data / f"book_{edition}.json").open() as file:
with (self.config.p_data_toc / f"{edition}.json").open() as file:
book = json.load(file)
chapters = book["chapters"]
for chapter in chapters:
for section in chapter["sections"]:
ident = section["ident"]
section["content"] = None
section["slide"] = None
with (self.config.p_data_sections / f"{ident}.md").open() as sfile:
section_content = sfile.read()
section["content"] = section_content
with (self.config.p_data_slides / f"{ident}.md").open() as sfile:
section_content = sfile.read()
section["slide"] = section_content
edition_name = book["title"]
self.__build_book_index(book)
self.__build_slide_index(book)
@@ -371,8 +373,11 @@ class Build:
)
def __parse_snippets(self):
with (self.config.p_data / "snippets.json").open() as file:
snippets = json.load(file)
snippets = {}
for md_file in Path(self.config.p_data_snippets).glob("*.md"):
with md_file.open() as file:
snippets[md_file.stem] = file.read()
with FiftyOhmHtmlRenderer(
question_renderer=self.__build_question,
@@ -384,12 +389,31 @@ class Build:
snippets[key] = renderer.render_inner(Document(value))
# Remove leading <p> and trailing </p>:
snippets[key] = snippets[key][3:-4]
return snippets
def __parse_contents(self):
with (self.config.p_data / "content.json").open() as file:
contents = json.load(file)
return contents
static = []
for static_file in Path(self.config.p_data_static).glob("*.html"):
if "sidebar" not in static_file.stem:
with static_file.open() as file:
content = file.read()
sidebar_file = self.config.p_data_static / f"{static_file.stem}_sidebar.html"
if not sidebar_file.exists():
sidebar = ""
else:
with sidebar_file.open() as file:
sidebar = file.read()
static.append(
{
"url_part": static_file.stem,
"content": content,
"sidebar": sidebar if sidebar != "" else None,
}
)
return static
def __build_index(self, snippets):
template = self.env.get_template("html/index.html")
+14 -9
View File
@@ -13,17 +13,22 @@ class Config:
else:
self.config = {}
self.question_access_token = self.get_config_value("directus_question_api_key")
self.question_base_url = self.get_config_value("directus_question_base_url", "https://fragenkatalog.darc.de/")
self.content_access_token = self.get_config_value("directus_content_api_key")
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_data = Path(self.get_config_value("content_path", "/var/www/content"))
self.p_fragenkatalog = Path(self.get_config_value("path_fragenkatalog", "./data/fragenkatalog3b.json"))
self.p_data_toc = self.p_data / "toc"
self.p_data = Path("./data")
self.p_data_photos = self.p_data / "photos"
self.p_data_pictures = self.p_data / "pictures"
self.p_data_contents = self.p_data / "contents"
self.p_data_questions = self.p_data_contents / "questions"
self.p_data_html = self.p_data_contents / "html"
self.p_data_photos = self.p_data_contents / "photos"
self.p_data_pictures = self.p_data_contents / "drawings"
self.p_data_sections = self.p_data_contents / "sections"
self.p_data_slides = self.p_data_contents / "slides"
self.p_data_snippets = self.p_data_contents / "snippets"
self.p_data_static = self.p_data_contents / "static"
self.p_data_fragenkatalog = self.p_data_questions / "fragenkatalog3b.json"
self.p_data_metadata = self.p_data_questions / "metadata3b.json"
self.p_build = Path("./build")
self.p_build_photos = self.p_build / "photos"
-162
View File
@@ -1,162 +0,0 @@
import json
from tqdm import tqdm
from api.directus import DirectusAPI
from .config import Config
class Download:
def __init__(self, api: DirectusAPI, content_api: DirectusAPI, config: Config):
self.api = api
self.config = config
self.content_api = content_api
def download_edition(self, edition):
in_edition = self.api.get_one("items/edition", {"filter": {"ident": {"_eq": edition}}})
in_chapters = self.api.get(
"items/chapter",
{"filter": {"edition": {"edition_id": {"_eq": in_edition["id"]}}}},
)
out_chapters = []
for in_chapter in tqdm(in_chapters, desc="Downloading edition"):
in_sections = self.api.get(
"items/section",
{"filter": {"chapter": {"chapter_id": {"_eq": in_chapter["id"]}}}},
)
out_chapters.append(
{
"title": in_chapter["title"],
"abstract": in_chapter["abstract"],
"ident": in_chapter["ident"],
"video_url": in_chapter["video_url"],
"sections": [],
}
)
for in_section in in_sections:
out_chapters[-1]["sections"].append(
{
"title": in_section["title"],
"ident": in_section["ident"],
"status": in_section["status"],
"class": in_section["class"],
"content": in_section["content"],
"slide": in_section["slide"],
"video_url": in_section["video_url"],
"questions": [],
}
)
out_edition = {
"title": in_edition["title"],
"abstract": in_edition["abstract"],
"chapters": out_chapters,
"edition": edition,
}
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):
questions = self.api.get(
"items/questions", params={"filter": {"class_3": {"_in": [1, 2, 3]}}, "sort": "position", "limit": -1}
)
result = {}
for question in tqdm(questions, desc="Downloading metadata"):
result[question["id"]] = {
"number": question["number"],
"picture_question": question["picture_question"] if question["picture_question"] is not None else "",
"picture_a": question["picture_a"] if question["picture_a"] is not None else "",
"picture_b": question["picture_b"] if question["picture_b"] is not None else "",
"picture_c": question["picture_c"] if question["picture_c"] is not None else "",
"picture_d": question["picture_d"] if question["picture_d"] is not None else "",
"layout": question["layout"] if question["layout"] is not None else "",
}
# Serializing json:
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"):
# Save Photo to file
data = self.api.get_file("assets/" + photo["photo"])
with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file:
file.write(data)
# Save Alt-Text to file
prefix = ""
if photo["alt_text"] is not None:
if photo["alt_text_reviewed"] is None:
prefix = "Der folgende Alt-Text wurde noch nicht geprüft: "
with (self.config.p_data_photos / f"{photo['id']}.txt").open("w") as file:
file.write(prefix + photo["alt_text"])
def download_pictures(self):
self.config.p_data_pictures.mkdir(parents=True, exist_ok=True)
pictures = self.api.get("items/pictures", {"limit": -1})
for picture in tqdm(pictures, desc="Downloading pictures"):
# Save LaTeX sources to file
with (self.config.p_data_pictures / f"{picture['id']}include.tex").open("w") as file:
file.write(picture["latex"])
# Save Alt-Text to file
prefix = ""
if picture["alt_text"] is not None:
if picture["alt_text_reviewed"] is None:
prefix = "Der folgende Alt-Text wurde noch nicht geprüft: "
with (self.config.p_data_pictures / f"{picture['id']}.txt").open("w") as file:
file.write(prefix + picture["alt_text"])
# Download SVG
data = self.api.get_file(f"assets/{picture['picture']}")
with (self.config.p_data_pictures / f"{picture['id']}.svg").open("wb") as file:
file.write(data)
def download_includes(self):
includes = self.api.get("items/include", params={"limit": -1})
result = {}
for include in tqdm(includes, desc="Downloading includes"):
result[include["ident"]] = include["content"]
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 (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"):
contents.append(
{
"url_part": content["url_part"],
"content": content["content"],
"sidebar": content["sidebar"],
}
)
with (self.config.p_data / "content.json").open("w", encoding="utf-8") as file:
json.dump(contents, file, ensure_ascii=False, indent=4)
+3 -3
View File
@@ -46,7 +46,7 @@ Wenn man allerdings nur den Widerstand und die Spannung kennt und den entspreche
$ I = \dfrac{U}{R} $
[question:64]
[question:NB504]
Würde man die 10 V-Batterie in dem Beispiel durch eine 20 V-Batterie ersetzen, dann würde sich auch der Strom von 1 mA auf 2 mA erhöhen. Wenn man also die Spannung verdoppelt, dann verdoppelt sich auch der Strom. Entsprechend würde sich auch der Strom auf 0,5 mA halbieren, wenn man die Spannung auf 5 V halbieren würde.
@@ -73,8 +73,8 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
---
[question:397]
[question:515]
[question:BB102]
[question:BE105]
<indepth>
*QSO* ist eine Abkürzung, die ursprünglich aus der Morsetelegrafie stammt und "ich habe direkten Kontakt zu Station X" bedeutet. Daran angelehnt, sprechen Funkamateure oftmals von einem QSO, wenn sie eine erfolgreiche Funkverbindung meinen.
+1 -1
View File
@@ -75,7 +75,7 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
---
[question:2482]
[question:NB201]
---
## Slide Tabelle
+10 -10
View File
@@ -4,11 +4,14 @@ import pytest
from jinja2 import Environment, FileSystemLoader
from mistletoe import Document
import src.config as config
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
conf = config.Config()
def parse_katalog():
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
with open(conf.p_data_fragenkatalog) as fragenkatalog_file:
fragenkatalog = json.load(fragenkatalog_file)
questions = {}
@@ -28,17 +31,15 @@ def parse_katalog():
env = Environment(loader=FileSystemLoader("templates/html"))
question_template = env.get_template("question.html")
questions = parse_katalog()
def question_stub(input):
def question_stub(number):
questions = parse_katalog()
"""Combines the original question dataset from BNetzA with our internal metadata"""
with open("data/metadata3b.json") as metadata_file:
with open(conf.p_data_metadata) as metadata_file:
metadata = json.load(metadata_file)
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
question = questions[number]
metadata = metadata[f"{input}"]
metadata = metadata[f"{number}"]
if "answer_a" in question:
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
@@ -71,9 +72,8 @@ def question_stub(input):
def include_stub(include):
with open("data/includes.json") as file:
includes = json.load(file)
return includes.get(include)
with open(conf.p_data_html / f"{include}.html") as file:
return file.read()
@pytest.mark.skip("Requires special files")
+8 -7
View File
@@ -5,11 +5,14 @@ import pytest
from jinja2 import Environment, FileSystemLoader
from mistletoe import Document
import src.config as config
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
conf = config.Config()
def parse_katalog():
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
with open(conf.p_data_fragenkatalog) as fragenkatalog_file:
fragenkatalog = json.load(fragenkatalog_file)
questions = {}
@@ -43,14 +46,13 @@ def filter_shuffle_answers(seq):
return seq
def question_stub(input):
def question_stub(number):
questions = parse_katalog()
"""Combines the original question dataset from BNetzA with our internal metadata"""
with open("data/metadata3b.json") as metadata_file:
with open(conf.p_data_metadata) as metadata_file:
metadata = json.load(metadata_file)
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
question = questions[number]
metadata = metadata[f"{input}"]
metadata = metadata[f"{number}"]
if "answer_a" in question:
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
@@ -86,7 +88,6 @@ 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()
@pytest.mark.skip("Requires special files")