Pull explanations from 50ohm.de when available
This commit is contained in:
@@ -420,6 +420,50 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self.assertNotIn("af-explanation-low-confidence", back_high)
|
||||
self.assertNotIn("low confidence", back_high)
|
||||
|
||||
def test_cc_by_credit_gated_on_provenance_not_source_domain(self):
|
||||
"""The CC BY derivative-work credit must follow explicit
|
||||
provenance, not the citation domain: an explanation merely
|
||||
citing a 50ohm.de page is NOT a derivative work and must not
|
||||
carry the credit, while one marked as derived from a worked
|
||||
solution must."""
|
||||
derived = {
|
||||
"revision": 2,
|
||||
"explanation": "Worked-solution port.",
|
||||
"source": "https://50ohm.de/NEA_x.html#AD106",
|
||||
"confidence": 8,
|
||||
"provenance": aa.PROVENANCE_50OHM_SOLUTION,
|
||||
}
|
||||
cited_only = {
|
||||
"revision": 1,
|
||||
"explanation": "Independently written, just cites a study page.",
|
||||
"source": "https://50ohm.de/NEA_x.html#XX101",
|
||||
"confidence": 8,
|
||||
}
|
||||
derived_html = aa.render_explanation(derived)
|
||||
cited_html = aa.render_explanation(cited_only)
|
||||
self.assertIn("af-explanation-credit", derived_html)
|
||||
self.assertIn("CC BY 4.0", derived_html)
|
||||
self.assertIn("50ohm.de-Autorenteam", derived_html)
|
||||
self.assertNotIn("af-explanation-credit", cited_html)
|
||||
self.assertNotIn("translated", cited_html)
|
||||
|
||||
def test_root_deck_description_carries_attribution(self):
|
||||
self._build_all()
|
||||
apkg = self.out_dir / "amateurfunk-betriebliche-kenntnisse.apkg"
|
||||
db_path, _media, _names = extract_collection(apkg, self.root / "b")
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
decks = json.loads(conn.execute("select decks from col").fetchone()[0])
|
||||
finally:
|
||||
conn.close()
|
||||
descs = [d["desc"] for d in decks.values() if d["desc"]]
|
||||
self.assertEqual(len(descs), 1) # only the root deck carries it
|
||||
desc = descs[0]
|
||||
self.assertIn("CC BY 4.0", desc)
|
||||
self.assertIn("govdata.de/dl-de/by-2-0", desc)
|
||||
self.assertIn("50ohm.de-Autorenteam", desc)
|
||||
self.assertIn("Bundesnetzagentur", desc)
|
||||
|
||||
def test_missing_explanation_leaves_card_unchanged(self):
|
||||
self._build_all()
|
||||
apkg = self.out_dir / "amateurfunk-technische-kenntnisse-e.apkg"
|
||||
@@ -501,6 +545,48 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self._build_all()
|
||||
self.assertIn("author", str(ctx.exception))
|
||||
|
||||
def test_schema_accepts_valid_provenance(self):
|
||||
path = self.root / "exp_provenance.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": aa.PROVENANCE_50OHM_SOLUTION,
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loaded = aa.load_explanations(path)
|
||||
self.assertEqual(
|
||||
loaded["NA101"]["provenance"], aa.PROVENANCE_50OHM_SOLUTION,
|
||||
)
|
||||
|
||||
def test_schema_rejects_unknown_provenance_value(self):
|
||||
path = self.root / "exp_badprov.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": "made-up",
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(aa.AnkiBuildError):
|
||||
aa.load_explanations(path)
|
||||
|
||||
def test_schema_rejects_unhashable_provenance(self):
|
||||
# A malformed but valid-JSON provenance value (list/object) is
|
||||
# unhashable; the validator must surface AnkiBuildError, never a
|
||||
# raw TypeError from the set-membership test.
|
||||
for bad in ([], {}):
|
||||
path = self.root / "exp_unhashable.json"
|
||||
path.write_text(
|
||||
json.dumps({"NA101": {
|
||||
"revision": 1, "explanation": "ok", "source": "ok",
|
||||
"confidence": 5, "provenance": bad,
|
||||
}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(aa.AnkiBuildError):
|
||||
aa.load_explanations(path)
|
||||
|
||||
def test_invalid_explanation_schema_raises_build_error(self):
|
||||
self.explanations_path.write_text(
|
||||
json.dumps({
|
||||
@@ -617,6 +703,34 @@ class TestRealExplanationsFile(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual([], offenders)
|
||||
|
||||
def test_safety_distance_block_uses_transmitter_power_not_erp(self):
|
||||
"""AK106-AK112 must feed the EIRP formula with transmitter power.
|
||||
|
||||
The relation is `P_EIRP = P_S · 10^((g_d+2.15-a)/10)` with `P_S`
|
||||
the transmitter (Sender) power; writing `P_EIRP = P_ERP · 10^…`
|
||||
double-counts the antenna gain (ERP already includes it). The
|
||||
upstream 50ohm solutions carry exactly this mislabel, so guard
|
||||
the whole block against it regressing back in.
|
||||
"""
|
||||
repo = Path(__file__).resolve().parent
|
||||
explanations_path = repo / "explanations.json"
|
||||
if not explanations_path.exists():
|
||||
self.skipTest("explanations.json not available")
|
||||
explanations = aa.load_explanations(explanations_path)
|
||||
block = [f"AK1{n:02d}" for n in range(6, 13)] # AK106..AK112
|
||||
for key in block:
|
||||
if key not in explanations:
|
||||
continue
|
||||
body = explanations[key]["explanation"]
|
||||
self.assertIn(
|
||||
"P_S", body,
|
||||
f"{key}: EIRP should be computed from transmitter power P_S",
|
||||
)
|
||||
self.assertNotIn(
|
||||
r"P_\mathrm{EIRP} = P_\mathrm{ERP}", body,
|
||||
f"{key}: EIRP must not be derived by scaling ERP by the gain",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user