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

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

View File

@@ -546,6 +546,7 @@
display: flex;
flex-direction: column;
gap: var(--space-2xs);
min-width: 0;
}
/* New-file queue items */
@@ -618,6 +619,8 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 0;
min-width: 100%;
}
.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]; }); }
// ── 1. TFE multi-file queue ────────────────────────────────────────────
var picker = document.getElementById('tfe-files-input');
var queue = document.getElementById('tfe-file-queue');
var empty = document.getElementById('tfe-file-queue-empty');
var picker = document.getElementById('tfe-files-input');
var queue = document.getElementById('tfe-file-queue');
var empty = document.getElementById('tfe-file-queue-empty');
var sortHint = document.getElementById('tfe-file-queue-sort-hint');
if (picker && queue) {
console.log('[file-upload-queue] init TFE queue picker=', picker, 'multiple=', picker.multiple);
var fileArray = [];
@@ -66,8 +67,9 @@ window.XamxamInitFileUploads = function () {
function renderQueue() {
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';
if (sortHint) sortHint.style.display = '';
fileArray.forEach(function (file, idx) {
var li = document.createElement('li');
li.className = 'fq-item';
@@ -76,8 +78,7 @@ window.XamxamInitFileUploads = function () {
'<span class="fq-drag-handle" title="R\u00e9ordonner">\u2820</span>' +
'<span class="fq-icon">' + iconFor(file) + '</span>' +
'<span class="fq-info"><span class="fq-name">' + esc(file.name) + '</span>' +
'<span class="fq-size">' + humanSize(file.size) + '</span>' +
'<input type="text" class="fq-label admin-file-label-input" placeholder="L\u00e9gende / description (optionnel)"></span>' +
'<span class="fq-size">' + humanSize(file.size) + '</span></span>' +
'<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);
queue.appendChild(li);

View File

@@ -5,15 +5,17 @@
* HTMX fragment: returns the combined Format(s) + Fichiers block.
* 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)
* 2. Note d'intention (PDF, 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):
* - Site web → URL + label fields
* - Vidéo → TODO: PeerTube upload (notice shown)
* - Audio → TODO: PeerTube upload (notice shown)
* Format-specific extra inputs (separate #format-extras-block so toggling
* formats does not destroy file queue state):
* - Site web → URL field only
* - Vidéo → PeerTube upload or direct file input
* - Audio → PeerTube upload or direct file input
* - (all others: Écriture, Performance, Objet éditorial, Installation, Autre)
* → no extra input needed beyond the standard TFE file upload
*
@@ -68,7 +70,10 @@ $websiteUrl = htmlspecialchars($_POST['website_url'] ?? '');
$websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
$hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragment';
$hasAnnexesChecked = !empty($_POST['has_annexes']);
?>
<!-- ═══════════════════ Format(s) + Fichiers (stable) ═══════════════════ -->
<div id="format-fichiers-block">
<input type="hidden" name="admin_mode" value="<?= $adminMode ? '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"
<?= !$adminMode ? 'required aria-required="true"' : '' ?>
hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block"
hx-target="#format-extras-block"
hx-select="#format-extras-block"
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">
<legend class="sr-only">Format(s) du TFE</legend>
<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"
aria-label="Fichiers sélectionnés (réordonnable)"></ul>
<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>
<!-- ── Annexes ── -->
<?php
$hasAnnexesChecked = !empty($_POST['has_annexes']);
?>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="has_annexes" value="1"
<?= $hasAnnexesChecked ? 'checked' : '' ?>
hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block"
hx-trigger="change"
hx-include="[name='formats[]'], [name='website_url'], [name='website_label'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']"
hx-swap="outerHTML">
Ce TFE comporte des annexes
</label>
<div id="annexes-input-block">
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="has_annexes" value="1"
<?= $hasAnnexesChecked ? 'checked' : '' ?>
hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#annexes-input-block"
hx-select="#annexes-input-block"
hx-trigger="change"
hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']"
hx-swap="outerHTML">
Ce TFE comporte des annexes
</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>
<?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 ── -->
<?php if ($hasSiteWeb): ?>
<!-- Site web -->
<fieldset class="fichiers-format-extra" id="fichiers-website">
<legend>Site web</legend>
<!-- ── 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): ?>
<div class="admin-form-group">
<label for="website_url">URL du site<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<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>
</div>
</div>
<div class="admin-form-group">
<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 endif; ?>
<?php if ($hasVideo): ?>
<fieldset class="fichiers-format-extra" id="fichiers-video">
<legend>Vidéo</legend>
<?php if ($hasVideo): ?>
<?php if ($peerTubeEnabled): ?>
<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">
<input type="file" id="peertube_video" name="peertube_video"
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>
<?php else: ?>
<div class="admin-form-group fichiers-todo-notice">
<p>
🚧 <strong>À venir :</strong> l'upload vidéo sera géré directement via l'API PeerTube.
La vidéo sera hébergée sur l'instance PeerTube de l'école et intégrée
comme lecteur embarqué sur la page du TFE.
</p>
<p class="fichiers-todo-workaround">
En attendant, déposez votre vidéo dans le champ TFE ci-dessus (ZIP si besoin).
</p>
<div class="admin-form-group">
<label for="tfe-video-upload">Fichier vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<div class="admin-file-input">
<input type="file" id="tfe-video-upload" name="files[]"
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
<?= !$adminMode ? 'required' : '' ?>>
<small>MP4, WebM ou MOV. Max 500 MB.</small>
</div>
</div>
<?php endif; ?>
</fieldset>
<?php endif; ?>
<?php endif; ?>
<?php if ($hasAudio): ?>
<fieldset class="fichiers-format-extra" id="fichiers-audio">
<legend>Audio</legend>
<?php if ($hasAudio): ?>
<?php if ($peerTubeEnabled): ?>
<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">
<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"
@@ -270,19 +258,18 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
</div>
</div>
<?php else: ?>
<div class="admin-form-group fichiers-todo-notice">
<p>
🚧 <strong>À venir :</strong> l'upload audio sera géré via l'API PeerTube.
Le fichier audio sera hébergé sur l'instance PeerTube de l'école et
intégré comme lecteur embarqué sur la page du TFE.
</p>
<p class="fichiers-todo-workaround">
En attendant, déposez votre fichier audio dans le champ TFE ci-dessus (ZIP si besoin).
</p>
<div class="admin-form-group">
<label for="tfe-audio-upload">Fichier audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?> :</label>
<div class="admin-file-input">
<input type="file" id="tfe-audio-upload" name="files[]"
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
<?= !$adminMode ? 'required' : '' ?>>
<small>MP3, OGG, WAV, FLAC ou AAC. Max 500 MB.</small>
</div>
</div>
<?php endif; ?>
</fieldset>
<?php endif; ?>
<?php endif; ?>
</div>
</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)
$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] ?? [];
unset($_SESSION['form_data_share_' . $slug]);
@@ -305,8 +305,6 @@ function renderShareLinkForm(string $slug, array $link): void
$n = old($formData, "jury_lecteur_externe:$i");
if ($n !== '') $lecteursExternes[] = ['name' => $n];
}
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true;
$promoteurUlbConditional = true;

View File

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