Merge branch 'main' into siunitx
This commit is contained in:
@@ -167,7 +167,3 @@ test/acceptanceTestSlides.html
|
||||
.DS_Store
|
||||
.envrc
|
||||
config/config.json
|
||||
|
||||
data/*
|
||||
!data/fragenkatalog3b.json
|
||||
!data/README.txt
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,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,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"
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"mistletoe==1.4.0",
|
||||
"requests==2.32.4",
|
||||
"tqdm==4.67.1",
|
||||
"gitpython==3.1.46",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
@@ -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>
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user