Merge pull request #143 from DARC-e-V/feature/SWL

Feature/swl
This commit is contained in:
Matthias Jung
2026-08-23 21:44:33 +02:00
committed by GitHub
10 changed files with 181 additions and 32 deletions
+1 -1
View File
@@ -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;
});
+1
View File
@@ -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" },
+3 -3
View File
@@ -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] || {};
+1
View File
@@ -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")
+1
View File
@@ -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,
+38 -6
View File
@@ -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,7 +486,9 @@ 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:
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
+1
View File
@@ -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)
+1
View File
@@ -2,6 +2,7 @@ from enum import StrEnum
class Edition(StrEnum):
swl = "SWL"
n = "N"
e = "E"
a = "A"
+26 -18
View File
@@ -92,6 +92,12 @@
<div class="col"><a href="A_course_index.html" class="btn w-100 text-white" style="background-color: #3BB583; border-color: #3BB583;">E -> A</a></div>
</div>
<br>
<h5>Kurs für die DARC-SWL-Prüfung</h5>
<p>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.</p>
<div class="row row-cols-lg-1 g-2">
<div class="col"><a href="SWL_course_index.html" class="btn btn-outline-dark w-100">SWL<br><small>Kurzwellenhören</small></a></div>
</div>
<br>
<h5>Fragestunden</h5>
<p>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.</p>
<div class="row row-cols-lg-2">
@@ -101,7 +107,7 @@
<br>
<h5>DARC Chat</h5>
<p>Der DARC e. V. betreibt eine eigene Matrix-Instanz. Dort gibt es einen Raum, in dem du deine Fragen stellen kannst:</p>
<div class="row row-cols-lg-3">
<div class="row row-cols-lg-1">
<div class="col"><a href="https://matrix.to/#/#ausbildung:darc.de" class="btn btn-outline-dark w-100">💬 Ausbildung</a></div>
</div>
<br>
@@ -113,17 +119,18 @@
</div>
</div>
</div>
<div class="col col-lg-8 row g-5 pb-5 m-0 row-cols-1 row-cols-lg-2">
<div class="col col-lg-8">
<div class="row g-5 pb-5 row-cols-1 row-cols-lg-2">
<div class="feature col">
<div class="feature-icon bg-primary bg-gradient">
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#5f6368"><path d="m380-300 280-180-280-180v360ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>
</div>
<h2>Videokurse</h2>
<p>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. </p>
<div class="d-grid gap-2 d-sm-flex flex-sm-wrap mb-5">
<a href="https://www.youtube.com/playlist?list=PLDpWnjHk5ERbcgpLPUaU0iTsD-wrmfENk" class="btn btn-outline-dark">Zum Videokurs Klasse N</a>
<a href="https://www.youtube.com/playlist?list=PL_JZo16Mzs5XGeN5N81MwQ70dp7478VkM" class="btn btn-outline-dark">Vorlesung Klasse N+E</a>
<a href="https://www.youtube.com/playlist?list=PL_JZo16Mzs5XebdJhoIDLNp1MObBy7F5d" class="btn btn-outline-dark">Vorlesung Klasse A</a>
<div class="row row-cols-lg-3 g-2">
<div class="col"><a href="https://www.youtube.com/playlist?list=PLDpWnjHk5ERbcgpLPUaU0iTsD-wrmfENk" class="btn btn-outline-dark w-100">Videokurs<br><small>Klasse N</small></a></div>
<div class="col"><a href="https://www.youtube.com/playlist?list=PL_JZo16Mzs5XGeN5N81MwQ70dp7478VkM" class="btn btn-outline-dark w-100">Vorlesung<br><small>Klasse N+E</small></a></div>
<div class="col"><a href="https://www.youtube.com/playlist?list=PL_JZo16Mzs5XebdJhoIDLNp1MObBy7F5d" class="btn btn-outline-dark w-100">Vorlesung<br><small>Klasse A</small></a></div>
</div>
</div>
<div class="feature col">
@@ -133,9 +140,9 @@
</div>
<h2>Buch Klasse N</h2>
<p>Die Lernhinhalte für die Klasse N gibt es auch als gedrucktes Buch. Es ist passend zum Online-Kurs ausgelegt.</p>
<div class="d-grid gap-2 d-md-block mb-5">
<a href="https://darcverlag.de/DARC-Amateurfunklehrgang-Klasse-N" class="btn btn-outline-dark">Beim DARC-Verlag kaufen</a>
<a href="https://www.amazon.de/dp/388692100X" class="btn btn-outline-dark">Amazon</a>
<div class="row row-cols-lg-2 g-2 mb-5">
<div class="col"><a href="https://darcverlag.de/DARC-Amateurfunklehrgang-Klasse-N" class="btn btn-outline-dark w-100">Beim DARC-Verlag kaufen</a></div>
<div class="col"><a href="https://www.amazon.de/dp/388692100X" class="btn btn-outline-dark w-100">Bei Amazon Kaufen</a></div>
</div>
</div>
<div class="feature col">
@@ -144,9 +151,9 @@
</div>
<h2>Kurse</h2>
<p>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!</p>
<div class="d-grid gap-2 d-md-block mb-5">
<a href="kurse_vor_ort_karte.html" class="btn btn-outline-dark">Präsenz</a>
<a href="kurse_vor_ort_liste.html" class="btn btn-outline-dark">Online</a>
<div class="d-flex gap-2 mb-5">
<a href="kurse_vor_ort_karte.html" class="btn btn-outline-dark flex-fill">Präsenz</a>
<a href="kurse_vor_ort_liste.html" class="btn btn-outline-dark flex-fill">Online</a>
</div>
</div>
<div class="feature col">
@@ -155,8 +162,8 @@
</div>
<h2>Ausbildungs&shy;paten</h2>
<p>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.</p>
<div class="d-grid gap-2 d-md-block mb-5">
<a href="patenkarte.html" class="btn btn-outline-dark">Zur Patenkarte</a>
<div class="d-grid gap-2 mb-5">
<a href="patenkarte.html" class="btn btn-outline-dark w-100">Zur Patenkarte</a>
</div>
</div>
<div class="feature col">
@@ -166,7 +173,7 @@
<h2>Fragenkatalog</h2>
<p>Den Fragenkatalog mit den amtlichen Prüfungsfragen gibt es digital bei der <a href="https://50ohm.de/fk">Bundesnetzagentur</a>. Beim DARC-Verlag kann man auch eine gedruckte Version bestellen.</p>
<div class="d-grid gap-2 d-md-block mb-5">
<a href="https://darcverlag.de/Fragenkatalog-Klasse-N" class="btn btn-outline-dark">Beim DARC-Verlag kaufen</a>
<a href="https://darcverlag.de/Fragenkatalog-Klasse-N" class="btn btn-outline-dark w-100">Beim DARC-Verlag kaufen</a>
</div>
</div>
<div class="feature col">
@@ -175,9 +182,10 @@
</div>
<h2>Trainings App</h2>
<p>Zum Trainieren der Fragen gibt es die 50Ohm App. Sie ist passend zum Online-Kurs ausgelegt.</p>
<div class="d-grid gap-2 d-md-block mb-5">
<a href="https://play.google.com/store/apps/details?id=de.darc.fuenfizigohm" class="btn btn-outline-dark">Android</a>
<a href="https://apps.apple.com/de/app/50ohm/id6474642114" class="btn btn-outline-dark">iOS</a>
<div class="d-flex gap-2 mb-5">
<a href="https://play.google.com/store/apps/details?id=de.darc.fuenfizigohm" class="btn btn-outline-dark flex-fill">Android</a>
<a href="https://apps.apple.com/de/app/50ohm/id6474642114" class="btn btn-outline-dark flex-fill">iOS</a>
</div>
</div>
</div>
</div>
+104
View File
@@ -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()