feat: add objet field (tfe/thèse/frart) with share-link restriction and site-settings toggles

This commit is contained in:
Pontoporeia
2026-04-22 14:06:05 +02:00
parent dbaabaf8a0
commit d961f9533c
12 changed files with 128 additions and 10 deletions

View File

@@ -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`

View File

@@ -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;

View File

@@ -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'] ?? '',

View File

@@ -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
<fieldset>
<legend>Informations du TFE</legend>
<?php if (count($allowedObjet) > 1): ?>
<div class="admin-form-group">
<label>Type de travail&nbsp;: <span class="asterisk">*</span></label>
<div class="form-radio-group">
<?php foreach ($allowedObjet as $objetVal): ?>
<label class="admin-checkbox-label">
<input type="radio" name="objet" value="<?= htmlspecialchars($objetVal) ?>"
<?= (old($formData, 'objet') ?: $allowedObjet[0]) === $objetVal ? 'checked' : '' ?> required>
<?= htmlspecialchars(ucfirst($objetVal)) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<input type="hidden" name="objet" value="<?= htmlspecialchars($allowedObjet[0]) ?>">
<?php endif; ?>
<?php $name = 'titre'; $label = 'Titre :'; $value = old($formData, 'titre'); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old($formData, 'subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old($formData, 'auteurice'); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>

View File

@@ -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'
);
}

View File

@@ -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();

View File

@@ -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);
}

Binary file not shown.

View File

@@ -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-<random>, 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,

Binary file not shown.

View File

@@ -16,6 +16,7 @@
<thead>
<tr>
<th scope="col">Lien</th>
<th scope="col">Objet</th>
<th scope="col">Statut</th>
<th scope="col">Mot de passe</th>
<th scope="col">Utilisations</th>
@@ -48,6 +49,13 @@
<code style="font-size:var(--step--2);color:var(--text-secondary);"><?= htmlspecialchars($link['slug']) ?></code>
<input type="hidden" id="url-<?= $link['id'] ?>" value="<?= $fullUrl ?>">
</td>
<td>
<?php if ($link['objet_restriction']): ?>
<span class="status-badge"><?= htmlspecialchars($link['objet_restriction']) ?></span>
<?php else: ?>
<span style="color:var(--text-secondary);font-size:var(--step--2);">Tous</span>
<?php endif; ?>
</td>
<td>
<?php if ($isExpired): ?>
<span class="status-badge status-pending"><?= $statusLabel ?></span>
@@ -114,6 +122,16 @@
<form method="post" action="actions/acces-etudiante.php" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="create">
<div>
<label for="create-objet">Type d'objet (optionnel)</label>
<select id="create-objet" name="objet_restriction">
<option value="">— Tous les types —</option>
<option value="tfe">TFE</option>
<option value="thèse">Thèse</option>
<option value="frart">Frart</option>
</select>
<small>Restreint ce lien à un seul type de soumission.</small>
</div>
<div>
<label for="create-password">Mot de passe (optionnel)</label>
<input type="password" id="create-password" name="password" autocomplete="new-password">

View File

@@ -106,6 +106,52 @@
</form>
</section>
<!-- ══════════════════════════════════════════════════════════════
TYPES D'OBJET
══════════════════════════════════════════════════════════════ -->
<section aria-labelledby="settings-objet-title">
<h2 id="settings-objet-title">Types de travaux</h2>
<p>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.</p>
<p class="param-note">Le type <strong>TFE</strong> est toujours actif et ne peut pas être désactivé.</p>
<form method="post" action="actions/settings.php" class="param-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="section" value="objet_types">
<fieldset>
<legend>Types disponibles</legend>
<label class="param-checkbox param-checkbox--disabled">
<input type="checkbox" disabled checked>
<span>
<strong>TFE</strong><br>
<small>Travail de fin d'études — toujours actif</small>
</span>
</label>
<label class="param-checkbox">
<input type="checkbox" name="objet_these_enabled" value="1"
<?= ($siteSettings['objet_these_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
<span>
<strong>Thèse</strong><br>
<small>Thèses doctorales</small>
</span>
</label>
<label class="param-checkbox">
<input type="checkbox" name="objet_frart_enabled" value="1"
<?= ($siteSettings['objet_frart_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
<span>
<strong>Frart</strong><br>
<small>Formation de recherche en art</small>
</span>
</label>
</fieldset>
<button type="submit">Enregistrer</button>
</form>
</section>
<!-- ══════════════════════════════════════════════════════════════
RELAY SMTP
══════════════════════════════════════════════════════════════ -->