Gestion des référents depuis la fiche bénévole
Un poste avait bien un `lead_account_id`, lu par l'écran référent
(`/mobile-referent`) et par les droits d'écriture (`ensure_can_write_position`),
mais rien ne permettait de le renseigner : aucun écran ne l'exposait, et
l'API ne validait même pas que la valeur envoyée désignait un compte référent
existant de la bonne édition.
Backend :
- `PATCH /editions/{id}/accounts/{account_id}` (nouveau) : change le `role`
d'un compte, seulement entre LEAD et VOLUNTEER (lien magique dans les deux
cas) — 422 `account.role_change_not_allowed` dès que COORDINATOR est
impliqué (mot de passe en jeu, jamais touché ici). C'est ce qui permet de
faire passer le compte d'un bénévole en référent.
- `POST`/`PATCH /positions` : `lead_account_id` vérifié — doit désigner un
compte `Role.LEAD` de la même édition, sinon 422
`position.lead_must_be_referent_account` (silencieusement accepté
jusqu'ici, y compris un compte d'une autre édition ou du mauvais rôle).
Frontend (`ConfigVolunteers.tsx`) : nouvelle section « Référent des postes »
sur la fiche du bénévole, juste après l'accès de connexion — mêmes badges à
cocher que les vœux de poste/artiste. Cocher un poste crée ou met à niveau
(LEAD) l'accès de connexion du bénévole automatiquement ; décocher retire
seulement le poste, sans jamais rétrograder l'accès. Un bénévole dont le
compte est déjà COORDINATOR ne peut pas être désigné référent (il a déjà
accès à tout). Miroir complet côté mock (`accounts.update`,
`checkLeadAccount`) pour que la démonstration se comporte pareil.
Vérifié : 204 tests backend pertinents (14 nouveaux : 6 sur `PATCH accounts`,
8 sur la validation `lead_account_id`), ruff, alembic check ; typecheck/
lint/build/smoke frontend ; parcours complet testé en direct sur le jeu de
démo (création de compte, promotion LEAD, rejet COORDINATOR, affectation
référent) avant nettoyage des données de test.
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
db11d9a3ba
commit
8fcc22115f
@@ -0,0 +1,51 @@
|
||||
import uuid
|
||||
|
||||
from app.models import Role
|
||||
|
||||
|
||||
def _url(edition, suffix=""):
|
||||
return f"/editions/{edition.id}/accounts{suffix}"
|
||||
|
||||
|
||||
def test_promotes_a_volunteer_account_to_lead(client, edition, make_account):
|
||||
account = make_account(Role.VOLUNTEER, email="v@ex.fr")
|
||||
resp = client.patch(_url(edition, f"/{account.id}"), json={"role": "LEAD"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["role"] == "LEAD"
|
||||
assert resp.json()["auth_method"] == "magic_link"
|
||||
|
||||
|
||||
def test_demotes_a_lead_account_to_volunteer(client, edition, make_account):
|
||||
account = make_account(Role.LEAD, email="l@ex.fr")
|
||||
resp = client.patch(_url(edition, f"/{account.id}"), json={"role": "VOLUNTEER"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["role"] == "VOLUNTEER"
|
||||
|
||||
|
||||
def test_cannot_change_role_of_a_coordinator_account(client, edition, make_account):
|
||||
account = make_account(Role.COORDINATOR, email="c@ex.fr", password="password123")
|
||||
resp = client.patch(_url(edition, f"/{account.id}"), json={"role": "LEAD"})
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "account.role_change_not_allowed"
|
||||
|
||||
|
||||
def test_cannot_promote_a_volunteer_to_coordinator(client, edition, make_account):
|
||||
account = make_account(Role.VOLUNTEER, email="v2@ex.fr")
|
||||
resp = client.patch(_url(edition, f"/{account.id}"), json={"role": "COORDINATOR"})
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "account.role_change_not_allowed"
|
||||
|
||||
|
||||
def test_unknown_account_is_404(client, edition):
|
||||
assert (
|
||||
client.patch(_url(edition, f"/{uuid.uuid4()}"), json={"role": "LEAD"}).status_code
|
||||
== 404
|
||||
)
|
||||
|
||||
|
||||
def test_volunteer_role_forbidden(edition, make_account, as_account):
|
||||
volunteer_account = make_account(Role.VOLUNTEER, email="v3@ex.fr")
|
||||
other = make_account(Role.VOLUNTEER, email="v4@ex.fr")
|
||||
api = as_account(volunteer_account)
|
||||
resp = api.patch(_url(edition, f"/{other.id}"), json={"role": "LEAD"})
|
||||
assert resp.status_code == 403
|
||||
@@ -1,5 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from app.models import Role
|
||||
|
||||
|
||||
def _url(edition, suffix=""):
|
||||
return f"/editions/{edition.id}/positions{suffix}"
|
||||
@@ -35,3 +37,75 @@ def test_unknown_edition_is_404(client):
|
||||
assert (
|
||||
client.get(f"/editions/{uuid.uuid4()}/positions").status_code == 404
|
||||
)
|
||||
|
||||
|
||||
def test_sets_a_referent(client, edition, make_account):
|
||||
lead = make_account(Role.LEAD, email="referent@ex.fr")
|
||||
created = client.post(
|
||||
_url(edition),
|
||||
json={"name": "Bar", "category": "BAR"},
|
||||
)
|
||||
pid = created.json()["id"]
|
||||
|
||||
resp = client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": str(lead.id)})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["lead_account_id"] == str(lead.id)
|
||||
|
||||
|
||||
def test_referent_must_have_the_lead_role(client, edition, make_account):
|
||||
volunteer_account = make_account(Role.VOLUNTEER, email="v@ex.fr")
|
||||
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
|
||||
pid = created.json()["id"]
|
||||
|
||||
resp = client.patch(
|
||||
_url(edition, f"/{pid}"), json={"lead_account_id": str(volunteer_account.id)}
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
|
||||
|
||||
|
||||
def test_referent_must_belong_to_the_same_edition(client, edition, session, make_account):
|
||||
import datetime as dt
|
||||
|
||||
from app.models import Edition
|
||||
|
||||
other_edition = Edition(year=2027, start_date=dt.date(2027, 7, 1), end_date=dt.date(2027, 7, 4))
|
||||
session.add(other_edition)
|
||||
session.commit()
|
||||
session.refresh(other_edition)
|
||||
|
||||
other_lead = make_account(Role.LEAD, email="stranger@ex.fr")
|
||||
other_lead.edition_id = other_edition.id
|
||||
session.add(other_lead)
|
||||
session.commit()
|
||||
|
||||
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
|
||||
pid = created.json()["id"]
|
||||
|
||||
resp = client.patch(
|
||||
_url(edition, f"/{pid}"), json={"lead_account_id": str(other_lead.id)}
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
|
||||
|
||||
|
||||
def test_unknown_referent_is_422(client, edition):
|
||||
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
|
||||
pid = created.json()["id"]
|
||||
|
||||
resp = client.patch(
|
||||
_url(edition, f"/{pid}"), json={"lead_account_id": str(uuid.uuid4())}
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
|
||||
|
||||
|
||||
def test_clearing_the_referent_is_allowed(client, edition, make_account):
|
||||
lead = make_account(Role.LEAD, email="referent2@ex.fr")
|
||||
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
|
||||
pid = created.json()["id"]
|
||||
client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": str(lead.id)})
|
||||
|
||||
resp = client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": None})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["lead_account_id"] is None
|
||||
|
||||
Reference in New Issue
Block a user