Compare commits
6
Commits
7d63063024
...
v1.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2fab7fd3 | ||
|
|
0ec5de50bb | ||
|
|
fb191449d1 | ||
|
|
4e3b2237ff | ||
|
|
7de6984ebc | ||
|
|
2237779e56 |
No files matched your search
@@ -5,10 +5,30 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
||||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.4] – 2026-11-03
|
||||||
|
|
||||||
|
### Aktualisiert
|
||||||
|
- Template PDF erneuert. Neues und altes PDF sind nun enthalten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.3] – 2026-10-07
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- internal development changes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.3.2] – 2026-10-07
|
## [1.3.2] – 2026-10-07
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Input-Dateien hinzufügen**: Bei leerem Input-Ordner können `.xlsx`-Dateien per Drag-and-drop oder Dateiauswahl in den Input-Ordner kopiert werden
|
||||||
|
- **Input-Ordner öffnen**: Ein Button öffnet den Input-Ordner direkt im Explorer
|
||||||
|
- **Input-Ordner neu einlesen**: Dateiliste kann über Button aktualisiert werden
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
- EXE erkennt laufende Entwicklungsinstanz und fragt vor dem Neustart
|
- EXE erkennt laufende Entwicklungsinstanz und fragt vor dem Neustart
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ Um das Programm zu starten: Doppelklick auf `Tätigkeitsnachweis-Generator.exe`
|
|||||||
## Bedienung
|
## Bedienung
|
||||||
|
|
||||||
1. **Input-Datei wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `input/`-Ordner
|
1. **Input-Datei wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `input/`-Ordner
|
||||||
|
- Bei leerem Input-Ordner: „Input-Ordner öffnen“ öffnet den Explorer.
|
||||||
|
- Excel-Dateien können per Drag-and-drop hochgeladen werden. Sie werden in den
|
||||||
|
Input-Ordner kopiert; danach wird die Dateiliste automatisch neu eingelesen.
|
||||||
|
- Nach manuellem Kopieren im Explorer „Input-Ordner neu einlesen“ anklicken.
|
||||||
- Nach Auswahl wird die Datei sofort validiert
|
- Nach Auswahl wird die Datei sofort validiert
|
||||||
- Angezeigt werden: Anzahl Mitarbeiter, Antragsnummern, Einträge pro Monat
|
- Angezeigt werden: Anzahl Mitarbeiter, Antragsnummern, Einträge pro Monat
|
||||||
2. **Template wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `template/`-Ordner
|
2. **Template wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `template/`-Ordner
|
||||||
@@ -54,4 +58,4 @@ Mapping von Input-Feldern auf Template-Zellen:
|
|||||||
|
|
||||||
- Pro selektiertem Mitarbeiter wird eine `.xlsx`-Datei erstellt
|
- Pro selektiertem Mitarbeiter wird eine `.xlsx`-Datei erstellt
|
||||||
- Jeder Monat wird als eigenes Sheet (Kopie des Templates) eingefügt
|
- Jeder Monat wird als eigenes Sheet (Kopie des Templates) eingefügt
|
||||||
- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx`
|
- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx`
|
||||||
+65
-2
@@ -32,6 +32,14 @@ from src.writer import generate_activity_reports
|
|||||||
from version_check import UPDATE_DIR, get_latest_version
|
from version_check import UPDATE_DIR, get_latest_version
|
||||||
|
|
||||||
|
|
||||||
|
def open_input_folder(directory: Path):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.startfile(str(directory))
|
||||||
|
else:
|
||||||
|
subprocess.run(["open" if sys.platform == "darwin" else "xdg-open", str(directory)], check=True)
|
||||||
|
|
||||||
|
|
||||||
def set_active_employee(employee_name: str):
|
def set_active_employee(employee_name: str):
|
||||||
"""Merkt die zuletzt angeklickte Person für die Monatsliste."""
|
"""Merkt die zuletzt angeklickte Person für die Monatsliste."""
|
||||||
st.session_state.active_month_employee = employee_name
|
st.session_state.active_month_employee = employee_name
|
||||||
@@ -155,9 +163,64 @@ col1, col2 = st.columns(2)
|
|||||||
with col1:
|
with col1:
|
||||||
st.subheader("1. Input-Datei")
|
st.subheader("1. Input-Datei")
|
||||||
input_files = get_available_input_files(app_config)
|
input_files = get_available_input_files(app_config)
|
||||||
|
for message in st.session_state.pop("input_upload_messages", []):
|
||||||
|
st.success(message)
|
||||||
|
for message in st.session_state.pop("input_upload_errors", []):
|
||||||
|
st.error(message)
|
||||||
if not input_files:
|
if not input_files:
|
||||||
st.warning("Keine .xlsx-Dateien im input/-Ordner gefunden.")
|
with st.container(border=True, key="input_file_panel"):
|
||||||
|
st.markdown("**Excel-Datei hinzufügen**")
|
||||||
|
st.caption("Keine .xlsx-Dateien im Input/-Ordner gefunden.")
|
||||||
|
input_directory = Path(app_config.input_dir)
|
||||||
|
uploaded_files = st.file_uploader(
|
||||||
|
"Excel-Dateien hierher ziehen oder auswählen",
|
||||||
|
type=["xlsx"], accept_multiple_files=True,
|
||||||
|
key=f"input_upload_{st.session_state.get('input_upload_generation', 0)}",
|
||||||
|
label_visibility="collapsed",
|
||||||
|
)
|
||||||
|
if uploaded_files:
|
||||||
|
saved, errors = [], []
|
||||||
|
for uploaded_file in uploaded_files:
|
||||||
|
try:
|
||||||
|
name = uploaded_file.name
|
||||||
|
if (Path(name).name != name or "/" in name or "\\" in name
|
||||||
|
or Path(name).suffix.lower() != ".xlsx" or name.startswith("~")):
|
||||||
|
raise ValueError("Bitte eine .xlsx-Datei mit gültigem Dateinamen auswählen.")
|
||||||
|
input_directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination = input_directory / name
|
||||||
|
counter = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
with destination.open("xb") as output:
|
||||||
|
output.write(uploaded_file.getbuffer())
|
||||||
|
break
|
||||||
|
except FileExistsError:
|
||||||
|
counter += 1
|
||||||
|
destination = input_directory / f"{Path(name).stem} ({counter}).xlsx"
|
||||||
|
saved.append(f"{destination.name} wurde in den Input-Ordner kopiert.")
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
errors.append(f"{uploaded_file.name} konnte nicht übernommen werden: {exc}")
|
||||||
|
st.session_state.input_upload_messages = saved
|
||||||
|
st.session_state.input_upload_errors = errors
|
||||||
|
st.session_state.input_upload_generation = st.session_state.get("input_upload_generation", 0) + 1
|
||||||
|
st.session_state.last_input_file = None
|
||||||
|
st.rerun()
|
||||||
|
st.caption("Dateien hierher ziehen oder auswählen. Sie werden automatisch im Input-Ordner gespeichert.")
|
||||||
|
folder_action, refresh_action = st.columns(2)
|
||||||
|
with folder_action:
|
||||||
|
if st.button("Ordner öffnen", icon="📁", key="open_input_directory", use_container_width=True):
|
||||||
|
try:
|
||||||
|
open_input_folder(input_directory)
|
||||||
|
except (OSError, subprocess.SubprocessError) as exc:
|
||||||
|
st.error(f"Der Input-Ordner konnte nicht geöffnet werden: {exc}")
|
||||||
|
with refresh_action:
|
||||||
|
if st.button("Neu einlesen", icon="🔄", key="refresh_input_directory", use_container_width=True,
|
||||||
|
help="Dateiliste aktualisieren, nachdem Dateien im Explorer hinzugefügt wurden."):
|
||||||
|
st.rerun()
|
||||||
selected_input = None
|
selected_input = None
|
||||||
|
st.session_state.parsed_records = []
|
||||||
|
st.session_state.input_validated = False
|
||||||
|
st.session_state.last_input_file = None
|
||||||
else:
|
else:
|
||||||
# Bei nur einer Datei: automatisch selektieren (keine Leerauswahl)
|
# Bei nur einer Datei: automatisch selektieren (keine Leerauswahl)
|
||||||
if len(input_files) == 1:
|
if len(input_files) == 1:
|
||||||
@@ -484,7 +547,7 @@ if "generated_files" in st.session_state and st.session_state.generated_files:
|
|||||||
key=f"open_folder_{i}",
|
key=f"open_folder_{i}",
|
||||||
on_click=open_path,
|
on_click=open_path,
|
||||||
args=(str(fpath.parent),),
|
args=(str(fpath.parent),),
|
||||||
help="Ordner im Finder öffnen",
|
help="Ordner im Explorer öffnen",
|
||||||
use_container_width=True,
|
use_container_width=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def main():
|
|||||||
# ── Datei öffnen (macOS) ──────────────────────────────────────────
|
# ── Datei öffnen (macOS) ──────────────────────────────────────────
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
print()
|
print()
|
||||||
print("[INFO] Öffne Datei im Finder…")
|
print("[INFO] Öffne Datei im Explorer")
|
||||||
subprocess.run(["open", str(output_file)], check=False)
|
subprocess.run(["open", str(output_file)], check=False)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,34 @@
|
|||||||
|
# Aktualisieren der PDF Datei
|
||||||
|
|
||||||
|
Wenn sich die PDF-Datei welche als Vorlagen genutzt wird geändert hat, kann diese nicht direkt vom Programm verwendet werden, da es die zu verwendenden Felder nicht erkennen kann. Wir müssen ihm kurz zeigen wo die zu nutzenden Felder sind.
|
||||||
|
|
||||||
|
Öffne Dazu eine bereits bestehende *.odg Datei im Ordner template und schau Dir die Textfelder an, welche in die entsprechenden Felder gelegt, damit das Programm diese später erkennen kann.
|
||||||
|
|
||||||
|
Öffne nun das neue PDF mit dem Programm Libre Office Writer und erzeuge in jedem Formularfeld ein Textfeld mit dem entspechenden Feldnamen wie im Beispiel-odg. Die Felder heißen einfach so wie sie im PDF genutzt werden plus Unterstrich Field. Also:
|
||||||
|
- Antragsnummer_Field
|
||||||
|
- Name_Field
|
||||||
|
- Qulifikation_Field
|
||||||
|
- Funktion_Field
|
||||||
|
- Stellenanteil_Field
|
||||||
|
- Monat_Field
|
||||||
|
|
||||||
|
In das Feld Tätigkeiten muss kein neues Textfeld eingefügt werden, weil es schon ein Textfeld ist, daher kann in dieses direkt
|
||||||
|
- Tätigkeiten_Field
|
||||||
|
|
||||||
|
eingetragen werden.
|
||||||
|
|
||||||
|
Auf Seite zwei muss nur ein Feld
|
||||||
|
- Datum_Field
|
||||||
|
|
||||||
|
erzeugt werden. Denn dieses kann einfach kopiert und im zweiten Datumfeld plaziert werden.
|
||||||
|
Für die beiden Ortsfelder muss auch jeweils ein Textfeld eingefügt werden, dieses enthält aber einfach den Text:
|
||||||
|
- Dresden
|
||||||
|
|
||||||
|
da es vom Programm nicht geändert wird
|
||||||
|
|
||||||
|
# Styling
|
||||||
|
Um einen einheitlichen look zu garantieren müssen alle eingefügten Textfelder wie folgt gestyled sein:
|
||||||
|
- das Textfeld selber füllt das komplette Feld bis an alle 4 Ränder aus
|
||||||
|
- Textgröße: 12 pt
|
||||||
|
- Font/Schriftart: Arial
|
||||||
|
- Absatz Textausrichtung: mittig
|
||||||
@@ -17,11 +17,15 @@ build = importlib.util.module_from_spec(spec)
|
|||||||
spec.loader.exec_module(build)
|
spec.loader.exec_module(build)
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_NAMES = ["template/A Vorlage.pdf", "template/B Vorlage.PDF"]
|
||||||
|
IGNORED_NAMES = ["template/_alt.pdf", "template/.versteckt.pdf", "template/A Vorlage.odg"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def release(tmp_path):
|
def release(tmp_path):
|
||||||
directory = tmp_path / "build"
|
directory = tmp_path / "build"
|
||||||
directory.mkdir()
|
directory.mkdir()
|
||||||
for name in deploy.PACKAGE_FILES:
|
for name in deploy.PACKAGE_FILES + TEMPLATE_NAMES:
|
||||||
path = directory / name
|
path = directory / name
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(b"test release payload")
|
path.write_bytes(b"test release payload")
|
||||||
@@ -33,13 +37,15 @@ def release(tmp_path):
|
|||||||
|
|
||||||
def test_deployment_packages_release_without_work_files(release):
|
def test_deployment_packages_release_without_work_files(release):
|
||||||
directory, target = release
|
directory, target = release
|
||||||
|
for name in IGNORED_NAMES:
|
||||||
|
(directory / name).write_bytes(b"ignored")
|
||||||
for folder in ["input", "output"]:
|
for folder in ["input", "output"]:
|
||||||
(directory / folder).mkdir()
|
(directory / folder).mkdir()
|
||||||
(directory / folder / "private.xlsx").write_bytes(b"private")
|
(directory / folder / "private.xlsx").write_bytes(b"private")
|
||||||
result = deploy.deploy(directory, target)
|
result = deploy.deploy(directory, target)
|
||||||
assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip"
|
assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip"
|
||||||
with zipfile.ZipFile(result) as archive:
|
with zipfile.ZipFile(result) as archive:
|
||||||
assert set(archive.namelist()) == set(deploy.PACKAGE_FILES + ["input/", "output/"])
|
assert set(archive.namelist()) == set(deploy.PACKAGE_FILES + TEMPLATE_NAMES + ["input/", "output/"])
|
||||||
assert archive.testzip() is None
|
assert archive.testzip() is None
|
||||||
assert not list(target.glob(".upload-*"))
|
assert not list(target.glob(".upload-*"))
|
||||||
# An unchanged release can be retried without overwriting the published ZIP.
|
# An unchanged release can be retried without overwriting the published ZIP.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from src.writer import generate_activity_reports
|
|||||||
|
|
||||||
CONFIG_DIR = PROJECT_ROOT / "config"
|
CONFIG_DIR = PROJECT_ROOT / "config"
|
||||||
INPUT_FILE = Path(__file__).resolve().parent / "fixture" / "2026 JM.xlsx"
|
INPUT_FILE = Path(__file__).resolve().parent / "fixture" / "2026 JM.xlsx"
|
||||||
TEMPLATE_FILE = PROJECT_ROOT / "template" / "Tätigkeitsnachweise Stellenförderung.pdf"
|
TEMPLATE_FILE = PROJECT_ROOT / "template" / "06-2026 Tätigkeitsnachweise Stellenförderung.pdf"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.3.2
|
1.3.4
|
||||||
@@ -21,7 +21,7 @@ from datetime import date
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from deploy import deploy, write_release_manifest
|
from deploy import deploy, is_template_pdf, write_release_manifest
|
||||||
|
|
||||||
# ── Pfade ───────────────────────────────────────────────────────
|
# ── Pfade ───────────────────────────────────────────────────────
|
||||||
BUILD_PIPELINE_DIR = Path(__file__).resolve().parent
|
BUILD_PIPELINE_DIR = Path(__file__).resolve().parent
|
||||||
@@ -144,6 +144,20 @@ def update_changelog(
|
|||||||
print(f" ✓ CHANGELOG.md aktualisiert → [{new_version}]")
|
print(f" ✓ CHANGELOG.md aktualisiert → [{new_version}]")
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_changelog_edit() -> None:
|
||||||
|
"""Gibt dem Benutzer Zeit, den neuen Changelog-Eintrag zu bearbeiten.
|
||||||
|
|
||||||
|
Der Build liest CHANGELOG.md erst nach dieser Pause; Änderungen in der
|
||||||
|
Datei fließen also in den Build ein.
|
||||||
|
"""
|
||||||
|
print(f"\n✏️ Bitte den neuen Eintrag jetzt bearbeiten und speichern:\n {CHANGELOG_PATH}")
|
||||||
|
try:
|
||||||
|
input(" Danach Enter drücken, um den Build fortzusetzen (Strg+C bricht ab) … ")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nAbgebrochen.")
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
def _build_sections(bump_type: str, message: Optional[str]) -> str:
|
def _build_sections(bump_type: str, message: Optional[str]) -> str:
|
||||||
"""Baut die Sektionen für den Changelog-Eintrag."""
|
"""Baut die Sektionen für den Changelog-Eintrag."""
|
||||||
if message:
|
if message:
|
||||||
@@ -223,10 +237,17 @@ def _prepare_build_dir() -> None:
|
|||||||
shutil.copy2(f, config_dst / f.name)
|
shutil.copy2(f, config_dst / f.name)
|
||||||
|
|
||||||
shutil.copy2(CHANGELOG_PATH, BUILD_OUTPUT_DIR / "CHANGELOG.md")
|
shutil.copy2(CHANGELOG_PATH, BUILD_OUTPUT_DIR / "CHANGELOG.md")
|
||||||
shutil.copy2(
|
|
||||||
APP_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf",
|
# Alle Template-PDFs ausliefern (ohne Dateien mit '_' oder '.' am Anfang).
|
||||||
BUILD_OUTPUT_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf",
|
# Veraltete PDFs aus früheren Builds werden entfernt.
|
||||||
)
|
tpl_src = APP_DIR / "template"
|
||||||
|
tpl_dst = BUILD_OUTPUT_DIR / "template"
|
||||||
|
wanted = {p.name for p in tpl_src.iterdir() if is_template_pdf(p)}
|
||||||
|
for old_pdf in tpl_dst.iterdir():
|
||||||
|
if is_template_pdf(old_pdf) and old_pdf.name not in wanted:
|
||||||
|
old_pdf.unlink()
|
||||||
|
for name in sorted(wanted):
|
||||||
|
shutil.copy2(tpl_src / name, tpl_dst / name)
|
||||||
|
|
||||||
# README für input-Ordner
|
# README für input-Ordner
|
||||||
input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
||||||
@@ -346,6 +367,10 @@ def main() -> None:
|
|||||||
"--message", "-m",
|
"--message", "-m",
|
||||||
help="Changelog-Beschreibung für diese Version",
|
help="Changelog-Beschreibung für diese Version",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-pause", action="store_true",
|
||||||
|
help="Nicht auf die Bearbeitung des Changelogs warten (z.B. für CI)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -399,6 +424,8 @@ def main() -> None:
|
|||||||
if not args.no_bump:
|
if not args.no_bump:
|
||||||
print("\n📝 Changelog …")
|
print("\n📝 Changelog …")
|
||||||
update_changelog(new_version, bump_type, args.message)
|
update_changelog(new_version, bump_type, args.message)
|
||||||
|
if not args.no_pause and sys.stdin.isatty():
|
||||||
|
_wait_for_changelog_edit()
|
||||||
_prepare_build_dir()
|
_prepare_build_dir()
|
||||||
exe_path = _run_pyinstaller()
|
exe_path = _run_pyinstaller()
|
||||||
final_exe = _copy_to_build(exe_path)
|
final_exe = _copy_to_build(exe_path)
|
||||||
|
|||||||
@@ -18,10 +18,27 @@ MANIFEST_NAME = "release.json"
|
|||||||
PACKAGE_FILES = [
|
PACKAGE_FILES = [
|
||||||
EXE_NAME, "README.md", "CHANGELOG.md",
|
EXE_NAME, "README.md", "CHANGELOG.md",
|
||||||
"config/settings.ini", "config/input-fields.json", "config/output-fields.json",
|
"config/settings.ini", "config/input-fields.json", "config/output-fields.json",
|
||||||
"template/Tätigkeitsnachweise Stellenförderung.pdf",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def is_template_pdf(path: Path) -> bool:
|
||||||
|
"""Nur *.pdf, die nicht mit '_' oder '.' beginnen, gelten als Template."""
|
||||||
|
return path.is_file() and path.suffix.lower() == ".pdf" and not path.name.startswith(("_", "."))
|
||||||
|
|
||||||
|
|
||||||
|
def template_files(base_dir: Path) -> list[str]:
|
||||||
|
"""Relative Pfade aller Template-PDFs in base_dir/template (sortiert)."""
|
||||||
|
folder = base_dir / "template"
|
||||||
|
if not folder.is_dir():
|
||||||
|
return []
|
||||||
|
return [f"template/{p.name}" for p in sorted(folder.iterdir()) if is_template_pdf(p)]
|
||||||
|
|
||||||
|
|
||||||
|
def package_files(build_dir: Path) -> list[str]:
|
||||||
|
"""Alle Dateien des Release-Pakets: feste Dateien plus alle Template-PDFs."""
|
||||||
|
return PACKAGE_FILES + template_files(build_dir)
|
||||||
|
|
||||||
|
|
||||||
def normalize_version(version: str) -> str:
|
def normalize_version(version: str) -> str:
|
||||||
if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version):
|
if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version):
|
||||||
raise ValueError(f"Ungültige Versionsnummer: {version}")
|
raise ValueError(f"Ungültige Versionsnummer: {version}")
|
||||||
@@ -40,7 +57,7 @@ def write_release_manifest(build_dir: Path, version: str) -> None:
|
|||||||
"""Record the version and all payload hashes after a successful build."""
|
"""Record the version and all payload hashes after a successful build."""
|
||||||
manifest = {
|
manifest = {
|
||||||
"version": normalize_version(version),
|
"version": normalize_version(version),
|
||||||
"files": {name: file_hash(build_dir / name) for name in PACKAGE_FILES},
|
"files": {name: file_hash(build_dir / name) for name in package_files(build_dir)},
|
||||||
}
|
}
|
||||||
(build_dir / MANIFEST_NAME).write_text(
|
(build_dir / MANIFEST_NAME).write_text(
|
||||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
@@ -51,7 +68,7 @@ def validate_release(build_dir: Path) -> str:
|
|||||||
raise ValueError("Kein bestätigter Build vorhanden. Bitte zuerst build.bat ausführen.")
|
raise ValueError("Kein bestätigter Build vorhanden. Bitte zuerst build.bat ausführen.")
|
||||||
manifest = json.loads((build_dir / MANIFEST_NAME).read_text(encoding="utf-8"))
|
manifest = json.loads((build_dir / MANIFEST_NAME).read_text(encoding="utf-8"))
|
||||||
version = normalize_version(manifest["version"])
|
version = normalize_version(manifest["version"])
|
||||||
for name in PACKAGE_FILES:
|
for name in package_files(build_dir):
|
||||||
path = build_dir / name
|
path = build_dir / name
|
||||||
if not path.is_file() or file_hash(path) != manifest.get("files", {}).get(name):
|
if not path.is_file() or file_hash(path) != manifest.get("files", {}).get(name):
|
||||||
raise ValueError(f"Build-Datei fehlt oder wurde nach dem Build verändert: {name}. Bitte neu bauen.")
|
raise ValueError(f"Build-Datei fehlt oder wurde nach dem Build verändert: {name}. Bitte neu bauen.")
|
||||||
@@ -83,7 +100,7 @@ def deploy(build_dir: Path = BUILD_DIR, target_dir: Path = DEPLOY_DIR) -> Path:
|
|||||||
temporary = Path(temporary)
|
temporary = Path(temporary)
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as package:
|
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as package:
|
||||||
for name in PACKAGE_FILES:
|
for name in package_files(build_dir):
|
||||||
package.write(build_dir / name, name)
|
package.write(build_dir / name, name)
|
||||||
for directory in ["input/", "output/"]:
|
for directory in ["input/", "output/"]:
|
||||||
# Fixed timestamps keep repeated deployments byte-identical.
|
# Fixed timestamps keep repeated deployments byte-identical.
|
||||||
|
|||||||
Reference in new issue
Block a user