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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3bc0cb0770
commit
6c4297b5fc
@@ -269,6 +269,7 @@ fonctions d'IA. *(L'envoi d'emails par SMTP est en place — voir § 2.)*
|
|||||||
│ │ ├── importing/ # import CSV/Excel (parser, values, service)
|
│ │ ├── importing/ # import CSV/Excel (parser, values, service)
|
||||||
│ │ ├── models/ # tables SQLModel
|
│ │ ├── models/ # tables SQLModel
|
||||||
│ │ ├── validation/ # moteur pur (models, engine) + loader (base)
|
│ │ ├── validation/ # moteur pur (models, engine) + loader (base)
|
||||||
|
│ │ ├── bootstrap.py # instance vierge : édition + 1er compte
|
||||||
│ │ ├── seed.py # jeu de démonstration (python -m app.seed)
|
│ │ ├── seed.py # jeu de démonstration (python -m app.seed)
|
||||||
│ │ ├── i18n/
|
│ │ ├── i18n/
|
||||||
│ │ │ ├── __init__.py # t(key, **params)
|
│ │ │ ├── __init__.py # t(key, **params)
|
||||||
@@ -297,6 +298,7 @@ Aucune dépendance locale requise hors Docker.
|
|||||||
| Créer une migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "..."` |
|
| Créer une migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "..."` |
|
||||||
| Appliquer les migrations | `docker compose run --rm backend uv run alembic upgrade head` |
|
| Appliquer les migrations | `docker compose run --rm backend uv run alembic upgrade head` |
|
||||||
| Vérifier migration ↔ modèle | `docker compose run --rm backend uv run alembic check` (après `upgrade head`) |
|
| Vérifier migration ↔ modèle | `docker compose run --rm backend uv run alembic check` (après `upgrade head`) |
|
||||||
|
| Instance vierge (édition + 1er compte) | `docker compose run --rm backend uv run python -m app.bootstrap --name … --start … --end … --email … --password …` |
|
||||||
| Charger le jeu de démo | `docker compose run --rm backend uv run python -m app.seed` (après `alembic upgrade head`) |
|
| Charger le jeu de démo | `docker compose run --rm backend uv run python -m app.seed` (après `alembic upgrade head`) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -19,8 +19,37 @@ docker compose up --build
|
|||||||
- Documentation interactive : http://localhost:8000/docs
|
- Documentation interactive : http://localhost:8000/docs
|
||||||
|
|
||||||
Le frontend démarre en mode `mock` (jeu de démonstration en mémoire, aucun
|
Le frontend démarre en mode `mock` (jeu de démonstration en mémoire, aucun
|
||||||
backend requis). Pour le brancher sur l'API réelle : `VITE_API_MODE=http` et
|
backend requis).
|
||||||
`VITE_EDITION_ID=<uuid>` dans `.env` (voir `frontend/README.md`).
|
|
||||||
|
### Vraie instance, sans jeu de démo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d db backend
|
||||||
|
docker compose run --rm backend uv run alembic upgrade head
|
||||||
|
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 "un-mot-de-passe-solide"
|
||||||
|
```
|
||||||
|
|
||||||
|
La commande crée une édition vierge, le premier compte coordination (connexion
|
||||||
|
par mot de passe) et 3 créneaux types par défaut, puis affiche l'UUID de
|
||||||
|
l'édition. Renseigner alors dans `.env` :
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_MODE=http
|
||||||
|
VITE_EDITION_ID=<uuid affiché>
|
||||||
|
```
|
||||||
|
|
||||||
|
puis `docker compose up frontend`.
|
||||||
|
|
||||||
|
### Jeu de démonstration (optionnel)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm backend uv run python -m app.seed
|
||||||
|
```
|
||||||
|
|
||||||
|
120 bénévoles fictifs, 15 postes, 4 jours. Identifiants affichés en fin de
|
||||||
|
commande.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""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()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
|
from app.bootstrap import run
|
||||||
|
from app.models import Account, DayPart, Edition, Role
|
||||||
|
|
||||||
|
|
||||||
|
def _args(**overrides) -> argparse.Namespace:
|
||||||
|
base = {
|
||||||
|
"name": "Festival 2027",
|
||||||
|
"start": "2027-07-01",
|
||||||
|
"end": "2027-07-04",
|
||||||
|
"email": "coord@monfestival.fr",
|
||||||
|
"password": "un-mot-de-passe-solide",
|
||||||
|
"year": None,
|
||||||
|
"no_day_parts": False,
|
||||||
|
"retention_days": None,
|
||||||
|
}
|
||||||
|
base.update(overrides)
|
||||||
|
return argparse.Namespace(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_creates_edition_coordinator_and_default_day_parts(session):
|
||||||
|
edition = run(_args(), session)
|
||||||
|
|
||||||
|
reloaded = session.get(Edition, edition.id)
|
||||||
|
assert reloaded.name == "Festival 2027"
|
||||||
|
assert reloaded.year == 2027
|
||||||
|
assert reloaded.start_date == dt.date(2027, 7, 1)
|
||||||
|
|
||||||
|
account = session.exec(
|
||||||
|
select(Account).where(Account.edition_id == edition.id)
|
||||||
|
).one()
|
||||||
|
assert account.role is Role.COORDINATOR
|
||||||
|
assert account.auth_method == "password"
|
||||||
|
|
||||||
|
codes = [
|
||||||
|
d.code
|
||||||
|
for d in session.exec(
|
||||||
|
select(DayPart).where(DayPart.edition_id == edition.id)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
assert set(codes) == {"MATIN", "APRES_MIDI", "SOIREE"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_day_parts_flag(session):
|
||||||
|
edition = run(_args(no_day_parts=True), session)
|
||||||
|
assert (
|
||||||
|
session.exec(
|
||||||
|
select(DayPart).where(DayPart.edition_id == edition.id)
|
||||||
|
).all()
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_short_password(session):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
run(_args(password="court"), session)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_end_before_start(session):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
run(_args(start="2027-07-04", end="2027-07-01"), session)
|
||||||
Reference in New Issue
Block a user