Files
benevoles/backend/tests/test_api_stats.py
T
AdamThomasandClaude Opus 5.5 d722a81a87
ci-cd / build-backend (push) Successful in 31s
ci-cd / build-frontend (push) Successful in 19s
ci-cd / deploy (push) Successful in 12s
Retrait des disponibilités et des limites de temps dans la journée
Disponibilités déclarées supprimées (table `availability`, migration 0022,
API, éditeur de la fiche bénévole, badge de couverture du panneau
d'affectation, colonnes des statistiques) ainsi que les règles
OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus
de disponibilité et de repos du solveur et le filtre « Surchargés » de la
vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis
disponible » du formulaire public est conservée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:29:58 +02:00

190 lines
6.7 KiB
Python

"""Statistiques d'une édition (inscriptions, réponses, paiements, besoins)."""
import datetime as dt
from app.models import (
Assignment,
FeePaymentStatus,
PendingFormSubmission,
Role,
VolunteerArtist,
VolunteerCompanionWish,
VolunteerPositionWish,
)
from tests.factories import (
dt_at,
make_artist,
make_position,
make_shift,
make_volunteer,
)
def _url(edition):
return f"/editions/{edition.id}/stats"
def _registered_on(session, volunteer, day):
volunteer.created_at = dt.datetime(2026, 6, day, 10, 0)
session.add(volunteer)
session.commit()
def test_is_coordinator_only(edition, make_account, as_account):
lead = make_account(Role.LEAD, email="lead@e.fr")
assert as_account(lead).get(_url(edition)).status_code == 403
def test_empty_edition(client, edition):
resp = client.get(_url(edition))
assert resp.status_code == 200
body = resp.json()
assert body["registrations"]["total"] == 0
assert body["registrations"]["by_day"] == []
assert body["payments"]["total_collected"] == 0
assert body["needs"]["by_day"] == []
def test_registrations_evolution_is_cumulative_with_gap_days_filled(client, edition, session):
for day in (1, 1, 3):
_registered_on(session, make_volunteer(session, edition), day)
by_day = client.get(_url(edition)).json()["registrations"]["by_day"]
assert by_day == [
{"date": "2026-06-01", "count": 2, "cumulative": 2},
{"date": "2026-06-02", "count": 0, "cumulative": 2},
{"date": "2026-06-03", "count": 1, "cumulative": 3},
]
def test_answers_count_volunteers_per_filled_field(client, edition, session):
bar = make_position(session, edition, name="Bar")
artist = make_artist(session, edition)
full = make_volunteer(session, edition, first_name="Alex", email="alex@ex.fr")
full.phone = "0600000000"
full.comment = "Dispo le soir"
full.companion_wish_note = "Sam"
full.self_declared_available = True
other = make_volunteer(session, edition, first_name="Sam")
session.add_all([
full,
VolunteerPositionWish(volunteer_id=full.id, position_id=bar.id),
VolunteerArtist(volunteer_id=full.id, artist_id=artist.id),
VolunteerCompanionWish(volunteer_id=full.id, companion_id=other.id),
])
session.commit()
body = client.get(_url(edition)).json()
answers = body["answers"]
assert answers["total"] == 2
for key in (
"email", "phone", "comment", "companion_note", "available",
"position_wishes", "artist_wishes", "companion_wishes",
):
assert answers[key] == 1, key
assert answers["photo"] == 0
assert body["artist_wishes"] == [
{"id": str(artist.id), "name": "Chilla", "date": "2026-07-10", "count": 1}
]
def test_payments_split_online_on_site_and_unfinished(client, edition, session):
donor = make_volunteer(session, edition, first_name="Alex")
donor.willing_to_pay_fee = True
donor.fee_payment_label = "Mécénat"
donor.fee_amount = 50.0
donor.fee_payment_status = FeePaymentStatus.PAID
donor.mecenat_address = "1 rue du Festival"
classic = make_volunteer(session, edition, first_name="Sam")
classic.willing_to_pay_fee = True
classic.fee_payment_label = "Cotisation classique"
classic.fee_amount = 12.5
classic.fee_payment_status = FeePaymentStatus.PAID
on_site = make_volunteer(session, edition, first_name="Jo")
on_site.willing_to_pay_fee = True
make_volunteer(session, edition, first_name="Lou")
session.add_all([
donor,
classic,
on_site,
# Formulaire rempli, paiement jamais abouti : pas de bénévole créé.
PendingFormSubmission(
edition_id=edition.id, payment_label="Mécénat", payment_amount=30.0,
checkout_status=FeePaymentStatus.FAILED,
),
])
session.commit()
payments = client.get(_url(edition)).json()["payments"]
assert payments["online"] == 2
assert payments["on_site"] == 1
assert payments["none"] == 1
assert payments["pending_unfinished"] == 1
assert payments["total_collected"] == 62.5
assert payments["mecenat_count"] == 1
assert payments["mecenat_total"] == 50.0
assert payments["by_option"] == [
{"label": "Cotisation classique", "mecenat": False, "count": 1, "amount_total": 12.5},
{"label": "Mécénat", "mecenat": True, "count": 1, "amount_total": 50.0},
]
def test_needs_are_grouped_by_festival_day(client, edition, session):
# Début de journée = 06:00 (fixture) : la soirée du 9 déborde après minuit.
bar = make_position(session, edition, name="Bar")
late = make_shift(
session, edition, bar, start=dt_at(9, 22), end=dt_at(10, 0), min_staff=1, ideal_staff=2,
)
make_shift(
session, edition, bar, start=dt_at(10, 0), end=dt_at(10, 1), min_staff=1, ideal_staff=1,
)
alex = make_volunteer(session, edition, first_name="Alex")
sam = make_volunteer(session, edition, first_name="Sam")
session.add_all([
Assignment(edition_id=edition.id, volunteer_id=alex.id, shift_id=late.id),
VolunteerPositionWish(volunteer_id=alex.id, position_id=bar.id),
VolunteerPositionWish(volunteer_id=sam.id, position_id=bar.id),
])
session.commit()
needs = client.get(_url(edition)).json()["needs"]
assert needs["by_day"] == [
{
"date": "2026-07-09",
"shifts": 2,
"min_hours": 3.0,
"ideal_hours": 5.0,
"assigned_hours": 2.0,
}
]
assert needs["by_position"] == [
{
"position_id": str(bar.id),
"name": "Bar",
"shifts": 2,
"min_slots": 2,
"ideal_slots": 3,
"assigned": 1,
"filled": 1,
"wishes": 2,
}
]
assert needs["volunteers_with_assignment"] == 1
def test_filled_slots_never_count_overstaffing(client, edition, session):
# 3 affectés sur un créneau qui n'en demande que 2 : 2 places pourvues,
# pas 3 — sinon un sureffectif masquerait un trou ailleurs.
bar = make_position(session, edition, name="Bar")
full = make_shift(session, edition, bar, start=dt_at(9, 18), end=dt_at(9, 20), ideal_staff=2)
make_shift(session, edition, bar, start=dt_at(9, 20), end=dt_at(9, 22), ideal_staff=2)
for name in ("A", "B", "C"):
v = make_volunteer(session, edition, first_name=name)
session.add(Assignment(edition_id=edition.id, volunteer_id=v.id, shift_id=full.id))
session.commit()
position = client.get(_url(edition)).json()["needs"]["by_position"][0]
assert position["assigned"] == 3
assert position["filled"] == 2
assert position["ideal_slots"] == 4