mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
feat: jury composition + banner image upload
- migration 004: thesis_supervisors.role + is_external; view adds jury_president/jury_promoteurs/jury_lecteurs - migration 005: theses.banner_path; view exposes t.banner_path and t.license_id - Database: getThesisJury(), setThesisJury(), setBannerPath() - admin/add.php: jury fieldset (président/promoteur/lecteurs + externe checkboxes, JS add/remove rows); banner file input - admin/edit.php: jury fieldset pre-populated from DB; banner preview + remove checkbox + upload; multipart form - admin/actions/formulaire.php: parse jury fields → setThesisJury(); banner upload to banners/ - tfe.php: three conditional jury rows (président·e, promoteur·ice, lecteur·ices) - schema.sql: updated thesis_supervisors, theses, v_theses_full, v_theses_public definitions - admin.css: fieldset, jury-row, jury-entry, btn-remove styles
This commit is contained in:
@@ -84,9 +84,22 @@ try {
|
||||
$problematique = sanitize_string($_POST["problématique"] ?? '');
|
||||
$durationInfo = sanitize_string($_POST["duration_info"] ?? '');
|
||||
|
||||
// Supervisor(s)
|
||||
$promoteuriceRaw = sanitize_string($_POST["promoteurice"] ?? '');
|
||||
$supervisorNames = !empty($promoteuriceRaw) ? array_map('trim', explode(',', $promoteuriceRaw)) : [];
|
||||
// Jury members
|
||||
$juryMembers = [];
|
||||
if (!empty(trim($_POST['jury_president'] ?? ''))) {
|
||||
$juryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0];
|
||||
}
|
||||
if (!empty(trim($_POST['jury_promoteur'] ?? ''))) {
|
||||
$juryMembers[] = ['name' => trim($_POST['jury_promoteur']), 'role' => 'promoteur',
|
||||
'is_external' => isset($_POST['jury_promoteur_ext']) ? 1 : 0];
|
||||
}
|
||||
foreach ($_POST['jury_lecteurs'] ?? [] as $i => $name) {
|
||||
$name = trim($name);
|
||||
if ($name !== '') {
|
||||
$juryMembers[] = ['name' => $name, 'role' => 'lecteur',
|
||||
'is_external' => isset($_POST['jury_lecteurs_ext'][$i]) ? 1 : 0];
|
||||
}
|
||||
}
|
||||
|
||||
// Keywords (max 10)
|
||||
$tagRaw = sanitize_string($_POST["tag"] ?? '');
|
||||
@@ -119,6 +132,7 @@ try {
|
||||
|
||||
// File uploads
|
||||
$couverture = $_FILES["couverture"] ?? null;
|
||||
$bannerFile = $_FILES["banner"] ?? null;
|
||||
$files = $_FILES["files"] ?? null;
|
||||
|
||||
// ===== CREATE OR FIND AUTHOR =====
|
||||
@@ -164,14 +178,8 @@ try {
|
||||
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)");
|
||||
$stmt->execute([$thesisId, $authorId]);
|
||||
|
||||
// ===== LINK SUPERVISORS TO THESIS =====
|
||||
foreach ($supervisorNames as $index => $supervisorName) {
|
||||
if (!empty($supervisorName)) {
|
||||
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
|
||||
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
|
||||
}
|
||||
}
|
||||
// ===== LINK JURY TO THESIS =====
|
||||
$db->setThesisJury($thesisId, $juryMembers);
|
||||
|
||||
// ===== LINK LANGUAGES TO THESIS =====
|
||||
foreach ($languageIds as $languageId) {
|
||||
@@ -201,7 +209,8 @@ try {
|
||||
// Create necessary directories — outside the webroot (security items #3 & #4).
|
||||
// Files are served through /media.php, never directly via a URL path.
|
||||
$uploadBaseDir = STORAGE_ROOT . "/theses/{$annee}/{$identifier}/";
|
||||
$coverDir = STORAGE_ROOT . "/covers/";
|
||||
$coverDir = STORAGE_ROOT . "/covers/";
|
||||
$bannerDir = STORAGE_ROOT . "/banners/";
|
||||
|
||||
if (!file_exists($uploadBaseDir)) {
|
||||
mkdir($uploadBaseDir, 0755, true);
|
||||
@@ -209,6 +218,9 @@ try {
|
||||
if (!file_exists($coverDir)) {
|
||||
mkdir($coverDir, 0755, true);
|
||||
}
|
||||
if (!file_exists($bannerDir)) {
|
||||
mkdir($bannerDir, 0755, true);
|
||||
}
|
||||
|
||||
// Define security constraints
|
||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip'];
|
||||
@@ -252,6 +264,30 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Process banner image
|
||||
if ($bannerFile && isset($bannerFile["error"]) && $bannerFile["error"] === UPLOAD_ERR_OK) {
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($bannerFile["tmp_name"]);
|
||||
$fileExtension = strtolower(pathinfo($bannerFile["name"], PATHINFO_EXTENSION));
|
||||
$allowedBannerMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
$allowedBannerExts = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
$maxBannerSize = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
if (in_array($mimeType, $allowedBannerMimes) && in_array($fileExtension, $allowedBannerExts)
|
||||
&& $bannerFile["size"] <= $maxBannerSize) {
|
||||
$randomName = bin2hex(random_bytes(16));
|
||||
$safeFileName = $randomName . "." . $fileExtension;
|
||||
$targetFile = $bannerDir . $safeFileName;
|
||||
if (move_uploaded_file($bannerFile["tmp_name"], $targetFile)) {
|
||||
chmod($targetFile, 0644);
|
||||
$db->setBannerPath($thesisId, "banners/" . $safeFileName);
|
||||
error_log("Banner image uploaded: " . $safeFileName);
|
||||
}
|
||||
} else {
|
||||
error_log("Invalid or oversized banner image: " . $bannerFile["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
// Process thesis files
|
||||
if ($files && is_array($files["name"])) {
|
||||
for ($i = 0; $i < count($files["name"]); $i++) {
|
||||
|
||||
@@ -79,19 +79,67 @@ function wasSelected($key, $value) {
|
||||
value="<?= old('mail') ?>">
|
||||
</div>
|
||||
|
||||
<!-- Promoteur interne -->
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="promoteurice">Promoteur·ice interne :</label>
|
||||
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
|
||||
value="<?= old('promoteurice') ?>">
|
||||
</div>
|
||||
<!-- Composition du jury -->
|
||||
<fieldset class="admin-fieldset">
|
||||
<legend class="admin-fieldset-legend">Composition du jury</legend>
|
||||
|
||||
<!-- Promoteur externe -->
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="promoteurice_externe">Promoteur·ice externe :</label>
|
||||
<input class="admin-input" type="text" id="promoteurice_externe" name="promoteurice_externe"
|
||||
value="<?= old('promoteurice_externe') ?>">
|
||||
</div>
|
||||
<!-- Président·e -->
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="jury_president">Président·e :</label>
|
||||
<input class="admin-input" type="text" id="jury_president" name="jury_president"
|
||||
value="<?= old('jury_president') ?>"
|
||||
placeholder="Nom du/de la président·e (interne)">
|
||||
</div>
|
||||
|
||||
<!-- Promoteur·ice -->
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="jury_promoteur">Promoteur·ice :</label>
|
||||
<div class="admin-jury-row">
|
||||
<input class="admin-input" type="text" id="jury_promoteur" name="jury_promoteur"
|
||||
value="<?= old('jury_promoteur') ?>" placeholder="Nom">
|
||||
<label class="admin-checkbox-label admin-jury-ext">
|
||||
<input type="checkbox" name="jury_promoteur_ext" value="1"
|
||||
<?= wasSelected('jury_promoteur_ext', '1') ? 'checked' : '' ?>>
|
||||
Externe
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lecteur·ices (dynamic) -->
|
||||
<div class="admin-form-row" style="align-items:start;">
|
||||
<label class="admin-label">Lecteur·ices :</label>
|
||||
<div id="jury-lecteurs-list" class="admin-jury-list">
|
||||
<!-- rows injected by JS; start with one empty row -->
|
||||
<div class="admin-jury-entry">
|
||||
<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">
|
||||
<label class="admin-checkbox-label admin-jury-ext">
|
||||
<input type="checkbox" name="jury_lecteurs_ext[0]" value="1"> Externe
|
||||
</label>
|
||||
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="admin-btn-secondary" style="margin-top:.5rem;"
|
||||
onclick="addJuryRow()">+ Ajouter un·e lecteur·ice</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<script>
|
||||
var juryIdx = 1;
|
||||
function addJuryRow() {
|
||||
var list = document.getElementById('jury-lecteurs-list');
|
||||
var div = document.createElement('div');
|
||||
div.className = 'admin-jury-entry';
|
||||
div.innerHTML = '<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">'
|
||||
+ '<label class="admin-checkbox-label admin-jury-ext">'
|
||||
+ '<input type="checkbox" name="jury_lecteurs_ext[' + juryIdx + ']" value="1"> Externe'
|
||||
+ '</label>'
|
||||
+ '<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>';
|
||||
list.appendChild(div);
|
||||
juryIdx++;
|
||||
}
|
||||
function removeJuryRow(btn) {
|
||||
btn.closest('.admin-jury-entry').remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Année -->
|
||||
<div class="admin-form-row">
|
||||
@@ -234,6 +282,15 @@ function wasSelected($key, $value) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image bannière -->
|
||||
<div class="admin-form-row" style="align-items:start;">
|
||||
<label class="admin-label">Image bannière (accueil) :</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="banner" name="banner" accept="image/jpeg,image/png,image/webp">
|
||||
<p class="admin-hint">JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fichiers -->
|
||||
<div class="admin-form-row" style="align-items:start;">
|
||||
<label class="admin-label">Fichiers du TFE :</label>
|
||||
|
||||
@@ -84,19 +84,23 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Update supervisors
|
||||
$pdo->prepare("DELETE FROM thesis_supervisors WHERE thesis_id = ?")->execute([$thesisId]);
|
||||
$supervisorsRaw = trim($_POST['promoteurice'] ?? '');
|
||||
if (!empty($supervisorsRaw)) {
|
||||
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
|
||||
foreach ($supervisors as $index => $supervisorName) {
|
||||
if (!empty($supervisorName)) {
|
||||
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
|
||||
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
|
||||
}
|
||||
// Update jury
|
||||
$editJuryMembers = [];
|
||||
if (!empty(trim($_POST['jury_president'] ?? ''))) {
|
||||
$editJuryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0];
|
||||
}
|
||||
if (!empty(trim($_POST['jury_promoteur'] ?? ''))) {
|
||||
$editJuryMembers[] = ['name' => trim($_POST['jury_promoteur']), 'role' => 'promoteur',
|
||||
'is_external' => isset($_POST['jury_promoteur_ext']) ? 1 : 0];
|
||||
}
|
||||
foreach ($_POST['jury_lecteurs'] ?? [] as $i => $name) {
|
||||
$name = trim($name);
|
||||
if ($name !== '') {
|
||||
$editJuryMembers[] = ['name' => $name, 'role' => 'lecteur',
|
||||
'is_external' => isset($_POST['jury_lecteurs_ext'][$i]) ? 1 : 0];
|
||||
}
|
||||
}
|
||||
$db->setThesisJury($thesisId, $editJuryMembers);
|
||||
|
||||
// Update languages
|
||||
$pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]);
|
||||
@@ -134,6 +138,38 @@ try {
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
// Handle banner upload/removal (after commit, outside transaction)
|
||||
$bannerDir = defined('STORAGE_ROOT') ? STORAGE_ROOT . "/banners/" : null;
|
||||
if ($bannerDir && !file_exists($bannerDir)) {
|
||||
mkdir($bannerDir, 0755, true);
|
||||
}
|
||||
if (isset($_POST['remove_banner'])) {
|
||||
// Unlink existing banner file if present
|
||||
$currentBannerPath = $pdo->query("SELECT banner_path FROM theses WHERE id = $thesisId")->fetchColumn();
|
||||
if ($currentBannerPath && $bannerDir) {
|
||||
$absPath = STORAGE_ROOT . '/' . $currentBannerPath;
|
||||
if (file_exists($absPath)) unlink($absPath);
|
||||
}
|
||||
$db->setBannerPath($thesisId, null);
|
||||
} elseif (isset($_FILES['banner']) && $_FILES['banner']['error'] === UPLOAD_ERR_OK && $bannerDir) {
|
||||
$bannerFile = $_FILES['banner'];
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($bannerFile["tmp_name"]);
|
||||
$fileExtension = strtolower(pathinfo($bannerFile["name"], PATHINFO_EXTENSION));
|
||||
$allowedBannerMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
$allowedBannerExts = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
if (in_array($mimeType, $allowedBannerMimes) && in_array($fileExtension, $allowedBannerExts)
|
||||
&& $bannerFile["size"] <= 5 * 1024 * 1024) {
|
||||
$randomName = bin2hex(random_bytes(16));
|
||||
$safeFileName = $randomName . '.' . $fileExtension;
|
||||
if (move_uploaded_file($bannerFile["tmp_name"], $bannerDir . $safeFileName)) {
|
||||
chmod($bannerDir . $safeFileName, 0644);
|
||||
$db->setBannerPath($thesisId, "banners/" . $safeFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$success = "TFE mis à jour avec succès!";
|
||||
|
||||
// Regenerate CSRF token
|
||||
@@ -162,6 +198,9 @@ try {
|
||||
$stmt->execute([$thesisId]);
|
||||
$currentFormats = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Load jury
|
||||
$jury = $db->getThesisJury($thesisId);
|
||||
|
||||
// Load reference data
|
||||
$orientations = $db->getAllOrientations();
|
||||
$apPrograms = $db->getAllAPPrograms();
|
||||
@@ -195,7 +234,7 @@ try {
|
||||
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="edit.php?id=<?= $thesisId ?>" class="admin-form">
|
||||
<form method="post" action="edit.php?id=<?= $thesisId ?>" class="admin-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
|
||||
<div class="admin-form-row">
|
||||
@@ -251,11 +290,95 @@ try {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="promoteurice">Promoteur·ice(s) :</label>
|
||||
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
|
||||
value="<?= htmlspecialchars($thesis['supervisors'] ?? '') ?>">
|
||||
</div>
|
||||
<!-- Composition du jury -->
|
||||
<?php
|
||||
// Pre-split jury by role for easy pre-population
|
||||
$juryPresident = null;
|
||||
$juryPromoteur = null;
|
||||
$juryPromoteurExt = 0;
|
||||
$juryLecteurs = [];
|
||||
foreach ($jury as $jm) {
|
||||
if ($jm['role'] === 'president') {
|
||||
$juryPresident = $jm['name'];
|
||||
} elseif ($jm['role'] === 'promoteur') {
|
||||
$juryPromoteur = $jm['name'];
|
||||
$juryPromoteurExt = (int)$jm['is_external'];
|
||||
} elseif ($jm['role'] === 'lecteur') {
|
||||
$juryLecteurs[] = $jm;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<fieldset class="admin-fieldset">
|
||||
<legend class="admin-fieldset-legend">Composition du jury</legend>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="jury_president">Président·e :</label>
|
||||
<input class="admin-input" type="text" id="jury_president" name="jury_president"
|
||||
value="<?= htmlspecialchars($juryPresident ?? '') ?>"
|
||||
placeholder="Nom (interne)">
|
||||
</div>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="jury_promoteur">Promoteur·ice :</label>
|
||||
<div class="admin-jury-row">
|
||||
<input class="admin-input" type="text" id="jury_promoteur" name="jury_promoteur"
|
||||
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom">
|
||||
<label class="admin-checkbox-label admin-jury-ext">
|
||||
<input type="checkbox" name="jury_promoteur_ext" value="1"
|
||||
<?= $juryPromoteurExt ? 'checked' : '' ?>> Externe
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-row" style="align-items:start;">
|
||||
<label class="admin-label">Lecteur·ices :</label>
|
||||
<div>
|
||||
<div id="jury-lecteurs-list" class="admin-jury-list">
|
||||
<?php if (empty($juryLecteurs)): ?>
|
||||
<div class="admin-jury-entry">
|
||||
<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">
|
||||
<label class="admin-checkbox-label admin-jury-ext">
|
||||
<input type="checkbox" name="jury_lecteurs_ext[0]" value="1"> Externe
|
||||
</label>
|
||||
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($juryLecteurs as $li => $lm): ?>
|
||||
<div class="admin-jury-entry">
|
||||
<input class="admin-input" type="text" name="jury_lecteurs[]"
|
||||
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom">
|
||||
<label class="admin-checkbox-label admin-jury-ext">
|
||||
<input type="checkbox" name="jury_lecteurs_ext[<?= $li ?>]" value="1"
|
||||
<?= $lm['is_external'] ? 'checked' : '' ?>> Externe
|
||||
</label>
|
||||
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="admin-btn-secondary" style="margin-top:.5rem;"
|
||||
onclick="addJuryRow()">+ Ajouter un·e lecteur·ice</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<script>
|
||||
var juryIdx = <?= max(count($juryLecteurs), 1) ?>;
|
||||
function addJuryRow() {
|
||||
var list = document.getElementById('jury-lecteurs-list');
|
||||
var div = document.createElement('div');
|
||||
div.className = 'admin-jury-entry';
|
||||
div.innerHTML = '<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">'
|
||||
+ '<label class="admin-checkbox-label admin-jury-ext">'
|
||||
+ '<input type="checkbox" name="jury_lecteurs_ext[' + juryIdx + ']" value="1"> Externe'
|
||||
+ '</label>'
|
||||
+ '<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>';
|
||||
list.appendChild(div);
|
||||
juryIdx++;
|
||||
}
|
||||
function removeJuryRow(btn) {
|
||||
btn.closest('.admin-jury-entry').remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label" for="license_id">Licence :</label>
|
||||
@@ -334,6 +457,25 @@ try {
|
||||
value="<?= htmlspecialchars($thesis['baiu_link'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<!-- Image bannière -->
|
||||
<div class="admin-form-row" style="align-items:start;">
|
||||
<label class="admin-label">Image bannière (accueil) :</label>
|
||||
<div>
|
||||
<?php if (!empty($thesis['banner_path'])): ?>
|
||||
<div style="margin-bottom:.5rem;">
|
||||
<img src="/media.php?path=<?= urlencode($thesis['banner_path']) ?>"
|
||||
alt="Bannière actuelle"
|
||||
style="max-width:320px;max-height:100px;object-fit:cover;border:1px solid #444;">
|
||||
<label class="admin-checkbox-label" style="margin-top:.35rem;display:block;">
|
||||
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="banner" accept="image/jpeg,image/png,image/webp">
|
||||
<p class="admin-hint">JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<label class="admin-label">Publication :</label>
|
||||
<label class="admin-checkbox-label">
|
||||
|
||||
Reference in New Issue
Block a user