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
|
||||
|
||||
- [ ] **`storage/schema.sql`** — update `v_theses_full` to expose `t.license_id` raw FK
|
||||
(edit form currently queries theses directly — lower priority)
|
||||
- [x] **`storage/schema.sql`** — `v_theses_full` now exposes `t.license_id` raw FK (done as part of 004/005 view rebuild)
|
||||
|
||||
#### 3g — TFE public page
|
||||
|
||||
@@ -133,90 +132,43 @@ internal/external).
|
||||
Current state: `supervisors` table + `thesis_supervisors` junction with a bare
|
||||
`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'`
|
||||
— role values: `'president'`, `'promoteur'`, `'lecteur'`
|
||||
- `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)
|
||||
- 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:
|
||||
- `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='lecteur' THEN s.name END) as lecteurs`
|
||||
- `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`
|
||||
- Keep the existing `supervisors` column (all names) for backwards compat
|
||||
- Migration SQL must DROP + CREATE the view
|
||||
|
||||
#### 4b — `src/Database.php`
|
||||
|
||||
- [ ] `getThesisJury(int $thesisId): array` — fetch all supervisors for a thesis with
|
||||
their role and is_external flag:
|
||||
```sql
|
||||
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)`
|
||||
- [x] `getThesisJury(int $thesisId): array` — fetch all supervisors for a thesis with
|
||||
their role and is_external flag
|
||||
- [x] `setThesisJury(int $thesisId, array $juryMembers): void` — delete + re-insert
|
||||
|
||||
#### 4c — Add form (`public/admin/add.php`)
|
||||
|
||||
Remove the following existing fields:
|
||||
- [ ] Remove `promoteurice` (single internal promoter text input)
|
||||
- [ ] 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)
|
||||
- [x] Remove `promoteurice` and `promoteurice_externe` fields
|
||||
- [x] Add **"Composition du jury"** fieldset: président·e, promoteur·ice + externe checkbox, dynamic lecteur·ices list with JS add/remove
|
||||
|
||||
#### 4d — Add action (`public/admin/actions/formulaire.php`)
|
||||
|
||||
- [ ] Remove parsing of `$_POST['promoteurice']` and `$_POST['promoteurice_externe']`
|
||||
- [ ] 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)`
|
||||
- [x] Removed old promoteurice parsing; parse jury fields; call `$db->setThesisJury()`
|
||||
|
||||
#### 4e — Edit form (`public/admin/edit.php`)
|
||||
|
||||
- [ ] Load `$jury = $db->getThesisJury($thesisId)` alongside other data
|
||||
- [ ] Replace old `promoteurice` text input with the same **"Composition du jury"**
|
||||
fieldset as in add.php; pre-populate fields from `$jury` array
|
||||
- [ ] In POST handler: remove old supervisor logic; call `$db->setThesisJury()`
|
||||
- [x] Load `$jury = $db->getThesisJury($thesisId)`; jury fieldset pre-populated from DB
|
||||
- [x] POST handler calls `$db->setThesisJury()`
|
||||
|
||||
#### 4f — TFE public page (`public/tfe.php`)
|
||||
|
||||
- [ ] Replace "Promoteur·ice interne :" single meta row with three conditional rows:
|
||||
- "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
|
||||
- [x] Three conditional jury rows: Président·e, Promoteur·ice, Lecteur·ices
|
||||
|
||||
---
|
||||
|
||||
@@ -229,48 +181,26 @@ home grid (wider, shorter aspect ratio).
|
||||
|
||||
#### 5a — Schema / DB
|
||||
|
||||
- [ ] **`storage/migrations/005_add_banner.sql`**:
|
||||
- `ALTER TABLE theses ADD COLUMN banner_path TEXT` — stores path relative to
|
||||
`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`
|
||||
- [x] **`storage/migrations/005_add_banner.sql`** — `ALTER TABLE theses ADD COLUMN banner_path TEXT`
|
||||
- [x] **`storage/schema.sql`** — `banner_path TEXT` in `theses`; `t.banner_path` in view
|
||||
|
||||
#### 5b — Add form (`public/admin/add.php`)
|
||||
|
||||
- [ ] Add "Image bannière (page d'accueil)" file input 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
|
||||
- [x] "Image bannière" file input added after couverture row
|
||||
|
||||
#### 5c — Add action (`public/admin/actions/formulaire.php`)
|
||||
|
||||
- [ ] Process `$_FILES['banner']` similarly to `$_FILES['couverture']`:
|
||||
- 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)
|
||||
- [x] Banner upload: MIME check, 5 MB cap, save to `banners/`, call `setBannerPath()`
|
||||
|
||||
#### 5d — Edit form (`public/admin/edit.php`)
|
||||
|
||||
- [ ] Display existing banner as `<img>` preview if `$thesis['banner_path']` is set
|
||||
(served via `/media.php?path=…`)
|
||||
- [ ] 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`
|
||||
- [x] Banner preview img shown; remove_banner checkbox; new banner upload input
|
||||
- [x] POST handler: unlinks old file on remove; processes new upload via `setBannerPath()`
|
||||
|
||||
#### 5e — `src/Database.php`
|
||||
|
||||
- [ ] `getThesis()` and `getThesisById()` — already return all columns from the view;
|
||||
after adding `banner_path` to the view, they automatically expose it — verify
|
||||
- [ ] `getPublishedTheses()` — same: view-sourced, automatic after view update
|
||||
- [ ] Optionally add `setBannerPath(int $thesisId, ?string $path): void` for clarity
|
||||
- [x] `banner_path` exposed via view — verified; `getThesisById()` / `getPublishedTheses()` pick it up automatically
|
||||
- [x] `setBannerPath(int $thesisId, ?string $path): void` added
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user