Fix form field required states & missing fields per spec

- Admin add: add contact_public checkbox (matching edit form)
- All forms: formats checkbox-list now required
- All forms: jury promoteur·ice interne required, lecteur·ice interne/externe required
- All forms: licence select now required
- Admin edit: add E-mail de confirmation fieldset
- Partage: contact always visible when provided (no contact_public field)
- Partage: filter PACS from AP programs dropdown
- Server-side validation: formats, jury, licence required (create + edit controllers)
- Autofocus mappings for new validation errors
- No duplicate asterisks — verified across all rendered fields
- fix: add missing old() function in admin edit controller
- refactor: move admin email field to Backoffice as Contact interne, never send email
- Untrack admin.log (covered by .gitignore)
This commit is contained in:
Pontoporeia
2026-05-07 19:54:52 +02:00
parent 51f9f56e09
commit 696259afae
11 changed files with 307 additions and 45 deletions

View File

@@ -174,19 +174,25 @@ class ThesisCreateController
try {
$thesisId = $this->db->createThesis([
'year' => $data['annee'],
'orientation_id' => $data['orientationId'],
'ap_program_id' => $data['apProgramId'],
'finality_id' => $data['finalityId'],
'title' => $data['titre'],
'subtitle' => $data['subtitle'],
'synopsis' => $data['synopsis'],
'file_size_info' => $data['durationInfo'],
'baiu_link' => $data['lien'],
'year' => $data['annee'],
'orientation_id' => $data['orientationId'],
'ap_program_id' => $data['apProgramId'],
'finality_id' => $data['finalityId'],
'title' => $data['titre'],
'subtitle' => $data['subtitle'],
'synopsis' => $data['synopsis'],
'context_note' => $data['contextNote'],
'file_size_info' => $data['durationInfo'],
'baiu_link' => $data['lien'],
'license_id' => $data['licenseId'],
'license_custom' => $data['licenseCustom'],
'access_type_id' => $data['accessTypeId'],
'objet' => $data['objet'],
'objet' => $data['objet'],
'remarks' => $data['remarks'],
'jury_points' => $data['juryPoints'],
'exemplaire_baiu' => $data['exemplaireBaiu'],
'exemplaire_erg' => $data['exemplaireErg'],
'cc4r' => $data['cc4r'],
]);
$identifier = $this->db->getThesisIdentifier($thesisId);
@@ -247,15 +253,28 @@ class ThesisCreateController
if (str_contains($message, 'langue')) {
return 'languages';
}
if (str_contains($message, 'promoteur')) {
return 'jury_promoteur';
}
if (str_contains($message, 'lecteur·ice interne')) {
return 'jury_lecteur_interne[]';
}
if (str_contains($message, 'lecteur·ice externe')) {
return 'jury_lecteur_externe[]';
}
if (str_contains($message, 'format')) {
return 'formats';
}
if (str_contains($message, 'mots-clés')) {
return 'tag';
}
if (str_contains($message, 'licence')) {
return 'license_id';
}
if (str_contains($message, 'Lien URL')) {
return 'lien';
}
if (str_contains($message, 'e-mail de confirmation')) {
return 'confirmation_email';
}
return null;
}
@@ -285,7 +304,13 @@ class ThesisCreateController
}
$mail = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
$showContact = !empty($post['contact_public']) ? true : false;
// contact_public: respected if present (admin form); defaults to true for student forms
// where the spec says contact is always visible when provided.
if (array_key_exists('contact_public', $post)) {
$showContact = !empty($post['contact_public']);
} else {
$showContact = $mail !== '';
}
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
@@ -326,6 +351,9 @@ class ThesisCreateController
// Jury members — new structure: separate interne/externe lecteurs
$juryMembers = [];
$hasPromoteur = false;
$hasLecteurInt = false;
$hasLecteurExt = false;
if (!empty(trim($post['jury_promoteur'] ?? ''))) {
$juryMembers[] = [
'name' => trim($post['jury_promoteur']),
@@ -333,6 +361,7 @@ class ThesisCreateController
'is_external' => 0,
'is_ulb' => 0,
];
$hasPromoteur = true;
}
if (!empty(trim($post['jury_promoteur_ulb_name'] ?? ''))) {
$juryMembers[] = [
@@ -346,12 +375,14 @@ class ThesisCreateController
$name = trim($name);
if ($name !== '') {
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
$hasLecteurInt = true;
}
}
foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
$name = trim($name);
if ($name !== '') {
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
$hasLecteurExt = true;
}
}
// Keep backwards compat with old jury_lecteurs (from old-style forms)
@@ -364,6 +395,11 @@ class ThesisCreateController
'role' => 'lecteur',
'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
];
if (isset($post['jury_lecteurs_ext'][$i]) && $post['jury_lecteurs_ext'][$i]) {
$hasLecteurExt = true;
} else {
$hasLecteurInt = true;
}
}
}
}
@@ -371,6 +407,16 @@ class ThesisCreateController
$juryMembers[] = ['name' => trim($post['jury_president']), 'role' => 'president', 'is_external' => 0];
}
if (!$hasPromoteur) {
throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.');
}
if (!$hasLecteurInt) {
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice interne.');
}
if (!$hasLecteurExt) {
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice externe.');
}
// Keywords (max 10)
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
$keywords = $tagRaw !== '' ? array_map('trim', explode(',', $tagRaw)) : [];
@@ -386,13 +432,19 @@ class ThesisCreateController
throw new Exception('Veuillez sélectionner au moins une langue.');
}
// Formats (optional)
// Formats (at least one required)
$formatIds = isset($post['formats']) && is_array($post['formats'])
? array_map('intval', $post['formats'])
: [];
if (empty($formatIds)) {
throw new Exception('Veuillez sélectionner au moins un format.');
}
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
$licenseCustom = trim($post['license_custom'] ?? '');
if (!$licenseId && $licenseCustom === '') {
throw new Exception('Veuillez sélectionner une licence ou en préciser une.');
}
// Access type — must be one of the enabled types; default 2 (Interne)
$accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
@@ -413,20 +465,39 @@ class ThesisCreateController
}
}
// Confirmation e-mail (optional)
$confirmationEmail = trim($post['confirmation_email'] ?? '');
if ($confirmationEmail !== '') {
$confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL);
if ($confirmationEmail === false) {
throw new Exception("L'adresse e-mail de confirmation n'est pas valide.");
// Contact interne (optional, admin-only)
$contactInterne = trim($post['contact_interne'] ?? '');
if ($contactInterne !== '') {
$contactInterne = filter_var($contactInterne, FILTER_VALIDATE_EMAIL);
if ($contactInterne === false) {
throw new Exception("L'adresse de contact interne n'est pas valide.");
}
}
// Note contextuelle (optional, max 1500 chars)
$contextNote = $this->sanitiseString($post['context_note'] ?? '');
if (mb_strlen($contextNote) > 1500) {
$contextNote = mb_substr($contextNote, 0, 1500);
}
// Backoffice fields (admin only)
$remarks = trim($post['remarks'] ?? '');
$juryPoints = $post['jury_points'] ?? null;
if ($juryPoints !== null && $juryPoints !== '') {
$juryPoints = filter_var($juryPoints, FILTER_VALIDATE_FLOAT);
if ($juryPoints === false || $juryPoints < 0 || $juryPoints > 20) {
throw new Exception('La note du jury doit être comprise entre 0 et 20.');
}
}
$exemplaireBaiu = !empty($post['exemplaire_baiu']);
$exemplaireErg = !empty($post['exemplaire_erg']);
$cc4r = !empty($post['cc2r']);
return compact(
'authorNames',
'mail',
'showContact',
'confirmationEmail',
'contactInterne',
'annee',
'orientationId',
'apProgramId',
@@ -443,7 +514,13 @@ class ThesisCreateController
'licenseCustom',
'lien',
'accessTypeId',
'objet'
'objet',
'contextNote',
'remarks',
'juryPoints',
'exemplaireBaiu',
'exemplaireErg',
'cc4r'
);
}