mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
formulaire: default interne, unpublished, contact toggle, settings section
This commit is contained in:
11
TODO.md
11
TODO.md
@@ -12,3 +12,14 @@
|
|||||||
- [x] Fix nginx/SETUP.md manual step to use just manage-admin-users instead of raw htpasswd
|
- [x] Fix nginx/SETUP.md manual step to use just manage-admin-users instead of raw htpasswd
|
||||||
- [x] Fix root README.md dead reference to docs/TODO.SECURITY.md
|
- [x] Fix root README.md dead reference to docs/TODO.SECURITY.md
|
||||||
- [x] Update root README.md project structure (remove nginx/scripts/ entry)
|
- [x] Update root README.md project structure (remove nginx/scripts/ entry)
|
||||||
|
- [x] Form default visibility: "Interne" (access_type_id=2) set at DB insert level
|
||||||
|
- [x] New TFE always created unpublished (is_published=0 hardcoded in createThesis)
|
||||||
|
- [x] Contact checkbox: `show_contact` column on authors; checkbox in add/edit forms; tfe.php shows contact only if enabled
|
||||||
|
- [x] Migration 008: site_settings table + show_contact column + rebuilt views with author_email/author_show_contact/access_type_id
|
||||||
|
- [x] Formulaire section in parametres.php: toggle switches for Interdit/Interne/Libre access types
|
||||||
|
- [x] Libre option disabled by default (access_type_libre_enabled=0)
|
||||||
|
- [x] Add visibility select in add.php, filtered by enabled access types, defaulting to Interne
|
||||||
|
- [x] Edit.php: pre-populate contact email from DB; show contact_public checkbox with current state
|
||||||
|
- [x] tfe.php: contact shown from author_email+show_contact; baiu_link relabeled as "Lien"
|
||||||
|
- [x] actions/settings.php: handler for formulaire settings form
|
||||||
|
- [x] CSS: admin-toggle pill switches + admin-settings-toggles layout + admin-form-group
|
||||||
|
|||||||
32
public/admin/actions/settings.php
Normal file
32
public/admin/actions/settings.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||||
|
AdminAuth::requireLogin();
|
||||||
|
|
||||||
|
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||||
|
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||||
|
App::flash('error', "Erreur de sécurité : token invalide.");
|
||||||
|
header('Location: /admin/parametres.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
$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.");
|
||||||
|
} else {
|
||||||
|
App::flash('error', "Section inconnue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
header('Location: /admin/parametres.php');
|
||||||
|
exit;
|
||||||
@@ -63,6 +63,16 @@ function wasSelected($key, $value) {
|
|||||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
|
|
||||||
|
<!-- Contact visibility -->
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="contact_public" value="1"
|
||||||
|
<?= isset($formData['contact_public']) ? 'checked' : '' ?>>
|
||||||
|
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||||
|
</label>
|
||||||
|
<small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@@ -105,6 +115,26 @@ function wasSelected($key, $value) {
|
|||||||
|
|
||||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Visibility select — only show options enabled in settings
|
||||||
|
$accessOptions = array_map(function($at) {
|
||||||
|
return ['id' => $at['id'], 'name' => $at['name']];
|
||||||
|
}, $enabledAccessTypes);
|
||||||
|
// Default: Interne (id=2)
|
||||||
|
$defaultAccessType = 2;
|
||||||
|
$selectedAccessType = isset($formData['access_type_id'])
|
||||||
|
? (int)$formData['access_type_id']
|
||||||
|
: $defaultAccessType;
|
||||||
|
$name = 'access_type_id';
|
||||||
|
$label = 'Visibilité / Accès :';
|
||||||
|
$options = $accessOptions;
|
||||||
|
$selected = $selectedAccessType;
|
||||||
|
$placeholder = null;
|
||||||
|
$required = true;
|
||||||
|
$attrs = [];
|
||||||
|
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||||
|
?>
|
||||||
|
|
||||||
<div class="admin-form-footer">
|
<div class="admin-form-footer">
|
||||||
<button type="submit" name="go" class="admin-btn">Soumettre</button>
|
<button type="submit" name="go" class="admin-btn">Soumettre</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,17 @@ try {
|
|||||||
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
||||||
|
|
||||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = array_merge(['autocomplete' => 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = array_merge(['autocomplete' => 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
<?php $name = 'mail'; $label = 'Contact :'; $value = ''; $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'mail'; $label = 'Contact :'; $value = htmlspecialchars($currentAuthorEmail ?? ''); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
|
|
||||||
|
<!-- Contact visibility -->
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="contact_public" value="1"
|
||||||
|
<?= !empty($currentAuthorShowContact) ? 'checked' : '' ?>>
|
||||||
|
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||||
|
</label>
|
||||||
|
<small>Si cette case est cochée, le contact apparaît sur la page publique du TFE.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ $credentialsFile = APP_ROOT . '/config/admin_credentials.php';
|
|||||||
$hasPassword = defined('ADMIN_PASSWORD_HASH');
|
$hasPassword = defined('ADMIN_PASSWORD_HASH');
|
||||||
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
|
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
$db = new Database();
|
||||||
|
$siteSettings = $db->getAllSettings();
|
||||||
|
|
||||||
if (empty($_SESSION['csrf_token'])) {
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,58 @@ if (empty($_SESSION['csrf_token'])) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════════════════
|
||||||
SECTION 2 — Compte administrateur
|
SECTION 2 — Formulaire
|
||||||
|
══════════════════════════════════════════════════════════════ -->
|
||||||
|
<section class="admin-settings-section" aria-labelledby="settings-formulaire-title">
|
||||||
|
<h2 class="admin-settings-section__title" id="settings-formulaire-title">Formulaire</h2>
|
||||||
|
|
||||||
|
<p>Options de visibilité disponibles dans le formulaire d'ajout de TFE.</p>
|
||||||
|
<p><small>L'option <strong>Libre</strong> ne sera activée qu'à partir de l'année académique prochaine.</small></p>
|
||||||
|
|
||||||
|
<form method="post" action="actions/settings.php" class="admin-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
<input type="hidden" name="section" value="formulaire">
|
||||||
|
|
||||||
|
<div class="admin-settings-toggles">
|
||||||
|
<label class="admin-toggle-row">
|
||||||
|
<span class="admin-toggle-label">
|
||||||
|
<strong>Interdit</strong>
|
||||||
|
<small>TFE non disponible en physique ni sur le site</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" name="access_type_interdit_enabled" value="1"
|
||||||
|
class="admin-toggle"
|
||||||
|
<?= ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-toggle-row">
|
||||||
|
<span class="admin-toggle-label">
|
||||||
|
<strong>Interne</strong>
|
||||||
|
<small>TFE accessible uniquement sur place en physique</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" name="access_type_interne_enabled" value="1"
|
||||||
|
class="admin-toggle"
|
||||||
|
<?= ($siteSettings['access_type_interne_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-toggle-row admin-toggle-row--disabled">
|
||||||
|
<span class="admin-toggle-label">
|
||||||
|
<strong>Libre</strong>
|
||||||
|
<small>Libre accès — disponible à partir de l'année académique prochaine</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" name="access_type_libre_enabled" value="1"
|
||||||
|
class="admin-toggle"
|
||||||
|
<?= ($siteSettings['access_type_libre_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-form-footer">
|
||||||
|
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════
|
||||||
|
SECTION 3 — Compte administrateur
|
||||||
══════════════════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════════════════ -->
|
||||||
<section class="admin-settings-section" aria-labelledby="settings-account-title">
|
<section class="admin-settings-section" aria-labelledby="settings-account-title">
|
||||||
<h2 class="admin-settings-section__title" id="settings-account-title">Compte administrateur</h2>
|
<h2 class="admin-settings-section__title" id="settings-account-title">Compte administrateur</h2>
|
||||||
|
|||||||
@@ -1164,3 +1164,83 @@
|
|||||||
height: 50vh;
|
height: 50vh;
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Settings: formulaire toggles ──────────────────────────────────────────── */
|
||||||
|
.admin-settings-toggles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-bottom: var(--space-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-m);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: var(--space-xs) var(--space-m);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle-row--disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle-label strong {
|
||||||
|
font-size: var(--step-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle-label small {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--step--2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Native checkbox styled as toggle pill */
|
||||||
|
.admin-toggle {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
background: var(--border-primary);
|
||||||
|
border-radius: 11px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle:checked {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toggle:checked::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form group (for checkbox inside .admin-form) ──────────────────────────── */
|
||||||
|
.admin-form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3xs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,18 +149,42 @@ extract($ctrl->handle());
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($data['baiu_link'])): ?>
|
<?php if (!empty($data['author_email']) && !empty($data['author_show_contact'])): ?>
|
||||||
<?php
|
|
||||||
$_contactHref = htmlspecialchars($data['baiu_link']);
|
|
||||||
$_contactLabel = preg_replace('#^https?://#i', '', rtrim($data['baiu_link'], '/'));
|
|
||||||
?>
|
|
||||||
<div>
|
<div>
|
||||||
<dt>Contact :</dt>
|
<dt>Contact :</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a href="<?= $_contactHref ?>" target="_blank" rel="noopener">
|
<?php
|
||||||
<?= htmlspecialchars($_contactLabel) ?>
|
$_contact = $data['author_email'];
|
||||||
|
$_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false;
|
||||||
|
$_isEmail = !$_isUrl && str_contains($_contact, '@');
|
||||||
|
if ($_isUrl):
|
||||||
|
?>
|
||||||
|
<a href="<?= htmlspecialchars($_contact) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= htmlspecialchars(preg_replace('#^https?://#i', '', rtrim($_contact, '/'))) ?>
|
||||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php elseif ($_isEmail): ?>
|
||||||
|
<a href="mailto:<?= htmlspecialchars($_contact) ?>"><?= htmlspecialchars($_contact) ?></a>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= htmlspecialchars($_contact) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($data['baiu_link'])): ?>
|
||||||
|
<?php
|
||||||
|
$_baiuHref = htmlspecialchars($data['baiu_link']);
|
||||||
|
$_baiuLabel = preg_replace('#^https?://#i', '', rtrim($data['baiu_link'], '/'));
|
||||||
|
?>
|
||||||
|
<div>
|
||||||
|
<dt>Lien :</dt>
|
||||||
|
<dd>
|
||||||
|
<a href="<?= $_baiuHref ?>" target="_blank" rel="noopener">
|
||||||
|
<?= htmlspecialchars($_baiuLabel) ?>
|
||||||
|
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -809,21 +809,24 @@ class Database {
|
|||||||
/**
|
/**
|
||||||
* Find or create an author
|
* Find or create an author
|
||||||
*/
|
*/
|
||||||
public function findOrCreateAuthor($name, $email = null) {
|
public function findOrCreateAuthor($name, $email = null, bool $showContact = false) {
|
||||||
$stmt = $this->pdo->prepare("SELECT id FROM authors WHERE name = ?");
|
$stmt = $this->pdo->prepare("SELECT id FROM authors WHERE name = ?");
|
||||||
$stmt->execute([$name]);
|
$stmt->execute([$name]);
|
||||||
$author = $stmt->fetch();
|
$author = $stmt->fetch();
|
||||||
|
|
||||||
if ($author) {
|
if ($author) {
|
||||||
if ($email && $email !== '') {
|
if ($email && $email !== '') {
|
||||||
$updateStmt = $this->pdo->prepare("UPDATE authors SET email = ? WHERE id = ?");
|
$updateStmt = $this->pdo->prepare("UPDATE authors SET email = ?, show_contact = ? WHERE id = ?");
|
||||||
$updateStmt->execute([$email, $author['id']]);
|
$updateStmt->execute([$email, $showContact ? 1 : 0, $author['id']]);
|
||||||
|
} else {
|
||||||
|
$updateStmt = $this->pdo->prepare("UPDATE authors SET show_contact = ? WHERE id = ?");
|
||||||
|
$updateStmt->execute([$showContact ? 1 : 0, $author['id']]);
|
||||||
}
|
}
|
||||||
return $author['id'];
|
return $author['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare("INSERT INTO authors (name, email) VALUES (?, ?)");
|
$stmt = $this->pdo->prepare("INSERT INTO authors (name, email, show_contact) VALUES (?, ?, ?)");
|
||||||
$stmt->execute([$name, $email]);
|
$stmt->execute([$name, $email, $showContact ? 1 : 0]);
|
||||||
return $this->pdo->lastInsertId();
|
return $this->pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,6 +1051,77 @@ class Database {
|
|||||||
return $stmt->fetchAll();
|
return $stmt->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// SITE SETTINGS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single site setting value by key. Returns $default if not found.
|
||||||
|
*/
|
||||||
|
public function getSetting(string $key, string $default = ''): string {
|
||||||
|
$stmt = $this->pdo->prepare("SELECT value FROM site_settings WHERE key = ? LIMIT 1");
|
||||||
|
$stmt->execute([$key]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? (string) $row['value'] : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all site settings as an associative array [ key => value ].
|
||||||
|
*/
|
||||||
|
public function getAllSettings(): array {
|
||||||
|
$stmt = $this->pdo->query("SELECT key, value FROM site_settings");
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
$out = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$out[$r['key']] = $r['value'];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a site setting.
|
||||||
|
*/
|
||||||
|
public function setSetting(string $key, string $value): void {
|
||||||
|
$this->pdo->prepare(
|
||||||
|
"INSERT INTO site_settings (key, value, updated_at)
|
||||||
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP"
|
||||||
|
)->execute([$key, $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return access types that are enabled in the add-thesis form,
|
||||||
|
* filtered by site_settings toggles.
|
||||||
|
* 'Libre' (id=1) is excluded unless access_type_libre_enabled = '1'.
|
||||||
|
* 'Interne' (id=2) is excluded unless access_type_interne_enabled = '1'.
|
||||||
|
* 'Interdit' (id=3) is excluded unless access_type_interdit_enabled = '1'.
|
||||||
|
*/
|
||||||
|
public function getEnabledFormAccessTypes(): array {
|
||||||
|
$settings = $this->getAllSettings();
|
||||||
|
$all = $this->getAccessTypes();
|
||||||
|
$map = [
|
||||||
|
'Libre' => $settings['access_type_libre_enabled'] ?? '0',
|
||||||
|
'Interne' => $settings['access_type_interne_enabled'] ?? '1',
|
||||||
|
'Interdit' => $settings['access_type_interdit_enabled'] ?? '1',
|
||||||
|
];
|
||||||
|
return array_values(array_filter($all, fn($at) => ($map[$at['name']] ?? '0') === '1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the show_contact flag for the first author of a thesis.
|
||||||
|
*/
|
||||||
|
public function setAuthorShowContact(int $thesisId, bool $show): void {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"UPDATE authors SET show_contact = ?
|
||||||
|
WHERE id = (
|
||||||
|
SELECT author_id FROM thesis_authors
|
||||||
|
WHERE thesis_id = ?
|
||||||
|
ORDER BY author_order LIMIT 1
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$show ? 1 : 0, $thesisId]);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// JURY METHODS
|
// JURY METHODS
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -1439,7 +1513,8 @@ class Database {
|
|||||||
foreach ($authors as $index => $author) {
|
foreach ($authors as $index => $author) {
|
||||||
$name = trim($author['name'] ?? '');
|
$name = trim($author['name'] ?? '');
|
||||||
if ($name === '') continue;
|
if ($name === '') continue;
|
||||||
$authorId = $this->findOrCreateAuthor($name, $author['email'] ?? null);
|
$showContact = !empty($author['show_contact']);
|
||||||
|
$authorId = $this->findOrCreateAuthor($name, $author['email'] ?? null, $showContact);
|
||||||
$stmt->execute([$thesisId, $authorId, $index + 1]);
|
$stmt->execute([$thesisId, $authorId, $index + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1453,8 +1528,10 @@ class Database {
|
|||||||
orientation_id, ap_program_id, finality_id,
|
orientation_id, ap_program_id, finality_id,
|
||||||
synopsis, file_size_info,
|
synopsis, file_size_info,
|
||||||
baiu_link, license_id,
|
baiu_link, license_id,
|
||||||
|
access_type_id,
|
||||||
|
is_published,
|
||||||
submitted_at
|
submitted_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
@@ -1469,6 +1546,7 @@ class Database {
|
|||||||
!empty($data['file_size_info']) ? $data['file_size_info'] : null,
|
!empty($data['file_size_info']) ? $data['file_size_info'] : null,
|
||||||
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
||||||
isset($data['license_id']) ? $data['license_id'] : null,
|
isset($data['license_id']) ? $data['license_id'] : null,
|
||||||
|
isset($data['access_type_id']) ? (int)$data['access_type_id'] : 2, // default: Interne
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$thesisId = (int)$this->pdo->lastInsertId();
|
$thesisId = (int)$this->pdo->lastInsertId();
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class ThesisCreateController
|
|||||||
'languages' => $this->db->getAllLanguages(),
|
'languages' => $this->db->getAllLanguages(),
|
||||||
'formatTypes' => $this->db->getAllFormatTypes(),
|
'formatTypes' => $this->db->getAllFormatTypes(),
|
||||||
'licenseTypes' => $this->db->getAllLicenseTypes(),
|
'licenseTypes' => $this->db->getAllLicenseTypes(),
|
||||||
|
'enabledAccessTypes' => $this->db->getEnabledFormAccessTypes(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class ThesisCreateController
|
|||||||
$data = $this->validateAndSanitise($post);
|
$data = $this->validateAndSanitise($post);
|
||||||
|
|
||||||
// ── 2. Find / create author ───────────────────────────────────────────
|
// ── 2. Find / create author ───────────────────────────────────────────
|
||||||
$authorId = $this->db->findOrCreateAuthor($data['auteurName'], $data['mail'] ?: null);
|
$authorId = $this->db->findOrCreateAuthor($data['auteurName'], $data['mail'] ?: null, $data['showContact']);
|
||||||
error_log("ThesisCreateController: author ID $authorId");
|
error_log("ThesisCreateController: author ID $authorId");
|
||||||
|
|
||||||
// ── 3–4. DB writes in a transaction ───────────────────────────────────
|
// ── 3–4. DB writes in a transaction ───────────────────────────────────
|
||||||
@@ -125,6 +126,7 @@ class ThesisCreateController
|
|||||||
'file_size_info' => $data['durationInfo'],
|
'file_size_info' => $data['durationInfo'],
|
||||||
'baiu_link' => $data['lien'],
|
'baiu_link' => $data['lien'],
|
||||||
'license_id' => $data['licenseId'],
|
'license_id' => $data['licenseId'],
|
||||||
|
'access_type_id' => $data['accessTypeId'],
|
||||||
'author_id' => $authorId,
|
'author_id' => $authorId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -193,6 +195,7 @@ class ThesisCreateController
|
|||||||
);
|
);
|
||||||
|
|
||||||
$mail = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
|
$mail = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
|
||||||
|
$showContact = !empty($post['contact_public']) ? true : false;
|
||||||
|
|
||||||
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
|
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
|
||||||
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
|
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
|
||||||
@@ -265,6 +268,12 @@ class ThesisCreateController
|
|||||||
|
|
||||||
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||||||
|
|
||||||
|
// Access type — must be one of the enabled types; default 2 (Interne)
|
||||||
|
$accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
|
||||||
|
if ($accessTypeId === false || $accessTypeId <= 0) {
|
||||||
|
$accessTypeId = 2; // Interne
|
||||||
|
}
|
||||||
|
|
||||||
// External link (optional)
|
// External link (optional)
|
||||||
$lien = '';
|
$lien = '';
|
||||||
if (!empty($post['lien'])) {
|
if (!empty($post['lien'])) {
|
||||||
@@ -275,10 +284,10 @@ class ThesisCreateController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return compact(
|
return compact(
|
||||||
'auteurName', 'mail', 'annee', 'orientationId', 'apProgramId',
|
'auteurName', 'mail', 'showContact', 'annee', 'orientationId', 'apProgramId',
|
||||||
'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo',
|
'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo',
|
||||||
'juryMembers', 'keywords', 'languageIds', 'formatIds',
|
'juryMembers', 'keywords', 'languageIds', 'formatIds',
|
||||||
'licenseId', 'lien'
|
'licenseId', 'lien', 'accessTypeId'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ class ThesisEditController
|
|||||||
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
|
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
|
||||||
$currentContextNote = $rawRow['context_note'] ?? '';
|
$currentContextNote = $rawRow['context_note'] ?? '';
|
||||||
|
|
||||||
|
// Author contact info (from view)
|
||||||
|
$currentAuthorEmail = $thesis['author_email'] ?? '';
|
||||||
|
$currentAuthorShowContact = (bool)($thesis['author_show_contact'] ?? false);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'thesis' => $thesis,
|
'thesis' => $thesis,
|
||||||
'currentLanguages' => $currentLanguages,
|
'currentLanguages' => $currentLanguages,
|
||||||
@@ -106,6 +110,8 @@ class ThesisEditController
|
|||||||
'currentLicenseId' => $currentLicenseId,
|
'currentLicenseId' => $currentLicenseId,
|
||||||
'currentAccessTypeId' => $currentAccessTypeId,
|
'currentAccessTypeId' => $currentAccessTypeId,
|
||||||
'currentContextNote' => $currentContextNote,
|
'currentContextNote' => $currentContextNote,
|
||||||
|
'currentAuthorEmail' => $currentAuthorEmail,
|
||||||
|
'currentAuthorShowContact' => $currentAuthorShowContact,
|
||||||
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
|
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,7 @@ class ThesisEditController
|
|||||||
|
|
||||||
// ── 2. Authors ────────────────────────────────────────────────────
|
// ── 2. Authors ────────────────────────────────────────────────────
|
||||||
$authorsRaw = trim($post['auteurice'] ?? '');
|
$authorsRaw = trim($post['auteurice'] ?? '');
|
||||||
|
$showContact = !empty($post['contact_public']);
|
||||||
$authorEntries = [];
|
$authorEntries = [];
|
||||||
if ($authorsRaw !== '') {
|
if ($authorsRaw !== '') {
|
||||||
foreach (array_map('trim', explode(',', $authorsRaw)) as $i => $name) {
|
foreach (array_map('trim', explode(',', $authorsRaw)) as $i => $name) {
|
||||||
@@ -169,6 +176,7 @@ class ThesisEditController
|
|||||||
$authorEntries[] = [
|
$authorEntries[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'email' => $i === 0 ? ($post['mail'] ?? null) : null,
|
'email' => $i === 0 ? ($post['mail'] ?? null) : null,
|
||||||
|
'show_contact' => $i === 0 ? $showContact : false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
89
storage/migrations/008_formulaire_settings.sql
Normal file
89
storage/migrations/008_formulaire_settings.sql
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
-- Migration 008: Formulaire settings + contact visibility
|
||||||
|
-- Adds site_settings key-value table for admin-configurable options
|
||||||
|
-- Adds show_contact column to authors table
|
||||||
|
-- Adds author_email + author_show_contact to views
|
||||||
|
|
||||||
|
-- ── 1. site_settings ─────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS site_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Default formulaire settings:
|
||||||
|
-- access_type_interdit_enabled = 1 (Interdit is available in the add form)
|
||||||
|
-- access_type_interne_enabled = 1 (Interne is available in the add form)
|
||||||
|
-- access_type_libre_enabled = 0 (Libre is NOT yet available — next academic year)
|
||||||
|
INSERT OR IGNORE INTO site_settings (key, value) VALUES
|
||||||
|
('access_type_interdit_enabled', '1'),
|
||||||
|
('access_type_interne_enabled', '1'),
|
||||||
|
('access_type_libre_enabled', '0');
|
||||||
|
|
||||||
|
-- ── 2. show_contact on authors ────────────────────────────────────────────────
|
||||||
|
-- NOTE: SQLite has no IF NOT EXISTS for ALTER TABLE.
|
||||||
|
-- The migrate.sh script guards against re-running; ignore errors on existing DBs.
|
||||||
|
ALTER TABLE authors ADD COLUMN show_contact INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- ── 3. Rebuild views to expose author_email and author_show_contact ───────────
|
||||||
|
DROP VIEW IF EXISTS v_theses_public;
|
||||||
|
DROP VIEW IF EXISTS v_theses_full;
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS 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,
|
||||||
|
t.access_type_id,
|
||||||
|
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 tg.name) as keywords,
|
||||||
|
-- First author's email and contact-visibility flag
|
||||||
|
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_email,
|
||||||
|
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_show_contact
|
||||||
|
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_tags tt ON t.id = tt.thesis_id
|
||||||
|
LEFT JOIN tags tg ON tt.tag_id = tg.id
|
||||||
|
GROUP BY t.id;
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS v_theses_public AS
|
||||||
|
SELECT * FROM v_theses_full
|
||||||
|
WHERE is_published = 1;
|
||||||
Binary file not shown.
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS authors (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT,
|
email TEXT,
|
||||||
|
show_contact INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -278,6 +279,21 @@ CREATE TABLE IF NOT EXISTS thesis_files (
|
|||||||
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
|
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SITE SETTINGS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS site_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO site_settings (key, value) VALUES
|
||||||
|
('access_type_interdit_enabled', '1'),
|
||||||
|
('access_type_interne_enabled', '1'),
|
||||||
|
('access_type_libre_enabled', '0');
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- STATIC PAGES / CONTENT MANAGEMENT
|
-- STATIC PAGES / CONTENT MANAGEMENT
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@@ -375,6 +391,7 @@ SELECT
|
|||||||
at.name as access_type,
|
at.name as access_type,
|
||||||
lt.name as license_type,
|
lt.name as license_type,
|
||||||
t.license_id,
|
t.license_id,
|
||||||
|
t.access_type_id,
|
||||||
t.jury_points,
|
t.jury_points,
|
||||||
t.submitted_at,
|
t.submitted_at,
|
||||||
t.defense_date,
|
t.defense_date,
|
||||||
@@ -389,7 +406,10 @@ SELECT
|
|||||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs,
|
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 tg.name) as keywords
|
GROUP_CONCAT(DISTINCT tg.name) as keywords,
|
||||||
|
-- First author's email and contact-visibility flag
|
||||||
|
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_email,
|
||||||
|
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_show_contact
|
||||||
FROM theses t
|
FROM theses t
|
||||||
LEFT JOIN orientations o ON t.orientation_id = o.id
|
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||||
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||||
|
|||||||
Reference in New Issue
Block a user