Files
benevoles/backend/app/bootstrap.py
T
AdamThomasandClaude Sonnet 5 6c4297b5fc Ajout de python -m app.bootstrap : instance vierge sans jeu de démo
Crée une édition + le premier compte coordination (mot de passe) + 3 créneaux
types par défaut, et affiche l'UUID à mettre dans VITE_EDITION_ID. Documenté
dans README (« Vraie instance, sans jeu de démo »).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 21:05:14 +02:00

122 lines
3.8 KiB
Python

"""Crée une édition vierge et son premier compte coordination.
À utiliser pour démarrer une vraie instance, sans le jeu de démonstration :
docker compose run --rm backend uv run python -m app.bootstrap \\
--name "Festival 2026" --start 2026-07-01 --end 2026-07-04 \\
--email coordination@monfestival.fr --password "mot-de-passe-solide"
Imprime l'UUID de l'édition à reporter dans VITE_EDITION_ID.
"""
import argparse
import datetime as dt
import sys
from sqlmodel import Session, select
from app.auth.passwords import MIN_LENGTH, hash_password
from app.db import engine
from app.models import Account, DayPart, Edition, Role
# (code, libellé, début, fin) — fin <= début ⇒ passe minuit.
_DEFAULT_DAY_PARTS = [
("MATIN", "Matin", dt.time(8, 0), dt.time(13, 0)),
("APRES_MIDI", "Après-midi", dt.time(13, 0), dt.time(19, 0)),
("SOIREE", "Soirée", dt.time(19, 0), dt.time(2, 0)),
]
def _parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="python -m app.bootstrap")
parser.add_argument("--name", required=True)
parser.add_argument("--start", required=True, help="AAAA-MM-JJ")
parser.add_argument("--end", required=True, help="AAAA-MM-JJ")
parser.add_argument("--email", required=True, help="Compte coordination")
parser.add_argument("--password", required=True)
parser.add_argument("--year", type=int, default=None)
parser.add_argument(
"--no-day-parts",
action="store_true",
help="Ne pas créer les créneaux types par défaut",
)
parser.add_argument("--retention-days", type=int, default=None)
return parser.parse_args(argv)
def run(args: argparse.Namespace, session: Session) -> Edition:
start = dt.date.fromisoformat(args.start)
end = dt.date.fromisoformat(args.end)
if end < start:
raise SystemExit("--end doit être postérieur ou égal à --start")
if len(args.password) < MIN_LENGTH:
raise SystemExit(f"--password : au moins {MIN_LENGTH} caractères")
edition = Edition(
year=args.year or start.year,
name=args.name,
start_date=start,
end_date=end,
retention_days=args.retention_days,
)
session.add(edition)
session.flush()
existing = session.exec(
select(Account)
.where(Account.edition_id == edition.id)
.where(Account.email == args.email.strip())
).first()
if existing is not None:
raise SystemExit("Un compte avec cet email existe déjà.")
session.add(
Account(
edition_id=edition.id,
email=args.email.strip(),
role=Role.COORDINATOR,
password_hash=hash_password(args.password),
)
)
if not args.no_day_parts:
for order, (code, label, begin, finish) in enumerate(
_DEFAULT_DAY_PARTS
):
session.add(
DayPart(
edition_id=edition.id,
code=code,
label=label,
start_time=begin,
end_time=finish,
order=order,
)
)
session.commit()
session.refresh(edition)
return edition
def main() -> None:
args = _parse_args(sys.argv[1:])
with Session(engine) as session:
edition = run(args, session)
print(f"Édition créée : {edition.id}")
print(f" {edition.name} ({edition.start_date} → {edition.end_date})")
print(f"Compte coordination : {args.email} (mot de passe défini)")
if not args.no_day_parts:
print(
"Créneaux types : "
+ ", ".join(code for code, *_ in _DEFAULT_DAY_PARTS)
)
print()
print("Frontend : VITE_API_MODE=http "
f"VITE_EDITION_ID={edition.id}")
if __name__ == "__main__":
main()