Files
xamxam/app/templates/admin/edit.php
Pontoporeia a83dc1c74e feat: multi-type file upload with sort order, labels, and expanded MIME support
- DB migration 007: add sort_order + display_label to thesis_files
- Database: getThesisFiles ordered by sort_order; insertThesisFile accepts label/order;
  new reorderThesisFiles() and updateThesisFileLabel() methods
- ThesisCreateController + ThesisEditController: expand allowed MIME/exts to include
  audio (mp3/ogg/wav/flac/aac/m4a), video (webm/mov/ogv), image (gif/webp),
  archives (tar/gz), any-ext via octet-stream; max size raised to 500 MB;
  accept file_labels[] and file_orders[] POST fields; detectFileType() helper
- MediaController: expanded MIME allowlist; HTTP Range support for audio/video;
  force-download for unknown types; inline for known displayable types
- fieldset-files.php: sortable queue UI with SortableJS, per-file labels, 500 MB hint
- templates/admin/edit.php: existing files as sortable list with drag handles,
  type icons, label inputs, delete checkboxes, hidden sort-order fields
- file-upload-queue.js: new JS replacing file-preview.js — sortable new-file queue,
  per-file labels, hidden order fields on submit, backward-compat legacy preview
- tfe.php: renders audio (<audio>), all video formats, images, PDF, and
  download-only 'other' files; reads display_label; sorted by sort_order
- tfe.css + form.css: styles for audio player, download files, sortable queue,
  drag handles, file type badges, label inputs
- .htaccess + .user.ini: upload_max_filesize=512M / post_max_size=520M
2026-05-05 11:04:52 +02:00

245 lines
13 KiB
PHP

<main id="main-content">
<h1>Modifier un TFE</h1>
<form method="post" action="/admin/actions/edit.php" class="admin-form" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<?php
// Build a merged $formData that includes contact_public for the fieldset partial.
$formData = array_merge($formData ?? [], ['contact_public' => $currentAuthorShowContact ?? false]);
// Build old()-compatible callable from the already-loaded $thesis data.
$editOldFn = function (string $key, string $default = '') use ($thesis, $formData, $currentAuthorEmail) {
if (!empty($formData[$key])) return htmlspecialchars($formData[$key]);
$map = [
'titre' => htmlspecialchars($thesis['title']),
'subtitle' => htmlspecialchars($thesis['subtitle'] ?? ''),
'auteurice'=> htmlspecialchars($thesis['authors'] ?? ''),
'mail' => htmlspecialchars($currentAuthorEmail ?? ''),
'synopsis' => htmlspecialchars($thesis['synopsis'] ?? ''),
];
return $map[$key] ?? $default;
};
$editWithAutofocusFn = function (string $field, array $attrs = []) use ($autofocusField) {
if ($autofocusField === $field) $attrs['autofocus'] = true;
return $attrs;
};
$allowedObjet = [];
$synopsisExtra = '';
$oldFn = $editOldFn;
$withAutofocusFn = $editWithAutofocusFn;
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
// Restore $formData to original for downstream partials.
$formData = $_SESSION['form_data'] ?? [];
?>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
$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;
}
}
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
?>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php
$editFormData = [
'année' => $thesis['year'],
'orientation' => $thesis['orientation'],
'ap' => $thesis['ap_program'],
'finality' => $thesis['finality_type'],
'languages' => $currentLanguages,
'formats' => $currentFormats,
'tag' => $thesis['keywords'] ?? '',
];
$editAcademicOldFn = function (string $key, string $default = '') use ($editFormData) {
return isset($editFormData[$key]) && !is_array($editFormData[$key])
? htmlspecialchars((string)$editFormData[$key])
: $default;
};
$oldFn = $editAcademicOldFn;
$withAutofocusFn = $editWithAutofocusFn;
$formData = $editFormData;
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
?>
<!-- ═══════════════════ Fichiers ═══════════════════ -->
<fieldset>
<legend>Fichiers</legend>
<!-- Cover image -->
<div class="admin-form-group">
<label>Image de couverture :</label>
<div class="admin-file-input">
<?php if (!empty($currentCover)): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode($currentCover['file_path']) ?>"
alt="Couverture actuelle" style="max-height:180px;">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
</label>
</div>
<?php endif; ?>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png" data-preview="fp-couverture">
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentCover) ? 'JPG, PNG. Max 20 MB.' : 'Laisser vide pour conserver la couverture actuelle. JPG, PNG. Max 20 MB.' ?></small>
</div>
</div>
<!-- Existing thesis files — sortable, with labels -->
<?php $thesisFilesList = array_values(array_filter($currentFiles, fn($f) => $f['file_type'] !== 'cover')); ?>
<?php if (!empty($thesisFilesList)): ?>
<div class="admin-form-group">
<label>Fichiers du TFE existants :</label>
<small style="display:block;margin-bottom:var(--space-2xs);color:var(--text-tertiary)">
Glissez-déposez les lignes pour réordonner les fichiers sur la page publique.
</small>
<ul id="existing-files-sortable" class="admin-file-list sortable-list">
<?php foreach ($thesisFilesList as $f):
$fExt = strtolower(pathinfo($f['file_path'] ?? '', PATHINFO_EXTENSION));
$fType = $f['file_type'] ?? 'other';
$fIcon = match(true) {
$fType === 'main' || $fExt === 'pdf' => '📄',
in_array($fExt, ['jpg','jpeg','png','gif','webp']) => '🖼️',
$fType === 'video' || in_array($fExt, ['mp4','webm','mov','ogv']) => '🎬',
$fType === 'audio' || in_array($fExt, ['mp3','ogg','wav','flac','aac','m4a']) => '🔊',
$fType === 'caption' || $fExt === 'vtt' => '💬',
default => '📎',
};
?>
<li class="admin-file-list-item" data-file-id="<?= (int)$f['id'] ?>">
<!-- Hidden field carries sort order (updated by JS) -->
<input type="hidden" name="file_sort_order[]" value="<?= (int)$f['id'] ?>">
<span class="admin-file-drag-handle" title="Réordonner">⠿</span>
<span class="admin-file-icon-col"><?= $fIcon ?></span>
<span class="admin-file-info">
<a href="/media.php?path=<?= urlencode($f['file_path']) ?>" target="_blank" rel="noopener" class="admin-file-name">
<?= htmlspecialchars($f['file_name'] ?? basename($f['file_path'])) ?>
</a>
<span class="admin-file-meta-row">
<span class="admin-file-type-badge"><?= htmlspecialchars($fType) ?></span>
<?php if (!empty($f['file_size'])): ?>
<span class="admin-file-size"><?= number_format($f['file_size'] / 1024 / 1024, 2) ?> MB</span>
<?php endif; ?>
</span>
<input type="text"
name="file_label[<?= (int)$f['id'] ?>]"
value="<?= htmlspecialchars($f['display_label'] ?? '') ?>"
placeholder="Légende / description (optionnel)"
class="admin-file-label-input">
</span>
<label class="admin-checkbox-label admin-file-delete">
<input type="checkbox" name="delete_files[]" value="<?= (int)$f['id'] ?>">
Supprimer
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- New thesis files -->
<div class="admin-form-group admin-files-fieldgroup">
<label>Ajouter des fichiers du TFE :</label>
<div class="admin-file-input">
<input type="file" id="tfe-files-input"
name="files[]" multiple
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt"
class="tfe-file-picker">
<small class="admin-file-hint">
Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier.
</small>
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" aria-label="Nouveaux fichiers (réordonnable)"></ul>
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun nouveau fichier sélectionné.</p>
</div>
</div>
<!-- Banner image -->
<div class="admin-form-group">
<label>Image bannière (accueil) :</label>
<div class="admin-file-input">
<?php if (!empty($thesis['banner_path'])): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode($thesis['banner_path']) ?>"
alt="Bannière actuelle">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
</label>
</div>
<?php endif; ?>
<input type="file" name="banner" id="banner" accept="image/jpeg,image/png,image/webp" data-preview="fp-banner">
<div id="fp-banner" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($thesis['banner_path']) ? 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' : 'Laisser vide pour conserver la bannière actuelle. JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' ?></small>
</div>
</div>
</fieldset>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php
$editMetaFormData = [
'license_id' => $currentLicenseId,
'duration_info' => $thesis['file_size_info'] ?? '',
'lien' => $thesis['baiu_link'] ?? '',
'access_type_id' => $currentAccessTypeId,
];
$editMetaOldFn = function (string $key, string $default = '') use ($editMetaFormData) {
return isset($editMetaFormData[$key])
? htmlspecialchars((string)$editMetaFormData[$key])
: $default;
};
$oldFn = $editMetaOldFn;
$withAutofocusFn = $editWithAutofocusFn;
$formData = $editMetaFormData;
$defaultAccessTypeId = $currentAccessTypeId;
$showDescription = true;
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
?>
<!-- ═══════════════════ Note contextuelle ═══════════════════ -->
<fieldset>
<legend>Note contextuelle</legend>
<div>
<label for="context_note">Note contextuelle :</label>
<div>
<textarea id="context_note" name="context_note"
rows="4" maxlength="1500"><?= htmlspecialchars($currentContextNote ?? '') ?></textarea>
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
</div>
</div>
</fieldset>
<!-- ═══════════════════ Publication ════════════════════════ -->
<fieldset>
<legend>Publication</legend>
<div>
<label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1"
<?= $thesis['is_published'] ? 'checked' : '' ?>>
Publier ce TFE sur le site public
</label>
</div>
</fieldset>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/thanks.php?id=<?= $thesisId ?>" class="admin-btn-secondary admin-cancel-link">Annuler</a>
</div>
</form>
</main>