- 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>
125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
"""Export Excel de la liste des bénévoles d'une édition — voir
|
|
`app/api/volunteers.py:export_volunteers`. Même bibliothèque que l'import
|
|
(`openpyxl`, `app/importing/parser.py`), en écriture cette fois.
|
|
|
|
Texte en dur (en-têtes de colonnes, libellés) : comme `app/sheets/pdf.py` et
|
|
`app/badges/pdf.py`, un document généré n'est pas un texte d'interface —
|
|
pas de `app.i18n.t()` ici, contrairement aux réponses de l'API."""
|
|
|
|
import io
|
|
import uuid
|
|
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Font
|
|
|
|
from app.api.schemas import VolunteerRead
|
|
|
|
_HEADER = [
|
|
"Prénom",
|
|
"Nom",
|
|
"Email",
|
|
"Téléphone",
|
|
"Disponible",
|
|
"Postes souhaités",
|
|
"Artistes souhaités",
|
|
"Compagnons souhaités",
|
|
"Note de compagnon",
|
|
"Cotisation choisie",
|
|
"Montant (€)",
|
|
"Statut du paiement",
|
|
"Payée le",
|
|
"Mécénat — adresse",
|
|
"Mécénat — ville",
|
|
"Mécénat — code postal",
|
|
"Mécénat — pays",
|
|
"Commentaire",
|
|
"Inscrit le",
|
|
]
|
|
|
|
_FEE_STATUS_LABELS = {
|
|
"PENDING": "En attente",
|
|
"PAID": "Payée",
|
|
"FAILED": "Échouée",
|
|
"EXPIRED": "Expirée",
|
|
}
|
|
|
|
# Caractères qui font d'une cellule Excel/LibreOffice une formule plutôt
|
|
# qu'un texte littéral — injection de formule (CWE-1236) : la plupart de ces
|
|
# champs viennent du formulaire public, sans authentification (nom, email,
|
|
# téléphone, commentaire, note de compagnon, adresse de mécénat…). Un
|
|
# préfixe `'` force l'interprétation en texte, sans changer la valeur
|
|
# affichée.
|
|
_FORMULA_PREFIXES = ("=", "+", "-", "@")
|
|
|
|
|
|
def _safe_text(value: str) -> str:
|
|
return f"'{value}" if value.startswith(_FORMULA_PREFIXES) else value
|
|
|
|
|
|
def _yes_no(value: bool) -> str:
|
|
return "Oui" if value else "Non"
|
|
|
|
|
|
def _join_names(ids: list[uuid.UUID], names: dict[uuid.UUID, str]) -> str:
|
|
return _safe_text(", ".join(names[i] for i in ids if i in names))
|
|
|
|
|
|
def _fmt_dt(value) -> str:
|
|
return value.strftime("%d/%m/%Y %H:%M") if value else ""
|
|
|
|
|
|
def generate_volunteers_xlsx(
|
|
volunteers: list[VolunteerRead],
|
|
*,
|
|
position_names: dict[uuid.UUID, str],
|
|
artist_names: dict[uuid.UUID, str],
|
|
volunteer_names: dict[uuid.UUID, str],
|
|
) -> bytes:
|
|
"""`position_names`/`artist_names`/`volunteer_names` : cartes id → nom
|
|
construites une fois par l'appelant (pas de requête par bénévole) —
|
|
voir `app/api/volunteers.py:export_volunteers`."""
|
|
workbook = Workbook()
|
|
sheet = workbook.active
|
|
sheet.title = "Bénévoles"
|
|
sheet.append(_HEADER)
|
|
for cell in sheet[1]:
|
|
cell.font = Font(bold=True)
|
|
|
|
for v in volunteers:
|
|
sheet.append(
|
|
[
|
|
_safe_text(v.first_name),
|
|
_safe_text(v.last_name),
|
|
_safe_text(v.email or ""),
|
|
_safe_text(v.phone or ""),
|
|
_yes_no(v.self_declared_available),
|
|
_join_names(v.wished_position_ids, position_names),
|
|
_join_names(v.artist_ids, artist_names),
|
|
_join_names(v.wished_companion_ids, volunteer_names),
|
|
_safe_text(v.companion_wish_note or ""),
|
|
_safe_text(v.fee_payment_label or ""),
|
|
v.fee_amount if v.fee_amount is not None else "",
|
|
_FEE_STATUS_LABELS.get(v.fee_payment_status, ""),
|
|
_fmt_dt(v.fee_paid_at),
|
|
_safe_text(v.mecenat_address or ""),
|
|
_safe_text(v.mecenat_city or ""),
|
|
_safe_text(v.mecenat_postal_code or ""),
|
|
_safe_text(v.mecenat_country or ""),
|
|
_safe_text(v.comment or ""),
|
|
_fmt_dt(v.created_at),
|
|
]
|
|
)
|
|
|
|
for column_cells in sheet.columns:
|
|
length = max(
|
|
(len(str(c.value)) for c in column_cells if c.value is not None),
|
|
default=8,
|
|
)
|
|
sheet.column_dimensions[column_cells[0].column_letter].width = min(
|
|
max(length + 2, 10), 40
|
|
)
|
|
|
|
buffer = io.BytesIO()
|
|
workbook.save(buffer)
|
|
return buffer.getvalue()
|