Split technical questions into per-class decks
This commit is contained in:
+66
-20
@@ -7,10 +7,10 @@ script does:
|
||||
1. Read the per-edition directory written by `amateurfunk_fetch.py`
|
||||
(the JSON catalog, the `svgs/` folder, and the per-edition
|
||||
`manifest.json`).
|
||||
2. Split the catalog into three categories — one per top-level
|
||||
Prüfungsteil: Technische / Betriebliche / Vorschriften. (The
|
||||
license-class axis is mapped into tags, not into separate
|
||||
packages.)
|
||||
2. Split the catalog into five categories. Betriebliche and
|
||||
Vorschriften get one each. Technische is additionally fanned
|
||||
out per license class into three decks (N / E / A) via a
|
||||
strict equality split on the question's `class` field.
|
||||
3. For each category, render every question as an Anki note: front
|
||||
shows the (shuffled) A/B/C/D choices, back names the displayed
|
||||
position of the correct answer.
|
||||
@@ -20,8 +20,8 @@ script does:
|
||||
|
||||
The script is intentionally a single file with stdlib only. Readability
|
||||
beats cleverness here — most of the bytes below are docstrings and
|
||||
comments. Performance is a non-goal (we build three small decks once
|
||||
per upstream edition).
|
||||
comments. Performance is a non-goal (we build a handful of small
|
||||
decks once per upstream edition).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -75,6 +75,16 @@ TOP_LEVEL_PREFIX = "Prüfungsfragen im Prüfungsteil: "
|
||||
# search for.
|
||||
CLASS_TAGS = {"1": "N", "2": "E", "3": "A"}
|
||||
|
||||
# Ordered class digits used when fanning Technische Kenntnisse out
|
||||
# into one deck per license class. Order controls the on-disk file
|
||||
# ordering and the Anki deck-tree ordering.
|
||||
CLASS_ORDER = [("1", "N"), ("2", "E"), ("3", "A")]
|
||||
|
||||
# Short title of the top-level Prüfungsteil that gets fanned out
|
||||
# per license class. The other two (Betriebliche, Vorschriften) are
|
||||
# shared across all candidates and stay as a single deck each.
|
||||
TECHNISCHE_SHORT_TITLE = "Technische Kenntnisse"
|
||||
|
||||
# Fallback build epoch if neither the manifest nor `--epoch` supplies
|
||||
# one. Picked as 0 so missing-metadata builds are still deterministic
|
||||
# and obviously wrong (timestamps would all show 1970).
|
||||
@@ -164,12 +174,18 @@ def load_latest_catalog(data_dir):
|
||||
|
||||
|
||||
def collect_categories(catalog):
|
||||
"""Split the catalog into one `Category` per top-level section.
|
||||
"""Split the catalog into one `Category` per output `.apkg`.
|
||||
|
||||
Per DESIGN.md §3 axis 1, the catalog's three top-level sections
|
||||
are exactly the three Prüfungsteile. We walk each subtree and
|
||||
flatten every question it contains, recording the section path so
|
||||
cards can show a breadcrumb and we can emit path-shaped tags.
|
||||
Per DESIGN.md §3 axis 1 the catalog has three top-level sections
|
||||
(the three Prüfungsteile). Technische Kenntnisse is additionally
|
||||
fanned out per license class (N/E/A) — see DESIGN.md §7 "Output
|
||||
layout". Betriebliche and Vorschriften are class-1-only in the
|
||||
data but apply to every candidate, so they stay as one category
|
||||
each.
|
||||
|
||||
For every category we flatten the subtree into a list of
|
||||
`QuestionItem`s carrying the section path, used downstream for
|
||||
the breadcrumb and the `pfad-*` tags.
|
||||
"""
|
||||
sections = catalog.get("sections")
|
||||
if not isinstance(sections, list):
|
||||
@@ -181,17 +197,47 @@ def collect_categories(catalog):
|
||||
questions = []
|
||||
_collect_questions(section, (title,), questions)
|
||||
short_title = _short_category_title(title)
|
||||
categories.append(
|
||||
Category(
|
||||
title=title,
|
||||
short_title=short_title,
|
||||
slug=slugify(short_title),
|
||||
questions=questions,
|
||||
if short_title == TECHNISCHE_SHORT_TITLE:
|
||||
categories.extend(_split_by_class(title, short_title, questions))
|
||||
else:
|
||||
categories.append(
|
||||
Category(
|
||||
title=title,
|
||||
short_title=short_title,
|
||||
slug=slugify(short_title),
|
||||
questions=questions,
|
||||
)
|
||||
)
|
||||
)
|
||||
return categories
|
||||
|
||||
|
||||
def _split_by_class(title, short_title, questions):
|
||||
"""Fan one Prüfungsteil out into one `Category` per license class.
|
||||
|
||||
Strict equality split on the question's `class` field: a class-1
|
||||
question lands in the N deck only, class-2 in E only, class-3 in
|
||||
A only. This matches the catalog's `class` field as written, not
|
||||
the cumulative study pool (a class-A candidate who wants every
|
||||
Technische question imports all three decks).
|
||||
|
||||
The short title uses Anki's `::` deck-hierarchy separator so the
|
||||
three decks render as children of a shared parent in Anki's deck
|
||||
browser (e.g. `Amateurfunk::Technische Kenntnisse::N`).
|
||||
"""
|
||||
base_slug = slugify(short_title)
|
||||
for digit, letter in CLASS_ORDER:
|
||||
subset = [
|
||||
item for item in questions
|
||||
if str(item.question.get("class")) == digit
|
||||
]
|
||||
yield Category(
|
||||
title=f"{title} — Klasse {letter}",
|
||||
short_title=f"{short_title}::{letter}",
|
||||
slug=f"{base_slug}-{letter.lower()}",
|
||||
questions=subset,
|
||||
)
|
||||
|
||||
|
||||
def _collect_questions(section, path, out):
|
||||
"""Recurse into a section node and append every question to `out`.
|
||||
|
||||
@@ -1203,8 +1249,8 @@ def build_all(data_dir, out_dir, seed, override_epoch=None):
|
||||
"""Build every category's `.apkg` and return their result dicts.
|
||||
|
||||
Loads the latest fetched catalog, picks a build epoch, then walks
|
||||
the three top-level categories writing one `.apkg` each. Raises
|
||||
`AnkiBuildError` on configuration / catalog problems.
|
||||
every category writing one `.apkg` each. Raises `AnkiBuildError`
|
||||
on configuration / catalog problems.
|
||||
"""
|
||||
edition_dir, manifest, catalog = load_latest_catalog(data_dir)
|
||||
build_epoch = build_epoch_from_manifest(manifest, override_epoch)
|
||||
|
||||
Reference in New Issue
Block a user