waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ tools updatebericht.py

9,5 KB Rohdatei
tools/updatebericht.py — 234 Zeilen
1 """Baut ``docs/updatebericht.md`` — den Bericht zur jeweils aktuellen Fassung.
2
3 Warum es diesen Bericht gibt
4 ----------------------------
5 ``CHANGELOG.md`` fuehrt die ganze Geschichte, und der Etikettentext im Git
6 fasst eine Fassung fuer Anwender zusammen. Beides erreicht aber niemanden, der
7 die Anwendung nur installiert hat: Der Changelog liegt im Quelltextarchiv, das
8 Etikett im Git. Dieser Bericht ist das Blatt, das man jemandem zusammen mit der
9 Installationsdatei in die Hand gibt.
10
11 Warum er erzeugt und nicht geschrieben wird
12 -------------------------------------------
13 Weil er sonst driftet. Genau das ist in diesem Projekt zweimal passiert - die
14 Sperrdatei stand zwei Fassungen lang still, und die Anzahl der Paketpruefungen
15 wanderte durch neun Dokumentstellen. Der Mittelteil dieses Berichts stammt
16 deshalb **Wort fuer Wort** aus dem Abschnitt der Fassung in ``CHANGELOG.md``;
17 die Fassungsnummer kommt aus ``app/package.json``. Von Hand geschrieben ist nur
18 der Rahmen, und der aendert sich zwischen Fassungen nicht.
19
20 ``app/tests/dokumentation.test.ts`` haelt fest, dass der Bericht zur Fassung
21 passt und den Changelog-Abschnitt unveraendert enthaelt.
22
23 Aufruf
24 ------
25 python tools/updatebericht.py
26 python tools/updatebericht.py --pruefen # meldet nur, ob er aktuell ist
27 """
28
29 from __future__ import annotations
30
31 import io
32 import json
33 import re
34 import sys
35 from pathlib import Path
36
37 WURZEL = Path(__file__).resolve().parent.parent
38 PAKET = WURZEL / 'app' / 'package.json'
39 CHANGELOG = WURZEL / 'CHANGELOG.md'
40 ZIEL = WURZEL / 'docs' / 'updatebericht.md'
41
42 #: Ueberschrift eines Fassungsabschnitts: "## [0.23.0] — 2026-08-29".
43 #: Der Gedankenstrich ist ein Halbgeviertstrich, kein Bindestrich.
44 FASSUNGSKOPF = re.compile(r'^## \[(?P<nummer>[^\]]+)\](?:\s*—\s*(?P<datum>\S+))?\s*$')
45
46
47 def fassung() -> str:
48 return json.loads(PAKET.read_text(encoding='utf-8'))['version']
49
50
51 def abschnitt(nummer: str) -> tuple[str, list[str]]:
52 """Der Changelog-Abschnitt einer Fassung: Datum und Zeilen ohne Ueberschrift."""
53 zeilen = CHANGELOG.read_text(encoding='utf-8').split('\n')
54
55 start = None
56 datum = ''
57 for i, zeile in enumerate(zeilen):
58 treffer = FASSUNGSKOPF.match(zeile)
59 if treffer is not None and treffer.group('nummer') == nummer:
60 start = i
61 datum = treffer.group('datum') or ''
62 break
63 if start is None:
64 raise SystemExit(f'ABBRUCH: CHANGELOG.md hat keinen Abschnitt „## [{nummer}]“.')
65
66 ende = len(zeilen)
67 for i in range(start + 1, len(zeilen)):
68 # Hinter der aeltesten Fassung steht ein HTML-Kommentar, der zu keiner
69 # gehoert. Ohne diese Bedingung liefe der letzte Abschnitt bis zum
70 # Dateiende und zoege ihn mit hinein. Heute trifft das nichts - erzeugt
71 # wird immer die Fassung aus der package.json, und die ist nie die
72 # aelteste. Die Bedingung steht hier, damit das so bleibt.
73 if FASSUNGSKOPF.match(zeilen[i]) or zeilen[i].startswith('<!--'):
74 ende = i
75 break
76
77 inhalt = zeilen[start + 1 : ende]
78 while inhalt and not inhalt[0].strip():
79 inhalt.pop(0)
80 while inhalt and not inhalt[-1].strip():
81 inhalt.pop()
82 # Die Trennlinie vor der naechsten Fassung gehoert nicht zum Abschnitt.
83 while inhalt and inhalt[-1].strip() == '---':
84 inhalt.pop()
85 while inhalt and not inhalt[-1].strip():
86 inhalt.pop()
87
88 return datum, [verweise_umhaengen(zeile) for zeile in inhalt]
89
90
91 #: Verweise im Changelog gelten vom Wurzelverzeichnis aus, der Bericht liegt in
92 #: ``docs/``. Ohne Umhaengen zeigte ``docs/stand.md`` von dort auf
93 #: ``docs/docs/stand.md`` - ein toter Verweis in einem erzeugten Dokument, den
94 #: niemand bemerkt, weil ihn niemand von Hand geschrieben hat.
95 VERWEIS = re.compile(r'\]\((?P<ziel>[^)]+)\)')
96
97
98 def verweise_umhaengen(zeile: str) -> str:
99 """Rechnet relative Verweise vom Wurzelverzeichnis nach ``docs/`` um."""
100
101 def ersetzen(treffer: re.Match[str]) -> str:
102 ziel = treffer.group('ziel')
103 # Adressen und reine Sprungmarken bleiben, wie sie sind.
104 if ziel.startswith(('http://', 'https://', 'mailto:', '#')):
105 return treffer.group(0)
106 if ziel.startswith('docs/'):
107 neues_ziel = ziel[len('docs/') :]
108 else:
109 # Alles Uebrige liegt im Wurzelverzeichnis, also eine Ebene hoeher.
110 neues_ziel = '../' + ziel
111 return '](' + neues_ziel + ')'
112
113 return VERWEIS.sub(ersetzen, zeile)
114
115
116 RAHMEN_KOPF = """# Updatebericht zur Fassung {nummer}
117
118 **Stand {datum}.** Was sich geändert hat, wie Sie aktualisieren und was diese
119 Fassung ausdrücklich nicht leistet.
120
121 > Dieser Bericht ist **erzeugt, nicht von Hand geschrieben**. Der Abschnitt
122 > „Was sich geändert hat“ stammt Wort für Wort aus
123 > [CHANGELOG.md](../CHANGELOG.md), die Fassungsnummer aus `app/package.json`.
124 > Neu erzeugen mit `python tools/updatebericht.py`. Die vollständige
125 > Geschichte aller Fassungen steht im Änderungsverlauf; hier steht eine.
126
127 ---
128
129 ## Was sich geändert hat
130
131 """
132
133 RAHMEN_FUSS = """
134 ---
135
136 ## Wie Sie aktualisieren
137
138 Es gibt **keine automatische Aktualisierung**. Die Anwendung sucht von sich aus
139 nie nach einer neuen Fassung — sie stellt überhaupt keine Verbindung her.
140
141 1. Die Datei `Waffensachkunde Lernsoftware-{nummer}-Setup-x64.exe` starten.
142 2. Windows meldet sich, weil die Datei nicht signiert ist: „Weitere
143 Informationen“ → „Trotzdem ausführen“. Das kommt bei **jeder** neuen
144 Fassung wieder, auch wenn Sie den Hinweis schon einmal weggeklickt haben —
145 jede Setup-Datei ist für das Betriebssystem eine neue, unbekannte Datei.
146 Warum das so ist, steht in [installation.md](installation.md).
147 3. Der Assistent installiert über die vorhandene Fassung. Eine vorherige
148 Deinstallation ist nicht nötig.
149
150 **Ihr Lernstand bleibt.** Er liegt nicht im Programmverzeichnis, sondern unter
151 `%APPDATA%\\Waffensachkunde Lernsoftware`, und das Installationsprogramm fasst
152 dieses Verzeichnis nicht an — auch beim Deinstallieren nicht
153 (`deleteAppDataOnUninstall: false`). Bringt eine Fassung eine neue Form der
154 Datenbank mit, wird der vorhandene Lernstand beim ersten Öffnen **nur
155 ergänzt**: Es kommen Tabellen und Spalten dazu, nichts wird geändert oder
156 gelöscht. Geprüft ist das für jeden Schritt von der ersten Form bis zur
157 heutigen.
158
159 **Der Weg zurück ist versperrt.** Sobald eine neuere Fassung Ihren Lernstand
160 einmal angefasst hat, öffnet ihn eine ältere nicht mehr; sie meldet
161 stattdessen „Der Lernstand wurde mit einer neueren Programmversion angelegt“.
162 Das ist Absicht: Eine ältere Fassung kennt die neuen Felder nicht, und was sie
163 schriebe, wäre unvollständig, ohne dass es jemand bemerkte. Dieselbe Sperre
164 gilt für Sicherungen — eine Sicherung aus einer neueren Fassung lässt sich in
165 eine ältere nicht einspielen.
166
167 **Deshalb vor dem Aktualisieren sichern**, wenn Sie sich die Rückkehr
168 offenhalten wollen: Startbildschirm → „Lernstand sichern und übertragen“ →
169 „Sicherung speichern …“. Diese Sicherung stammt noch aus der alten Fassung und
170 lässt sich dort auch wieder einspielen.
171
172 ## Was diese Fassung nicht leistet
173
174 - **Sie ist nicht signiert.** Windows SmartScreen meldet sich beim ersten
175 Start. Das sagt nichts darüber, ob die Anwendung schädlich ist — nur, dass
176 das Betriebssystem den Herausgeber nicht kennt.
177 - **Es gibt keine Fassung für macOS.** Der Bau ist vorbereitet, aber kein Paket
178 ist je entstanden, und nichts an dieser Software lief je auf einem Mac.
179 - **Barrierefreiheit ist gebaut, aber nicht nachgewiesen.** Was automatisch
180 prüfbar ist, wird bei jedem Lauf geprüft; die Szenarien am echten
181 Screenreader stehen aus. Einzelheiten in
182 [stand.md](stand.md), Abschnitt 5.
183 - **Über das Bestehen der Prüfung entscheidet allein der zuständige
184 Prüfungsausschuss.** Diese Anwendung übt den schriftlichen Teil; den
185 praktischen Teil nach § 2 Abs. 3 AWaffV bildet sie nicht ab.
186
187 ## Woran Sie die Angaben nachprüfen können
188
189 - Die installierte Fassung steht in der Anwendung: Startbildschirm →
190 **Systemzustand** → „Programmversion“. Daneben steht der Baustand; trägt er
191 ein `+`, stammt das Paket aus einem unfertigen Arbeitsbaum.
192 - Der vollständige Änderungsverlauf: [CHANGELOG.md](../CHANGELOG.md).
193 - Wo die Software steht — auch mit ihren Lücken: [stand.md](stand.md).
194 - Welche Daten entstehen und wo sie liegen:
195 [datenschutz.md](datenschutz.md).
196 - Der Quelltext steht unter der European Union Public Licence 1.2 und ist
197 öffentlich einsehbar: <https://olaf-willerding.de/quelltext>. Klonen können
198 Sie ihn mit `git clone https://olaf-willerding.de/git/waffensachkunde.git`.
199 """
200
201
202 def bericht() -> str:
203 nummer = fassung()
204 datum, inhalt = abschnitt(nummer)
205 if not datum:
206 raise SystemExit(f'ABBRUCH: Der Abschnitt „## [{nummer}]“ nennt kein Datum.')
207
208 return (
209 RAHMEN_KOPF.format(nummer=nummer, datum=datum)
210 + '\n'.join(inhalt)
211 + '\n'
212 + RAHMEN_FUSS.format(nummer=nummer)
213 )
214
215
216 def main() -> int:
217 neu = bericht()
218
219 if '--pruefen' in sys.argv:
220 alt = ZIEL.read_text(encoding='utf-8') if ZIEL.exists() else ''
221 if alt == neu:
222 print(f'{ZIEL.relative_to(WURZEL)} ist aktuell.')
223 return 0
224 print(f'{ZIEL.relative_to(WURZEL)} ist NICHT aktuell — neu erzeugen mit:')
225 print(' python tools/updatebericht.py')
226 return 1
227
228 io.open(ZIEL, 'w', encoding='utf-8', newline='').write(neu)
229 print(f'Geschrieben: {ZIEL.relative_to(WURZEL)} ({len(neu)} Zeichen, Fassung {fassung()}).')
230 return 0
231
232
233 if __name__ == '__main__':
234 raise SystemExit(main())