diff --git a/assets/exam-simulator.js b/assets/exam-simulator.js index 2efb9aa..977c06a 100644 --- a/assets/exam-simulator.js +++ b/assets/exam-simulator.js @@ -38,7 +38,7 @@ if (!value) return []; var seen = new Set(); return value.toUpperCase().split(/[\s+,]+/).filter(function (questionId) { - if (!/^[A-Z]{2}\d{3}$/.test(questionId) || seen.has(questionId)) return false; + if (!/^[A-Z]{2,3}\d{3}$/.test(questionId) || seen.has(questionId)) return false; seen.add(questionId); return true; }); diff --git a/assets/homework.js b/assets/homework.js index b500b8b..c0279f2 100644 --- a/assets/homework.js +++ b/assets/homework.js @@ -2,6 +2,7 @@ "use strict"; var COURSE_CHOICES = [ + { edition: "SWL", label: "SWL-Kurs", detail: "Kurzwellenhören", color: "#FFFFFF" }, { edition: "N", label: "N-Kurs", detail: "N", color: "#47ABE8" }, { edition: "NE", label: "E-Kurs", detail: "N + E", color: "#FE756C" }, { edition: "NEA", label: "A-Kurs", detail: "N + E + A", color: "#3BB583" }, diff --git a/assets/question-catalog.js b/assets/question-catalog.js index 4b682f5..357803f 100644 --- a/assets/question-catalog.js +++ b/assets/question-catalog.js @@ -12,9 +12,9 @@ function walk(section, sectionIndex, category) { (section.questions || []).forEach(function (question) { - var part = sectionIndex === 0 - ? partForClass[String(question.class)] - : partForSection[sectionIndex]; + var part = String(question.class) === "SWL" + ? "SWL" + : (sectionIndex === 0 ? partForClass[String(question.class)] : partForSection[sectionIndex]); if (!part) return; var questionMetadata = metadata[question.number] || {}; diff --git a/build.py b/build.py index a8f4c68..d23027e 100644 --- a/build.py +++ b/build.py @@ -6,6 +6,7 @@ conf = config.Config() # Build Everything: bd = build.Build(conf) bd.build_website() +bd.build_edition("SWL") bd.build_edition("N") bd.build_edition("E") bd.build_edition("A") diff --git a/main.py b/main.py index 51bf833..78cd7c9 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ app = typer.Typer() @app.command() def main( edition: Annotated[list[Edition], typer.Option(help="Edition to build, can be specified multiple times.")] = [ # noqa: B006 -- default value is required for typer + Edition.swl, Edition.n, Edition.e, Edition.a, diff --git a/src/build.py b/src/build.py index 41be79f..c9d9521 100644 --- a/src/build.py +++ b/src/build.py @@ -26,10 +26,32 @@ class Build: self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers with self.config.p_data_fragenkatalog.open(encoding="utf-8") as file: self.fragenkatalog = json.load(file) + + supplemental_questions = {} + if self.config.p_data_fragenkatalog_swl.exists(): + with self.config.p_data_fragenkatalog_swl.open(encoding="utf-8") as file: + swl_fragenkatalog = json.load(file) + supplemental_questions = self.__parse_katalog(swl_fragenkatalog) + self.fragenkatalog["sections"].extend(swl_fragenkatalog.get("sections", [])) + with self.config.p_data_metadata.open(encoding="utf-8") as file: self.question_metadata = json.load(file) self.questions = self.__parse_katalog(self.fragenkatalog) + for number in supplemental_questions: + self.question_metadata.setdefault( + number, + { + "directus_id": "", + "picture_question": "", + "picture_a": "", + "picture_b": "", + "picture_c": "", + "picture_d": "", + "layout": "", + }, + ) + self.question_index = {} self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE) @@ -143,8 +165,10 @@ class Build: def __build_exam_simulator(self): self.config.p_build_assets.mkdir(parents=True, exist_ok=True) - shutil.copyfile(self.config.p_data_fragenkatalog, self.config.p_build_assets / "fragenkatalog.json") - shutil.copyfile(self.config.p_data_metadata, self.config.p_build_assets / "metadata.json") + with (self.config.p_build_assets / "fragenkatalog.json").open("w", encoding="utf-8") as file: + json.dump(self.fragenkatalog, file, ensure_ascii=False, indent=4) + with (self.config.p_build_assets / "metadata.json").open("w", encoding="utf-8") as file: + json.dump(self.question_metadata, file, ensure_ascii=False, indent=4) (self.config.p_build_assets / "exam-questions.json").unlink(missing_ok=True) picture_fields = ("picture_question", "picture_a", "picture_b", "picture_c", "picture_d") @@ -191,8 +215,11 @@ class Build: def __build_homework_tool(self): toc_target = self.config.p_build_assets / "toc" toc_target.mkdir(parents=True, exist_ok=True) - for edition in ("N", "NE", "NEA", "E", "EA", "A"): - shutil.copyfile(self.config.p_data_toc / f"{edition}.json", toc_target / f"{edition}.json") + for edition in ("SWL", "N", "NE", "NEA", "E", "EA", "A"): + source = self.config.p_data_toc / f"{edition}.json" + if not source.exists(): + source = self.config.p_data_toc / f"{edition.lower()}.json" + shutil.copyfile(source, toc_target / f"{edition}.json") homework_template = self.env.get_template("html/homework.html") page = self.__build_page( @@ -415,9 +442,12 @@ class Build: self.config.p_build.mkdir(exist_ok=True) edition = edition.upper() + toc_file = self.config.p_data_toc / f"{edition}.json" + if not toc_file.exists(): + toc_file = self.config.p_data_toc / f"{edition.lower()}.json" with ( - (self.config.p_data_toc / f"{edition}.json").open(encoding="utf-8") as file, + toc_file.open(encoding="utf-8") as file, Progress( TaskProgressColumn(), BarColumn(), @@ -456,9 +486,11 @@ class Build: with (self.config.p_data_sections / f"{ident}.md").open(encoding="utf-8") as sfile: section_content = sfile.read() section["content"] = section_content - with (self.config.p_data_slides / f"{ident}.md").open(encoding="utf-8") as sfile: - section_content = sfile.read() - section["slide"] = section_content + slide_file = self.config.p_data_slides / f"{ident}.md" + if slide_file.exists(): + with slide_file.open(encoding="utf-8") as sfile: + section_content = sfile.read() + section["slide"] = section_content if section["content"] is not None: self.__collect_question_occurrences( diff --git a/src/config.py b/src/config.py index 97d1dbb..e18e344 100644 --- a/src/config.py +++ b/src/config.py @@ -29,6 +29,7 @@ class Config: self.p_data_solutions = self.p_data_contents / "solutions" self.p_data_fragenkatalog = self.p_data_questions / "fragenkatalog3b.json" + self.p_data_fragenkatalog_swl = self.p_data_questions / "fragenkatalog_swl.json" self.p_data_metadata = self.p_data_questions / "metadata3b.json" self.p_build = Path(self.get_config_value("output", "./build") if build_path is None else build_path) diff --git a/src/edition.py b/src/edition.py index 2a250c5..fa079f2 100644 --- a/src/edition.py +++ b/src/edition.py @@ -2,6 +2,7 @@ from enum import StrEnum class Edition(StrEnum): + swl = "SWL" n = "N" e = "E" a = "A" diff --git a/templates/html/index.html b/templates/html/index.html index dee994b..669b1da 100644 --- a/templates/html/index.html +++ b/templates/html/index.html @@ -92,6 +92,12 @@
E -> A

+
Kurs für die DARC-SWL-Prüfung
+

Du möchtest erst einmal im Amateurfunk zuhören? Als DARC-Mitglied kannst du nach der DE-Prüfung ein persönliches DE-Kennzeichen erhalten und SWL-QSL-Karten über die DARC-QSL-Vermittlung versenden.

+
+
SWL
Kurzwellenhören
+
+
Fragestunden

Tauchen beim Selbststudium Fragen auf? Unsere ehrenamtlichen Helfer bieten für DARC-Mitglieder und Gäste eine Fragestunde Mittwochs um 19:00 Uhr und Sonntags um 18:00 Uhr auf dem DARC-Videokonferenzserver an.

@@ -101,7 +107,7 @@
DARC Chat

Der DARC e. V. betreibt eine eigene Matrix-Instanz. Dort gibt es einen Raum, in dem du deine Fragen stellen kannst:

-
+
@@ -113,17 +119,18 @@
-
-
+
+
+

Videokurse

In Zusammenarbeit mit DL2YMR ist ein Videokurs für die Klasse N entstanden, der auf seinem YouTube-Kanal abrufbar ist. Weiterhin gibt es noch Vorlesungen für N+E sowie A.

-
@@ -133,9 +140,9 @@

Buch Klasse N

Die Lernhinhalte für die Klasse N gibt es auch als gedrucktes Buch. Es ist passend zum Online-Kurs ausgelegt.

-
@@ -144,9 +151,9 @@

Kurse

Einige unserer Ortsverbände bieten Präsenz- oder Onlinekurse mit erfahrenen Ausbildern an. Finde heraus, ob es einen für dich passenden Kurs bei dir in der Nähe gibt!

-
@@ -155,8 +162,8 @@

Ausbildungs­paten

Unsere Ausbildungspaten sind erfahrene Funkamateure, die dich gerne unterstützen. Sie können mit dir auch praktisch üben und nützliche Tipps für die Praxis geben.

-
@@ -166,7 +173,7 @@

Fragenkatalog

Den Fragenkatalog mit den amtlichen Prüfungsfragen gibt es digital bei der Bundesnetzagentur. Beim DARC-Verlag kann man auch eine gedruckte Version bestellen.

@@ -175,11 +182,12 @@

Trainings App

Zum Trainieren der Fragen gibt es die 50Ohm App. Sie ist passend zum Online-Kurs ausgelegt.

-
- Android - iOS +
+ Android + iOS
-
+
+
diff --git a/test/test_swl_question_catalog.py b/test/test_swl_question_catalog.py new file mode 100644 index 0000000..828e0e7 --- /dev/null +++ b/test/test_swl_question_catalog.py @@ -0,0 +1,104 @@ +import json + +from src.build import Build +from src.config import Config + + +def write_catalog(path, number): + path.write_text( + json.dumps( + { + "sections": [ + { + "title": f"Katalog {number}", + "questions": [ + { + "number": number, + "class": "SWL" if number.startswith("SWL") else "1", + "question": f"Frage {number}", + "answer_a": "A", + "answer_b": "B", + "answer_c": "C", + "answer_d": "D", + } + ], + } + ] + } + ), + encoding="utf-8", + ) + + +def prepare_content(tmp_path, include_swl): + questions_path = tmp_path / "contents" / "questions" + questions_path.mkdir(parents=True) + write_catalog(questions_path / "fragenkatalog3b.json", "NA101") + (questions_path / "metadata3b.json").write_text( + json.dumps( + { + "NA101": { + "directus_id": "1", + "picture_question": "", + "picture_a": "", + "picture_b": "", + "picture_c": "", + "picture_d": "", + "layout": "", + } + } + ), + encoding="utf-8", + ) + if include_swl: + write_catalog(questions_path / "fragenkatalog_swl.json", "SWL001") + + +def test_optional_swl_catalog_is_loaded_with_default_metadata(tmp_path): + prepare_content(tmp_path, include_swl=True) + + build = Build(Config(content_path=tmp_path, build_path=tmp_path / "build")) + + assert set(build.questions) == {"NA101", "SWL001"} + assert build.question_metadata["SWL001"]["layout"] == "" + assert len(build.fragenkatalog["sections"]) == 2 + + +def test_missing_optional_swl_catalog_is_allowed(tmp_path): + prepare_content(tmp_path, include_swl=False) + + build = Build(Config(content_path=tmp_path, build_path=tmp_path / "build")) + + assert set(build.questions) == {"NA101"} + assert len(build.fragenkatalog["sections"]) == 1 + + +def test_lowercase_swl_toc_is_supported(tmp_path): + prepare_content(tmp_path, include_swl=True) + toc_path = tmp_path / "toc" + section_path = tmp_path / "contents" / "sections" + toc_path.mkdir() + section_path.mkdir() + (section_path / "swl_intro.md").write_text("Einführung", encoding="utf-8") + (toc_path / "swl.json").write_text( + json.dumps( + { + "title": "SWL-Kurs", + "edition": "SWL", + "chapters": [ + { + "title": "Einstieg", + "ident": "swl_einstieg", + "sections": [{"title": "Einführung", "ident": "swl_intro"}], + } + ], + } + ), + encoding="utf-8", + ) + + build = Build(Config(content_path=tmp_path, build_path=tmp_path / "build")) + + build.build_edition("SWL") + assert (tmp_path / "build" / "SWL_course_index.html").exists() + assert (tmp_path / "build" / "SWL_swl_intro.html").exists()