formulaire: default interne, unpublished, contact toggle, settings section

This commit is contained in:
Pontoporeia
2026-04-15 11:57:55 +02:00
parent 67a4aaac26
commit 0cb4451218
13 changed files with 490 additions and 44 deletions

11
TODO.md
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
</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> <span class="sr-only">(ouvre dans un nouvel onglet)</span>
</a> </a>
</dd> </dd>
</div> </div>
<?php endif; ?> <?php endif; ?>

View File

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

View File

@@ -70,12 +70,13 @@ class ThesisCreateController
public function loadFormData(): array public function loadFormData(): array
{ {
return [ return [
'orientations' => $this->db->getAllOrientations(), 'orientations' => $this->db->getAllOrientations(),
'apPrograms' => $this->db->getAllAPPrograms(), 'apPrograms' => $this->db->getAllAPPrograms(),
'finalityTypes' => $this->db->getAllFinalityTypes(), 'finalityTypes' => $this->db->getAllFinalityTypes(),
'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");
// ── 34. DB writes in a transaction ─────────────────────────────────── // ── 34. 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,
]); ]);
@@ -192,7 +194,8 @@ class ThesisCreateController
'Nom/Prénom/Pseudo' 'Nom/Prénom/Pseudo'
); );
$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'
); );
} }

View File

@@ -91,22 +91,28 @@ 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,
'currentFormats' => $currentFormats, 'currentFormats' => $currentFormats,
'jury' => $jury, 'jury' => $jury,
'orientations' => $orientations, 'orientations' => $orientations,
'apPrograms' => $apPrograms, 'apPrograms' => $apPrograms,
'finalityTypes' => $finalityTypes, 'finalityTypes' => $finalityTypes,
'languages' => $languages, 'languages' => $languages,
'formatTypes' => $formatTypes, 'formatTypes' => $formatTypes,
'licenseTypes' => $licenseTypes, 'licenseTypes' => $licenseTypes,
'accessTypes' => $accessTypes, 'accessTypes' => $accessTypes,
'currentLicenseId' => $currentLicenseId, 'currentLicenseId' => $currentLicenseId,
'currentAccessTypeId' => $currentAccessTypeId, 'currentAccessTypeId' => $currentAccessTypeId,
'currentContextNote' => $currentContextNote, 'currentContextNote' => $currentContextNote,
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']), 'currentAuthorEmail' => $currentAuthorEmail,
'currentAuthorShowContact' => $currentAuthorShowContact,
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
]; ];
} }
@@ -162,13 +168,15 @@ 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) {
if ($name !== '') { if ($name !== '') {
$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,
]; ];
} }
} }

View 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.

View File

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