Split technical questions into per-class decks
This commit is contained in:
@@ -43,7 +43,9 @@ BNetzA ZIP ──[Stage 1: amateurfunk_fetch.py]──► data/<slug>/
|
|||||||
└── manifest.json
|
└── manifest.json
|
||||||
|
|
||||||
data/ ──[Stage 2: amateurfunk_anki.py]──► anki/
|
data/ ──[Stage 2: amateurfunk_anki.py]──► anki/
|
||||||
├── amateurfunk-technische-kenntnisse.apkg
|
├── amateurfunk-technische-kenntnisse-n.apkg
|
||||||
|
├── amateurfunk-technische-kenntnisse-e.apkg
|
||||||
|
├── amateurfunk-technische-kenntnisse-a.apkg
|
||||||
├── amateurfunk-betriebliche-kenntnisse.apkg
|
├── amateurfunk-betriebliche-kenntnisse.apkg
|
||||||
└── amateurfunk-kenntnisse-von-vorschriften.apkg
|
└── amateurfunk-kenntnisse-von-vorschriften.apkg
|
||||||
```
|
```
|
||||||
@@ -66,9 +68,12 @@ data/ ──[Stage 2: amateurfunk_anki.py]──► anki/
|
|||||||
|
|
||||||
1. Read the latest edition from `data/` (following
|
1. Read the latest edition from `data/` (following
|
||||||
`manifest-latest.json` to a per-edition directory).
|
`manifest-latest.json` to a per-edition directory).
|
||||||
2. Split the catalog into three categories — one per top-level
|
2. Split the catalog into five categories. Betriebliche and
|
||||||
Prüfungsteil (Technische / Betriebliche / Vorschriften). The
|
Vorschriften get one deck each (shared across every candidate).
|
||||||
license-class axis is mapped into tags, not separate packages.
|
Technische is additionally fanned out per license class into three
|
||||||
|
decks (N / E / A) using a strict equality split on the question's
|
||||||
|
`class` field. The `klasse-N|E|A` tag is still emitted on every
|
||||||
|
note for inside-Anki filtering.
|
||||||
3. Render every question as an Anki note: shuffled A/B/C/D choices on
|
3. Render every question as an Anki note: shuffled A/B/C/D choices on
|
||||||
the front, the displayed position of the correct answer on the
|
the front, the displayed position of the correct answer on the
|
||||||
back. Inline `$...$` LaTeX is converted to MathJax `\(...\)`
|
back. Inline `$...$` LaTeX is converted to MathJax `\(...\)`
|
||||||
@@ -101,7 +106,8 @@ SVG dark-mode handling, schema choices) live in `DESIGN.md` §7.
|
|||||||
- Start from `DESIGN.md` — it has the JSON schema, the question/answer
|
- Start from `DESIGN.md` — it has the JSON schema, the question/answer
|
||||||
conventions (answer A is always correct upstream → consumers shuffle
|
conventions (answer A is always correct upstream → consumers shuffle
|
||||||
before display), the LaTeX-in-questions caveat, the exam-structure
|
before display), the LaTeX-in-questions caveat, the exam-structure
|
||||||
rationale for the three Anki packages, and per-stage design notes.
|
rationale for the Anki package layout (Betriebliche, Vorschriften,
|
||||||
|
and Technische split per license class), and per-stage design notes.
|
||||||
- Do not invent new download URLs; the ones in `DESIGN.md` were
|
- Do not invent new download URLs; the ones in `DESIGN.md` were
|
||||||
verified against the live BNetzA site.
|
verified against the live BNetzA site.
|
||||||
- When BNetzA publishes a new edition, expect a new
|
- When BNetzA publishes a new edition, expect a new
|
||||||
|
|||||||
@@ -491,28 +491,43 @@ output artifacts.
|
|||||||
|
|
||||||
```
|
```
|
||||||
anki/
|
anki/
|
||||||
amateurfunk-technische-kenntnisse.apkg (~2.5 MB, 1374 cards)
|
amateurfunk-technische-kenntnisse-n.apkg (195 cards)
|
||||||
amateurfunk-betriebliche-kenntnisse.apkg (~130 KB, 172 cards)
|
amateurfunk-technische-kenntnisse-e.apkg (463 cards)
|
||||||
amateurfunk-kenntnisse-von-vorschriften.apkg (~75 KB, 204 cards)
|
amateurfunk-technische-kenntnisse-a.apkg (716 cards)
|
||||||
|
amateurfunk-betriebliche-kenntnisse.apkg (172 cards)
|
||||||
|
amateurfunk-kenntnisse-von-vorschriften.apkg (204 cards)
|
||||||
```
|
```
|
||||||
|
|
||||||
One `.apkg` per top-level Prüfungsteil. The card counts match the
|
Five `.apkg` files. Betriebliche and Vorschriften are shared across
|
||||||
study pools from §3 axis 1: Technische = 1374 (cumulative N+E+A);
|
every candidate (class-1-only in the data per §3 axis 2) and stay as
|
||||||
Betriebliche = 172 (shared across all candidates); Vorschriften = 204
|
one deck each. Technische is fanned out per license class using a
|
||||||
(also shared). License class is conveyed via tags
|
**strict equality split** on the question's `class` field — class-1
|
||||||
(`klasse-N` / `klasse-E` / `klasse-A`), not separate packages —
|
questions land in the N deck only, class-2 in E only, class-3 in A
|
||||||
Anki users browse decks by topic and filter by class.
|
only. The card counts therefore equal the new-at-this-class slices
|
||||||
|
from §3, not the cumulative study pools: a candidate studying for
|
||||||
|
class E imports Technische-N + Technische-E + Betriebliche +
|
||||||
|
Vorschriften.
|
||||||
|
|
||||||
|
The Technische deck names use Anki's `::` hierarchy separator
|
||||||
|
(`Amateurfunk::Technische Kenntnisse::N`) so the three decks render
|
||||||
|
as children of a shared parent in Anki's deck browser. The
|
||||||
|
`klasse-N` / `klasse-E` / `klasse-A` tag is still emitted on every
|
||||||
|
note — redundant within each Technische deck but useful in Betr/Vor
|
||||||
|
for in-Anki filtering, and harmless besides.
|
||||||
|
|
||||||
### Steps
|
### Steps
|
||||||
|
|
||||||
1. **Load** the latest catalog: follow `manifest-latest.json` to a
|
1. **Load** the latest catalog: follow `manifest-latest.json` to a
|
||||||
per-edition directory, read the catalog JSON, the per-edition
|
per-edition directory, read the catalog JSON, the per-edition
|
||||||
`manifest.json`, and index the `svgs/` folder.
|
`manifest.json`, and index the `svgs/` folder.
|
||||||
2. **Categorize** the question tree into three `Category` objects,
|
2. **Categorize** the question tree into five `Category` objects.
|
||||||
one per top-level Prüfungsteil. Each category carries the flat
|
Betriebliche and Vorschriften get one each. Technische is
|
||||||
list of every question that lives anywhere under it, along with
|
additionally split into three sub-categories (one per license
|
||||||
each question's path through the section tree (used for the card
|
class) via a strict equality match on the question's `class`
|
||||||
breadcrumb and the path tags).
|
field. Each category carries the flat list of every question that
|
||||||
|
lives anywhere under it, along with each question's path through
|
||||||
|
the section tree (used for the card breadcrumb and the path
|
||||||
|
tags).
|
||||||
3. **For each category, render every question** as an Anki note:
|
3. **For each category, render every question** as an Anki note:
|
||||||
- Compute a stable per-question seed from `--seed` + question
|
- Compute a stable per-question seed from `--seed` + question
|
||||||
number; shuffle the A/B/C/D choices using it. The displayed
|
number; shuffle the A/B/C/D choices using it. The displayed
|
||||||
@@ -594,13 +609,19 @@ byte-identical sha256 on each `.apkg`. Verified during review.
|
|||||||
- **Slug for filenames.** `slugify()` maps German umlauts and ß
|
- **Slug for filenames.** `slugify()` maps German umlauts and ß
|
||||||
to ASCII digraphs before NFKD-normalizing the rest, then keeps
|
to ASCII digraphs before NFKD-normalizing the rest, then keeps
|
||||||
only `[a-z0-9-]`. Yields readable, sortable filenames
|
only `[a-z0-9-]`. Yields readable, sortable filenames
|
||||||
(`amateurfunk-technische-kenntnisse.apkg`).
|
(`amateurfunk-technische-kenntnisse-n.apkg`). The three
|
||||||
|
Technische decks append the class letter (`-n`, `-e`, `-a`)
|
||||||
|
to the shared base slug; Betriebliche and Vorschriften use the
|
||||||
|
base slug as-is.
|
||||||
- **Tags.** Each note carries `klasse-N|E|A` plus
|
- **Tags.** Each note carries `klasse-N|E|A` plus
|
||||||
`pfad-<slugified-section>` for every section level below the
|
`pfad-<slugified-section>` for every section level below the
|
||||||
top-level Prüfungsteil. Per-question number tags are
|
top-level Prüfungsteil. The `klasse-*` tag is redundant within
|
||||||
deliberately *not* emitted — they would create ~1750 singletons
|
the three Technische decks (every card already shares one class)
|
||||||
in Anki's tag tree, and the number is already in the dedicated
|
but is still emitted there for uniformity, and remains the
|
||||||
`Number` field for search.
|
primary filter axis inside the Betriebliche/Vorschriften decks.
|
||||||
|
Per-question number tags are deliberately *not* emitted — they
|
||||||
|
would create ~1750 singletons in Anki's tag tree, and the number
|
||||||
|
is already in the dedicated `Number` field for search.
|
||||||
- **Breadcrumb consistency.** The visible card breadcrumb and the
|
- **Breadcrumb consistency.** The visible card breadcrumb and the
|
||||||
stored `Path` field both go through one `display_path()`
|
stored `Path` field both go through one `display_path()`
|
||||||
helper, so they never drift. The boilerplate
|
helper, so they never drift. The boilerplate
|
||||||
@@ -618,8 +639,11 @@ Network-free, fixture-driven. Real Anki is NOT required to
|
|||||||
verify the output — tests open the `.apkg` ZIP directly, parse
|
verify the output — tests open the `.apkg` ZIP directly, parse
|
||||||
the embedded SQLite, and check structural invariants.
|
the embedded SQLite, and check structural invariants.
|
||||||
|
|
||||||
- Top-level structure: one `.apkg` per Prüfungsteil; expected
|
- Top-level structure: five `.apkg` files (Betriebliche +
|
||||||
filename slugs.
|
Vorschriften + Technische×{N,E,A}); expected filename slugs.
|
||||||
|
- Technische class partition: each Technische deck contains
|
||||||
|
exactly the questions whose `class` field matches that letter,
|
||||||
|
and no others.
|
||||||
- Note count and field shape per category.
|
- Note count and field shape per category.
|
||||||
- Tag mapping: a class-1 question carries `klasse-N`, no
|
- Tag mapping: a class-1 question carries `klasse-N`, no
|
||||||
`nummer-*` tags.
|
`nummer-*` tags.
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ make test # run both test suites
|
|||||||
make clean # remove data/ and anki/
|
make clean # remove data/ and anki/
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: three `.apkg` files under `anki/`, one per Prüfungsteil
|
Output: five `.apkg` files under `anki/`. Betriebliche and
|
||||||
(Technische / Betriebliche / Vorschriften). Import them into Anki —
|
Vorschriften get one deck each (shared across all license classes);
|
||||||
re-importing a newer build preserves your review history.
|
Technische is split per class into three decks (N / E / A) following
|
||||||
|
the catalog's `class` field. A class-A candidate who wants every
|
||||||
|
Technische question imports all three Technische decks. Re-importing
|
||||||
|
a newer build preserves your review history.
|
||||||
|
|
||||||
## Exam question source
|
## Exam question source
|
||||||
|
|
||||||
|
|||||||
+66
-20
@@ -7,10 +7,10 @@ script does:
|
|||||||
1. Read the per-edition directory written by `amateurfunk_fetch.py`
|
1. Read the per-edition directory written by `amateurfunk_fetch.py`
|
||||||
(the JSON catalog, the `svgs/` folder, and the per-edition
|
(the JSON catalog, the `svgs/` folder, and the per-edition
|
||||||
`manifest.json`).
|
`manifest.json`).
|
||||||
2. Split the catalog into three categories — one per top-level
|
2. Split the catalog into five categories. Betriebliche and
|
||||||
Prüfungsteil: Technische / Betriebliche / Vorschriften. (The
|
Vorschriften get one each. Technische is additionally fanned
|
||||||
license-class axis is mapped into tags, not into separate
|
out per license class into three decks (N / E / A) via a
|
||||||
packages.)
|
strict equality split on the question's `class` field.
|
||||||
3. For each category, render every question as an Anki note: front
|
3. For each category, render every question as an Anki note: front
|
||||||
shows the (shuffled) A/B/C/D choices, back names the displayed
|
shows the (shuffled) A/B/C/D choices, back names the displayed
|
||||||
position of the correct answer.
|
position of the correct answer.
|
||||||
@@ -20,8 +20,8 @@ script does:
|
|||||||
|
|
||||||
The script is intentionally a single file with stdlib only. Readability
|
The script is intentionally a single file with stdlib only. Readability
|
||||||
beats cleverness here — most of the bytes below are docstrings and
|
beats cleverness here — most of the bytes below are docstrings and
|
||||||
comments. Performance is a non-goal (we build three small decks once
|
comments. Performance is a non-goal (we build a handful of small
|
||||||
per upstream edition).
|
decks once per upstream edition).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -75,6 +75,16 @@ TOP_LEVEL_PREFIX = "Prüfungsfragen im Prüfungsteil: "
|
|||||||
# search for.
|
# search for.
|
||||||
CLASS_TAGS = {"1": "N", "2": "E", "3": "A"}
|
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
|
# Fallback build epoch if neither the manifest nor `--epoch` supplies
|
||||||
# one. Picked as 0 so missing-metadata builds are still deterministic
|
# one. Picked as 0 so missing-metadata builds are still deterministic
|
||||||
# and obviously wrong (timestamps would all show 1970).
|
# and obviously wrong (timestamps would all show 1970).
|
||||||
@@ -164,12 +174,18 @@ def load_latest_catalog(data_dir):
|
|||||||
|
|
||||||
|
|
||||||
def collect_categories(catalog):
|
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
|
Per DESIGN.md §3 axis 1 the catalog has three top-level sections
|
||||||
are exactly the three Prüfungsteile. We walk each subtree and
|
(the three Prüfungsteile). Technische Kenntnisse is additionally
|
||||||
flatten every question it contains, recording the section path so
|
fanned out per license class (N/E/A) — see DESIGN.md §7 "Output
|
||||||
cards can show a breadcrumb and we can emit path-shaped tags.
|
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")
|
sections = catalog.get("sections")
|
||||||
if not isinstance(sections, list):
|
if not isinstance(sections, list):
|
||||||
@@ -181,17 +197,47 @@ def collect_categories(catalog):
|
|||||||
questions = []
|
questions = []
|
||||||
_collect_questions(section, (title,), questions)
|
_collect_questions(section, (title,), questions)
|
||||||
short_title = _short_category_title(title)
|
short_title = _short_category_title(title)
|
||||||
categories.append(
|
if short_title == TECHNISCHE_SHORT_TITLE:
|
||||||
Category(
|
categories.extend(_split_by_class(title, short_title, questions))
|
||||||
title=title,
|
else:
|
||||||
short_title=short_title,
|
categories.append(
|
||||||
slug=slugify(short_title),
|
Category(
|
||||||
questions=questions,
|
title=title,
|
||||||
|
short_title=short_title,
|
||||||
|
slug=slugify(short_title),
|
||||||
|
questions=questions,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return categories
|
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):
|
def _collect_questions(section, path, out):
|
||||||
"""Recurse into a section node and append every question to `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.
|
"""Build every category's `.apkg` and return their result dicts.
|
||||||
|
|
||||||
Loads the latest fetched catalog, picks a build epoch, then walks
|
Loads the latest fetched catalog, picks a build epoch, then walks
|
||||||
the three top-level categories writing one `.apkg` each. Raises
|
every category writing one `.apkg` each. Raises `AnkiBuildError`
|
||||||
`AnkiBuildError` on configuration / catalog problems.
|
on configuration / catalog problems.
|
||||||
"""
|
"""
|
||||||
edition_dir, manifest, catalog = load_latest_catalog(data_dir)
|
edition_dir, manifest, catalog = load_latest_catalog(data_dir)
|
||||||
build_epoch = build_epoch_from_manifest(manifest, override_epoch)
|
build_epoch = build_epoch_from_manifest(manifest, override_epoch)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def make_catalog():
|
|||||||
"questions": [
|
"questions": [
|
||||||
question("NA101", "1", "Was ist A?", "NA101_q"),
|
question("NA101", "1", "Was ist A?", "NA101_q"),
|
||||||
question("EA202", "2", "Was ist E?", None),
|
question("EA202", "2", "Was ist E?", None),
|
||||||
|
question("AA303", "3", "Was ist A-Klasse?", None),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -115,7 +116,7 @@ class TestAnkiBuild(unittest.TestCase):
|
|||||||
self.data_dir = make_fetched_data(self.root)
|
self.data_dir = make_fetched_data(self.root)
|
||||||
self.out_dir = self.root / "anki"
|
self.out_dir = self.root / "anki"
|
||||||
|
|
||||||
def test_builds_one_apkg_per_top_level_category(self):
|
def test_builds_one_apkg_per_category(self):
|
||||||
results = aa.build_all(self.data_dir, self.out_dir, seed="test-seed")
|
results = aa.build_all(self.data_dir, self.out_dir, seed="test-seed")
|
||||||
paths = sorted(path.name for path in self.out_dir.glob("*.apkg"))
|
paths = sorted(path.name for path in self.out_dir.glob("*.apkg"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -123,14 +124,46 @@ class TestAnkiBuild(unittest.TestCase):
|
|||||||
[
|
[
|
||||||
"amateurfunk-betriebliche-kenntnisse.apkg",
|
"amateurfunk-betriebliche-kenntnisse.apkg",
|
||||||
"amateurfunk-kenntnisse-von-vorschriften.apkg",
|
"amateurfunk-kenntnisse-von-vorschriften.apkg",
|
||||||
"amateurfunk-technische-kenntnisse.apkg",
|
"amateurfunk-technische-kenntnisse-a.apkg",
|
||||||
|
"amateurfunk-technische-kenntnisse-e.apkg",
|
||||||
|
"amateurfunk-technische-kenntnisse-n.apkg",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertEqual([r["questions"] for r in results], [2, 1, 1])
|
by_deck = {r["deck"]: r["questions"] for r in results}
|
||||||
|
self.assertEqual(
|
||||||
|
by_deck,
|
||||||
|
{
|
||||||
|
"Amateurfunk::Technische Kenntnisse::N": 1,
|
||||||
|
"Amateurfunk::Technische Kenntnisse::E": 1,
|
||||||
|
"Amateurfunk::Technische Kenntnisse::A": 1,
|
||||||
|
"Amateurfunk::Betriebliche Kenntnisse": 1,
|
||||||
|
"Amateurfunk::Kenntnisse von Vorschriften": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_technische_decks_partition_strictly_by_class_field(self):
|
||||||
|
aa.build_all(self.data_dir, self.out_dir, seed="test-seed")
|
||||||
|
per_class_numbers = {}
|
||||||
|
for letter in ("n", "e", "a"):
|
||||||
|
apkg = self.out_dir / f"amateurfunk-technische-kenntnisse-{letter}.apkg"
|
||||||
|
db_path, _media, _names = extract_collection(apkg, self.root / letter)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
numbers = [
|
||||||
|
row[0].split(aa.FIELD_SEP, 1)[0]
|
||||||
|
for row in conn.execute("select flds from notes")
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
per_class_numbers[letter.upper()] = sorted(numbers)
|
||||||
|
self.assertEqual(
|
||||||
|
per_class_numbers,
|
||||||
|
{"N": ["NA101"], "E": ["EA202"], "A": ["AA303"]},
|
||||||
|
)
|
||||||
|
|
||||||
def test_apkg_contains_notes_cards_and_media(self):
|
def test_apkg_contains_notes_cards_and_media(self):
|
||||||
aa.build_all(self.data_dir, self.out_dir, seed="test-seed")
|
aa.build_all(self.data_dir, self.out_dir, seed="test-seed")
|
||||||
apkg = self.out_dir / "amateurfunk-technische-kenntnisse.apkg"
|
apkg = self.out_dir / "amateurfunk-technische-kenntnisse-n.apkg"
|
||||||
db_path, media, names = extract_collection(apkg, self.root)
|
db_path, media, names = extract_collection(apkg, self.root)
|
||||||
|
|
||||||
self.assertIn("collection.anki2", names)
|
self.assertIn("collection.anki2", names)
|
||||||
@@ -150,8 +183,8 @@ class TestAnkiBuild(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
note_count = conn.execute("select count(*) from notes").fetchone()[0]
|
note_count = conn.execute("select count(*) from notes").fetchone()[0]
|
||||||
card_count = conn.execute("select count(*) from cards").fetchone()[0]
|
card_count = conn.execute("select count(*) from cards").fetchone()[0]
|
||||||
self.assertEqual(note_count, 2)
|
self.assertEqual(note_count, 1)
|
||||||
self.assertEqual(card_count, 2)
|
self.assertEqual(card_count, 1)
|
||||||
fields = [row[0] for row in conn.execute("select flds from notes")]
|
fields = [row[0] for row in conn.execute("select flds from notes")]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user