mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
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:
24
TODO.md
24
TODO.md
@@ -13,7 +13,6 @@
|
|||||||
- [x] `AboutController.php` — removed credits, sourceCode DB loading
|
- [x] `AboutController.php` — removed credits, sourceCode DB loading
|
||||||
- [x] `templates/public/about.php` — hardcoded source code URL, hardcoded credits HTML
|
- [x] `templates/public/about.php` — hardcoded source code URL, hardcoded credits HTML
|
||||||
- [x] `apropos.css` — `.apropos-toc-source` styles
|
- [x] `apropos.css` — `.apropos-toc-source` styles
|
||||||
- [x] `.gitignore` — ignore `app/storage/logs/*.log`
|
|
||||||
|
|
||||||
## Duplicate TFE submission prevention (fixes)
|
## Duplicate TFE submission prevention (fixes)
|
||||||
- [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata
|
- [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata
|
||||||
@@ -141,6 +140,14 @@
|
|||||||
- [x] Recapitulatif: show promoteur·ice ULB and lecteur·ices interne/externe
|
- [x] Recapitulatif: show promoteur·ice ULB and lecteur·ices interne/externe
|
||||||
- [x] Migration: `014_tfe_form_fields.sql` — ALTER + view rebuild
|
- [x] Migration: `014_tfe_form_fields.sql` — ALTER + view rebuild
|
||||||
|
|
||||||
|
- [x] Fix `Call to undefined function old()` in admin edit page — define `old()` in `app/public/admin/edit.php` (was only in `add.php`)
|
||||||
|
- [x] Add Note contextuelle and Backoffice fieldsets to admin add form (matching edit form)
|
||||||
|
- [x] `Database::createThesis()` — add `context_note`, `remarks`, `jury_points`, `exemplaire_baiu`, `exemplaire_erg`, `cc4r` columns
|
||||||
|
- [x] `ThesisCreateController::validateAndSanitise()` — handle new admin-only fields
|
||||||
|
- [x] `ThesisCreateController::submit()` — pass new fields to `createThesis()`
|
||||||
|
- [x] Replace admin E-mail de confirmation fieldset with Contact interne in Backoffice section (add + edit)
|
||||||
|
- [x] Remove confirmation email sending from add/edit (admin never sent; student partage unchanged)
|
||||||
|
|
||||||
## Refactor form structure per spec (student vs admin)
|
## Refactor form structure per spec (student vs admin)
|
||||||
- [x] Remove `jury_president` field from student-facing forms (edit keeps it as optional)
|
- [x] Remove `jury_president` field from student-facing forms (edit keeps it as optional)
|
||||||
- [x] Jury: split into promoteur·ice interne, promoteur·ice ULB, lecteur·ice interne, lecteur·ice externe — each with +add button
|
- [x] Jury: split into promoteur·ice interne, promoteur·ice ULB, lecteur·ice interne, lecteur·ice externe — each with +add button
|
||||||
@@ -158,3 +165,18 @@
|
|||||||
- [x] Fichiers: cover image hint updated to 4:3 ratio
|
- [x] Fichiers: cover image hint updated to 4:3 ratio
|
||||||
- [x] All three form pages (admin add, admin edit, partage) updated
|
- [x] All three form pages (admin add, admin edit, partage) updated
|
||||||
- [x] Controllers updated: `collectJuryMembers`, `validateAndSanitise`, `buildFileSizeInfo`, `license_custom`, `cc2r`→`cc4r` mapping
|
- [x] Controllers updated: `collectJuryMembers`, `validateAndSanitise`, `buildFileSizeInfo`, `license_custom`, `cc2r`→`cc4r` mapping
|
||||||
|
|
||||||
|
## Fix form field required states & missing fields per spec
|
||||||
|
- [x] Admin add: add `contact_public` checkbox (matching edit form)
|
||||||
|
- [x] Admin add + partage + admin edit: formats checkbox-list `$required = true`
|
||||||
|
- [x] All forms: jury promoteur·ice interne `required` attribute
|
||||||
|
- [x] All forms: jury lecteur·ice interne `required` attribute (at least one)
|
||||||
|
- [x] All forms: jury lecteur·ice externe `required` attribute (at least one)
|
||||||
|
- [x] All forms: licence select `$required = true`
|
||||||
|
- [x] Admin edit: add "E-mail de confirmation" fieldset
|
||||||
|
- [x] Partage: contact always visible (POST handler defaults `showContact` to true when no `contact_public` key present)
|
||||||
|
- [x] Partage: filter PACS from AP programs dropdown
|
||||||
|
- [x] Verify no duplicate asterisks on any field
|
||||||
|
- [x] Admin add: `contact_public` POST handling in ThesisCreateController for admin submissions
|
||||||
|
- [x] Server-side validation: formats required, jury members required, licence required (ThesisCreateController + ThesisEditController)
|
||||||
|
- [x] Autofocus mappings for new validation errors (format, jury, licence)
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ $autofocusField = App::consumeAutofocus();
|
|||||||
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
|
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
|
||||||
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
|
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
|
||||||
|
|
||||||
|
function old($key, $default = "") {
|
||||||
|
global $formData;
|
||||||
|
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$ctrl = ThesisEditController::create();
|
$ctrl = ThesisEditController::create();
|
||||||
$view = $ctrl->load($thesisId);
|
$view = $ctrl->load($thesisId);
|
||||||
|
|||||||
@@ -106,12 +106,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Asterisk appended to labels of required fields */
|
/* Asterisk on required field labels */
|
||||||
label:has(+ input:required:not([type="hidden"]))::after,
|
.asterisk {
|
||||||
label:has(+ select:required)::after,
|
|
||||||
label:has(+ textarea:required)::after,
|
|
||||||
label:has(+ div > input:required)::after {
|
|
||||||
content: " *";
|
|
||||||
color: var(--error, #c00);
|
color: var(--error, #c00);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
die('Erreur lors du chargement du formulaire.');
|
die('Erreur lors du chargement du formulaire.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out PACS from AP programs for student forms (spec: admin-only AP)
|
||||||
|
$apPrograms = array_values(array_filter($apPrograms, fn($ap) => ($ap['name'] ?? '') !== 'PACS'));
|
||||||
|
|
||||||
$formData = $_SESSION['form_data_share_' . $slug] ?? [];
|
$formData = $_SESSION['form_data_share_' . $slug] ?? [];
|
||||||
unset($_SESSION['form_data_share_' . $slug]);
|
unset($_SESSION['form_data_share_' . $slug]);
|
||||||
|
|
||||||
@@ -303,7 +306,7 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
<!-- ═══════════════════ Format(s) ═══════════════════ -->
|
<!-- ═══════════════════ Format(s) ═══════════════════ -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Format(s)</legend>
|
<legend>Format(s)</legend>
|
||||||
<?php $name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
<?php $name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
|
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
|
||||||
|
|||||||
@@ -181,12 +181,18 @@ class ThesisCreateController
|
|||||||
'title' => $data['titre'],
|
'title' => $data['titre'],
|
||||||
'subtitle' => $data['subtitle'],
|
'subtitle' => $data['subtitle'],
|
||||||
'synopsis' => $data['synopsis'],
|
'synopsis' => $data['synopsis'],
|
||||||
|
'context_note' => $data['contextNote'],
|
||||||
'file_size_info' => $data['durationInfo'],
|
'file_size_info' => $data['durationInfo'],
|
||||||
'baiu_link' => $data['lien'],
|
'baiu_link' => $data['lien'],
|
||||||
'license_id' => $data['licenseId'],
|
'license_id' => $data['licenseId'],
|
||||||
'license_custom' => $data['licenseCustom'],
|
'license_custom' => $data['licenseCustom'],
|
||||||
'access_type_id' => $data['accessTypeId'],
|
'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);
|
$identifier = $this->db->getThesisIdentifier($thesisId);
|
||||||
@@ -247,15 +253,28 @@ class ThesisCreateController
|
|||||||
if (str_contains($message, 'langue')) {
|
if (str_contains($message, 'langue')) {
|
||||||
return 'languages';
|
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')) {
|
if (str_contains($message, 'mots-clés')) {
|
||||||
return 'tag';
|
return 'tag';
|
||||||
}
|
}
|
||||||
|
if (str_contains($message, 'licence')) {
|
||||||
|
return 'license_id';
|
||||||
|
}
|
||||||
if (str_contains($message, 'Lien URL')) {
|
if (str_contains($message, 'Lien URL')) {
|
||||||
return 'lien';
|
return 'lien';
|
||||||
}
|
}
|
||||||
if (str_contains($message, 'e-mail de confirmation')) {
|
|
||||||
return 'confirmation_email';
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +304,13 @@ class ThesisCreateController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$mail = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
|
$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);
|
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
|
||||||
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
|
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
|
||||||
@@ -326,6 +351,9 @@ class ThesisCreateController
|
|||||||
|
|
||||||
// Jury members — new structure: separate interne/externe lecteurs
|
// Jury members — new structure: separate interne/externe lecteurs
|
||||||
$juryMembers = [];
|
$juryMembers = [];
|
||||||
|
$hasPromoteur = false;
|
||||||
|
$hasLecteurInt = false;
|
||||||
|
$hasLecteurExt = false;
|
||||||
if (!empty(trim($post['jury_promoteur'] ?? ''))) {
|
if (!empty(trim($post['jury_promoteur'] ?? ''))) {
|
||||||
$juryMembers[] = [
|
$juryMembers[] = [
|
||||||
'name' => trim($post['jury_promoteur']),
|
'name' => trim($post['jury_promoteur']),
|
||||||
@@ -333,6 +361,7 @@ class ThesisCreateController
|
|||||||
'is_external' => 0,
|
'is_external' => 0,
|
||||||
'is_ulb' => 0,
|
'is_ulb' => 0,
|
||||||
];
|
];
|
||||||
|
$hasPromoteur = true;
|
||||||
}
|
}
|
||||||
if (!empty(trim($post['jury_promoteur_ulb_name'] ?? ''))) {
|
if (!empty(trim($post['jury_promoteur_ulb_name'] ?? ''))) {
|
||||||
$juryMembers[] = [
|
$juryMembers[] = [
|
||||||
@@ -346,12 +375,14 @@ class ThesisCreateController
|
|||||||
$name = trim($name);
|
$name = trim($name);
|
||||||
if ($name !== '') {
|
if ($name !== '') {
|
||||||
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
|
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
|
||||||
|
$hasLecteurInt = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
|
foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
|
||||||
$name = trim($name);
|
$name = trim($name);
|
||||||
if ($name !== '') {
|
if ($name !== '') {
|
||||||
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
|
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
|
||||||
|
$hasLecteurExt = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep backwards compat with old jury_lecteurs (from old-style forms)
|
// Keep backwards compat with old jury_lecteurs (from old-style forms)
|
||||||
@@ -364,6 +395,11 @@ class ThesisCreateController
|
|||||||
'role' => 'lecteur',
|
'role' => 'lecteur',
|
||||||
'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
|
'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];
|
$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)
|
// Keywords (max 10)
|
||||||
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
||||||
$keywords = $tagRaw !== '' ? array_map('trim', explode(',', $tagRaw)) : [];
|
$keywords = $tagRaw !== '' ? array_map('trim', explode(',', $tagRaw)) : [];
|
||||||
@@ -386,13 +432,19 @@ class ThesisCreateController
|
|||||||
throw new Exception('Veuillez sélectionner au moins une langue.');
|
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'])
|
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
||||||
? array_map('intval', $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;
|
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
$licenseCustom = trim($post['license_custom'] ?? '');
|
$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)
|
// Access type — must be one of the enabled types; default 2 (Interne)
|
||||||
$accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
|
$accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
|
||||||
@@ -413,20 +465,39 @@ class ThesisCreateController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmation e-mail (optional)
|
// Contact interne (optional, admin-only)
|
||||||
$confirmationEmail = trim($post['confirmation_email'] ?? '');
|
$contactInterne = trim($post['contact_interne'] ?? '');
|
||||||
if ($confirmationEmail !== '') {
|
if ($contactInterne !== '') {
|
||||||
$confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL);
|
$contactInterne = filter_var($contactInterne, FILTER_VALIDATE_EMAIL);
|
||||||
if ($confirmationEmail === false) {
|
if ($contactInterne === false) {
|
||||||
throw new Exception("L'adresse e-mail de confirmation n'est pas valide.");
|
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(
|
return compact(
|
||||||
'authorNames',
|
'authorNames',
|
||||||
'mail',
|
'mail',
|
||||||
'showContact',
|
'showContact',
|
||||||
'confirmationEmail',
|
'contactInterne',
|
||||||
'annee',
|
'annee',
|
||||||
'orientationId',
|
'orientationId',
|
||||||
'apProgramId',
|
'apProgramId',
|
||||||
@@ -443,7 +514,13 @@ class ThesisCreateController
|
|||||||
'licenseCustom',
|
'licenseCustom',
|
||||||
'lien',
|
'lien',
|
||||||
'accessTypeId',
|
'accessTypeId',
|
||||||
'objet'
|
'objet',
|
||||||
|
'contextNote',
|
||||||
|
'remarks',
|
||||||
|
'juryPoints',
|
||||||
|
'exemplaireBaiu',
|
||||||
|
'exemplaireErg',
|
||||||
|
'cc4r'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,54 @@ class ThesisEditController
|
|||||||
throw new InvalidArgumentException('ID de TFE invalide.');
|
throw new InvalidArgumentException('ID de TFE invalide.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Basic validation (same required fields as create) ──────────────────
|
||||||
|
$errors = [];
|
||||||
|
$titre = trim($post['titre'] ?? '');
|
||||||
|
if ($titre === '') $errors[] = 'Le titre est requis.';
|
||||||
|
$auteurice = trim($post['auteurice'] ?? '');
|
||||||
|
if ($auteurice === '') $errors[] = "L'auteur·ice est requis.";
|
||||||
|
$synopsis = trim($post['synopsis'] ?? '');
|
||||||
|
if ($synopsis === '') $errors[] = 'Le synopsis est requis.';
|
||||||
|
$annee = intval($post['année'] ?? 0);
|
||||||
|
if ($annee < 2000 || $annee > ((int)date('Y') + 1)) $errors[] = "L'année est invalide.";
|
||||||
|
$orientationId = intval($post['orientation'] ?? 0);
|
||||||
|
if ($orientationId <= 0) $errors[] = "L'orientation est requise.";
|
||||||
|
$apProgramId = intval($post['ap'] ?? 0);
|
||||||
|
if ($apProgramId <= 0) $errors[] = "L'atelier pluridisciplinaire est requis.";
|
||||||
|
$finalityId = intval($post['finality'] ?? 0);
|
||||||
|
if ($finalityId <= 0) $errors[] = 'La finalité est requise.';
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
$langIds = isset($post['languages']) && is_array($post['languages']) ? $post['languages'] : [];
|
||||||
|
if (empty($langIds)) $errors[] = 'Au moins une langue est requise.';
|
||||||
|
|
||||||
|
// Formats
|
||||||
|
$fmtIds = isset($post['formats']) && is_array($post['formats']) ? $post['formats'] : [];
|
||||||
|
if (empty($fmtIds)) $errors[] = 'Au moins un format est requis.';
|
||||||
|
|
||||||
|
// Licence
|
||||||
|
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
|
$licenseCustom = trim($post['license_custom'] ?? '');
|
||||||
|
if (!$licenseId && $licenseCustom === '') $errors[] = 'Une licence est requise.';
|
||||||
|
|
||||||
|
// Jury
|
||||||
|
$hasPromoteur = !empty(trim($post['jury_promoteur'] ?? ''));
|
||||||
|
$hasLecteurInt = false;
|
||||||
|
$hasLecteurExt = false;
|
||||||
|
foreach ($post['jury_lecteur_interne'] ?? [] as $n) {
|
||||||
|
if (trim((string)$n) !== '') { $hasLecteurInt = true; break; }
|
||||||
|
}
|
||||||
|
foreach ($post['jury_lecteur_externe'] ?? [] as $n) {
|
||||||
|
if (trim((string)$n) !== '') { $hasLecteurExt = true; break; }
|
||||||
|
}
|
||||||
|
if (!$hasPromoteur) $errors[] = 'Un·e promoteur·ice interne est requis.';
|
||||||
|
if (!$hasLecteurInt) $errors[] = 'Au moins un·e lecteur·ice interne est requis.';
|
||||||
|
if (!$hasLecteurExt) $errors[] = 'Au moins un·e lecteur·ice externe est requis.';
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
throw new RuntimeException(implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
$this->db->beginTransaction();
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -531,6 +579,33 @@ class ThesisEditController
|
|||||||
if (str_contains($message, 'auteur') || str_contains($message, 'Auteur')) {
|
if (str_contains($message, 'auteur') || str_contains($message, 'Auteur')) {
|
||||||
return 'auteurice';
|
return 'auteurice';
|
||||||
}
|
}
|
||||||
|
if (str_contains($message, 'orientation')) {
|
||||||
|
return 'orientation';
|
||||||
|
}
|
||||||
|
if (str_contains($message, 'atelier')) {
|
||||||
|
return 'ap';
|
||||||
|
}
|
||||||
|
if (str_contains($message, 'finalité')) {
|
||||||
|
return 'finality';
|
||||||
|
}
|
||||||
|
if (str_contains($message, 'langue')) {
|
||||||
|
return 'languages';
|
||||||
|
}
|
||||||
|
if (str_contains($message, 'format')) {
|
||||||
|
return 'formats';
|
||||||
|
}
|
||||||
|
if (str_contains($message, 'licence')) {
|
||||||
|
return 'license_id';
|
||||||
|
}
|
||||||
|
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[]';
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1848,13 +1848,16 @@ class Database
|
|||||||
INSERT INTO theses (
|
INSERT INTO theses (
|
||||||
identifier, title, subtitle, year,
|
identifier, title, subtitle, year,
|
||||||
orientation_id, ap_program_id, finality_id,
|
orientation_id, ap_program_id, finality_id,
|
||||||
synopsis, file_size_info,
|
synopsis, context_note, file_size_info,
|
||||||
baiu_link, license_id, license_custom,
|
baiu_link, license_id, license_custom,
|
||||||
access_type_id,
|
access_type_id,
|
||||||
objet,
|
objet,
|
||||||
is_published,
|
is_published,
|
||||||
|
remarks, jury_points,
|
||||||
|
exemplaire_baiu, exemplaire_erg,
|
||||||
|
cc4r,
|
||||||
submitted_at
|
submitted_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
');
|
');
|
||||||
|
|
||||||
$validObjet = ['tfe', 'thèse', 'frart'];
|
$validObjet = ['tfe', 'thèse', 'frart'];
|
||||||
@@ -1869,12 +1872,18 @@ class Database
|
|||||||
(int)$data['ap_program_id'],
|
(int)$data['ap_program_id'],
|
||||||
(int)$data['finality_id'],
|
(int)$data['finality_id'],
|
||||||
$data['synopsis'],
|
$data['synopsis'],
|
||||||
|
!empty($data['context_note']) ? $data['context_note'] : null,
|
||||||
!empty($data['file_size_info']) ? $data['file_size_info'] : null,
|
!empty($data['file_size_info']) ? $data['file_size_info'] : null,
|
||||||
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
||||||
$data['license_id'] ?? null,
|
$data['license_id'] ?? null,
|
||||||
!empty($data['license_custom']) ? $data['license_custom'] : null,
|
!empty($data['license_custom']) ? $data['license_custom'] : null,
|
||||||
isset($data['access_type_id']) ? (int)$data['access_type_id'] : 2, // default: Interne
|
isset($data['access_type_id']) ? (int)$data['access_type_id'] : 2, // default: Interne
|
||||||
$objet,
|
$objet,
|
||||||
|
!empty($data['remarks']) ? $data['remarks'] : null,
|
||||||
|
isset($data['jury_points']) && $data['jury_points'] !== '' ? (float)$data['jury_points'] : null,
|
||||||
|
!empty($data['exemplaire_baiu']) ? 1 : 0,
|
||||||
|
!empty($data['exemplaire_erg']) ? 1 : 0,
|
||||||
|
!empty($data['cc4r']) ? 1 : 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (int)$this->pdo->lastInsertId();
|
return (int)$this->pdo->lastInsertId();
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<!-- ═══════════════════ Contact ═══════════════════ -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Contact</legend>
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="contact_public" value="1"
|
||||||
|
<?= !empty($formData['contact_public']) ? 'checked' : '' ?>>
|
||||||
|
Rendre le contact visible publiquement sur la fiche du TFE
|
||||||
|
</label>
|
||||||
|
<small>L'adresse est toujours conservée en interne comme contact de référence.</small>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
|
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Langue(s)</legend>
|
<legend>Langue(s)</legend>
|
||||||
@@ -26,7 +39,7 @@
|
|||||||
<!-- ═══════════════════ Format(s) ═══════════════════ -->
|
<!-- ═══════════════════ Format(s) ═══════════════════ -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Format(s)</legend>
|
<legend>Format(s)</legend>
|
||||||
<?php $name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
<?php $name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
|
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
|
||||||
@@ -82,10 +95,62 @@
|
|||||||
include APP_ROOT . '/templates/partials/form/fieldset-licence-explanation.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-licence-explanation.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- ═══════════════════ E-mail de confirmation ═══════════════ -->
|
<!-- ═══════════════════ Note contextuelle ═══════════════════ -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>E-mail de confirmation</legend>
|
<legend>Note contextuelle</legend>
|
||||||
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail :'; $value = old('confirmation_email'); $type = 'email'; $required = false; $placeholder = 'ton.email@exemple.be'; $hint = 'Optionnel — pour envoyer un récapitulatif de la soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<div>
|
||||||
|
<label for="context_note">Note contextuelle :</label>
|
||||||
|
<div>
|
||||||
|
<textarea id="context_note" name="context_note"
|
||||||
|
rows="4" maxlength="1500"><?= htmlspecialchars($formData['context_note'] ?? '') ?></textarea>
|
||||||
|
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- ═══════════════════ Backoffice ═══════════════════ -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Backoffice</legend>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label for="jury_points">Points :</label>
|
||||||
|
<input type="number" id="jury_points" name="jury_points"
|
||||||
|
value="<?= htmlspecialchars($formData['jury_points'] ?? '') ?>"
|
||||||
|
step="0.01" min="0" max="20" placeholder="sur 20">
|
||||||
|
<small>Note du jury (interne, non visible publiquement).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label for="remarks">Remarques :</label>
|
||||||
|
<textarea id="remarks" name="remarks" rows="4"><?= htmlspecialchars($formData['remarks'] ?? '') ?></textarea>
|
||||||
|
<small>Notes internes (non visibles publiquement).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label for="contact_interne">Contact interne :</label>
|
||||||
|
<input type="email" id="contact_interne" name="contact_interne"
|
||||||
|
value="<?= htmlspecialchars($formData['contact_interne'] ?? '') ?>"
|
||||||
|
placeholder="ton.email@exemple.be">
|
||||||
|
<small>Adresse de contact interne (non visible publiquement).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="exemplaire_baiu" value="1"
|
||||||
|
<?= !empty($formData['exemplaire_baiu']) ? 'checked' : '' ?>>
|
||||||
|
Exemplaire physique BAIU
|
||||||
|
</label>
|
||||||
|
<small>Case logistique : cocher si un exemplaire physique est disponible à la BAIU.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="exemplaire_erg" value="1"
|
||||||
|
<?= !empty($formData['exemplaire_erg']) ? 'checked' : '' ?>>
|
||||||
|
Exemplaire physique ERG
|
||||||
|
</label>
|
||||||
|
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<legend>Format(s)</legend>
|
<legend>Format(s)</legend>
|
||||||
<?php
|
<?php
|
||||||
$checkedFormats = $formData['formats'] ?? $currentFormats;
|
$checkedFormats = $formData['formats'] ?? $currentFormats;
|
||||||
$name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $checkedFormats;
|
$name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $checkedFormats; $required = true;
|
||||||
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
|
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
|
||||||
?>
|
?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -301,6 +301,14 @@
|
|||||||
<small>Notes internes (non visibles publiquement).</small>
|
<small>Notes internes (non visibles publiquement).</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label for="contact_interne">Contact interne :</label>
|
||||||
|
<input type="email" id="contact_interne" name="contact_interne"
|
||||||
|
value="<?= htmlspecialchars($currentRaw['contact_interne'] ?? $currentAuthorEmail ?? '') ?>"
|
||||||
|
placeholder="ton.email@exemple.be">
|
||||||
|
<small>Adresse de contact interne (non visible publiquement).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-form-group">
|
<div class="admin-form-group">
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
<input type="checkbox" name="exemplaire_baiu" value="1"
|
<input type="checkbox" name="exemplaire_baiu" value="1"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ $defaultAccessTypeId = $defaultAccessTypeId ?? 2;
|
|||||||
<h3>Licence du TFE</h3>
|
<h3>Licence du TFE</h3>
|
||||||
<?php
|
<?php
|
||||||
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
|
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
|
||||||
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —';
|
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —'; $required = true;
|
||||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Promoteur·ice interne -->
|
<!-- Promoteur·ice interne -->
|
||||||
<div>
|
<div>
|
||||||
<label for="jury_promoteur">Promoteur·ice interne :</label>
|
<label for="jury_promoteur">Promoteur·ice interne : <span class="asterisk">*</span></label>
|
||||||
<input type="text" id="jury_promoteur" name="jury_promoteur"
|
<input type="text" id="jury_promoteur" name="jury_promoteur"
|
||||||
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom">
|
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($showPromoteurUlb): ?>
|
<?php if ($showPromoteurUlb): ?>
|
||||||
@@ -86,11 +86,11 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Lecteur·ice(s) interne -->
|
<!-- Lecteur·ice(s) interne -->
|
||||||
<fieldset class="admin-jury-lecteurs">
|
<fieldset class="admin-jury-lecteurs">
|
||||||
<legend>Lecteur·ice(s) interne</legend>
|
<legend>Lecteur·ice(s) interne <span class="asterisk">*</span></legend>
|
||||||
<div id="jury-lecteurs-internes-list" class="admin-jury-list">
|
<div id="jury-lecteurs-internes-list" class="admin-jury-list">
|
||||||
<?php if (empty($lecteursInternes)): ?>
|
<?php if (empty($lecteursInternes)): ?>
|
||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_interne[]" placeholder="Nom"
|
<input type="text" name="jury_lecteur_interne[]" placeholder="Nom" required
|
||||||
aria-label="Lecteur·ice interne 1 — nom">
|
aria-label="Lecteur·ice interne 1 — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -100,6 +100,7 @@ if ($addMode && function_exists('old')) {
|
|||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_interne[]"
|
<input type="text" name="jury_lecteur_interne[]"
|
||||||
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
||||||
|
<?= $li === 0 ? 'required' : '' ?>
|
||||||
aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom">
|
aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -115,11 +116,11 @@ if ($addMode && function_exists('old')) {
|
|||||||
|
|
||||||
<!-- Lecteur·ice(s) externe -->
|
<!-- Lecteur·ice(s) externe -->
|
||||||
<fieldset class="admin-jury-lecteurs">
|
<fieldset class="admin-jury-lecteurs">
|
||||||
<legend>Lecteur·ice(s) externe</legend>
|
<legend>Lecteur·ice(s) externe <span class="asterisk">*</span></legend>
|
||||||
<div id="jury-lecteurs-externes-list" class="admin-jury-list">
|
<div id="jury-lecteurs-externes-list" class="admin-jury-list">
|
||||||
<?php if (empty($lecteursExternes)): ?>
|
<?php if (empty($lecteursExternes)): ?>
|
||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_externe[]" placeholder="Nom"
|
<input type="text" name="jury_lecteur_externe[]" placeholder="Nom" required
|
||||||
aria-label="Lecteur·ice externe 1 — nom">
|
aria-label="Lecteur·ice externe 1 — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
@@ -129,6 +130,7 @@ if ($addMode && function_exists('old')) {
|
|||||||
<div class="admin-jury-entry">
|
<div class="admin-jury-entry">
|
||||||
<input type="text" name="jury_lecteur_externe[]"
|
<input type="text" name="jury_lecteur_externe[]"
|
||||||
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
|
||||||
|
<?= $li === 0 ? 'required' : '' ?>
|
||||||
aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom">
|
aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom">
|
||||||
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
|
||||||
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
|
||||||
|
|||||||
Reference in New Issue
Block a user