Photo du bénévole ajoutée par la coordination
Section « Photo » sur la fiche bénévole (ajouter / remplacer / retirer),
PUT/DELETE /editions/{id}/volunteers/{vid}/photo (COORDINATION),
validation de l'image partagée avec le formulaire public (read_photo).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
54cba467fc
commit
6892e0307d
@@ -3,11 +3,14 @@
|
||||
`resolve_owned_ids`/`set_artists`/`set_wished_positions` sont utilisés à la
|
||||
fois par `volunteers.py` (fiche éditée par la coordination) et `forms.py`
|
||||
(vœux saisis depuis le formulaire public) — un seul endroit pour la
|
||||
validation « ces ids appartiennent bien à cette édition »."""
|
||||
validation « ces ids appartiennent bien à cette édition ». Idem pour
|
||||
`read_photo` (photo d'un bénévole)."""
|
||||
|
||||
import io
|
||||
import uuid
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from sqlalchemy import delete
|
||||
from sqlmodel import Session, select
|
||||
|
||||
@@ -77,3 +80,42 @@ def set_wished_positions(
|
||||
filter_value=volunteer.id,
|
||||
pairs=[{"volunteer_id": volunteer.id, "position_id": pid} for pid in ids],
|
||||
)
|
||||
|
||||
|
||||
MAX_PHOTO_BYTES = 8 * 1024 * 1024
|
||||
ALLOWED_PHOTO_TYPES = {"image/png", "image/jpeg"}
|
||||
|
||||
|
||||
async def read_photo(photo: UploadFile | None) -> tuple[bytes, str] | None:
|
||||
"""Validation partagée par le formulaire public (inscription immédiate
|
||||
ou en attente de paiement) et l'ajout d'une photo par la coordination
|
||||
depuis la fiche (`volunteers.py:set_volunteer_photo`) — `None` si aucun
|
||||
fichier fourni. `Content-Type` est
|
||||
un en-tête fourni par l'appelant, jamais vérifié par lui-même (ce
|
||||
endpoint est public, sans authentification) : un fichier qui ne se
|
||||
décode pas comme une vraie image malgré un `Content-Type` correct
|
||||
(`image/png`/`image/jpeg` revendiqué) est rejeté ici plutôt que de
|
||||
planter plus tard la génération des badges/feuilles de poste
|
||||
(`app/badges/pdf.py`/`app/sheets/pdf.py`, qui lisent ces octets sans
|
||||
filet) — même validation que l'import d'image des badges
|
||||
(`app/api/badges.py:_pixel_size`, réservé à la coordination)."""
|
||||
if photo is None or not photo.filename:
|
||||
return None
|
||||
content_type = photo.content_type or ""
|
||||
if content_type not in ALLOWED_PHOTO_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=422, detail={"code": "form.photo_type_not_supported"}
|
||||
)
|
||||
content = await photo.read()
|
||||
if len(content) > MAX_PHOTO_BYTES:
|
||||
raise HTTPException(status_code=413, detail={"code": "form.photo_too_large"})
|
||||
if not content:
|
||||
return None
|
||||
try:
|
||||
with Image.open(io.BytesIO(content)) as img:
|
||||
img.verify()
|
||||
except (UnidentifiedImageError, OSError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=422, detail={"code": "form.photo_type_not_supported"}
|
||||
) from exc
|
||||
return content, content_type
|
||||
|
||||
Reference in New Issue
Block a user