Files
AdamThomasandClaude Sonnet 5 111d1e5c4b Afficher la photo du bénévole sur son écran et sur l'écran référent
Photo transmise via le formulaire public (Volunteer.photo) : nouveau
composant partagé VolunteerAvatar (photo si disponible, sinon repli sur
le carré d'initiales existant, même gabarit dans les deux cas).

- Écran bénévole (VolunteerHomeView) : photo dans le bandeau d'accueil,
  à côté du message de bienvenue.
- Écran référent (MobileLeadPage) : photo devant le nom de chaque membre
  de l'équipe, à la place du carré d'initiales.

GET .../volunteers/{id}/photo n'était accessible qu'à COORDINATION/
RÉFÉRENT ; ouvert aussi à l'intéressé lui-même (même garde que
get_volunteer), sinon l'écran bénévole ne pouvait jamais charger sa
propre photo.

Vérifié : pytest (274 passés, dont le nouveau test d'accès à sa propre
photo), ruff, tsc --noEmit, eslint, vite build, smoke — tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:10:04 +02:00

270 lines
8.4 KiB
Python

"""Application des rôles : COORDINATION / RÉFÉRENT (LEAD) / BÉNÉVOLE."""
from app.models import Assignment, Role
from tests.factories import (
dt_at,
make_position,
make_shift,
make_volunteer,
)
def _e(edition, suffix=""):
return f"/editions/{edition.id}{suffix}"
def test_all_endpoints_require_authentication(raw_client, edition):
assert raw_client.get(_e(edition, "/volunteers")).status_code == 401
assert raw_client.get(_e(edition, "/matrix")).status_code == 401
assert raw_client.post(_e(edition, "/positions"), json={}).status_code == 401
def test_account_from_another_edition_is_forbidden(
session, as_account, edition, make_account
):
import datetime as dt
from app.models import Edition
other = Edition(
year=2027, start_date=dt.date(2027, 7, 1), end_date=dt.date(2027, 7, 4)
)
session.add(other)
session.commit()
session.refresh(other)
from app.models import Account
stranger = Account(
edition_id=other.id, email="x@e.fr", role=Role.COORDINATOR
)
session.add(stranger)
session.commit()
session.refresh(stranger)
client = as_account(stranger)
assert client.get(_e(edition, "/volunteers")).status_code == 403
# --------------------------------------------------------------------------- #
# BÉNÉVOLE #
# --------------------------------------------------------------------------- #
def test_volunteer_scope(session, edition, make_account, as_account):
me = make_volunteer(session, edition, email="me@e.fr")
other = make_volunteer(session, edition, email="other@e.fr")
account = make_account(Role.VOLUNTEER, volunteer_id=me.id, email="me@e.fr")
client = as_account(account)
assert client.get(_e(edition, "/volunteers")).status_code == 403
assert client.get(_e(edition, f"/volunteers/{me.id}")).status_code == 200
assert client.get(_e(edition, f"/volunteers/{other.id}")).status_code == 403
assert client.get(_e(edition, "/matrix")).status_code == 403
assert (
client.post(
_e(edition, "/volunteers"),
json={"first_name": "A", "last_name": "B"},
).status_code
== 403
)
def test_volunteer_sees_only_own_photo(session, edition, make_account, as_account):
me = make_volunteer(session, edition, email="me@e.fr")
other = make_volunteer(session, edition, email="other@e.fr")
me.photo = b"fake-bytes"
me.photo_content_type = "image/png"
other.photo = b"fake-bytes"
other.photo_content_type = "image/png"
session.add_all([me, other])
session.commit()
account = make_account(Role.VOLUNTEER, volunteer_id=me.id, email="me@e.fr")
client = as_account(account)
assert client.get(_e(edition, f"/volunteers/{me.id}/photo")).status_code == 200
assert client.get(_e(edition, f"/volunteers/{other.id}/photo")).status_code == 403
def test_volunteer_sees_only_own_assignments(
session, edition, make_account, as_account
):
me = make_volunteer(session, edition, email="me@e.fr")
other = make_volunteer(session, edition, email="other@e.fr")
position = make_position(session, edition)
shift = make_shift(
session, edition, position, start=dt_at(10, 9), end=dt_at(10, 12)
)
session.add_all(
[
Assignment(
edition_id=edition.id, volunteer_id=me.id, shift_id=shift.id
),
]
)
session.commit()
account = make_account(Role.VOLUNTEER, volunteer_id=me.id, email="me@e.fr")
client = as_account(account)
mine = client.get(_e(edition, "/assignments"))
assert mine.status_code == 200
assert all(a["volunteer_id"] == str(me.id) for a in mine.json())
assert (
client.get(
_e(edition, "/assignments"),
params={"volunteer_id": str(other.id)},
).status_code
== 403
)
# --------------------------------------------------------------------------- #
# RÉFÉRENT (LEAD) #
# --------------------------------------------------------------------------- #
def test_lead_write_is_limited_to_own_positions(
session, edition, make_account, as_account
):
lead = make_account(Role.LEAD, email="lead@e.fr")
mine = make_position(session, edition, name="Bar")
mine.lead_account_id = lead.id
session.add(mine)
session.commit()
theirs = make_position(session, edition, name="Régie")
client = as_account(lead)
# Lecture sur toute l'édition.
assert client.get(_e(edition, "/matrix")).status_code == 200
assert client.get(_e(edition, "/positions")).status_code == 200
# Création de poste réservée à la coordination.
assert (
client.post(
_e(edition, "/positions"),
json={"name": "N", "category": "BAR"},
).status_code
== 403
)
# PATCH : son poste oui, celui d'un autre non.
assert (
client.patch(
_e(edition, f"/positions/{mine.id}"), json={"location": "Chapiteau"}
).status_code
== 200
)
assert (
client.patch(
_e(edition, f"/positions/{theirs.id}"), json={"location": "X"}
).status_code
== 403
)
# Créneau : sur son poste oui, sur celui d'un autre non.
ok = client.post(
_e(edition, "/shifts"),
json={
"position_id": str(mine.id),
"starts_at": "2026-07-10T09:00:00",
"ends_at": "2026-07-10T12:00:00",
},
)
assert ok.status_code == 201
ko = client.post(
_e(edition, "/shifts"),
json={
"position_id": str(theirs.id),
"starts_at": "2026-07-10T09:00:00",
"ends_at": "2026-07-10T12:00:00",
},
)
assert ko.status_code == 403
# Ni bénévoles ni import.
assert (
client.post(
_e(edition, "/volunteers"),
json={"first_name": "A", "last_name": "B"},
).status_code
== 403
)
assert (
client.post(
_e(edition, "/import/analyze"),
files={"file": ("x.csv", b"a;b\n1;2\n", "text/csv")},
).status_code
== 403
)
def test_lead_assignment_scope(session, edition, make_account, as_account):
lead = make_account(Role.LEAD, email="lead@e.fr")
mine = make_position(session, edition, name="Bar")
mine.lead_account_id = lead.id
session.add(mine)
session.commit()
theirs = make_position(session, edition, name="Accueil")
my_shift = make_shift(
session, edition, mine, start=dt_at(10, 9), end=dt_at(10, 12)
)
their_shift = make_shift(
session, edition, theirs, start=dt_at(11, 9), end=dt_at(11, 12)
)
volunteer = make_volunteer(session, edition, email="v@e.fr")
client = as_account(lead)
ok = client.post(
_e(edition, "/assignments"),
json={
"volunteer_id": str(volunteer.id),
"shift_id": str(my_shift.id),
},
)
assert ok.status_code == 201
ko = client.post(
_e(edition, "/assignments"),
json={
"volunteer_id": str(volunteer.id),
"shift_id": str(their_shift.id),
},
)
assert ko.status_code == 403
# --------------------------------------------------------------------------- #
# Comptes (coordination uniquement) #
# --------------------------------------------------------------------------- #
def test_account_management_is_coordinator_only(
session, edition, coordinator, make_account, as_account
):
coord_client = as_account(coordinator)
created = coord_client.post(
_e(edition, "/accounts"),
json={
"email": "nouveau.referent@e.fr",
"role": "LEAD",
"volunteer_id": None,
},
)
assert created.status_code == 201
account_id = created.json()["id"]
assert coord_client.get(_e(edition, "/accounts")).status_code == 200
lead_client = as_account(make_account(Role.LEAD, email="lead@e.fr"))
assert lead_client.get(_e(edition, "/accounts")).status_code == 403
assert (
lead_client.delete(
_e(edition, f"/accounts/{account_id}")
).status_code
== 403
)
# `as_account` fixe l'identité active : on repasse en COORDINATION.
coord_client = as_account(coordinator)
assert (
coord_client.delete(
_e(edition, f"/accounts/{account_id}")
).status_code
== 204
)