Lien d'inscription protégé par jeton, reçus fiscaux mécénat, infobulles et revue de sécurité
ci-cd / build-backend (push) Successful in 1m12s
ci-cd / build-frontend (push) Successful in 18s
ci-cd / deploy (push) Successful in 12s

- Lien d'inscription protégé par un jeton (FormTemplate.access_token,
  /inscription/<token>), avec régénération depuis l'écran de configuration.
- Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en
  mécénat, donateur/montant (chiffres et toutes lettres en français,
  app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel.
- Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée.
- Infobulles (HelpTip et détail de cellule de la grille des besoins)
  repositionnées en position: fixed via JS, bornées à l'écran.
- Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une
  URL (fuyait le jeton d'inscription), sujet retiré du log de remise des
  emails (pouvait porter un nom de bénévole), photo du formulaire public
  revalidée par décodage réel avant stockage, export Excel protégé contre
  l'injection de formule, sondage du statut de paiement limité en
  fréquence, pyjwt mis à jour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-02 07:47:17 +02:00
co-authored by Claude Sonnet 5
parent a2b748566b
commit 096b7cd26a
44 changed files with 1228 additions and 156 deletions
+54
View File
@@ -25,6 +25,7 @@ from app.models import (
Availability,
DayPart,
Edition,
FeePaymentStatus,
Position,
PositionCategory,
Role,
@@ -203,6 +204,14 @@ ARTIST_SLOTS: list[tuple[dt.time, dt.time]] = [
(dt.time(21, 30), dt.time(23, 0)),
]
# Adresses postales des bénévoles « mécénat » — voir `_add_demo_payments`.
MECENAT_ADDRESSES = [
("12 rue des Lilas", "75020", "Paris"),
("3 impasse du Petit Bois", "44000", "Nantes"),
("8 avenue de la République", "69003", "Lyon"),
("21 chemin des Vignes", "33000", "Bordeaux"),
]
@dataclass
class Summary:
@@ -216,6 +225,8 @@ class Summary:
position_wishes: int = 0
artist_wishes: int = 0
companion_wishes: int = 0
fee_paid_classique: int = 0
fee_paid_mecenat: int = 0
warnings: Counter = field(default_factory=Counter)
blocking: int = 0
@@ -232,6 +243,8 @@ class Summary:
f" Vœux de poste déclarés : {self.position_wishes}",
f" Vœux d'artiste déclarés : {self.artist_wishes}",
f" Vœux de compagnon déclarés : {self.companion_wishes}",
f" Cotisation payée (classique) : {self.fee_paid_classique}",
f" Cotisation payée (mécénat) : {self.fee_paid_mecenat}",
f" Avertissements actifs : {sum(self.warnings.values())}",
]
for code, count in sorted(self.warnings.items()):
@@ -512,6 +525,7 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary:
n_assignments += _add_demo_warnings(
session, edition, shifts, reserved, assigned, availabilities
)
n_fee_classique, n_fee_mecenat = _add_demo_payments(session, volunteers, rng)
session.flush()
fill: Counter = Counter()
@@ -537,6 +551,8 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary:
position_wishes=n_position_wishes,
artist_wishes=n_artist_wishes,
companion_wishes=n_companion_wishes,
fee_paid_classique=n_fee_classique,
fee_paid_mecenat=n_fee_mecenat,
)
_verify(session, edition, summary)
return summary
@@ -610,6 +626,44 @@ def _add_demo_warnings(
return added
def _add_demo_payments(
session: Session, volunteers: list[Volunteer], rng: random.Random
) -> tuple[int, int]:
"""Quelques bénévoles ayant payé leur cotisation via SumUp — voir
CLAUDE.md § 2, Paiement de la cotisation. `fee_payment_label`/
`fee_amount` sont une copie figée (comme en production) : aucun
`FormPaymentOption` n'a besoin d'exister pour autant. Pour que l'export
Excel et les reçus fiscaux mécénat (`/editions/{id}/volunteers/
mecenat-receipts`) aient de quoi montrer quelque chose en démonstration."""
pool = rng.sample(volunteers, k=12)
classique, mecenat = pool[:8], pool[8:]
for volunteer in classique:
volunteer.fee_payment_label = "Cotisation classique"
volunteer.fee_amount = 15.0
volunteer.fee_payment_status = FeePaymentStatus.PAID
volunteer.fee_paid_at = dt.datetime.combine(
START - dt.timedelta(days=rng.randint(5, 45)), dt.time(rng.randint(9, 21))
)
session.add(volunteer)
for i, volunteer in enumerate(mecenat):
address, postal_code, city = MECENAT_ADDRESSES[i % len(MECENAT_ADDRESSES)]
volunteer.fee_payment_label = "Mécénat"
volunteer.fee_amount = rng.choice([30.0, 50.0, 80.0, 120.0])
volunteer.fee_payment_status = FeePaymentStatus.PAID
volunteer.fee_paid_at = dt.datetime.combine(
START - dt.timedelta(days=rng.randint(5, 45)), dt.time(rng.randint(9, 21))
)
volunteer.mecenat_address = address
volunteer.mecenat_postal_code = postal_code
volunteer.mecenat_city = city
volunteer.mecenat_country = "France"
session.add(volunteer)
return len(classique), len(mecenat)
def _find_rest_pair(shifts: list[Shift]) -> tuple[Shift, Shift] | None:
plain = sorted(shifts, key=lambda s: s.starts_at)
for i, first in enumerate(plain):