diff --git a/TODO.md b/TODO.md index 25941af..001d62a 100644 --- a/TODO.md +++ b/TODO.md @@ -11,4 +11,5 @@ - [x] Extract form CSS into `form.css`; load it in admin add/edit via `$extraCss` and in student partage form directly; `system.css` now only used by `system.php`; `partage/thanks.php` rewritten to use design-system classes - [x] Fix student form: add missing `v_smtp_active` view to `schema.sql` (SMTP was silently skipped on fresh installs); fix `thanks.php` redirect (was `/partage/thanks.php` — blocked by nginx PHP deny rule); route `/partage/thanks` through `index.php` special-case handler - [x] Merge all migration SQL into schema.sql; delete migrations/ folder; simplify migrate.sh (009 share_links, 014 ap_programs, 011 apropos seed, missing semicolon fix) +- [x] Add `objet` field (tfe/thèse/frart) to theses; `objet_restriction` on share_links; objet_these/frart_enabled site_settings; wire into partage form, parametres, and acces-etudiante - [x] Fix student form scroll (add `overflow-y:auto` to `.student-body`); move all remaining inline styles from partage error/password-gate pages into `form.css` diff --git a/app/public/admin/actions/acces-etudiante.php b/app/public/admin/actions/acces-etudiante.php index 498e3d4..1938a01 100644 --- a/app/public/admin/actions/acces-etudiante.php +++ b/app/public/admin/actions/acces-etudiante.php @@ -25,13 +25,14 @@ switch ($action) { $expiresRaw = !empty($_POST['expires_at']) ? trim($_POST['expires_at']) : null; $expiresAt = null; if ($expiresRaw) { - // datetime-local gives "YYYY-MM-DDTHH:MM" $expiresAt = date('Y-m-d H:i:s', strtotime($expiresRaw)); if ($expiresAt <= date('Y-m-d H:i:s')) { App::redirect('/admin/acces-etudiante.php', error: "La date d'expiration doit être dans le futur."); } } - $shareLink->create(1, $password, $expiresAt); + $objetRaw = $_POST['objet_restriction'] ?? ''; + $objetRestriction = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : null; + $shareLink->create(1, $password, $expiresAt, $objetRestriction); App::redirect('/admin/acces-etudiante.php', success: 'Lien d\'accès créé.'); break; diff --git a/app/public/admin/actions/settings.php b/app/public/admin/actions/settings.php index eedef5b..8d023b4 100644 --- a/app/public/admin/actions/settings.php +++ b/app/public/admin/actions/settings.php @@ -17,13 +17,16 @@ $db = new Database(); $section = $_POST['section'] ?? ''; if ($section === 'formulaire') { - // Save access-type toggle settings $allowed = ['access_type_libre_enabled', 'access_type_interne_enabled', 'access_type_interdit_enabled']; foreach ($allowed as $key) { $value = isset($_POST[$key]) ? '1' : '0'; $db->setSetting($key, $value); } App::flash('success', "Paramètres du formulaire mis à jour."); +} elseif ($section === 'objet_types') { + $db->setSetting('objet_these_enabled', isset($_POST['objet_these_enabled']) ? '1' : '0'); + $db->setSetting('objet_frart_enabled', isset($_POST['objet_frart_enabled']) ? '1' : '0'); + App::flash('success', "Types de travaux mis à jour."); } elseif ($section === 'smtp') { $smtpData = [ 'host' => $_POST['smtp_host'] ?? '', diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 267b755..e3b7dc1 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -185,6 +185,19 @@ function renderShareLinkForm(string $slug, array $link): void $formData = $_SESSION['form_data_share_' . $slug] ?? []; unset($_SESSION['form_data_share_' . $slug]); + // Determine allowed objet values for this link + $siteSettings = Database::getInstance()->getAllSettings(); + $objetRestriction = $link['objet_restriction'] ?? null; + if ($objetRestriction !== null) { + // Link is locked to one type — always show only that + $allowedObjet = [$objetRestriction]; + } else { + // Build from enabled site settings + $allowedObjet = ['tfe']; + if (($siteSettings['objet_these_enabled'] ?? '1') === '1') $allowedObjet[] = 'thèse'; + if (($siteSettings['objet_frart_enabled'] ?? '1') === '1') $allowedObjet[] = 'frart'; + } + // Generate a CSRF token specific to this share link (stored in session) $shareCsrfKey = 'share_csrf_' . $slug; if (empty($_SESSION[$shareCsrfKey])) { @@ -236,6 +249,23 @@ function renderShareLinkForm(string $slug, array $link): void
Informations du TFE + 1): ?> +
+ +
+ + + +
+
+ + + + 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index 7ae021a..ba0ba22 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -127,6 +127,7 @@ class ThesisCreateController 'baiu_link' => $data['lien'], 'license_id' => $data['licenseId'], 'access_type_id' => $data['accessTypeId'], + 'objet' => $data['objet'], 'author_id' => $authorId, ]); @@ -275,6 +276,10 @@ class ThesisCreateController $accessTypeId = 2; // Interne } + // Objet — restricted to valid values + $validObjet = ['tfe', 'thèse', 'frart']; + $objet = in_array($post['objet'] ?? '', $validObjet, true) ? $post['objet'] : 'tfe'; + // External link (optional) $lien = ''; if (!empty($post['lien'])) { @@ -298,7 +303,7 @@ class ThesisCreateController 'auteurName', 'mail', 'showContact', 'confirmationEmail', 'annee', 'orientationId', 'apProgramId', 'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo', 'juryMembers', 'keywords', 'languageIds', 'formatIds', - 'licenseId', 'lien', 'accessTypeId' + 'licenseId', 'lien', 'accessTypeId', 'objet' ); } diff --git a/app/src/Database.php b/app/src/Database.php index bc604f4..323f49e 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -1566,11 +1566,15 @@ class Database { synopsis, file_size_info, baiu_link, license_id, access_type_id, + objet, is_published, submitted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP) "); + $validObjet = ['tfe', 'thèse', 'frart']; + $objet = in_array($data['objet'] ?? '', $validObjet, true) ? $data['objet'] : 'tfe'; + $stmt->execute([ $identifier, $data['title'], @@ -1584,6 +1588,7 @@ class Database { !empty($data['baiu_link']) ? $data['baiu_link'] : null, isset($data['license_id']) ? $data['license_id'] : null, isset($data['access_type_id']) ? (int)$data['access_type_id'] : 2, // default: Interne + $objet, ]); $thesisId = (int)$this->pdo->lastInsertId(); diff --git a/app/src/ShareLink.php b/app/src/ShareLink.php index f7c33b6..74a2cda 100644 --- a/app/src/ShareLink.php +++ b/app/src/ShareLink.php @@ -48,16 +48,20 @@ class ShareLink * @param string|null $expiresAt ISO-8601 expiration date, null = never expires * @return array The created link row */ - public function create(int $createdBy, ?string $password = null, ?string $expiresAt = null): array + public function create(int $createdBy, ?string $password = null, ?string $expiresAt = null, ?string $objetRestriction = null): array { $slug = self::generateSlug(); $passwordHash = $password !== null ? password_hash($password, PASSWORD_BCRYPT) : null; + $validObjet = ['tfe', 'thèse', 'frart']; + $objetRestriction = ($objetRestriction !== null && in_array($objetRestriction, $validObjet, true)) + ? $objetRestriction + : null; $stmt = $this->db->getConnection()->prepare( - "INSERT INTO share_links (slug, password_hash, is_active, created_by, expires_at) - VALUES (?, ?, 1, ?, ?)" + "INSERT INTO share_links (slug, objet_restriction, password_hash, is_active, created_by, expires_at) + VALUES (?, ?, ?, 1, ?, ?)" ); - $stmt->execute([$slug, $passwordHash, $createdBy, $expiresAt]); + $stmt->execute([$slug, $objetRestriction, $passwordHash, $createdBy, $expiresAt]); return $this->findBySlug($slug); } diff --git a/app/storage/posterg.db b/app/storage/posterg.db index 23b4b12..7060bab 100644 Binary files a/app/storage/posterg.db and b/app/storage/posterg.db differ diff --git a/app/storage/schema.sql b/app/storage/schema.sql index d7f51d3..a26cc4e 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -163,6 +163,7 @@ CREATE TABLE IF NOT EXISTS theses ( -- Type of work is_doctoral BOOLEAN DEFAULT 0, -- 0 for TFE, 1 for doctoral thesis + objet TEXT NOT NULL DEFAULT 'tfe' CHECK (objet IN ('tfe', 'thèse', 'frart')), -- Academic details orientation_id INTEGER, @@ -295,7 +296,9 @@ INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interdit_enabled', '1'), ('access_type_interne_enabled', '1'), ('access_type_libre_enabled', '0'), - ('admin_password_hash', ''); + ('admin_password_hash', ''), + ('objet_these_enabled', '1'), + ('objet_frart_enabled', '1'); -- ============================================================================ -- STATIC PAGES / CONTENT MANAGEMENT @@ -331,6 +334,7 @@ INSERT OR IGNORE INTO pages (slug, title, content) VALUES CREATE TABLE IF NOT EXISTS share_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, -- Format: YYYYMMDD-, e.g. 20260416-a3f9k2 + objet_restriction TEXT CHECK (objet_restriction IN ('tfe', 'thèse', 'frart')), -- NULL = no restriction password_hash TEXT, -- bcrypt hash; NULL = no password required is_active INTEGER NOT NULL DEFAULT 1, -- 1 = active, 0 = disabled usage_count INTEGER NOT NULL DEFAULT 0, -- Number of successful submissions via this link @@ -478,6 +482,7 @@ SELECT t.subtitle, t.year, t.is_doctoral, + t.objet, o.name as orientation, ap.name as ap_program, ft.name as finality_type, diff --git a/app/storage/test.db b/app/storage/test.db index 39df89d..8be9e64 100644 Binary files a/app/storage/test.db and b/app/storage/test.db differ diff --git a/app/templates/admin/acces-etudiante.php b/app/templates/admin/acces-etudiante.php index 2b0cf61..b2ae72d 100644 --- a/app/templates/admin/acces-etudiante.php +++ b/app/templates/admin/acces-etudiante.php @@ -16,6 +16,7 @@ Lien + Objet Statut Mot de passe Utilisations @@ -48,6 +49,13 @@ + + + + + Tous + + @@ -114,6 +122,16 @@
+
+ + + Restreint ce lien à un seul type de soumission. +
diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php index 9e58c96..fa01ec5 100644 --- a/app/templates/admin/parametres.php +++ b/app/templates/admin/parametres.php @@ -106,6 +106,52 @@ + +
+

Types de travaux

+

Active ou désactive les types de travaux dans les formulaires et la consultation. Un type désactivé ne peut plus être soumis ni affiché sur le site.

+

Le type TFE est toujours actif et ne peut pas être désactivé.

+ +
+ + + +
+ Types disponibles + + + + + + +
+ + +
+
+