diff --git a/TODO.md b/TODO.md index 5f5cbd9..5a7b488 100644 --- a/TODO.md +++ b/TODO.md @@ -4,3 +4,11 @@ - [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] 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
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 diff --git a/app/migrations/applied/013_fix_remarks_keywords.php b/app/migrations/applied/013_fix_remarks_keywords.php index 64a8522..fc17bf6 100644 --- a/app/migrations/applied/013_fix_remarks_keywords.php +++ b/app/migrations/applied/013_fix_remarks_keywords.php @@ -40,7 +40,7 @@ try { $keywords = array_map('trim', explode(',', $raw)); foreach ($keywords as $kw) { $kw = trim($kw); - if ($kw === '' || mb_strlen($kw) > 100) continue; + if ($kw === '' || strlen($kw) > 100) continue; // Create tag if needed $insertTag->execute([$kw]); diff --git a/app/public/admin/index.php b/app/public/admin/index.php index 932d934..074d97f 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -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); diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 4a19e3c..7ca25a2 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -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 { diff --git a/app/public/assets/js/file-upload-queue.js b/app/public/assets/js/file-upload-queue.js index 1d67293..044fb60 100644 --- a/app/public/assets/js/file-upload-queue.js +++ b/app/public/assets/js/file-upload-queue.js @@ -38,9 +38,10 @@ window.XamxamInitFileUploads = function () { function esc(s) { return s.replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[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 () { '\u2820' + '' + iconFor(file) + '' + '' + esc(file.name) + '' + - '' + humanSize(file.size) + '' + - '' + + '' + humanSize(file.size) + '' + ''; li.querySelector('.fq-remove').onclick = (function (i) { return function () { fileArray.splice(i, 1); renderQueue(); }; })(idx); queue.appendChild(li); diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php index cf9169d..e0cb463 100644 --- a/app/public/partage/fichiers-fragment.php +++ b/app/public/partage/fichiers-fragment.php @@ -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']); ?> +
@@ -84,9 +89,10 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
hx-post="" - 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"> Format(s) du TFE
    @@ -165,46 +171,43 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm

      Aucun fichier sélectionné.

      +
    - - -
    - +
    +
    + +
    + +
    + +
    +
    - -
    - -
    - - - - - -
    - Site web + +
    +
    @@ -215,23 +218,12 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm Le TFE sera affiché comme un site embarqué sur sa page publique.
    -
    - - -
    -
    - + - -
    - Vidéo +
    - +
    -
    -

    - 🚧 À venir : 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. -

    -

    - En attendant, déposez votre vidéo dans le champ TFE ci-dessus (ZIP si besoin). -

    +
    + +
    + > + MP4, WebM ou MOV. Max 500 MB. +
    -
    - + - -
    - Audio +
    - +
    -
    -

    - 🚧 À venir : 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. -

    -

    - En attendant, déposez votre fichier audio dans le champ TFE ci-dessus (ZIP si besoin). -

    +
    + +
    + > + MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. +
    -
    - + +
    diff --git a/app/public/partage/index.php b/app/public/partage/index.php index e89f2a6..657ad4d 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -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; diff --git a/app/public/request-access.php b/app/public/request-access.php index 6db2f8c..b29a683 100644 --- a/app/public/request-access.php +++ b/app/public/request-access.php @@ -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(); diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index ec4698a..331dc77 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -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) { throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.'); } @@ -508,8 +504,8 @@ class ThesisCreateController // Note contextuelle (optional, max 1500 chars) $contextNote = $this->sanitiseString($post['context_note'] ?? ''); - if (mb_strlen($contextNote) > 1500) { - $contextNote = mb_substr($contextNote, 0, 1500); + if (strlen($contextNote) > 1500) { + $contextNote = substr($contextNote, 0, 1500); } // Backoffice fields (admin only) diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 0577661..61dc769 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -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[] if (isset($post['jury_lecteurs'])) { foreach ($post['jury_lecteurs'] as $i => $name) { diff --git a/app/src/Database.php b/app/src/Database.php index 79734ae..642d72f 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -1016,7 +1016,7 @@ class Database } $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); @@ -1036,11 +1036,11 @@ class Database } // 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) { continue; } - $minLen = min(mb_strlen($normNew), mb_strlen($normExisting)); + $minLen = min(strlen($normNew), strlen($normExisting)); if ($minLen >= 5) { // avoid matching very short fragments if (str_starts_with($normExisting, $normNew) || str_starts_with($normNew, $normExisting)) { return [ @@ -1055,8 +1055,8 @@ class Database // Levenshtein distance ≤ 10 % of the longer string. // levenshtein() is limited to 255 chars; use substrings for safety. - $a = mb_substr($normNew, 0, 255); - $b = mb_substr($normExisting, 0, 255); + $a = substr($normNew, 0, 255); + $b = substr($normExisting, 0, 255); $dist = levenshtein($a, $b); $threshold = (int)ceil($maxLen * 0.10); if ($dist <= $threshold) { diff --git a/app/storage/schema.sql b/app/storage/schema.sql index b418758..3c06ab4 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -66,8 +66,7 @@ INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Design et Politique du Multiple', 'DPM'), ('Atelier Pratiques Situées', 'APS'), ('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS'), - ('Récits et expérimentation', 'RE'), - ('PACS', 'PACS'); + ('Pratique de l''art - outils critiques, arts et contexte simultanés', 'PACS'); -- Master finality types CREATE TABLE IF NOT EXISTS finality_types ( diff --git a/app/templates/admin/add.php b/app/templates/admin/add.php index 63d611b..53faa1d 100644 --- a/app/templates/admin/add.php +++ b/app/templates/admin/add.php @@ -16,8 +16,6 @@ $juryPromoteursUlb = []; $lecteursInternes = []; $lecteursExternes = []; - $juryPresident = null; - $showPresident = false; $showPromoteurUlb = true; $promoteurUlbConditional = false; diff --git a/app/templates/admin/edit.php b/app/templates/admin/edit.php index f217640..d9fc8e9 100644 --- a/app/templates/admin/edit.php +++ b/app/templates/admin/edit.php @@ -44,11 +44,11 @@ $juryPromoteursUlb = []; $lecteursInternes = []; $lecteursExternes = []; - $juryPresident = null; foreach ($jury as $jm) { if ($jm['role'] === 'president') { - $juryPresident = $jm['name']; - } elseif ($jm['role'] === 'promoteur') { + continue; + } + if ($jm['role'] === 'promoteur') { if (($jm['is_ulb'] ?? 0) == 1) { $juryPromoteursUlb[] = $jm; } else { @@ -69,7 +69,6 @@ if (!empty($juryPromoteursUlb) && $juryPromoteurUlb === null) { $juryPromoteurUlb = $juryPromoteursUlb[0]['name']; } - $showPresident = true; $showPromoteurUlb = true; $promoteurUlbConditional = false; diff --git a/app/templates/partials/form/form.php b/app/templates/partials/form/form.php index 3aea8c8..a21d0a8 100644 --- a/app/templates/partials/form/form.php +++ b/app/templates/partials/form/form.php @@ -16,10 +16,10 @@ * array $orientations, $apPrograms, $finalityTypes, $languages, $formatTypes, $licenseTypes * * Jury data: - * ?string $juryPromoteur, $juryPromoteurUlb, $juryPresident + * ?string $juryPromoteur, $juryPromoteurUlb * array $juryPromoteurs, $juryPromoteursUlb * array $lecteursInternes, $lecteursExternes - * bool $showPresident, $showPromoteurUlb, $promoteurUlbConditional + * bool $showPromoteurUlb, $promoteurUlbConditional * * Licence / access: * bool $libreEnabled, $interneEnabled, $interditEnabled @@ -70,7 +70,6 @@ $juryPromoteursUlb = $juryPromoteursUlb ?? []; $lecteursInternes = $lecteursInternes ?? []; $lecteursExternes = $lecteursExternes ?? []; $juryPresident = $juryPresident ?? null; -$showPresident = $showPresident ?? false; $showPromoteurUlb = $showPromoteurUlb ?? true; $promoteurUlbConditional = $promoteurUlbConditional ?? false; diff --git a/app/templates/partials/form/jury-fieldset.php b/app/templates/partials/form/jury-fieldset.php index 8df1264..c6d7758 100644 --- a/app/templates/partials/form/jury-fieldset.php +++ b/app/templates/partials/form/jury-fieldset.php @@ -9,8 +9,7 @@ * $juryPromoteursUlb array [{name: string}] Multiple promoteurs ULB * $lecteursInternes array [{name: string}] * $lecteursExternes array [{name: string}] - * $juryPresident string|null President name (edit-only, optional) - * $showPresident bool Show president field (default: false) + * $juryPresident string|null (Deprecated — no longer displayed) * $showPromoteurUlb bool Show ULB promoteur field (default: true) * $promoteurUlbConditional bool If true, field is hidden unless finality=Approfondi * @@ -23,14 +22,12 @@ $juryPromoteurUlb = $juryPromoteurUlb ?? null; $juryPromoteursUlb = $juryPromoteursUlb ?? []; $lecteursInternes = $lecteursInternes ?? []; $lecteursExternes = $lecteursExternes ?? []; -$juryPresident = $juryPresident ?? null; -$showPresident = $showPresident ?? false; $showPromoteurUlb = $showPromoteurUlb ?? true; $promoteurUlbConditional = $promoteurUlbConditional ?? false; $adminMode = $adminMode ?? false; // 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')) { // jury_promoteur may be array (new form) or scalar (legacy) $promoteursOld = old('jury_promoteur'); @@ -52,7 +49,6 @@ if ($addMode && function_exists('old')) { } elseif (is_string($promoteursUlbOld) && trim($promoteursUlbOld) !== '') { $juryPromoteurUlb = $promoteursUlbOld; } - $juryPresident = old('jury_president') ?: null; for ($i = 0; $i < 10; $i++) { $n = old("jury_lecteur_interne:$i"); if ($n !== '') $lecteursInternes[] = ['name' => $n]; @@ -238,15 +234,6 @@ if ($addMode && function_exists('old')) { - - -
    - - -
    -