Fix edit form: is_published reset, contact decoupling, note label, author name case

- Fix #1: Add is_published to getThesisRawFields() SELECT so the publish
  checkbox stays checked when editing an already-published TFE.
- Fix #2: Rename 'Note contextuelle' → 'Note contextuelle relative à
  soutenance' in all templates and StudentEmail.
- Fix #3: Update findOrCreateAuthor to also UPDATE the author name when
  a record is found by name (fixes inability to capitalise names).
- Fix #4/#5: Decouple contact_interne (private author email) from
  contact_visible (public contact on TFE page). Add migration 037 to
  add contact_visible TEXT column to theses table and rebuild
  v_theses_full view. Update all controllers, templates, and DB methods
  to treat them independently.
- Fix #6: Investigated libre→interne restriction — no code barrier
  found; likely resolved by is_published fix.
This commit is contained in:
Pontoporeia
2026-06-08 18:31:10 +02:00
parent 3d524226a1
commit 3016c199bd
15 changed files with 164 additions and 37 deletions

17
TODO.md
View File

@@ -1,12 +1,9 @@
# TODO # TODO
- [x] Ajouter `PeerTubeService::deleteVideo()` pour supprimer une vidéo via l'API PeerTube - [x] Fix #1: TFE publié se dépublie quand on modifie ses données (is_published missing from getThesisRawFields SELECT)
- [x] Modifier `deleteThesisFileToTrash()` pour appeler `deleteVideo()` quand `file_path` commence par `peertube_ids:` - [x] Fix #2: Renommer "Note contextuelle" → "Note contextuelle relative à soutenance"
- [x] Modifier `hardDeleteThesis()` pour supprimer les vidéos PeerTube associées - [x] Fix #3: Impossible de mettre une majuscule au nom d'étudiant·e (findOrCreateAuthor n'update pas le name)
- [x] Commit + jj new - [x] Fix #4: Décorréler contact interne et contact visible (ajouter colonne contact_visible sur theses)
- [x] Ajouter l'affichage de la finalité sur la page publique TFE (tfe.php) - [x] Fix #5: "Contact public : non" partout, non modifiable, sans impact
- [x] Fix "ATELIERS PLURIDISCIPLINAIRES" mid-word break in repertoire column headers - [x] Fix #6: Investiguer "libre interne" impossible — aucune restriction trouvée dans le code admin, probablement causé par Fix #1 (is_published reset)
- [x] Mise à jour auto de l'identifiant quand l'année change en back-office - [ ] Commit + jj new
- [x] Améliorer les hints du champ contact dans le formulaire étudiant
- [x] Rendre le fichier TFE optionnel pour Site web / Performance / Installation (note d'intention reste obligatoire)
- [x] Augmenter les limites d'upload : vidéo/audio 8 GB, images/archives 1 GB, nginx 8 GB

View File

@@ -0,0 +1,80 @@
-- Migration 037: add contact_visible to theses, update v_theses_full.
--
-- contact_visible: the public-facing contact shown on the TFE page
-- (email, URL, social handle, etc.). Decoupled from the author's
-- internal email (authors.email), which remains the private contact
-- used for confirmation emails.
--
-- Also ensures is_published is exposed via the view.
ALTER TABLE theses ADD COLUMN contact_visible TEXT DEFAULT NULL;
-- Rebuild v_theses_full to include contact_visible
DROP VIEW IF EXISTS v_theses_public;
DROP VIEW IF EXISTS v_theses_full;
CREATE VIEW v_theses_full AS
SELECT
t.id,
t.identifier,
t.title,
t.subtitle,
t.year,
t.is_doctoral,
t.objet,
o.name as orientation,
ap.name as ap_program,
ft.name as finality_type,
t.synopsis,
t.context_note,
at.name as access_type,
lt.name as license_type,
t.license_id,
t.license_custom,
t.access_type_id,
t.is_published,
t.jury_points,
t.submitted_at,
t.defense_date,
t.published_at,
t.baiu_link,
t.exemplaire_baiu,
t.exemplaire_erg,
t.cc2r,
t.remarks,
t.jury_note_added,
t.contact_visible,
GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors,
GROUP_CONCAT(DISTINCT s.name) as supervisors,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) as languages,
GROUP_CONCAT(DISTINCT fmt.name) as formats,
GROUP_CONCAT(DISTINCT tg.name) as keywords,
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_interne,
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_public
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN finality_types ft ON t.finality_id = ft.id
LEFT JOIN access_types at ON t.access_type_id = at.id
LEFT JOIN license_types lt ON t.license_id = lt.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id
LEFT JOIN supervisors s ON ts.supervisor_id = s.id
LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id
LEFT JOIN languages l ON tl.language_id = l.id AND l.deleted_at IS NULL
LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id
LEFT JOIN format_types fmt ON tf.format_id = fmt.id
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
LEFT JOIN tags tg ON tt.tag_id = tg.id AND tg.deleted_at IS NULL
WHERE t.deleted_at IS NULL
GROUP BY t.id;
CREATE VIEW v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;

View File

@@ -410,6 +410,7 @@ function renderShareLinkForm(string $slug, array $link): void
$contactInterne = null; $contactInterne = null;
$contactPublic = false; $contactPublic = false;
$currentContextNote = null; $currentContextNote = null;
$currentContactVisible = null;
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">

View File

@@ -148,6 +148,7 @@ class ThesisCreateController
'subtitle' => $data['subtitle'], 'subtitle' => $data['subtitle'],
'synopsis' => $data['synopsis'], 'synopsis' => $data['synopsis'],
'context_note' => $data['contextNote'], 'context_note' => $data['contextNote'],
'contact_visible' => $data['contactVisible'] ?? '',
'baiu_link' => $data['lien'], 'baiu_link' => $data['lien'],
'license_id' => $data['licenseId'], 'license_id' => $data['licenseId'],
'license_custom' => $data['licenseCustom'], 'license_custom' => $data['licenseCustom'],
@@ -310,12 +311,17 @@ class ThesisCreateController
if ($contactInterne !== '') { if ($contactInterne !== '') {
$mail = $contactInterne; $mail = $contactInterne;
} }
// contact_public: respected if present (admin form); defaults to true for student forms // contact_visible: what appears publicly on the TFE page
// where the spec says contact is always visible when provided. // In admin mode: from contact_visible field. In student mode: from mail field.
$contactVisible = trim($post['contact_visible'] ?? '');
if ($contactVisible === '' && $mail !== '') {
$contactVisible = $mail;
}
// showContact for backwards compat
if (array_key_exists('contact_public', $post)) { if (array_key_exists('contact_public', $post)) {
$showContact = !empty($post['contact_public']); $showContact = !empty($post['contact_public']);
} else { } else {
$showContact = $mail !== ''; $showContact = $contactVisible !== '';
} }
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT); $annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
@@ -528,6 +534,7 @@ class ThesisCreateController
return compact( return compact(
'authorNames', 'authorNames',
'mail', 'mail',
'contactVisible',
'showContact', 'showContact',
'annee', 'annee',
'orientationId', 'orientationId',

View File

@@ -108,6 +108,7 @@ class ThesisEditController
$currentLicenseId = $rawRow['license_id'] ?? null; $currentLicenseId = $rawRow['license_id'] ?? null;
$currentAccessTypeId = $rawRow['access_type_id'] ?? null; $currentAccessTypeId = $rawRow['access_type_id'] ?? null;
$currentContextNote = $rawRow['context_note'] ?? ''; $currentContextNote = $rawRow['context_note'] ?? '';
$currentContactVisible = $rawRow['contact_visible'] ?? '';
// Author contact info (from view) // Author contact info (from view)
$contactInterne = $thesis['contact_interne'] ?? ''; $contactInterne = $thesis['contact_interne'] ?? '';
@@ -130,6 +131,7 @@ class ThesisEditController
'currentLicenseId' => $currentLicenseId, 'currentLicenseId' => $currentLicenseId,
'currentAccessTypeId' => $currentAccessTypeId, 'currentAccessTypeId' => $currentAccessTypeId,
'currentContextNote' => $currentContextNote, 'currentContextNote' => $currentContextNote,
'currentContactVisible' => $currentContactVisible,
'contactInterne' => $contactInterne, 'contactInterne' => $contactInterne,
'contactPublic' => $contactPublic, 'contactPublic' => $contactPublic,
'currentRaw' => $rawRow, 'currentRaw' => $rawRow,
@@ -206,6 +208,7 @@ class ThesisEditController
'finality_id' => ($v = intval($post['finality'] ?? 0)) > 0 ? $v : null, 'finality_id' => ($v = intval($post['finality'] ?? 0)) > 0 ? $v : null,
'synopsis' => trim($post['synopsis'] ?? ''), 'synopsis' => trim($post['synopsis'] ?? ''),
'context_note' => trim($post['context_note'] ?? ''), 'context_note' => trim($post['context_note'] ?? ''),
'contact_visible' => trim($post['contact_visible'] ?? ''),
'baiu_link' => trim($post['lien'] ?? ''), 'baiu_link' => trim($post['lien'] ?? ''),
'license_id' => filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null, 'license_id' => filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
'access_type_id' => filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null, 'access_type_id' => filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
@@ -232,10 +235,9 @@ class ThesisEditController
// ── 2. Authors (alphabetically sorted) ───────────────────────────── // ── 2. Authors (alphabetically sorted) ─────────────────────────────
$authorsRaw = trim($post['auteurice'] ?? ''); $authorsRaw = trim($post['auteurice'] ?? '');
$showContact = !empty($post['contact_public']); // contact_interne = private email of the first author (backoffice field)
// contact_interne (backoffice) takes precedence over mail (tfe-info fieldset)
$contactInterne = trim($post['contact_interne'] ?? ''); $contactInterne = trim($post['contact_interne'] ?? '');
$firstAuthorEmail = $contactInterne !== '' ? $contactInterne : ($post['mail'] ?? null); $firstAuthorEmail = $contactInterne !== '' ? $contactInterne : null;
$authorNames = []; $authorNames = [];
if ($authorsRaw !== '') { if ($authorsRaw !== '') {
$authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== '')); $authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== ''));
@@ -246,7 +248,7 @@ class ThesisEditController
$authorEntries[] = [ $authorEntries[] = [
'name' => $name, 'name' => $name,
'email' => $i === 0 ? $firstAuthorEmail : null, 'email' => $i === 0 ? $firstAuthorEmail : null,
'show_contact' => $i === 0 ? $showContact : false, 'show_contact' => $i === 0,
]; ];
} }
$this->db->setThesisAuthors($thesisId, $authorEntries); $this->db->setThesisAuthors($thesisId, $authorEntries);

View File

@@ -1037,8 +1037,8 @@ class Database
$cleanEmail = null; // don't steal another author's email $cleanEmail = null; // don't steal another author's email
} }
} }
$updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?'); $updateStmt = $this->pdo->prepare('UPDATE authors SET name = ?, email = ?, show_contact = ? WHERE id = ?');
$updateStmt->execute([$cleanEmail, $showContact ? 1 : 0, $author['id']]); $updateStmt->execute([$name, $cleanEmail, $showContact ? 1 : 0, $author['id']]);
return $author['id']; return $author['id'];
} }
@@ -2007,7 +2007,7 @@ class Database
public function getThesisRawFields(int $thesisId): ?array public function getThesisRawFields(int $thesisId): ?array
{ {
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
'SELECT license_id, license_custom, access_type_id, context_note, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r FROM theses WHERE id = ? LIMIT 1' 'SELECT license_id, license_custom, access_type_id, context_note, contact_visible, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, is_published FROM theses WHERE id = ? LIMIT 1'
); );
$stmt->execute([$thesisId]); $stmt->execute([$thesisId]);
$row = $stmt->fetch(); $row = $stmt->fetch();
@@ -2140,6 +2140,7 @@ class Database
finality_id = ?, finality_id = ?,
synopsis = ?, synopsis = ?,
context_note = ?, context_note = ?,
contact_visible = ?,
baiu_link = ?, baiu_link = ?,
license_id = ?, license_id = ?,
license_custom = ?, license_custom = ?,
@@ -2170,6 +2171,7 @@ class Database
$finality, $finality,
$data['synopsis'], $data['synopsis'],
!empty($data['context_note']) ? $data['context_note'] : null, !empty($data['context_note']) ? $data['context_note'] : null,
!empty($data['contact_visible']) ? $data['contact_visible'] : null,
!empty($data['baiu_link']) ? $data['baiu_link'] : null, !empty($data['baiu_link']) ? $data['baiu_link'] : null,
$license, $license,
!empty($data['license_custom']) ? $data['license_custom'] : null, !empty($data['license_custom']) ? $data['license_custom'] : null,
@@ -2215,7 +2217,7 @@ 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, context_note, synopsis, context_note, contact_visible,
baiu_link, license_id, license_custom, baiu_link, license_id, license_custom,
access_type_id, access_type_id,
objet, objet,
@@ -2224,7 +2226,7 @@ class Database
exemplaire_baiu, exemplaire_erg, exemplaire_baiu, exemplaire_erg,
cc2r, cc2r,
submitted_at submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
'); ');
$validObjet = ['tfe', 'thèse', 'frart']; $validObjet = ['tfe', 'thèse', 'frart'];
@@ -2246,6 +2248,7 @@ class Database
$finality ? (int)$finality : null, $finality ? (int)$finality : null,
$data['synopsis'], $data['synopsis'],
!empty($data['context_note']) ? $data['context_note'] : null, !empty($data['context_note']) ? $data['context_note'] : null,
!empty($data['contact_visible']) ? $data['contact_visible'] : null,
!empty($data['baiu_link']) ? $data['baiu_link'] : null, !empty($data['baiu_link']) ? $data['baiu_link'] : null,
$license ? (int)$license : null, $license ? (int)$license : null,
!empty($data['license_custom']) ? $data['license_custom'] : null, !empty($data['license_custom']) ? $data['license_custom'] : null,

View File

@@ -28,7 +28,7 @@ class StudentEmail
'Atelier pluridisciplinaire' => $thesis['ap_program'] ?? '', 'Atelier pluridisciplinaire' => $thesis['ap_program'] ?? '',
'Finalité' => $thesis['finality_type'] ?? '', 'Finalité' => $thesis['finality_type'] ?? '',
'Synopsis' => $thesis['synopsis'] ?? '', 'Synopsis' => $thesis['synopsis'] ?? '',
'Note contextuelle' => $thesis['context_note'] ?? '', 'Note contextuelle relative à soutenance' => $thesis['context_note'] ?? '',
'Langue(s)' => $thesis['languages'] ?? '', 'Langue(s)' => $thesis['languages'] ?? '',
'Format(s)' => $thesis['formats'] ?? '', 'Format(s)' => $thesis['formats'] ?? '',
'Mots-clés' => $thesis['keywords'] ?? '', 'Mots-clés' => $thesis['keywords'] ?? '',

View File

@@ -0,0 +1,2 @@
{"timestamp":"2026-06-09T10:12:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","actor":"127.0.0.1","action":"UPDATE","table":"theses","record_id":143,"old_data":{"id":143,"identifier":"2025-072","title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META","subtitle":null,"year":2025,"is_doctoral":0,"objet":"tfe","orientation_id":6,"ap_program_id":2,"finality_id":3,"synopsis":"Depuis une quinzaine dannées, Instagram sest imposé comme un acteur central du monde de lart visuel. Ce qui était un réseau social destiné au partage dimages personnelles est devenu, pour toute une génération dartistes, un lieu incontournable de visibilité, de circulation symbolique, de reconnaissance professionnelle. En soi, un dispositif de légitimation culturelle. Il ne sagit plus simplement dun outil de diffusion parmi dautres, mais dun environnement structurant, un écosystème dans lequel les artistes évoluent, négocient leurs existences publiques, et « construisent » leurs carrières. À la différence des lieux dexposition traditionnels (galeries, musées), Instagram est à la fois global, permanent, et massivement fréquenté. Il est devenu, pour reprendre les termes de la théorie critique, un milieu total, un espace dans lequel les frontières entre création, communication, autopromotion, performance de soi et marché sont brouillées, confondues, voire rendues indissociables.","context_note":null,"remarks":null,"access_type_id":2,"license_id":null,"jury_points":17.5,"jury_note_added":0,"submitted_at":"2026-06-08 08:33:14","defense_date":null,"published_at":null,"is_published":1,"baiu_link":null,"created_at":"2026-06-08 08:33:14","updated_at":"2026-06-08 08:33:36","exemplaire_baiu":0,"exemplaire_erg":0,"cc2r":0,"license_custom":null,"deleted_at":null,"contact_visible":null}}
{"timestamp":"2026-06-09T10:13:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","actor":"127.0.0.1","action":"UPDATE","table":"theses","record_id":143,"old_data":{"id":143,"identifier":"2025-072","title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META","subtitle":null,"year":2025,"is_doctoral":0,"objet":"tfe","orientation_id":6,"ap_program_id":2,"finality_id":3,"synopsis":"Depuis une quinzaine dannées, Instagram sest imposé comme un acteur central du monde de lart visuel. Ce qui était un réseau social destiné au partage dimages personnelles est devenu, pour toute une génération dartistes, un lieu incontournable de visibilité, de circulation symbolique, de reconnaissance professionnelle. En soi, un dispositif de légitimation culturelle. Il ne sagit plus simplement dun outil de diffusion parmi dautres, mais dun environnement structurant, un écosystème dans lequel les artistes évoluent, négocient leurs existences publiques, et « construisent » leurs carrières. À la différence des lieux dexposition traditionnels (galeries, musées), Instagram est à la fois global, permanent, et massivement fréquenté. Il est devenu, pour reprendre les termes de la théorie critique, un milieu total, un espace dans lequel les frontières entre création, communication, autopromotion, performance de soi et marché sont brouillées, confondues, voire rendues indissociables.","context_note":null,"remarks":null,"access_type_id":2,"license_id":null,"jury_points":17.5,"jury_note_added":0,"submitted_at":"2026-06-08 08:33:14","defense_date":null,"published_at":null,"is_published":1,"baiu_link":null,"created_at":"2026-06-08 08:33:14","updated_at":"2026-06-08 08:33:36","exemplaire_baiu":0,"exemplaire_erg":0,"cc2r":0,"license_custom":null,"deleted_at":null,"contact_visible":null}}

View File

@@ -0,0 +1,20 @@
{"timestamp":"2026-06-09T10:12:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","context":"thesis_edit_tx","exception":"PDOException","message":"SQLSTATE[HY000]: General error: 25 column index out of range","trace":"#0 /home/padlock/repos/xamxam/app/src/Database.php(2186): PDOStatement->execute()
#1 /home/padlock/repos/xamxam/app/src/Controllers/ThesisEditController.php(233): Database->updateThesis()
#2 /home/padlock/repos/xamxam/app/public/admin/actions/edit.php(36): ThesisEditController->save()
#3 /home/padlock/repos/xamxam/app/router.php(46): include('...')
#4 {main}","extra":{"thesis_id":143}}
{"timestamp":"2026-06-09T10:12:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","context":"thesis_edit","exception":"PDOException","message":"SQLSTATE[HY000]: General error: 25 column index out of range","trace":"#0 /home/padlock/repos/xamxam/app/src/Database.php(2186): PDOStatement->execute()
#1 /home/padlock/repos/xamxam/app/src/Controllers/ThesisEditController.php(233): Database->updateThesis()
#2 /home/padlock/repos/xamxam/app/public/admin/actions/edit.php(36): ThesisEditController->save()
#3 /home/padlock/repos/xamxam/app/router.php(46): include('...')
#4 {main}","extra":{"thesis_id":143}}
{"timestamp":"2026-06-09T10:13:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","context":"thesis_edit_tx","exception":"PDOException","message":"SQLSTATE[HY000]: General error: 25 column index out of range","trace":"#0 /home/padlock/repos/xamxam/app/src/Database.php(2186): PDOStatement->execute()
#1 /home/padlock/repos/xamxam/app/src/Controllers/ThesisEditController.php(233): Database->updateThesis()
#2 /home/padlock/repos/xamxam/app/public/admin/actions/edit.php(36): ThesisEditController->save()
#3 /home/padlock/repos/xamxam/app/router.php(46): include('...')
#4 {main}","extra":{"thesis_id":143}}
{"timestamp":"2026-06-09T10:13:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","context":"thesis_edit","exception":"PDOException","message":"SQLSTATE[HY000]: General error: 25 column index out of range","trace":"#0 /home/padlock/repos/xamxam/app/src/Database.php(2186): PDOStatement->execute()
#1 /home/padlock/repos/xamxam/app/src/Controllers/ThesisEditController.php(233): Database->updateThesis()
#2 /home/padlock/repos/xamxam/app/public/admin/actions/edit.php(36): ThesisEditController->save()
#3 /home/padlock/repos/xamxam/app/router.php(46): include('...')
#4 {main}","extra":{"thesis_id":143}}

View File

@@ -42,6 +42,7 @@
$contactInterne = null; $contactInterne = null;
$contactPublic = false; $contactPublic = false;
$currentContextNote = null; $currentContextNote = null;
$currentContactVisible = null;
include APP_ROOT . '/templates/partials/form/form.php'; include APP_ROOT . '/templates/partials/form/form.php';
?> ?>

View File

@@ -8,6 +8,7 @@
'subtitle' => $thesis['subtitle'] ?? '', 'subtitle' => $thesis['subtitle'] ?? '',
'auteurice' => $thesis['authors'] ?? '', 'auteurice' => $thesis['authors'] ?? '',
'mail' => $contactInterne ?? '', 'mail' => $contactInterne ?? '',
'contact_visible' => $currentContactVisible ?? '',
'synopsis' => $thesis['synopsis'] ?? '', 'synopsis' => $thesis['synopsis'] ?? '',
'tag' => $thesis['keywords'] ?? '', 'tag' => $thesis['keywords'] ?? '',
'année' => $thesis['year'], 'année' => $thesis['year'],

View File

@@ -45,10 +45,12 @@
<dt>Sous-titre</dt><dd><?= htmlspecialchars($thesis['subtitle']) ?></dd> <dt>Sous-titre</dt><dd><?= htmlspecialchars($thesis['subtitle']) ?></dd>
<?php endif; ?> <?php endif; ?>
<dt>Auteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['authors']) ?></dd> <dt>Auteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['authors']) ?></dd>
<?php if (!empty($thesis['contact_interne'])): ?> <?php if (!empty($thesis['contact_visible'])): ?>
<dt>Contact (interne)</dt><dd><?= htmlspecialchars($thesis['contact_interne']) ?></dd> <dt>Contact visible</dt><dd><?= htmlspecialchars($thesis['contact_visible']) ?></dd>
<?php endif; ?>
<?php if (!empty($thesis['contact_interne'])): ?>
<dt>Contact interne (privé)</dt><dd><?= htmlspecialchars($thesis['contact_interne']) ?></dd>
<?php endif; ?> <?php endif; ?>
<dt>Contact public</dt><dd><?= !empty($thesis['contact_public']) ? 'Oui' : 'Non' ?></dd>
<dt>Année</dt><dd><?= htmlspecialchars((string)$thesis['year']) ?></dd> <dt>Année</dt><dd><?= htmlspecialchars((string)$thesis['year']) ?></dd>
<dt>Objet</dt><dd><?= htmlspecialchars($thesis['objet'] ?? 'tfe') ?></dd> <dt>Objet</dt><dd><?= htmlspecialchars($thesis['objet'] ?? 'tfe') ?></dd>
<?php if ($thesis['is_doctoral']): ?> <?php if ($thesis['is_doctoral']): ?>
@@ -113,7 +115,7 @@
<dd class="recap-synopsis"><?= nl2br(htmlspecialchars($thesis['synopsis'] ?? '')) ?></dd> <dd class="recap-synopsis"><?= nl2br(htmlspecialchars($thesis['synopsis'] ?? '')) ?></dd>
<?php if ($thesis['context_note']): ?> <?php if ($thesis['context_note']): ?>
<dt>Note contextuelle</dt> <dt>Note contextuelle relative à soutenance</dt>
<dd class="recap-long-text"><?= nl2br(htmlspecialchars($thesis['context_note'])) ?></dd> <dd class="recap-long-text"><?= nl2br(htmlspecialchars($thesis['context_note'])) ?></dd>
<?php endif; ?> <?php endif; ?>

View File

@@ -58,12 +58,14 @@ $adminMode = $adminMode ?? false;
$hint = 'Séparez les auteur·ices par des virgules.'; $hint = 'Séparez les auteur·ices par des virgules.';
include APP_ROOT . '/templates/partials/form/text-field.php'; include APP_ROOT . '/templates/partials/form/text-field.php';
?> ?>
<?php if (!$adminMode): ?>
<?php <?php
$name = 'mail'; $label = 'Contact visible (optionnel) [mail/site/insta/etc.] :'; $value = $oldFn('mail'); $name = 'mail'; $label = 'Contact visible (optionnel) [mail/site/insta/etc.] :'; $value = $oldFn('mail');
$attrs = ['autocomplete' => 'email']; $attrs = ['autocomplete' => 'email'];
$hint = 'Un seul contact. Indiquez l\'URL complète pour un site (https://…), l\'adresse mail, le nom d\'utilisateur avec @ pour Instagram (@pseudo), ou l\'adresse complète pour Mastodon (@pseudo@instance). Ce contact sera visible publiquement sur la fiche du TFE.'; $hint = 'Un seul contact. Indiquez l\'URL complète pour un site (https://…), l\'adresse mail, le nom d\'utilisateur avec @ pour Instagram (@pseudo), ou l\'adresse complète pour Mastodon (@pseudo@instance). Ce contact sera visible publiquement sur la fiche du TFE.';
include APP_ROOT . '/templates/partials/form/text-field.php'; include APP_ROOT . '/templates/partials/form/text-field.php';
?> ?>
<?php endif; ?>
<div> <div>
<label for="synopsis">Synopsis :<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></label> <label for="synopsis">Synopsis :<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></label>

View File

@@ -32,7 +32,7 @@
* bool $showContact — Contact checkbox fieldset * bool $showContact — Contact checkbox fieldset
* bool $showCoverPreview — cover image preview + remove checkbox * bool $showCoverPreview — cover image preview + remove checkbox
* bool $showExistingFiles — existing thesis files list (deletable) * bool $showExistingFiles — existing thesis files list (deletable)
* bool $showBackoffice — Backoffice fieldset (context_note, jury_points, remarks, baiu_link, exemplaires, contact_interne, is_published) * bool $showBackoffice — Backoffice fieldset (context_note, jury_points, remarks, baiu_link, exemplaires, contact_visible, contact_interne, is_published)
* bool $showEmailConfirmation — E-mail de confirmation fieldset * bool $showEmailConfirmation — E-mail de confirmation fieldset
* string $helpFn — fn(string $key): string (for help blocks) * string $helpFn — fn(string $key): string (for help blocks)
@@ -408,9 +408,9 @@ if ($filesMode === 'add'): ?>
<fieldset> <fieldset>
<legend>Backoffice</legend> <legend>Backoffice</legend>
<!-- 1. Note contextuelle --> <!-- 1. Note contextuelle relative à soutenance -->
<div class="admin-form-group"> <div class="admin-form-group">
<label for="context_note">Note contextuelle :</label> <label for="context_note">Note contextuelle relative à soutenance :</label>
<div> <div>
<textarea id="context_note" name="context_note" <textarea id="context_note" name="context_note"
rows="4" maxlength="1500"><?= htmlspecialchars( rows="4" maxlength="1500"><?= htmlspecialchars(
@@ -479,16 +479,25 @@ if ($filesMode === 'add'): ?>
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small> <small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
</div> </div>
<!-- 7. Contact interne --> <!-- 7. Contact visible (public) -->
<div class="admin-form-group"> <div class="admin-form-group">
<label for="contact_interne">Contact interne :</label> <label for="contact_visible">Contact visible :</label>
<input type="text" id="contact_visible" name="contact_visible"
value="<?= htmlspecialchars($currentContactVisible ?? $formData['contact_visible'] ?? '') ?>"
placeholder="email, URL, @pseudo...">
<small>Contact affiché publiquement sur la page du TFE (email, site web, réseau social…). Laisser vide pour ne rien afficher.</small>
</div>
<!-- 8. Contact interne (privé) -->
<div class="admin-form-group">
<label for="contact_interne">Contact interne (privé) :</label>
<input type="email" id="contact_interne" name="contact_interne" <input type="email" id="contact_interne" name="contact_interne"
value="<?= htmlspecialchars($contactInterne ?? $formData['contact_interne'] ?? '') ?>" value="<?= htmlspecialchars($contactInterne ?? $formData['contact_interne'] ?? '') ?>"
placeholder="ton.email@exemple.be"> placeholder="ton.email@exemple.be">
<small>Adresse de contact interne (non visible publiquement). Peut être laissé vide.</small> <small>Email privé de l'étudiant·e, utilisé pour l'envoi de la confirmation du formulaire. Non visible publiquement.</small>
</div> </div>
<!-- 8. Publication --> <!-- 9. Publication -->
<div class="admin-form-group"> <div class="admin-form-group">
<label class="admin-checkbox-label"> <label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1" <input type="checkbox" name="is_published" value="1"

View File

@@ -185,16 +185,16 @@
<?php if (!empty($data["context_note"])): ?> <?php if (!empty($data["context_note"])): ?>
<p class="tfe-meta-item tfe-meta-note"> <p class="tfe-meta-item tfe-meta-note">
<span class="tfe-meta-label">Note :</span> <span class="tfe-meta-label">Note contextuelle relative à soutenance :</span>
<span class="tfe-note-value"><?= nl2br(htmlspecialchars($data["context_note"])) ?></span> <span class="tfe-note-value"><?= nl2br(htmlspecialchars($data["context_note"])) ?></span>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($data["contact_interne"]) && !empty($data["contact_public"])): ?> <?php if (!empty($data["contact_visible"])): ?>
<p class="tfe-meta-item"> <p class="tfe-meta-item">
<span class="tfe-meta-label">Contact :</span> <span class="tfe-meta-label">Contact :</span>
<?php <?php
$_contact = $data["contact_interne"]; $_contact = $data["contact_visible"];
$_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false; $_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false;
$_isEmail = !$_isUrl && str_contains($_contact, "@"); $_isEmail = !$_isUrl && str_contains($_contact, "@");
if ($_isUrl): if ($_isUrl):