@@ -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;
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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] || {};
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
+40
-8
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ from enum import StrEnum
|
||||
|
||||
|
||||
class Edition(StrEnum):
|
||||
swl = "SWL"
|
||||
n = "N"
|
||||
e = "E"
|
||||
a = "A"
|
||||
|
||||
+28
-20
@@ -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="feature col">
|
||||
<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­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,11 +182,12 @@
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<a rel="me" href="https://social.darc.de/@AJW"></a>
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user