mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04:52 +02:00
167 lines
5.6 KiB
Python
167 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import locale
|
|
import os
|
|
import re
|
|
from datetime import date, datetime
|
|
|
|
|
|
class DateValidationError(ValueError):
|
|
pass
|
|
|
|
|
|
def setup_system_locale() -> None:
|
|
try:
|
|
locale.setlocale(locale.LC_TIME, "")
|
|
except locale.Error:
|
|
pass
|
|
|
|
|
|
def system_date_pattern() -> str:
|
|
try:
|
|
system_pattern = locale.nl_langinfo(locale.D_FMT)
|
|
except (AttributeError, ValueError):
|
|
system_pattern = ""
|
|
year_position = system_pattern.find("%Y")
|
|
day_position = system_pattern.find("%d")
|
|
month_position = system_pattern.find("%m")
|
|
if (
|
|
year_position >= 0
|
|
and day_position >= 0
|
|
and month_position >= 0
|
|
and year_position < min(day_position, month_position)
|
|
):
|
|
return "%Y-%m-%d"
|
|
if day_position >= 0 and month_position >= 0 and day_position < month_position:
|
|
return "%d.%m.%Y"
|
|
locale_hint = " ".join(
|
|
filter(
|
|
None,
|
|
(
|
|
os.environ.get("LC_TIME"),
|
|
os.environ.get("LANGUAGE"),
|
|
os.environ.get("LANG"),
|
|
),
|
|
)
|
|
).lower()
|
|
day_first_languages = ("de", "at", "ch", "fr", "it", "es", "pt", "nl", "pl", "cs")
|
|
return "%d.%m.%Y" if locale_hint.startswith(day_first_languages) else "%Y-%m-%d"
|
|
|
|
|
|
def date_input_hint() -> str:
|
|
return "DD.MM.YYYY" if system_date_pattern() == "%d.%m.%Y" else "YYYY-MM-DD"
|
|
|
|
|
|
def parse_date_input(value: str, field_name: str, *, allow_empty: bool = True) -> date | None:
|
|
text = value.strip()
|
|
if not text:
|
|
if allow_empty:
|
|
return None
|
|
raise DateValidationError(f"{field_name} ist erforderlich.")
|
|
patterns = (system_date_pattern(), "%Y-%m-%d", "%d.%m.%Y")
|
|
for pattern in dict.fromkeys(patterns):
|
|
expected = r"\d{2}\.\d{2}\.\d{4}" if pattern == "%d.%m.%Y" else r"\d{4}-\d{2}-\d{2}"
|
|
if not re.fullmatch(expected, text):
|
|
continue
|
|
try:
|
|
return datetime.strptime(text, pattern).date()
|
|
except ValueError:
|
|
continue
|
|
formats = date_input_hint()
|
|
if formats != "YYYY-MM-DD":
|
|
formats += " oder YYYY-MM-DD"
|
|
raise DateValidationError(f"{field_name} muss ein gültiges Datum im Format {formats} sein.")
|
|
|
|
|
|
def normalize_date_input(value: str, field_name: str) -> str:
|
|
parsed = parse_date_input(value, field_name)
|
|
return parsed.isoformat() if parsed else ""
|
|
|
|
|
|
def format_date_for_display(value: str) -> str:
|
|
text = value.strip()
|
|
if not text:
|
|
return ""
|
|
try:
|
|
parsed = parse_iso_date(text, "Datum")
|
|
except DateValidationError:
|
|
return text
|
|
return parsed.strftime(system_date_pattern()) if parsed else ""
|
|
|
|
|
|
def parse_iso_date(value: str, field_name: str, *, allow_empty: bool = True) -> date | None:
|
|
text = value.strip()
|
|
if not text:
|
|
if allow_empty:
|
|
return None
|
|
raise DateValidationError(f"{field_name} ist erforderlich.")
|
|
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
|
|
raise DateValidationError(f"{field_name} muss das Format JJJJ-MM-TT haben.")
|
|
try:
|
|
return date.fromisoformat(text)
|
|
except ValueError as exc:
|
|
raise DateValidationError(f"{field_name} ist kein gültiges Kalenderdatum.") from exc
|
|
|
|
|
|
def validate_birth_date(value: str, *, today: date | None = None) -> date | None:
|
|
parsed = parse_iso_date(value, "Geburtsdatum")
|
|
if parsed is None:
|
|
return None
|
|
reference = today or date.today()
|
|
_validate_birth_date_value(parsed, reference)
|
|
return parsed
|
|
|
|
|
|
def validate_member_dates(
|
|
*,
|
|
birth_date: str,
|
|
accepted_at: str = "",
|
|
membership_started_at: str = "",
|
|
today: date | None = None,
|
|
) -> None:
|
|
reference = today or date.today()
|
|
birth = validate_birth_date(birth_date, today=reference)
|
|
accepted = _validate_not_future(accepted_at, "Aufnahmebeschluss", reference)
|
|
started = _validate_not_future(membership_started_at, "Mitglied seit", reference)
|
|
if birth and accepted and accepted < birth:
|
|
raise DateValidationError("Aufnahmebeschluss darf nicht vor dem Geburtsdatum liegen.")
|
|
if birth and started and started < birth:
|
|
raise DateValidationError("Mitgliedschaft darf nicht vor dem Geburtsdatum beginnen.")
|
|
if accepted and started and started < accepted:
|
|
raise DateValidationError("Mitgliedschaft darf nicht vor dem Aufnahmebeschluss beginnen.")
|
|
|
|
|
|
def calculate_age(birth_date: date, on_date: date | None = None) -> int:
|
|
reference = on_date or date.today()
|
|
return (
|
|
reference.year
|
|
- birth_date.year
|
|
- ((reference.month, reference.day) < (birth_date.month, birth_date.day))
|
|
)
|
|
|
|
|
|
def age_label(value: str, *, today: date | None = None) -> str:
|
|
if not value.strip():
|
|
return "Alter: —"
|
|
try:
|
|
parsed = parse_date_input(value, "Geburtsdatum")
|
|
if parsed:
|
|
_validate_birth_date_value(parsed, today or date.today())
|
|
except DateValidationError:
|
|
return "UNGÜLTIGES DATUM"
|
|
return f"Alter: {calculate_age(parsed, today)} Jahre" if parsed else "Alter: —"
|
|
|
|
|
|
def _validate_not_future(value: str, field_name: str, reference: date) -> date | None:
|
|
parsed = parse_iso_date(value, field_name)
|
|
if parsed and parsed > reference:
|
|
raise DateValidationError(f"{field_name} darf nicht in der Zukunft liegen.")
|
|
return parsed
|
|
|
|
|
|
def _validate_birth_date_value(parsed: date, reference: date) -> None:
|
|
if parsed > reference:
|
|
raise DateValidationError("Geburtsdatum darf nicht in der Zukunft liegen.")
|
|
if calculate_age(parsed, reference) > 120:
|
|
raise DateValidationError("Geburtsdatum ist unplausibel: Das berechnete Alter liegt über 120.")
|