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:
114
TODO.md
114
TODO.md
@@ -113,8 +113,7 @@ no seed data yet.
|
|||||||
|
|
||||||
#### 3f — View update
|
#### 3f — View update
|
||||||
|
|
||||||
- [ ] **`storage/schema.sql`** — update `v_theses_full` to expose `t.license_id` raw FK
|
- [x] **`storage/schema.sql`** — `v_theses_full` now exposes `t.license_id` raw FK (done as part of 004/005 view rebuild)
|
||||||
(edit form currently queries theses directly — lower priority)
|
|
||||||
|
|
||||||
#### 3g — TFE public page
|
#### 3g — TFE public page
|
||||||
|
|
||||||
@@ -133,90 +132,43 @@ internal/external).
|
|||||||
Current state: `supervisors` table + `thesis_supervisors` junction with a bare
|
Current state: `supervisors` table + `thesis_supervisors` junction with a bare
|
||||||
`supervisor_order` integer. No role distinction.
|
`supervisor_order` integer. No role distinction.
|
||||||
|
|
||||||
- [ ] **`storage/migrations/004_jury_roles.sql`**:
|
- [x] **`storage/migrations/004_jury_roles.sql`**:
|
||||||
- `ALTER TABLE thesis_supervisors ADD COLUMN role TEXT NOT NULL DEFAULT 'promoteur'`
|
- `ALTER TABLE thesis_supervisors ADD COLUMN role TEXT NOT NULL DEFAULT 'promoteur'`
|
||||||
— role values: `'president'`, `'promoteur'`, `'lecteur'`
|
— role values: `'president'`, `'promoteur'`, `'lecteur'`
|
||||||
- `ALTER TABLE thesis_supervisors ADD COLUMN is_external INTEGER NOT NULL DEFAULT 0`
|
- `ALTER TABLE thesis_supervisors ADD COLUMN is_external INTEGER NOT NULL DEFAULT 0`
|
||||||
— 1 = external, 0 = internal (replaces the old "promoteur externe" free-text field)
|
— 1 = external, 0 = internal (replaces the old "promoteur externe" free-text field)
|
||||||
- No data loss: existing rows get `role = 'promoteur'`, `is_external = 0`
|
- No data loss: existing rows get `role = 'promoteur'`, `is_external = 0`
|
||||||
- [ ] **`storage/schema.sql`** — add the two new columns to `thesis_supervisors`
|
- [x] **`storage/schema.sql`** — add the two new columns to `thesis_supervisors`
|
||||||
definition; update `v_theses_full` to expose jury members grouped by role:
|
definition; update `v_theses_full` to expose jury members grouped by role:
|
||||||
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='president' THEN s.name END) as jury_president`
|
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='president' THEN s.name END) as jury_president`
|
||||||
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='promoteur' THEN s.name END) as promoteurs`
|
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='promoteur' THEN s.name END) as jury_promoteurs`
|
||||||
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='lecteur' THEN s.name END) as lecteurs`
|
- `GROUP_CONCAT(DISTINCT CASE WHEN ts.role='lecteur' THEN s.name END) as jury_lecteurs`
|
||||||
- Keep the existing `supervisors` column (all names) for backwards compat
|
- Keep the existing `supervisors` column (all names) for backwards compat
|
||||||
- Migration SQL must DROP + CREATE the view
|
- Migration SQL must DROP + CREATE the view
|
||||||
|
|
||||||
#### 4b — `src/Database.php`
|
#### 4b — `src/Database.php`
|
||||||
|
|
||||||
- [ ] `getThesisJury(int $thesisId): array` — fetch all supervisors for a thesis with
|
- [x] `getThesisJury(int $thesisId): array` — fetch all supervisors for a thesis with
|
||||||
their role and is_external flag:
|
their role and is_external flag
|
||||||
```sql
|
- [x] `setThesisJury(int $thesisId, array $juryMembers): void` — delete + re-insert
|
||||||
SELECT s.id, s.name, ts.role, ts.is_external, ts.supervisor_order
|
|
||||||
FROM thesis_supervisors ts
|
|
||||||
JOIN supervisors s ON s.id = ts.supervisor_id
|
|
||||||
WHERE ts.thesis_id = ?
|
|
||||||
ORDER BY ts.supervisor_order
|
|
||||||
```
|
|
||||||
- [ ] `setThesisJury(int $thesisId, array $juryMembers): void` — within a transaction:
|
|
||||||
1. `DELETE FROM thesis_supervisors WHERE thesis_id = ?`
|
|
||||||
2. For each member `[name, role, is_external]`:
|
|
||||||
- `findOrCreateSupervisor($name)`
|
|
||||||
- `INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, supervisor_order)`
|
|
||||||
|
|
||||||
#### 4c — Add form (`public/admin/add.php`)
|
#### 4c — Add form (`public/admin/add.php`)
|
||||||
|
|
||||||
Remove the following existing fields:
|
- [x] Remove `promoteurice` and `promoteurice_externe` fields
|
||||||
- [ ] Remove `promoteurice` (single internal promoter text input)
|
- [x] Add **"Composition du jury"** fieldset: président·e, promoteur·ice + externe checkbox, dynamic lecteur·ices list with JS add/remove
|
||||||
- [ ] Remove `promoteurice_externe` (single external promoter text input)
|
|
||||||
|
|
||||||
Add a new **"Composition du jury"** fieldset section:
|
|
||||||
- [ ] **Président·e** — single text input `name="jury_president"` (one person, always internal)
|
|
||||||
- [ ] **Promoteur·ice** — single text input `name="jury_promoteur"` with checkbox
|
|
||||||
`name="jury_promoteur_ext"` value="1" → marks as external
|
|
||||||
- [ ] **Lecteur·ices** — dynamic list of up to N entries; each row has:
|
|
||||||
- text input `name="jury_lecteurs[]"` (person name)
|
|
||||||
- checkbox `name="jury_lecteurs_ext[]"` with matching index → "Externe"
|
|
||||||
- "Ajouter un·e lecteur·ice" button (JS adds a new row)
|
|
||||||
- "Supprimer" button per row (JS removes row)
|
|
||||||
- Minimum 0 lecteurs (field is optional)
|
|
||||||
|
|
||||||
#### 4d — Add action (`public/admin/actions/formulaire.php`)
|
#### 4d — Add action (`public/admin/actions/formulaire.php`)
|
||||||
|
|
||||||
- [ ] Remove parsing of `$_POST['promoteurice']` and `$_POST['promoteurice_externe']`
|
- [x] Removed old promoteurice parsing; parse jury fields; call `$db->setThesisJury()`
|
||||||
- [ ] Parse new jury fields:
|
|
||||||
```php
|
|
||||||
$juryMembers = [];
|
|
||||||
if (!empty($_POST['jury_president'])) {
|
|
||||||
$juryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0];
|
|
||||||
}
|
|
||||||
if (!empty($_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) {
|
|
||||||
if (!empty(trim($name))) {
|
|
||||||
$juryMembers[] = ['name' => trim($name), 'role' => 'lecteur',
|
|
||||||
'is_external' => isset($_POST['jury_lecteurs_ext'][$i]) ? 1 : 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [ ] Replace the supervisor insertion loop with `$db->setThesisJury($thesisId, $juryMembers)`
|
|
||||||
|
|
||||||
#### 4e — Edit form (`public/admin/edit.php`)
|
#### 4e — Edit form (`public/admin/edit.php`)
|
||||||
|
|
||||||
- [ ] Load `$jury = $db->getThesisJury($thesisId)` alongside other data
|
- [x] Load `$jury = $db->getThesisJury($thesisId)`; jury fieldset pre-populated from DB
|
||||||
- [ ] Replace old `promoteurice` text input with the same **"Composition du jury"**
|
- [x] POST handler calls `$db->setThesisJury()`
|
||||||
fieldset as in add.php; pre-populate fields from `$jury` array
|
|
||||||
- [ ] In POST handler: remove old supervisor logic; call `$db->setThesisJury()`
|
|
||||||
|
|
||||||
#### 4f — TFE public page (`public/tfe.php`)
|
#### 4f — TFE public page (`public/tfe.php`)
|
||||||
|
|
||||||
- [ ] Replace "Promoteur·ice interne :" single meta row with three conditional rows:
|
- [x] Three conditional jury rows: Président·e, Promoteur·ice, Lecteur·ices
|
||||||
- "Président·e du jury :" — `$data['jury_president']` (from updated view)
|
|
||||||
- "Promoteur·ice :" — `$data['promoteurs']` (from updated view)
|
|
||||||
- "Lecteur·ices :" — `$data['lecteurs']` (from updated view)
|
|
||||||
- Each row hidden if null/empty
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -229,48 +181,26 @@ home grid (wider, shorter aspect ratio).
|
|||||||
|
|
||||||
#### 5a — Schema / DB
|
#### 5a — Schema / DB
|
||||||
|
|
||||||
- [ ] **`storage/migrations/005_add_banner.sql`**:
|
- [x] **`storage/migrations/005_add_banner.sql`** — `ALTER TABLE theses ADD COLUMN banner_path TEXT`
|
||||||
- `ALTER TABLE theses ADD COLUMN banner_path TEXT` — stores path relative to
|
- [x] **`storage/schema.sql`** — `banner_path TEXT` in `theses`; `t.banner_path` in view
|
||||||
`STORAGE_ROOT`, same convention as `thesis_files.file_path`; NULL if no banner
|
|
||||||
- [ ] **`storage/schema.sql`** — add `banner_path TEXT` column to `theses` definition;
|
|
||||||
update `v_theses_full` to `SELECT t.banner_path`
|
|
||||||
|
|
||||||
#### 5b — Add form (`public/admin/add.php`)
|
#### 5b — Add form (`public/admin/add.php`)
|
||||||
|
|
||||||
- [ ] Add "Image bannière (page d'accueil)" file input row:
|
- [x] "Image bannière" file input added after couverture row
|
||||||
```html
|
|
||||||
<input type="file" name="banner" accept="image/jpeg,image/png,image/webp">
|
|
||||||
```
|
|
||||||
- [ ] Hint: "JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB."
|
|
||||||
- [ ] Placed after the existing "Image de couverture" row
|
|
||||||
|
|
||||||
#### 5c — Add action (`public/admin/actions/formulaire.php`)
|
#### 5c — Add action (`public/admin/actions/formulaire.php`)
|
||||||
|
|
||||||
- [ ] Process `$_FILES['banner']` similarly to `$_FILES['couverture']`:
|
- [x] Banner upload: MIME check, 5 MB cap, save to `banners/`, call `setBannerPath()`
|
||||||
- Allowed MIME: `image/jpeg`, `image/png`, `image/webp`
|
|
||||||
- Max size: 5 MB
|
|
||||||
- Save to `STORAGE_ROOT . "/banners/"` with a random hex filename
|
|
||||||
- Path stored as `"banners/" . $safeFileName`
|
|
||||||
- [ ] After thesis INSERT: `UPDATE theses SET banner_path = ? WHERE id = ?`
|
|
||||||
(or include `banner_path` in the initial INSERT column list)
|
|
||||||
|
|
||||||
#### 5d — Edit form (`public/admin/edit.php`)
|
#### 5d — Edit form (`public/admin/edit.php`)
|
||||||
|
|
||||||
- [ ] Display existing banner as `<img>` preview if `$thesis['banner_path']` is set
|
- [x] Banner preview img shown; remove_banner checkbox; new banner upload input
|
||||||
(served via `/media.php?path=…`)
|
- [x] POST handler: unlinks old file on remove; processes new upload via `setBannerPath()`
|
||||||
- [ ] Add file input `name="banner"` to replace/upload new banner
|
|
||||||
- [ ] Add checkbox `name="remove_banner"` to clear the current banner
|
|
||||||
- [ ] In POST handler:
|
|
||||||
- If `remove_banner` checked: `UPDATE theses SET banner_path = NULL WHERE id = ?`;
|
|
||||||
also unlink the file
|
|
||||||
- If new file uploaded: process as in 5c; update `banner_path`
|
|
||||||
|
|
||||||
#### 5e — `src/Database.php`
|
#### 5e — `src/Database.php`
|
||||||
|
|
||||||
- [ ] `getThesis()` and `getThesisById()` — already return all columns from the view;
|
- [x] `banner_path` exposed via view — verified; `getThesisById()` / `getPublishedTheses()` pick it up automatically
|
||||||
after adding `banner_path` to the view, they automatically expose it — verify
|
- [x] `setBannerPath(int $thesisId, ?string $path): void` added
|
||||||
- [ ] `getPublishedTheses()` — same: view-sourced, automatic after view update
|
|
||||||
- [ ] Optionally add `setBannerPath(int $thesisId, ?string $path): void` for clarity
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,22 @@ try {
|
|||||||
$problematique = sanitize_string($_POST["problématique"] ?? '');
|
$problematique = sanitize_string($_POST["problématique"] ?? '');
|
||||||
$durationInfo = sanitize_string($_POST["duration_info"] ?? '');
|
$durationInfo = sanitize_string($_POST["duration_info"] ?? '');
|
||||||
|
|
||||||
// Supervisor(s)
|
// Jury members
|
||||||
$promoteuriceRaw = sanitize_string($_POST["promoteurice"] ?? '');
|
$juryMembers = [];
|
||||||
$supervisorNames = !empty($promoteuriceRaw) ? array_map('trim', explode(',', $promoteuriceRaw)) : [];
|
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)
|
// Keywords (max 10)
|
||||||
$tagRaw = sanitize_string($_POST["tag"] ?? '');
|
$tagRaw = sanitize_string($_POST["tag"] ?? '');
|
||||||
@@ -119,6 +132,7 @@ try {
|
|||||||
|
|
||||||
// File uploads
|
// File uploads
|
||||||
$couverture = $_FILES["couverture"] ?? null;
|
$couverture = $_FILES["couverture"] ?? null;
|
||||||
|
$bannerFile = $_FILES["banner"] ?? null;
|
||||||
$files = $_FILES["files"] ?? null;
|
$files = $_FILES["files"] ?? null;
|
||||||
|
|
||||||
// ===== CREATE OR FIND AUTHOR =====
|
// ===== 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 = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)");
|
||||||
$stmt->execute([$thesisId, $authorId]);
|
$stmt->execute([$thesisId, $authorId]);
|
||||||
|
|
||||||
// ===== LINK SUPERVISORS TO THESIS =====
|
// ===== LINK JURY TO THESIS =====
|
||||||
foreach ($supervisorNames as $index => $supervisorName) {
|
$db->setThesisJury($thesisId, $juryMembers);
|
||||||
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 LANGUAGES TO THESIS =====
|
// ===== LINK LANGUAGES TO THESIS =====
|
||||||
foreach ($languageIds as $languageId) {
|
foreach ($languageIds as $languageId) {
|
||||||
@@ -201,7 +209,8 @@ try {
|
|||||||
// Create necessary directories — outside the webroot (security items #3 & #4).
|
// Create necessary directories — outside the webroot (security items #3 & #4).
|
||||||
// Files are served through /media.php, never directly via a URL path.
|
// Files are served through /media.php, never directly via a URL path.
|
||||||
$uploadBaseDir = STORAGE_ROOT . "/theses/{$annee}/{$identifier}/";
|
$uploadBaseDir = STORAGE_ROOT . "/theses/{$annee}/{$identifier}/";
|
||||||
$coverDir = STORAGE_ROOT . "/covers/";
|
$coverDir = STORAGE_ROOT . "/covers/";
|
||||||
|
$bannerDir = STORAGE_ROOT . "/banners/";
|
||||||
|
|
||||||
if (!file_exists($uploadBaseDir)) {
|
if (!file_exists($uploadBaseDir)) {
|
||||||
mkdir($uploadBaseDir, 0755, true);
|
mkdir($uploadBaseDir, 0755, true);
|
||||||
@@ -209,6 +218,9 @@ try {
|
|||||||
if (!file_exists($coverDir)) {
|
if (!file_exists($coverDir)) {
|
||||||
mkdir($coverDir, 0755, true);
|
mkdir($coverDir, 0755, true);
|
||||||
}
|
}
|
||||||
|
if (!file_exists($bannerDir)) {
|
||||||
|
mkdir($bannerDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Define security constraints
|
// Define security constraints
|
||||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip'];
|
$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
|
// Process thesis files
|
||||||
if ($files && is_array($files["name"])) {
|
if ($files && is_array($files["name"])) {
|
||||||
for ($i = 0; $i < count($files["name"]); $i++) {
|
for ($i = 0; $i < count($files["name"]); $i++) {
|
||||||
|
|||||||
@@ -79,19 +79,67 @@ function wasSelected($key, $value) {
|
|||||||
value="<?= old('mail') ?>">
|
value="<?= old('mail') ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Promoteur interne -->
|
<!-- Composition du jury -->
|
||||||
<div class="admin-form-row">
|
<fieldset class="admin-fieldset">
|
||||||
<label class="admin-label" for="promoteurice">Promoteur·ice interne :</label>
|
<legend class="admin-fieldset-legend">Composition du jury</legend>
|
||||||
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
|
|
||||||
value="<?= old('promoteurice') ?>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Promoteur externe -->
|
<!-- Président·e -->
|
||||||
<div class="admin-form-row">
|
<div class="admin-form-row">
|
||||||
<label class="admin-label" for="promoteurice_externe">Promoteur·ice externe :</label>
|
<label class="admin-label" for="jury_president">Président·e :</label>
|
||||||
<input class="admin-input" type="text" id="promoteurice_externe" name="promoteurice_externe"
|
<input class="admin-input" type="text" id="jury_president" name="jury_president"
|
||||||
value="<?= old('promoteurice_externe') ?>">
|
value="<?= old('jury_president') ?>"
|
||||||
</div>
|
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 -->
|
<!-- Année -->
|
||||||
<div class="admin-form-row">
|
<div class="admin-form-row">
|
||||||
@@ -234,6 +282,15 @@ function wasSelected($key, $value) {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Fichiers -->
|
||||||
<div class="admin-form-row" style="align-items:start;">
|
<div class="admin-form-row" style="align-items:start;">
|
||||||
<label class="admin-label">Fichiers du TFE :</label>
|
<label class="admin-label">Fichiers du TFE :</label>
|
||||||
|
|||||||
@@ -84,19 +84,23 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update supervisors
|
// Update jury
|
||||||
$pdo->prepare("DELETE FROM thesis_supervisors WHERE thesis_id = ?")->execute([$thesisId]);
|
$editJuryMembers = [];
|
||||||
$supervisorsRaw = trim($_POST['promoteurice'] ?? '');
|
if (!empty(trim($_POST['jury_president'] ?? ''))) {
|
||||||
if (!empty($supervisorsRaw)) {
|
$editJuryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0];
|
||||||
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
|
}
|
||||||
foreach ($supervisors as $index => $supervisorName) {
|
if (!empty(trim($_POST['jury_promoteur'] ?? ''))) {
|
||||||
if (!empty($supervisorName)) {
|
$editJuryMembers[] = ['name' => trim($_POST['jury_promoteur']), 'role' => 'promoteur',
|
||||||
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
|
'is_external' => isset($_POST['jury_promoteur_ext']) ? 1 : 0];
|
||||||
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
|
}
|
||||||
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
|
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
|
// Update languages
|
||||||
$pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]);
|
$pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]);
|
||||||
@@ -134,6 +138,38 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$db->commit();
|
$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!";
|
$success = "TFE mis à jour avec succès!";
|
||||||
|
|
||||||
// Regenerate CSRF token
|
// Regenerate CSRF token
|
||||||
@@ -162,6 +198,9 @@ try {
|
|||||||
$stmt->execute([$thesisId]);
|
$stmt->execute([$thesisId]);
|
||||||
$currentFormats = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
$currentFormats = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Load jury
|
||||||
|
$jury = $db->getThesisJury($thesisId);
|
||||||
|
|
||||||
// Load reference data
|
// Load reference data
|
||||||
$orientations = $db->getAllOrientations();
|
$orientations = $db->getAllOrientations();
|
||||||
$apPrograms = $db->getAllAPPrograms();
|
$apPrograms = $db->getAllAPPrograms();
|
||||||
@@ -195,7 +234,7 @@ try {
|
|||||||
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($success) ?></div>
|
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($success) ?></div>
|
||||||
<?php endif; ?>
|
<?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']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
|
||||||
<div class="admin-form-row">
|
<div class="admin-form-row">
|
||||||
@@ -251,11 +290,95 @@ try {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-form-row">
|
<!-- Composition du jury -->
|
||||||
<label class="admin-label" for="promoteurice">Promoteur·ice(s) :</label>
|
<?php
|
||||||
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
|
// Pre-split jury by role for easy pre-population
|
||||||
value="<?= htmlspecialchars($thesis['supervisors'] ?? '') ?>">
|
$juryPresident = null;
|
||||||
</div>
|
$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">
|
<div class="admin-form-row">
|
||||||
<label class="admin-label" for="license_id">Licence :</label>
|
<label class="admin-label" for="license_id">Licence :</label>
|
||||||
@@ -334,6 +457,25 @@ try {
|
|||||||
value="<?= htmlspecialchars($thesis['baiu_link'] ?? '') ?>">
|
value="<?= htmlspecialchars($thesis['baiu_link'] ?? '') ?>">
|
||||||
</div>
|
</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">
|
<div class="admin-form-row">
|
||||||
<label class="admin-label">Publication :</label>
|
<label class="admin-label">Publication :</label>
|
||||||
<label class="admin-checkbox-label">
|
<label class="admin-checkbox-label">
|
||||||
|
|||||||
@@ -584,3 +584,60 @@ html, body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Jury fieldset */
|
||||||
|
.admin-fieldset {
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin: .5rem 0 1rem;
|
||||||
|
background: rgba(255,255,255,.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-fieldset-legend {
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
padding: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-jury-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-jury-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-jury-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-jury-ext {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-remove {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #555;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: .2rem .45rem;
|
||||||
|
font-size: .8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.admin-btn-remove:hover {
|
||||||
|
border-color: #e55;
|
||||||
|
color: #e55;
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,10 +107,24 @@ $currentNav = '';
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($data['supervisors'])): ?>
|
<?php if (!empty($data['jury_president'])): ?>
|
||||||
<div class="tfe-meta-item">
|
<div class="tfe-meta-item">
|
||||||
<span class="label">Promoteur·ice interne :</span>
|
<span class="label">Président·e du jury :</span>
|
||||||
<span class="value"><?= htmlspecialchars($data['supervisors']) ?></span>
|
<span class="value"><?= htmlspecialchars($data['jury_president']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($data['jury_promoteurs'])): ?>
|
||||||
|
<div class="tfe-meta-item">
|
||||||
|
<span class="label">Promoteur·ice :</span>
|
||||||
|
<span class="value"><?= htmlspecialchars($data['jury_promoteurs']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($data['jury_lecteurs'])): ?>
|
||||||
|
<div class="tfe-meta-item">
|
||||||
|
<span class="label">Lecteur·ices :</span>
|
||||||
|
<span class="value"><?= htmlspecialchars($data['jury_lecteurs']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
@@ -731,6 +731,60 @@ class Database {
|
|||||||
return $this->getLicenseTypes();
|
return $this->getLicenseTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// JURY METHODS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all jury members for a thesis, with role and is_external flag.
|
||||||
|
*/
|
||||||
|
public function getThesisJury(int $thesisId): array {
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT s.id, s.name, ts.role, ts.is_external, ts.supervisor_order
|
||||||
|
FROM thesis_supervisors ts
|
||||||
|
JOIN supervisors s ON s.id = ts.supervisor_id
|
||||||
|
WHERE ts.thesis_id = ?
|
||||||
|
ORDER BY ts.supervisor_order
|
||||||
|
");
|
||||||
|
$stmt->execute([$thesisId]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the entire jury for a thesis in a single transaction.
|
||||||
|
* $juryMembers: array of ['name' => string, 'role' => string, 'is_external' => int]
|
||||||
|
*/
|
||||||
|
public function setThesisJury(int $thesisId, array $juryMembers): void {
|
||||||
|
$this->pdo->prepare("DELETE FROM thesis_supervisors WHERE thesis_id = ?")->execute([$thesisId]);
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, supervisor_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
foreach ($juryMembers as $order => $member) {
|
||||||
|
$name = trim($member['name'] ?? '');
|
||||||
|
if ($name === '') continue;
|
||||||
|
$supervisorId = $this->findOrCreateSupervisor($name);
|
||||||
|
$role = in_array($member['role'], ['president', 'promoteur', 'lecteur'])
|
||||||
|
? $member['role'] : 'promoteur';
|
||||||
|
$isExternal = isset($member['is_external']) ? (int)$member['is_external'] : 0;
|
||||||
|
$stmt->execute([$thesisId, $supervisorId, $role, $isExternal, $order + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// BANNER METHODS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (or clear) the banner_path for a thesis.
|
||||||
|
*/
|
||||||
|
public function setBannerPath(int $thesisId, ?string $path): void {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"UPDATE theses SET banner_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$path, $thesisId]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a thesis file record
|
* Insert a thesis file record
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[1772461679,1772461686]
|
[1774354709]
|
||||||
66
storage/migrations/004_jury_roles.sql
Normal file
66
storage/migrations/004_jury_roles.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- Migration 004: Add jury role and is_external columns to thesis_supervisors
|
||||||
|
-- Existing rows get role = 'promoteur', is_external = 0 (no data loss)
|
||||||
|
|
||||||
|
ALTER TABLE thesis_supervisors ADD COLUMN role TEXT NOT NULL DEFAULT 'promoteur';
|
||||||
|
ALTER TABLE thesis_supervisors ADD COLUMN is_external INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Recreate v_theses_full to include jury role columns
|
||||||
|
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,
|
||||||
|
o.name as orientation,
|
||||||
|
ap.name as ap_program,
|
||||||
|
ft.name as finality_type,
|
||||||
|
t.synopsis,
|
||||||
|
t.context_note,
|
||||||
|
t.duration_minutes,
|
||||||
|
t.duration_pages,
|
||||||
|
t.file_size_info,
|
||||||
|
at.name as access_type,
|
||||||
|
lt.name as license_type,
|
||||||
|
t.license_id,
|
||||||
|
t.jury_points,
|
||||||
|
t.submitted_at,
|
||||||
|
t.defense_date,
|
||||||
|
t.published_at,
|
||||||
|
t.is_published,
|
||||||
|
t.baiu_link,
|
||||||
|
GROUP_CONCAT(DISTINCT a.name) 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' THEN s.name END) as jury_promoteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||||
|
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||||
|
GROUP_CONCAT(DISTINCT k.keyword) as keywords
|
||||||
|
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
|
||||||
|
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_keywords tk ON t.id = tk.thesis_id
|
||||||
|
LEFT JOIN keywords k ON tk.keyword_id = k.id
|
||||||
|
GROUP BY t.id;
|
||||||
|
|
||||||
|
-- Recreate public view
|
||||||
|
DROP VIEW IF EXISTS v_theses_public;
|
||||||
|
|
||||||
|
CREATE VIEW v_theses_public AS
|
||||||
|
SELECT * FROM v_theses_full
|
||||||
|
WHERE is_published = 1;
|
||||||
65
storage/migrations/005_add_banner.sql
Normal file
65
storage/migrations/005_add_banner.sql
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
-- Migration 005: Add banner_path column to theses for home page card thumbnails
|
||||||
|
|
||||||
|
ALTER TABLE theses ADD COLUMN banner_path TEXT;
|
||||||
|
|
||||||
|
-- Recreate v_theses_full to include banner_path
|
||||||
|
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,
|
||||||
|
o.name as orientation,
|
||||||
|
ap.name as ap_program,
|
||||||
|
ft.name as finality_type,
|
||||||
|
t.synopsis,
|
||||||
|
t.context_note,
|
||||||
|
t.duration_minutes,
|
||||||
|
t.duration_pages,
|
||||||
|
t.file_size_info,
|
||||||
|
at.name as access_type,
|
||||||
|
lt.name as license_type,
|
||||||
|
t.license_id,
|
||||||
|
t.jury_points,
|
||||||
|
t.submitted_at,
|
||||||
|
t.defense_date,
|
||||||
|
t.published_at,
|
||||||
|
t.is_published,
|
||||||
|
t.baiu_link,
|
||||||
|
t.banner_path,
|
||||||
|
GROUP_CONCAT(DISTINCT a.name) 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' THEN s.name END) as jury_promoteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||||
|
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||||
|
GROUP_CONCAT(DISTINCT k.keyword) as keywords
|
||||||
|
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
|
||||||
|
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_keywords tk ON t.id = tk.thesis_id
|
||||||
|
LEFT JOIN keywords k ON tk.keyword_id = k.id
|
||||||
|
GROUP BY t.id;
|
||||||
|
|
||||||
|
-- Recreate public view
|
||||||
|
DROP VIEW IF EXISTS v_theses_public;
|
||||||
|
|
||||||
|
CREATE VIEW v_theses_public AS
|
||||||
|
SELECT * FROM v_theses_full
|
||||||
|
WHERE is_published = 1;
|
||||||
@@ -191,6 +191,9 @@ CREATE TABLE IF NOT EXISTS theses (
|
|||||||
-- External links
|
-- External links
|
||||||
baiu_link TEXT, -- Link to institutional repository
|
baiu_link TEXT, -- Link to institutional repository
|
||||||
|
|
||||||
|
-- Home page card banner (optional, landscape image)
|
||||||
|
banner_path TEXT, -- path relative to STORAGE_ROOT (e.g. "banners/abc.jpg")
|
||||||
|
|
||||||
-- Timestamps
|
-- Timestamps
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -217,11 +220,13 @@ CREATE TABLE IF NOT EXISTS thesis_authors (
|
|||||||
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
|
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Supervisors per thesis (can have multiple promoters)
|
-- Supervisors per thesis (jury: president, promoteur, lecteurs)
|
||||||
CREATE TABLE IF NOT EXISTS thesis_supervisors (
|
CREATE TABLE IF NOT EXISTS thesis_supervisors (
|
||||||
thesis_id INTEGER NOT NULL,
|
thesis_id INTEGER NOT NULL,
|
||||||
supervisor_id INTEGER NOT NULL,
|
supervisor_id INTEGER NOT NULL,
|
||||||
supervisor_order INTEGER DEFAULT 1,
|
supervisor_order INTEGER DEFAULT 1,
|
||||||
|
role TEXT NOT NULL DEFAULT 'promoteur', -- 'president'|'promoteur'|'lecteur'
|
||||||
|
is_external INTEGER NOT NULL DEFAULT 0, -- 0 = internal, 1 = external
|
||||||
PRIMARY KEY (thesis_id, supervisor_id),
|
PRIMARY KEY (thesis_id, supervisor_id),
|
||||||
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
|
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (supervisor_id) REFERENCES supervisors(id) ON DELETE CASCADE
|
FOREIGN KEY (supervisor_id) REFERENCES supervisors(id) ON DELETE CASCADE
|
||||||
@@ -360,14 +365,19 @@ SELECT
|
|||||||
t.file_size_info,
|
t.file_size_info,
|
||||||
at.name as access_type,
|
at.name as access_type,
|
||||||
lt.name as license_type,
|
lt.name as license_type,
|
||||||
|
t.license_id,
|
||||||
t.jury_points,
|
t.jury_points,
|
||||||
t.submitted_at,
|
t.submitted_at,
|
||||||
t.defense_date,
|
t.defense_date,
|
||||||
t.published_at,
|
t.published_at,
|
||||||
t.is_published,
|
t.is_published,
|
||||||
t.baiu_link,
|
t.baiu_link,
|
||||||
|
t.banner_path,
|
||||||
GROUP_CONCAT(DISTINCT a.name) as authors,
|
GROUP_CONCAT(DISTINCT a.name) as authors,
|
||||||
GROUP_CONCAT(DISTINCT s.name) as supervisors,
|
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' THEN s.name END) as jury_promoteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs,
|
||||||
GROUP_CONCAT(DISTINCT l.name) as languages,
|
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||||
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||||
GROUP_CONCAT(DISTINCT k.keyword) as keywords
|
GROUP_CONCAT(DISTINCT k.keyword) as keywords
|
||||||
|
|||||||
BIN
storage/test.db
BIN
storage/test.db
Binary file not shown.
Reference in New Issue
Block a user