Files
Wegwichtel/tools/python/moderate_routes.py
T
Florian Zumpe 4b8bdcad5f
Sonarqube Scanner / Prepare environment (push) Successful in 9s
Sonarqube Scanner / Build and test (push) Successful in 26s
Sonarqube Scanner / SonarQube analysis (push) Successful in 1m11s
Sonarqube Scanner / Deploy to Server (push) Successful in 8s
moderation preview of pictures and audio files
2026-10-03 15:48:04 +02:00

161 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Strecken direkt in der lokalen Wegwichtel-Datenbank moderieren."""
import argparse
from datetime import datetime, timezone
import os
from pathlib import Path
import re
import shlex
import sqlite3
import sys
from urllib.parse import urlsplit
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def environment_settings(project_root):
"""Liest die .env, ohne die Prozessumgebung zu verändern."""
settings = {}
env_file = project_root / ".env"
try:
contents = env_file.read_text(encoding="utf-8-sig")
except FileNotFoundError:
contents = ""
for number, line in enumerate(contents.splitlines(), 1):
line = re.sub(r'^\s*export\s+', '', line).strip()
if not line or line.startswith("#"):
continue
key, separator, value = line.partition("=")
if not separator:
raise ValueError(f"Ungültige .env-Angabe in Zeile {number}.")
try:
settings[key.strip()] = " ".join(shlex.split(value, comments=True))
except ValueError as error:
raise ValueError(f"Ungültige .env-Anführungszeichen in Zeile {number}.") from error
return settings
def database_path(project_root):
"""Bereits gesetzte Umgebungsvariablen haben Vorrang vor der .env."""
settings = environment_settings(project_root)
data_dir = os.environ.get("DATA_DIR", settings.get("DATA_DIR", "")) or "data"
return (project_root / data_dir / "wegwichtel.sqlite").resolve()
def route_id(value):
result = int(value)
if result < 1:
raise argparse.ArgumentTypeError("Die Strecken-ID muss eine positive Zahl sein.")
return result
def arguments(argv):
parser = argparse.ArgumentParser(description=__doc__)
group = parser.add_mutually_exclusive_group()
group.add_argument("--enable", "--enanble", "-e", type=route_id, metavar="ID",
help="Strecke mit dem aktuellen Zeitstempel freigeben")
group.add_argument("--disable", "-d", type=route_id, metavar="ID",
help="Freigabe zurückziehen (activated_at wird NULL)")
return parser.parse_args(argv)
def review_settings(project_root):
settings = environment_settings(project_root)
url = os.environ.get("MODERATION_REVIEW_URL", settings.get("MODERATION_REVIEW_URL", ""))
parsed = urlsplit(url)
if url and (parsed.scheme != "https" or not parsed.netloc or parsed.fragment or parsed.query
or parsed.username or parsed.password):
raise ValueError("MODERATION_REVIEW_URL muss eine HTTPS-Adresse ohne Zugangsdaten, Query oder Fragment sein.")
return url.rstrip("/")
def table_text(value):
plain = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', str(value or ""))
return " ".join(plain.split())
def print_table(headers, rows):
cells = [[table_text(value) for value in row] for row in [headers, *rows]]
widths = [max(len(row[index]) for row in cells) for index in range(len(headers))]
for row in cells:
print(" | ".join(value.ljust(width) for value, width in zip(row, widths)))
def show_pois(database, identifier, review_url, active):
rows = database.execute("""SELECT id, sequence, title, description, created_at, audio_path
FROM pois WHERE route_id=? ORDER BY sequence, id""", (identifier,)).fetchall()
print("Stationen:")
if not rows:
print("Keine Stationen vorhanden.")
return
details = []
for number, poi in enumerate(rows, 1):
access = []
if review_url and active:
base = f"{review_url}/routes/{identifier}/pois/{poi['id']}"
images = database.execute("SELECT id FROM poi_images WHERE poi_id=? ORDER BY sequence, id",
(poi["id"],)).fetchall()
access.extend(f"{base}/pictures/{image['id']}" for image in images)
if poi["audio_path"]:
access.append(f"{base}/audio")
description = poi["description"] or poi["title"]
details.append([poi["id"], number, description, poi["created_at"] or "–", " ; ".join(access) or "–"])
print_table(["ID", "Platz", "Beschreibung", "Erstellt am (UTC)", "Bilder / Ton (Anmeldung)"], details)
def list_routes(database, review_url=""):
rows = database.execute("SELECT id, name, status, activated_at FROM routes ORDER BY id").fetchall()
if not rows:
print("Keine Strecken vorhanden.")
return
print("ID\tStatus\tFreigegeben am (UTC)\tName")
for row in rows:
name = table_text(row["name"])
print(f'{row["id"]}\t{row["status"]}\t{row["activated_at"] or "nicht freigegeben"}\t{name}')
show_pois(database, row["id"], review_url, row["status"] == "active")
print()
def moderate(database, identifier, enable):
activated_at = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") if enable else None
with database:
database.execute("BEGIN IMMEDIATE")
row = database.execute("SELECT status FROM routes WHERE id = ?", (identifier,)).fetchone()
if row is None:
raise ValueError(f"Strecke {identifier} existiert nicht.")
if enable and row["status"] != "active":
raise ValueError(f"Strecke {identifier} ist gelöscht und kann nicht freigegeben werden.")
database.execute("UPDATE routes SET activated_at = ? WHERE id = ?", (activated_at, identifier))
if enable:
print(f"Strecke {identifier} freigegeben: {activated_at}")
else:
print(f"Freigabe von Strecke {identifier} zurückgezogen.")
def main(argv=None, project_root=None):
args = arguments(argv)
try:
path = database_path(project_root or PROJECT_ROOT)
listing = args.enable is None and args.disable is None
review_url = review_settings(project_root or PROJECT_ROOT) if listing else ""
mode = "ro" if listing else "rw"
database = sqlite3.connect(f"{path.as_uri()}?mode={mode}", uri=True, timeout=10)
try:
database.row_factory = sqlite3.Row
if listing:
list_routes(database, review_url)
else:
identifier = args.enable if args.enable is not None else args.disable
moderate(database, identifier, args.enable is not None)
finally:
database.close()
except (OSError, ValueError, sqlite3.Error) as error:
print(f"Moderation fehlgeschlagen: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())