Étape 21 : formulaire public d'inscription des bénévoles
Retire « constructeur de formulaire » des exclusions du CLAUDE.md (§ 5) — décision explicite prise en conversation. Un formulaire par édition (COORDINATION choisit les champs actifs parmi neuf : email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir de festival, vœu de compagnon en texte libre, disponibilité/ cotisation déclaratives, photo, commentaire ; nom/prénom toujours collectés) ouvert/fermé à volonté, avec un lien à partager (/inscription, aucun compte requis). Backend : FormTemplate/FormField (migration 0013), quatre nouveaux champs sur Volunteer (photo, companion_wish_note, self_declared_available, willing_to_pay_fee), app/api/forms.py — les deux seuls endpoints publics de toute l'API (GET .../form/public, POST .../form/public/submit), protégés par hCaptcha (app/forms/captcha.py, désactivé sans HCAPTCHA_SECRET_KEY) et un filet de sécurité global par édition (form_submission_limiter). resolve_owned_ids/set_artists/ set_wished_positions déplacés vers app/api/_common.py, partagés avec volunteers.py. Frontend : écran de configuration (/inscriptions) avec lien copiable et bascule ouvert/fermé, formulaire public (/inscription, hors RequireAuth) avec widget hCaptcha chargé dynamiquement, capture photo native (<input capture>), vœux de poste/artiste avec les mêmes règles que le back-end. Réponses du formulaire visibles et éditables depuis la fiche bénévole existante (ConfigVolunteers.tsx). Vérifié : pytest (273 passés), ruff, alembic check, tsc --noEmit, eslint, vite build, smoke (rendu SSR de chaque écran + flux de bout en bout du formulaire en mode démonstration) — tous verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
55c5d539a8
commit
82088f3b0c
@@ -20,6 +20,7 @@ import { AuthContext, AuthProvider, type AuthContextValue } from '@/auth/auth-co
|
||||
import { routes } from '@/app/router';
|
||||
import { BadgesPage } from '@/pages/BadgesPage';
|
||||
import { ConfigPage } from '@/pages/ConfigPage';
|
||||
import { FormBuilderPage } from '@/pages/FormBuilderPage';
|
||||
import { GrillePage } from '@/pages/GrillePage';
|
||||
import { MobileLeadPage } from '@/pages/MobileLeadPage';
|
||||
import { MobileVolunteerPage } from '@/pages/MobileVolunteerPage';
|
||||
@@ -81,6 +82,79 @@ async function checkAuth() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Exerce le formulaire public de bout en bout (mode mock) : configuration,
|
||||
* lecture publique, soumission, vœux (max 2 postes, un artiste par jour),
|
||||
* et le bénévole créé doit apparaître dans la liste de la coordination. */
|
||||
async function checkPublicForm() {
|
||||
const positions = await api.positions.list(e);
|
||||
const artists = await api.artists.list(e);
|
||||
const beforeCount = (await api.volunteers.list(e)).length;
|
||||
|
||||
await api.forms.setFields(e, {
|
||||
fields: ['EMAIL', 'POSITION_WISHES', 'ARTIST_WISHES', 'COMMENT'],
|
||||
});
|
||||
const config = await api.forms.getPublicConfig(e);
|
||||
if (!config.is_open) throw new Error('formulaire public : devrait être ouvert par défaut');
|
||||
if (config.positions.length !== positions.length) {
|
||||
throw new Error('formulaire public : postes manquants dans la config publique');
|
||||
}
|
||||
|
||||
const sameDay = artists.filter((a) => a.performance_date === artists[0]?.performance_date);
|
||||
let tooManyArtistsRejectedWith = 'non';
|
||||
if (sameDay.length >= 2) {
|
||||
try {
|
||||
await api.forms.submitPublic(e, {
|
||||
first_name: 'Test',
|
||||
last_name: 'Smoke',
|
||||
hcaptcha_token: 'dev',
|
||||
artist_wish_ids: [sameDay[0].id, sameDay[1].id],
|
||||
});
|
||||
} catch (err) {
|
||||
tooManyArtistsRejectedWith = err instanceof ApiError ? err.code : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
let tooManyPositionsRejectedWith = 'non';
|
||||
if (positions.length >= 3) {
|
||||
try {
|
||||
await api.forms.submitPublic(e, {
|
||||
first_name: 'Test',
|
||||
last_name: 'Smoke',
|
||||
hcaptcha_token: 'dev',
|
||||
position_wish_ids: positions.slice(0, 3).map((p) => p.id),
|
||||
});
|
||||
} catch (err) {
|
||||
tooManyPositionsRejectedWith = err instanceof ApiError ? err.code : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api.forms.submitPublic(e, {
|
||||
first_name: 'test',
|
||||
last_name: 'smoke',
|
||||
hcaptcha_token: 'dev',
|
||||
email: 'test.smoke@example.org',
|
||||
comment: 'Créé par le smoke test',
|
||||
position_wish_ids: positions.slice(0, 2).map((p) => p.id),
|
||||
artist_wish_ids: artists[0] ? [artists[0].id] : [],
|
||||
});
|
||||
const created = await api.volunteers.get(e, result.volunteer_id);
|
||||
const afterCount = (await api.volunteers.list(e)).length;
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
formOpenByDefault: config.is_open,
|
||||
tooManyPositionsRejectedWith,
|
||||
tooManyArtistsRejectedWith,
|
||||
createdVolunteerName: `${created.first_name} ${created.last_name}`,
|
||||
createdVolunteerAppearsInList: afterCount === beforeCount + 1,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function prefetchCore(client: QueryClient) {
|
||||
await Promise.all([
|
||||
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
|
||||
@@ -148,9 +222,11 @@ function renderPublic(path: string) {
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
await checkAuth().catch((err) => fail('checkAuth', err));
|
||||
await checkPublicForm().catch((err) => fail('checkPublicForm', err));
|
||||
|
||||
renderPublic('/connexion');
|
||||
renderPublic('/connexion/coordination');
|
||||
renderPublic('/inscription'); // formulaire public, hors de <RequireAuth/>
|
||||
|
||||
if (coordinator) {
|
||||
setAuthToken(coordinatorToken);
|
||||
@@ -158,6 +234,7 @@ if (coordinator) {
|
||||
await renderProtected('/benevoles', coordinator, createElement(VolunteerViewPage));
|
||||
await renderProtected('/configuration', coordinator, createElement(ConfigPage));
|
||||
await renderProtected('/badges', coordinator, createElement(BadgesPage));
|
||||
await renderProtected('/inscriptions', coordinator, createElement(FormBuilderPage));
|
||||
}
|
||||
if (volunteerAccount) {
|
||||
const vid = volunteerAccount.volunteer_id as string;
|
||||
|
||||
Reference in New Issue
Block a user