From 0cb4451218bc202ed242aa44bcc27f4dd0136cd0 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Wed, 15 Apr 2026 11:57:55 +0200 Subject: [PATCH] formulaire: default interne, unpublished, contact toggle, settings section --- TODO.md | 11 +++ public/admin/actions/settings.php | 32 ++++++ public/admin/add.php | 30 ++++++ public/admin/edit.php | 12 ++- public/admin/parametres.php | 57 ++++++++++- public/assets/css/admin.css | 80 +++++++++++++++ public/tfe.php | 38 ++++++-- src/Database.php | 92 ++++++++++++++++-- src/ThesisCreateController.php | 29 ++++-- src/ThesisEditController.php | 42 ++++---- .../migrations/008_formulaire_settings.sql | 89 +++++++++++++++++ storage/posterg.db | Bin 229376 -> 237568 bytes storage/schema.sql | 22 ++++- 13 files changed, 490 insertions(+), 44 deletions(-) create mode 100644 public/admin/actions/settings.php create mode 100644 storage/migrations/008_formulaire_settings.sql diff --git a/TODO.md b/TODO.md index 692871c..5ab051d 100644 --- a/TODO.md +++ b/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 root README.md dead reference to docs/TODO.SECURITY.md - [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 diff --git a/public/admin/actions/settings.php b/public/admin/actions/settings.php new file mode 100644 index 0000000..36d7920 --- /dev/null +++ b/public/admin/actions/settings.php @@ -0,0 +1,32 @@ +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; diff --git a/public/admin/add.php b/public/admin/add.php index 9837dee..c9546b1 100644 --- a/public/admin/add.php +++ b/public/admin/add.php @@ -63,6 +63,16 @@ function wasSelected($key, $value) { 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> + +
+ + Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE. +
+ + $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'; + ?> + diff --git a/public/admin/edit.php b/public/admin/edit.php index c5646e7..7772418 100644 --- a/public/admin/edit.php +++ b/public/admin/edit.php @@ -45,7 +45,17 @@ try { 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?> - 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> + 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> + + +
+ + Si cette case est cochée, le contact apparaît sur la page publique du TFE. +
getAllSettings(); + if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } @@ -56,7 +60,58 @@ if (empty($_SESSION['csrf_token'])) { +
+

Formulaire

+ +

Options de visibilité disponibles dans le formulaire d'ajout de TFE.

+

L'option Libre ne sera activée qu'à partir de l'année académique prochaine.

+ +
+ + + +
+ + + + + +
+ + +
+
+ +

Compte administrateur

diff --git a/public/assets/css/admin.css b/public/assets/css/admin.css index 435a665..5857fa3 100644 --- a/public/assets/css/admin.css +++ b/public/assets/css/admin.css @@ -1164,3 +1164,83 @@ height: 50vh; 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); +} diff --git a/public/tfe.php b/public/tfe.php index f429d38..65c8575 100644 --- a/public/tfe.php +++ b/public/tfe.php @@ -149,18 +149,42 @@ extract($ctrl->handle()); - - +
Contact :
- - + + + + (ouvre dans un nouvel onglet) + + + + + + +
+
+ + + + +
+
Lien :
+
+ + (ouvre dans un nouvel onglet) +
diff --git a/src/Database.php b/src/Database.php index 89831b1..8db1712 100644 --- a/src/Database.php +++ b/src/Database.php @@ -809,21 +809,24 @@ class Database { /** * 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->execute([$name]); $author = $stmt->fetch(); if ($author) { if ($email && $email !== '') { - $updateStmt = $this->pdo->prepare("UPDATE authors SET email = ? WHERE id = ?"); - $updateStmt->execute([$email, $author['id']]); + $updateStmt = $this->pdo->prepare("UPDATE authors SET email = ?, show_contact = ? WHERE 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']; } - $stmt = $this->pdo->prepare("INSERT INTO authors (name, email) VALUES (?, ?)"); - $stmt->execute([$name, $email]); + $stmt = $this->pdo->prepare("INSERT INTO authors (name, email, show_contact) VALUES (?, ?, ?)"); + $stmt->execute([$name, $email, $showContact ? 1 : 0]); return $this->pdo->lastInsertId(); } @@ -1048,6 +1051,77 @@ class Database { 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 // ======================================================================== @@ -1439,7 +1513,8 @@ class Database { foreach ($authors as $index => $author) { $name = trim($author['name'] ?? ''); 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]); } } @@ -1453,8 +1528,10 @@ class Database { orientation_id, ap_program_id, finality_id, synopsis, file_size_info, baiu_link, license_id, + access_type_id, + is_published, submitted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP) "); $stmt->execute([ @@ -1469,6 +1546,7 @@ class Database { !empty($data['file_size_info']) ? $data['file_size_info'] : null, !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 ]); $thesisId = (int)$this->pdo->lastInsertId(); diff --git a/src/ThesisCreateController.php b/src/ThesisCreateController.php index 6d6ec57..c35f93d 100644 --- a/src/ThesisCreateController.php +++ b/src/ThesisCreateController.php @@ -70,12 +70,13 @@ class ThesisCreateController public function loadFormData(): array { return [ - 'orientations' => $this->db->getAllOrientations(), - 'apPrograms' => $this->db->getAllAPPrograms(), - 'finalityTypes' => $this->db->getAllFinalityTypes(), - 'languages' => $this->db->getAllLanguages(), - 'formatTypes' => $this->db->getAllFormatTypes(), - 'licenseTypes' => $this->db->getAllLicenseTypes(), + 'orientations' => $this->db->getAllOrientations(), + 'apPrograms' => $this->db->getAllAPPrograms(), + 'finalityTypes' => $this->db->getAllFinalityTypes(), + 'languages' => $this->db->getAllLanguages(), + 'formatTypes' => $this->db->getAllFormatTypes(), + 'licenseTypes' => $this->db->getAllLicenseTypes(), + 'enabledAccessTypes' => $this->db->getEnabledFormAccessTypes(), ]; } @@ -107,7 +108,7 @@ class ThesisCreateController $data = $this->validateAndSanitise($post); // ── 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"); // ── 3–4. DB writes in a transaction ─────────────────────────────────── @@ -125,6 +126,7 @@ class ThesisCreateController 'file_size_info' => $data['durationInfo'], 'baiu_link' => $data['lien'], 'license_id' => $data['licenseId'], + 'access_type_id' => $data['accessTypeId'], 'author_id' => $authorId, ]); @@ -192,7 +194,8 @@ class ThesisCreateController '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); 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; + // 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) $lien = ''; if (!empty($post['lien'])) { @@ -275,10 +284,10 @@ class ThesisCreateController } return compact( - 'auteurName', 'mail', 'annee', 'orientationId', 'apProgramId', + 'auteurName', 'mail', 'showContact', 'annee', 'orientationId', 'apProgramId', 'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo', 'juryMembers', 'keywords', 'languageIds', 'formatIds', - 'licenseId', 'lien' + 'licenseId', 'lien', 'accessTypeId' ); } diff --git a/src/ThesisEditController.php b/src/ThesisEditController.php index 90e7a22..08f3e83 100644 --- a/src/ThesisEditController.php +++ b/src/ThesisEditController.php @@ -91,22 +91,28 @@ class ThesisEditController $currentAccessTypeId = $rawRow['access_type_id'] ?? null; $currentContextNote = $rawRow['context_note'] ?? ''; + // Author contact info (from view) + $currentAuthorEmail = $thesis['author_email'] ?? ''; + $currentAuthorShowContact = (bool)($thesis['author_show_contact'] ?? false); + return [ - 'thesis' => $thesis, - 'currentLanguages' => $currentLanguages, - 'currentFormats' => $currentFormats, - 'jury' => $jury, - 'orientations' => $orientations, - 'apPrograms' => $apPrograms, - 'finalityTypes' => $finalityTypes, - 'languages' => $languages, - 'formatTypes' => $formatTypes, - 'licenseTypes' => $licenseTypes, - 'accessTypes' => $accessTypes, - 'currentLicenseId' => $currentLicenseId, - 'currentAccessTypeId' => $currentAccessTypeId, - 'currentContextNote' => $currentContextNote, - 'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']), + 'thesis' => $thesis, + 'currentLanguages' => $currentLanguages, + 'currentFormats' => $currentFormats, + 'jury' => $jury, + 'orientations' => $orientations, + 'apPrograms' => $apPrograms, + 'finalityTypes' => $finalityTypes, + 'languages' => $languages, + 'formatTypes' => $formatTypes, + 'licenseTypes' => $licenseTypes, + 'accessTypes' => $accessTypes, + 'currentLicenseId' => $currentLicenseId, + 'currentAccessTypeId' => $currentAccessTypeId, + 'currentContextNote' => $currentContextNote, + 'currentAuthorEmail' => $currentAuthorEmail, + 'currentAuthorShowContact' => $currentAuthorShowContact, + 'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']), ]; } @@ -162,13 +168,15 @@ class ThesisEditController // ── 2. Authors ──────────────────────────────────────────────────── $authorsRaw = trim($post['auteurice'] ?? ''); + $showContact = !empty($post['contact_public']); $authorEntries = []; if ($authorsRaw !== '') { foreach (array_map('trim', explode(',', $authorsRaw)) as $i => $name) { if ($name !== '') { $authorEntries[] = [ - 'name' => $name, - 'email' => $i === 0 ? ($post['mail'] ?? null) : null, + 'name' => $name, + 'email' => $i === 0 ? ($post['mail'] ?? null) : null, + 'show_contact' => $i === 0 ? $showContact : false, ]; } } diff --git a/storage/migrations/008_formulaire_settings.sql b/storage/migrations/008_formulaire_settings.sql new file mode 100644 index 0000000..e46a5c7 --- /dev/null +++ b/storage/migrations/008_formulaire_settings.sql @@ -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; diff --git a/storage/posterg.db b/storage/posterg.db index 84582c71e2eb57efa51717ca8e3d5940ed8ffcbf..fdeeabfad4b66663e809b61e74612d8cfc1d184e 100644 GIT binary patch delta 1267 zcmc&zUr1YL6#xFjn|jrJiq+dKv3^iSba-825;fO4jp>yM(Uf?l-OI9@Bv&i#FBYp%5|*A;imAOJwJ&z_0Jo;5KNyZ@PTwrZCPcf+zwm2qnNss)2n$EgsM#frD)MQCPQ~pr6N}Lk@_1w>rckff z{Qevap@Fcuz6?F)%OLicB#3|EA_JT-^GW$JnU6yFBlC|SzWehi9=+BK>*`4gkZ0s7 zNo^Dqvg*f<5fZ2tVpdP(wWO9Sjgj&7`TF`rUr_Xis4pz}`=vmC06C(y5Ix7Vd~P4) zKL8Yj4*39(n9|0Ik%akM7-FK(z4%k|ah Ia=ePgKYYc5-T(jq delta 388 zcmXw#&npCB9EacU`@ZwedUwsFwFWylPFULVWBu56)@~_EtNa6$gA`F5C^gfb+1_Ua;sJ#NH}VkMvYT%Oa@qRKz1FZxBVLGtxr^;e4a}|7IQFl z!b9i+`71on=LC=99;}86?y9_q;uVGiL21NroUrjuW@Ng5O}fNW9Jk6{2U5UnlNob; zfA}~DCPj!uJvTyR+Qy~VHl>OhsD|`}w$ymVmX#WPQ_iSHZ#?&dh=l~X#IaFvwFRb_ zOb}CKY~l&OP13wx#`jxodq_MCkZrz~9#lh?