fix: resolve partage form submission issues

- Replace mb_strlen/mb_substr/mb_strtolower with strlen/substr/strtolower
  (mbstring extension missing on server, causing fatal error)
- Scope annexes checkbox HTMX swap to #annexes-input-block with hx-select
  (prevents duplicating entire page inside Fichiers fieldset)
- Split format+fichiers response: #format-fichiers-block (stable) and
  #format-extras-block (swappable, inside Fichiers fieldset). Format
  checkboxes use hx-select to extract only the extras, preserving file queue.
- Keep format extras inline in Fichiers fieldset (no sub-fieldsets). Remove
  website legend input (URL only).
- When PeerTube upload disabled, show direct file upload inputs for
  video/audio (name=files[]).
- Add "Glissez-déposez" sort hint below TFE file queue.
- Fix .fq-name overflow with width:0;min-width:100% chain.
- Remove legend placeholder from .fq-item.
- Merge "Récits et expérimentation" AP into "Narration Spéculative".
  Rename PACS to "Pratique de lart - outils critiques, arts et contexte
  simultanés".
- Remove président·e field from jury fieldset, form templates, and
  controller validation. Keep DB column and display logic for existing data.
This commit is contained in:
Pontoporeia
2026-05-09 16:58:39 +02:00
parent 59bbcf4642
commit cc0ae32df0
16 changed files with 119 additions and 152 deletions

View File

@@ -4,3 +4,11 @@
- [x] Keep specific layouts/classes in form.css (admin-form grid, checkbox-group layout, etc.) - [x] Keep specific layouts/classes in form.css (admin-form grid, checkbox-group layout, etc.)
- [x] Ensure selects, checkboxes, and radios are properly styled globally - [x] Ensure selects, checkboxes, and radios are properly styled globally
- [x] Converge towards the styled form appearance rather than unstyled - [x] Converge towards the styled form appearance rather than unstyled
- [x] Fix: replace mb_strlen/mb_substr/mb_strtolower with strlen/substr/strtolower (mbstring extension missing on server, caused fatal error on partage submit at ThesisCreateController line 511)
- [x] Fix: annexes checkbox in partage form clears other file inputs — scoped HTMX swap to #annexes-input-block instead of replacing entire #format-fichiers-block
- [x] Fix: website/video/audio inputs should be inline in Fichiers fieldset (not sub-fieldsets) — removed <fieldset class="fichiers-format-extra"> wrappers
- [x] Fix: video/audio show direct upload input when PeerTube disabled — parallel inputs: PeerTube upload when enabled, direct `files[]` upload when disabled
- [x] Fix: format checkboxes HTMX include missing has_annexes — added it so annexes state preserved across format changes
- [x] Fix: format checkbox toggle clears file inputs — split into two blocks: #format-fichiers-block (stable: TFE/annexes/couverture/note) and #format-extras-block (swappable: website/video/audio extras)
- [x] Fix: remove website label/legend input — website section now shows only URL field
- [x] Fix: format-extras not appearing — moved #format-extras-block inside Fichiers fieldset (after annexes), uses hx-select to extract from response

View File

@@ -40,7 +40,7 @@ try {
$keywords = array_map('trim', explode(',', $raw)); $keywords = array_map('trim', explode(',', $raw));
foreach ($keywords as $kw) { foreach ($keywords as $kw) {
$kw = trim($kw); $kw = trim($kw);
if ($kw === '' || mb_strlen($kw) > 100) continue; if ($kw === '' || strlen($kw) > 100) continue;
// Create tag if needed // Create tag if needed
$insertTag->execute([$kw]); $insertTag->execute([$kw]);

View File

@@ -165,18 +165,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
return $r ? (int)$r['id'] : null; return $r ? (int)$r['id'] : null;
}; };
// AP alias map: variant spellings → canonical DB name. // AP alias map: variant spellings → canonical code.
$apAliases = [ $apAliases = [
'l.i.e.n.s.' => 'Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'l.i.e.n.s.' => 'LIENS',
'liens' => 'Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'liens' => 'LIENS',
'lieux, interdisciplinarités, écologie, nécessité, systèmes' 'lieux, interdisciplinarités, écologie, nécessité, systèmes'
=> 'Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', => 'LIENS',
'récits et expérimentation' => 'Récits et expérimentation', 'récits et expérimentation' => 'NS',
'recits et experimentation' => 'Récits et expérimentation', 'recits et experimentation' => 'NS',
'atelier pratiques situées' => 'Atelier Pratiques Situées', 'atelier pratiques situées' => 'APS',
'design et politique du multiple' => 'Design et Politique du Multiple', 'design et politique du multiple' => 'DPM',
'narration spéculative' => 'Narration Spéculative', 'narration spéculative' => 'NS',
'pacs' => 'PACS', 'pacs' => 'PACS',
'pratique de l''art' => 'PACS',
]; ];
// Resolve an AP string (code or full name) → ap_program id. // Resolve an AP string (code or full name) → ap_program id.
@@ -184,14 +185,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$raw = trim($raw); $raw = trim($raw);
if ($raw === '') return null; if ($raw === '') return null;
// 1. Try alias map (lowercase key) // 1. Try alias map (lowercase key) → canonical code
$key = strtolower($raw); $key = strtolower($raw);
if (isset($apAliases[$key])) { if (isset($apAliases[$key])) {
$raw = $apAliases[$key]; $raw = $apAliases[$key];
} }
// 2. Exact name match // 2. Match by code
$s = $importPdo->prepare("SELECT id FROM ap_programs WHERE name = ?"); $s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$s->execute([$raw]); $s->execute([$raw]);
$r = $s->fetch(); $r = $s->fetch();
if ($r) return (int)$r['id']; if ($r) return (int)$r['id'];
@@ -257,7 +258,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($title === '') $missing[] = 'titre'; if ($title === '') $missing[] = 'titre';
if ($year === 0) $missing[] = 'année'; if ($year === 0) $missing[] = 'année';
throw new Exception("Champ(s) requis manquant(s) : " . implode(', ', $missing) throw new Exception("Champ(s) requis manquant(s) : " . implode(', ', $missing)
. " (id=\"" . ($identifier ?: '?') . "\", titre=\"" . mb_substr($title, 0, 80) . "\")"); . " (id=\"" . ($identifier ?: '?') . "\", titre=\"" . substr($title, 0, 80) . "\")");
} }
$orientationId = $resolveOrientation($orientationCode); $orientationId = $resolveOrientation($orientationCode);

View File

@@ -546,6 +546,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-2xs); gap: var(--space-2xs);
min-width: 0;
} }
/* New-file queue items */ /* New-file queue items */
@@ -618,6 +619,8 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 0;
min-width: 100%;
} }
.fq-size { .fq-size {

View File

@@ -38,9 +38,10 @@ window.XamxamInitFileUploads = function () {
function esc(s) { return s.replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); } function esc(s) { return s.replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
// ── 1. TFE multi-file queue ──────────────────────────────────────────── // ── 1. TFE multi-file queue ────────────────────────────────────────────
var picker = document.getElementById('tfe-files-input'); var picker = document.getElementById('tfe-files-input');
var queue = document.getElementById('tfe-file-queue'); var queue = document.getElementById('tfe-file-queue');
var empty = document.getElementById('tfe-file-queue-empty'); var empty = document.getElementById('tfe-file-queue-empty');
var sortHint = document.getElementById('tfe-file-queue-sort-hint');
if (picker && queue) { if (picker && queue) {
console.log('[file-upload-queue] init TFE queue picker=', picker, 'multiple=', picker.multiple); console.log('[file-upload-queue] init TFE queue picker=', picker, 'multiple=', picker.multiple);
var fileArray = []; var fileArray = [];
@@ -66,8 +67,9 @@ window.XamxamInitFileUploads = function () {
function renderQueue() { function renderQueue() {
queue.innerHTML = ''; queue.innerHTML = '';
if (!fileArray.length) { empty.style.display = ''; injectHiddenFields([]); return; } if (!fileArray.length) { empty.style.display = ''; if (sortHint) sortHint.style.display = 'none'; injectHiddenFields([]); return; }
empty.style.display = 'none'; empty.style.display = 'none';
if (sortHint) sortHint.style.display = '';
fileArray.forEach(function (file, idx) { fileArray.forEach(function (file, idx) {
var li = document.createElement('li'); var li = document.createElement('li');
li.className = 'fq-item'; li.className = 'fq-item';
@@ -76,8 +78,7 @@ window.XamxamInitFileUploads = function () {
'<span class="fq-drag-handle" title="R\u00e9ordonner">\u2820</span>' + '<span class="fq-drag-handle" title="R\u00e9ordonner">\u2820</span>' +
'<span class="fq-icon">' + iconFor(file) + '</span>' + '<span class="fq-icon">' + iconFor(file) + '</span>' +
'<span class="fq-info"><span class="fq-name">' + esc(file.name) + '</span>' + '<span class="fq-info"><span class="fq-name">' + esc(file.name) + '</span>' +
'<span class="fq-size">' + humanSize(file.size) + '</span>' + '<span class="fq-size">' + humanSize(file.size) + '</span></span>' +
'<input type="text" class="fq-label admin-file-label-input" placeholder="L\u00e9gende / description (optionnel)"></span>' +
'<button type="button" class="admin-btn-remove fq-remove" aria-label="Retirer ' + esc(file.name) + '">&#x2715;</button>'; '<button type="button" class="admin-btn-remove fq-remove" aria-label="Retirer ' + esc(file.name) + '">&#x2715;</button>';
li.querySelector('.fq-remove').onclick = (function (i) { return function () { fileArray.splice(i, 1); renderQueue(); }; })(idx); li.querySelector('.fq-remove').onclick = (function (i) { return function () { fileArray.splice(i, 1); renderQueue(); }; })(idx);
queue.appendChild(li); queue.appendChild(li);

View File

@@ -5,15 +5,17 @@
* HTMX fragment: returns the combined Format(s) + Fichiers block. * HTMX fragment: returns the combined Format(s) + Fichiers block.
* Called on every format checkbox change so the Fichiers fieldset adapts. * Called on every format checkbox change so the Fichiers fieldset adapts.
* *
* Fixed inputs (always present): * Fixed inputs (always present in #format-fichiers-block):
* 1. Image de couverture (optional) * 1. Image de couverture (optional)
* 2. Note d'intention (PDF, required unless adminMode) * 2. Note d'intention (PDF, required unless adminMode)
* 3. TFE — multi-file upload (required unless adminMode) * 3. TFE — multi-file upload (required unless adminMode)
* 4. Annexes checkbox + file input
* *
* Format-specific extra inputs (appended after the fixed three): * Format-specific extra inputs (separate #format-extras-block so toggling
* - Site web → URL + label fields * formats does not destroy file queue state):
* - Vidéo → TODO: PeerTube upload (notice shown) * - Site web → URL field only
* - Audio → TODO: PeerTube upload (notice shown) * - Vidéo → PeerTube upload or direct file input
* - Audio → PeerTube upload or direct file input
* - (all others: Écriture, Performance, Objet éditorial, Installation, Autre) * - (all others: Écriture, Performance, Objet éditorial, Installation, Autre)
* → no extra input needed beyond the standard TFE file upload * → no extra input needed beyond the standard TFE file upload
* *
@@ -68,7 +70,10 @@ $websiteUrl = htmlspecialchars($_POST['website_url'] ?? '');
$websiteLabel = htmlspecialchars($_POST['website_label'] ?? ''); $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
$hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragment'; $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragment';
$hasAnnexesChecked = !empty($_POST['has_annexes']);
?> ?>
<!-- ═══════════════════ Format(s) + Fichiers (stable) ═══════════════════ -->
<div id="format-fichiers-block"> <div id="format-fichiers-block">
<input type="hidden" name="admin_mode" value="<?= $adminMode ? '1' : '0' ?>"> <input type="hidden" name="admin_mode" value="<?= $adminMode ? '1' : '0' ?>">
<input type="hidden" name="edit_mode" value="<?= $editMode ? '1' : '0' ?>"> <input type="hidden" name="edit_mode" value="<?= $editMode ? '1' : '0' ?>">
@@ -84,9 +89,10 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
<fieldset class="admin-checkbox-group" <fieldset class="admin-checkbox-group"
<?= !$adminMode ? 'required aria-required="true"' : '' ?> <?= !$adminMode ? 'required aria-required="true"' : '' ?>
hx-post="<?= htmlspecialchars($hxPost) ?>" hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block" hx-target="#format-extras-block"
hx-select="#format-extras-block"
hx-trigger="change" hx-trigger="change"
hx-include="this, [name='website_url'], [name='website_label'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" hx-include="this, [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']"
hx-swap="outerHTML"> hx-swap="outerHTML">
<legend class="sr-only">Format(s) du TFE</legend> <legend class="sr-only">Format(s) du TFE</legend>
<ul> <ul>
@@ -165,46 +171,43 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" <ul id="tfe-file-queue" class="tfe-file-queue sortable-list"
aria-label="Fichiers sélectionnés (réordonnable)"></ul> aria-label="Fichiers sélectionnés (réordonnable)"></ul>
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun fichier sélectionné.</p> <p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun fichier sélectionné.</p>
<small class="admin-file-hint" id="tfe-file-queue-sort-hint" style="display:none;">Glissez-déposez les fichiers pour déterminer l'ordre d'affichage sur la page du TFE.</small>
</div> </div>
</div> </div>
<!-- ── Annexes ── --> <!-- ── Annexes ── -->
<?php <div id="annexes-input-block">
$hasAnnexesChecked = !empty($_POST['has_annexes']); <div class="admin-form-group">
?> <label class="admin-checkbox-label">
<div class="admin-form-group"> <input type="checkbox" name="has_annexes" value="1"
<label class="admin-checkbox-label"> <?= $hasAnnexesChecked ? 'checked' : '' ?>
<input type="checkbox" name="has_annexes" value="1" hx-post="<?= htmlspecialchars($hxPost) ?>"
<?= $hasAnnexesChecked ? 'checked' : '' ?> hx-target="#annexes-input-block"
hx-post="<?= htmlspecialchars($hxPost) ?>" hx-select="#annexes-input-block"
hx-target="#format-fichiers-block" hx-trigger="change"
hx-trigger="change" hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']"
hx-include="[name='formats[]'], [name='website_url'], [name='website_label'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']" hx-swap="outerHTML">
hx-swap="outerHTML"> Ce TFE comporte des annexes
Ce TFE comporte des annexes </label>
</label> </div>
<?php if ($hasAnnexesChecked): ?>
<div>
<?php
$name = 'annexes';
$label = 'Annexes :';
$accept = '.pdf,.zip,.tar,.gz';
$hint = 'PDF ou archives ZIP/TAR. Max 500 MB.';
$required = false;
$multiple = true;
include APP_ROOT . '/templates/partials/form/file-field.php';
?>
</div>
<?php endif; ?>
</div> </div>
<?php if ($hasAnnexesChecked): ?>
<div>
<?php
$name = 'annexes';
$label = 'Annexes :';
$accept = '.pdf,.zip,.tar,.gz';
$hint = 'PDF ou archives ZIP/TAR. Max 500 MB.';
$required = false;
$multiple = true;
include APP_ROOT . '/templates/partials/form/file-field.php';
?>
</div>
<?php endif; ?>
<!-- ── Format-specific extras ── --> <!-- ── Format-specific extras (swappable, inside Fichiers fieldset) ── -->
<div id="format-extras-block" style="display:flex;flex-direction:column;gap:var(--space-s);">
<?php if ($hasSiteWeb): ?> <?php if ($hasSiteWeb): ?>
<!-- Site web -->
<fieldset class="fichiers-format-extra" id="fichiers-website">
<legend>Site web</legend>
<div class="admin-form-group"> <div class="admin-form-group">
<label for="website_url">URL du site<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label> <label for="website_url">URL du site<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<div class="admin-file-input"> <div class="admin-file-input">
@@ -215,23 +218,12 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
<small>Le TFE sera affiché comme un site embarqué sur sa page publique.</small> <small>Le TFE sera affiché comme un site embarqué sur sa page publique.</small>
</div> </div>
</div> </div>
<div class="admin-form-group"> <?php endif; ?>
<label for="website_label">Légende :</label>
<input type="text" id="website_label" name="website_label"
value="<?= $websiteLabel ?>"
placeholder="Description du site (optionnel)"
class="admin-file-label-input"
style="max-width:400px;">
</div>
</fieldset>
<?php endif; ?>
<?php if ($hasVideo): ?> <?php if ($hasVideo): ?>
<fieldset class="fichiers-format-extra" id="fichiers-video">
<legend>Vidéo</legend>
<?php if ($peerTubeEnabled): ?> <?php if ($peerTubeEnabled): ?>
<div class="admin-form-group"> <div class="admin-form-group">
<label for="peertube_video">Fichier vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label> <label for="peertube_video">Fichier vidéo (PeerTube)<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<div class="admin-file-input"> <div class="admin-file-input">
<input type="file" id="peertube_video" name="peertube_video" <input type="file" id="peertube_video" name="peertube_video"
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov" accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
@@ -241,26 +233,22 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
</div> </div>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="admin-form-group fichiers-todo-notice"> <div class="admin-form-group">
<p> <label for="tfe-video-upload">Fichier vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
🚧 <strong>À venir :</strong> l'upload vidéo sera géré directement via l'API PeerTube. <div class="admin-file-input">
La vidéo sera hébergée sur l'instance PeerTube de l'école et intégrée <input type="file" id="tfe-video-upload" name="files[]"
comme lecteur embarqué sur la page du TFE. accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
</p> <?= !$adminMode ? 'required' : '' ?>>
<p class="fichiers-todo-workaround"> <small>MP4, WebM ou MOV. Max 500 MB.</small>
En attendant, déposez votre vidéo dans le champ TFE ci-dessus (ZIP si besoin). </div>
</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</fieldset> <?php endif; ?>
<?php endif; ?>
<?php if ($hasAudio): ?> <?php if ($hasAudio): ?>
<fieldset class="fichiers-format-extra" id="fichiers-audio">
<legend>Audio</legend>
<?php if ($peerTubeEnabled): ?> <?php if ($peerTubeEnabled): ?>
<div class="admin-form-group"> <div class="admin-form-group">
<label for="peertube_audio">Fichier audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label> <label for="peertube_audio">Fichier audio (PeerTube)<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<div class="admin-file-input"> <div class="admin-file-input">
<input type="file" id="peertube_audio" name="peertube_audio" <input type="file" id="peertube_audio" name="peertube_audio"
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a" accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
@@ -270,19 +258,18 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
</div> </div>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="admin-form-group fichiers-todo-notice"> <div class="admin-form-group">
<p> <label for="tfe-audio-upload">Fichier audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
🚧 <strong>À venir :</strong> l'upload audio sera géré via l'API PeerTube. <div class="admin-file-input">
Le fichier audio sera hébergé sur l'instance PeerTube de l'école et <input type="file" id="tfe-audio-upload" name="files[]"
intégré comme lecteur embarqué sur la page du TFE. accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
</p> <?= !$adminMode ? 'required' : '' ?>>
<p class="fichiers-todo-workaround"> <small>MP3, OGG, WAV, FLAC ou AAC. Max 500 MB.</small>
En attendant, déposez votre fichier audio dans le champ TFE ci-dessus (ZIP si besoin). </div>
</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</fieldset> <?php endif; ?>
<?php endif; ?> </div>
</fieldset><!-- /Fichiers --> </fieldset><!-- /Fichiers -->

View File

@@ -241,7 +241,7 @@ function renderShareLinkForm(string $slug, array $link): void
} }
// Filter out PACS from AP programs for student forms (spec: admin-only AP) // Filter out PACS from AP programs for student forms (spec: admin-only AP)
$apPrograms = array_values(array_filter($apPrograms, fn($ap) => ($ap['name'] ?? '') !== 'PACS')); $apPrograms = array_values(array_filter($apPrograms, fn($ap) => ($ap['code'] ?? '') !== '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]);
@@ -305,8 +305,6 @@ function renderShareLinkForm(string $slug, array $link): void
$n = old($formData, "jury_lecteur_externe:$i"); $n = old($formData, "jury_lecteur_externe:$i");
if ($n !== '') $lecteursExternes[] = ['name' => $n]; if ($n !== '') $lecteursExternes[] = ['name' => $n];
} }
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true; $showPromoteurUlb = true;
$promoteurUlbConditional = true; $promoteurUlbConditional = true;

View File

@@ -57,8 +57,8 @@ if ($thesisId <= 0 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
} }
// Truncate justification to a safe length // Truncate justification to a safe length
if (mb_strlen($justification) > 2000) { if (strlen($justification) > 2000) {
$justification = mb_substr($justification, 0, 2000); $justification = substr($justification, 0, 2000);
} }
$db = Database::getInstance(); $db = Database::getInstance();

View File

@@ -427,10 +427,6 @@ class ThesisCreateController
} }
} }
} }
if (!empty(trim($post['jury_president'] ?? ''))) {
$juryMembers[] = ['name' => trim($post['jury_president']), 'role' => 'president', 'is_external' => 0];
}
if (!$adminMode && !$hasPromoteur) { if (!$adminMode && !$hasPromoteur) {
throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.'); throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.');
} }
@@ -508,8 +504,8 @@ class ThesisCreateController
// Note contextuelle (optional, max 1500 chars) // Note contextuelle (optional, max 1500 chars)
$contextNote = $this->sanitiseString($post['context_note'] ?? ''); $contextNote = $this->sanitiseString($post['context_note'] ?? '');
if (mb_strlen($contextNote) > 1500) { if (strlen($contextNote) > 1500) {
$contextNote = mb_substr($contextNote, 0, 1500); $contextNote = substr($contextNote, 0, 1500);
} }
// Backoffice fields (admin only) // Backoffice fields (admin only)

View File

@@ -650,15 +650,6 @@ class ThesisEditController
} }
} }
// President (optional, admin-only)
if (!empty(trim($post['jury_president'] ?? ''))) {
$members[] = [
'name' => trim($post['jury_president']),
'role' => 'president',
'is_external' => 0,
];
}
// Backwards compat: old jury_lecteurs[] // Backwards compat: old jury_lecteurs[]
if (isset($post['jury_lecteurs'])) { if (isset($post['jury_lecteurs'])) {
foreach ($post['jury_lecteurs'] as $i => $name) { foreach ($post['jury_lecteurs'] as $i => $name) {

View File

@@ -1016,7 +1016,7 @@ class Database
} }
$normalise = static function (string $s): string { $normalise = static function (string $s): string {
return preg_replace('/[^a-z0-9]/u', '', mb_strtolower($s)); return preg_replace('/[^a-z0-9]/u', '', strtolower($s));
}; };
$normNew = $normalise($title); $normNew = $normalise($title);
@@ -1036,11 +1036,11 @@ class Database
} }
// Prefix match: one starts with the other (handles subtitle variations). // Prefix match: one starts with the other (handles subtitle variations).
$maxLen = max(mb_strlen($normNew), mb_strlen($normExisting)); $maxLen = max(strlen($normNew), strlen($normExisting));
if ($maxLen === 0) { if ($maxLen === 0) {
continue; continue;
} }
$minLen = min(mb_strlen($normNew), mb_strlen($normExisting)); $minLen = min(strlen($normNew), strlen($normExisting));
if ($minLen >= 5) { // avoid matching very short fragments if ($minLen >= 5) { // avoid matching very short fragments
if (str_starts_with($normExisting, $normNew) || str_starts_with($normNew, $normExisting)) { if (str_starts_with($normExisting, $normNew) || str_starts_with($normNew, $normExisting)) {
return [ return [
@@ -1055,8 +1055,8 @@ class Database
// Levenshtein distance ≤ 10 % of the longer string. // Levenshtein distance ≤ 10 % of the longer string.
// levenshtein() is limited to 255 chars; use substrings for safety. // levenshtein() is limited to 255 chars; use substrings for safety.
$a = mb_substr($normNew, 0, 255); $a = substr($normNew, 0, 255);
$b = mb_substr($normExisting, 0, 255); $b = substr($normExisting, 0, 255);
$dist = levenshtein($a, $b); $dist = levenshtein($a, $b);
$threshold = (int)ceil($maxLen * 0.10); $threshold = (int)ceil($maxLen * 0.10);
if ($dist <= $threshold) { if ($dist <= $threshold) {

View File

@@ -66,8 +66,7 @@ INSERT OR IGNORE INTO ap_programs (name, code) VALUES
('Design et Politique du Multiple', 'DPM'), ('Design et Politique du Multiple', 'DPM'),
('Atelier Pratiques Situées', 'APS'), ('Atelier Pratiques Situées', 'APS'),
('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS'), ('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS'),
('Récits et expérimentation', 'RE'), ('Pratique de l''art - outils critiques, arts et contexte simultanés', 'PACS');
('PACS', 'PACS');
-- Master finality types -- Master finality types
CREATE TABLE IF NOT EXISTS finality_types ( CREATE TABLE IF NOT EXISTS finality_types (

View File

@@ -16,8 +16,6 @@
$juryPromoteursUlb = []; $juryPromoteursUlb = [];
$lecteursInternes = []; $lecteursInternes = [];
$lecteursExternes = []; $lecteursExternes = [];
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true; $showPromoteurUlb = true;
$promoteurUlbConditional = false; $promoteurUlbConditional = false;

View File

@@ -44,11 +44,11 @@
$juryPromoteursUlb = []; $juryPromoteursUlb = [];
$lecteursInternes = []; $lecteursInternes = [];
$lecteursExternes = []; $lecteursExternes = [];
$juryPresident = null;
foreach ($jury as $jm) { foreach ($jury as $jm) {
if ($jm['role'] === 'president') { if ($jm['role'] === 'president') {
$juryPresident = $jm['name']; continue;
} elseif ($jm['role'] === 'promoteur') { }
if ($jm['role'] === 'promoteur') {
if (($jm['is_ulb'] ?? 0) == 1) { if (($jm['is_ulb'] ?? 0) == 1) {
$juryPromoteursUlb[] = $jm; $juryPromoteursUlb[] = $jm;
} else { } else {
@@ -69,7 +69,6 @@
if (!empty($juryPromoteursUlb) && $juryPromoteurUlb === null) { if (!empty($juryPromoteursUlb) && $juryPromoteurUlb === null) {
$juryPromoteurUlb = $juryPromoteursUlb[0]['name']; $juryPromoteurUlb = $juryPromoteursUlb[0]['name'];
} }
$showPresident = true;
$showPromoteurUlb = true; $showPromoteurUlb = true;
$promoteurUlbConditional = false; $promoteurUlbConditional = false;

View File

@@ -16,10 +16,10 @@
* array $orientations, $apPrograms, $finalityTypes, $languages, $formatTypes, $licenseTypes * array $orientations, $apPrograms, $finalityTypes, $languages, $formatTypes, $licenseTypes
* *
* Jury data: * Jury data:
* ?string $juryPromoteur, $juryPromoteurUlb, $juryPresident * ?string $juryPromoteur, $juryPromoteurUlb
* array $juryPromoteurs, $juryPromoteursUlb * array $juryPromoteurs, $juryPromoteursUlb
* array $lecteursInternes, $lecteursExternes * array $lecteursInternes, $lecteursExternes
* bool $showPresident, $showPromoteurUlb, $promoteurUlbConditional * bool $showPromoteurUlb, $promoteurUlbConditional
* *
* Licence / access: * Licence / access:
* bool $libreEnabled, $interneEnabled, $interditEnabled * bool $libreEnabled, $interneEnabled, $interditEnabled
@@ -70,7 +70,6 @@ $juryPromoteursUlb = $juryPromoteursUlb ?? [];
$lecteursInternes = $lecteursInternes ?? []; $lecteursInternes = $lecteursInternes ?? [];
$lecteursExternes = $lecteursExternes ?? []; $lecteursExternes = $lecteursExternes ?? [];
$juryPresident = $juryPresident ?? null; $juryPresident = $juryPresident ?? null;
$showPresident = $showPresident ?? false;
$showPromoteurUlb = $showPromoteurUlb ?? true; $showPromoteurUlb = $showPromoteurUlb ?? true;
$promoteurUlbConditional = $promoteurUlbConditional ?? false; $promoteurUlbConditional = $promoteurUlbConditional ?? false;

View File

@@ -9,8 +9,7 @@
* $juryPromoteursUlb array [{name: string}] Multiple promoteurs ULB * $juryPromoteursUlb array [{name: string}] Multiple promoteurs ULB
* $lecteursInternes array [{name: string}] * $lecteursInternes array [{name: string}]
* $lecteursExternes array [{name: string}] * $lecteursExternes array [{name: string}]
* $juryPresident string|null President name (edit-only, optional) * $juryPresident string|null (Deprecated — no longer displayed)
* $showPresident bool Show president field (default: false)
* $showPromoteurUlb bool Show ULB promoteur field (default: true) * $showPromoteurUlb bool Show ULB promoteur field (default: true)
* $promoteurUlbConditional bool If true, field is hidden unless finality=Approfondi * $promoteurUlbConditional bool If true, field is hidden unless finality=Approfondi
* *
@@ -23,14 +22,12 @@ $juryPromoteurUlb = $juryPromoteurUlb ?? null;
$juryPromoteursUlb = $juryPromoteursUlb ?? []; $juryPromoteursUlb = $juryPromoteursUlb ?? [];
$lecteursInternes = $lecteursInternes ?? []; $lecteursInternes = $lecteursInternes ?? [];
$lecteursExternes = $lecteursExternes ?? []; $lecteursExternes = $lecteursExternes ?? [];
$juryPresident = $juryPresident ?? null;
$showPresident = $showPresident ?? false;
$showPromoteurUlb = $showPromoteurUlb ?? true; $showPromoteurUlb = $showPromoteurUlb ?? true;
$promoteurUlbConditional = $promoteurUlbConditional ?? false; $promoteurUlbConditional = $promoteurUlbConditional ?? false;
$adminMode = $adminMode ?? false; $adminMode = $adminMode ?? false;
// Add-mode repopulation from flash data // Add-mode repopulation from flash data
$addMode = ($juryPromoteur === null && empty($juryPromoteurs) && $juryPromoteurUlb === null && empty($juryPromoteursUlb) && empty($lecteursInternes) && empty($lecteursExternes) && $juryPresident === null); $addMode = ($juryPromoteur === null && empty($juryPromoteurs) && $juryPromoteurUlb === null && empty($juryPromoteursUlb) && empty($lecteursInternes) && empty($lecteursExternes));
if ($addMode && function_exists('old')) { if ($addMode && function_exists('old')) {
// jury_promoteur may be array (new form) or scalar (legacy) // jury_promoteur may be array (new form) or scalar (legacy)
$promoteursOld = old('jury_promoteur'); $promoteursOld = old('jury_promoteur');
@@ -52,7 +49,6 @@ if ($addMode && function_exists('old')) {
} elseif (is_string($promoteursUlbOld) && trim($promoteursUlbOld) !== '') { } elseif (is_string($promoteursUlbOld) && trim($promoteursUlbOld) !== '') {
$juryPromoteurUlb = $promoteursUlbOld; $juryPromoteurUlb = $promoteursUlbOld;
} }
$juryPresident = old('jury_president') ?: null;
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
$n = old("jury_lecteur_interne:$i"); $n = old("jury_lecteur_interne:$i");
if ($n !== '') $lecteursInternes[] = ['name' => $n]; if ($n !== '') $lecteursInternes[] = ['name' => $n];
@@ -238,15 +234,6 @@ if ($addMode && function_exists('old')) {
</button> </button>
</fieldset> </fieldset>
<?php if ($showPresident): ?>
<!-- Président·e (admin edit only) -->
<div>
<label for="jury_president">Président·e :</label>
<input type="text" id="jury_president" name="jury_president"
value="<?= htmlspecialchars($juryPresident ?? '') ?>"
placeholder="Nom du/de la président·e (interne)">
</div>
<?php endif; ?>
</fieldset> </fieldset>
<script> <script>