mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
feat: add objet field (tfe/thèse/frart) with share-link restriction and site-settings toggles
This commit is contained in:
1
TODO.md
1
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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'] ?? '',
|
||||
|
||||
@@ -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 : <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'; ?>
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
@@ -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.
@@ -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">
|
||||
|
||||
@@ -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
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
|
||||
Reference in New Issue
Block a user