From bdd95341b09ce33cd9e930f34a2a58190735306f Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 7 May 2026 22:48:18 +0200 Subject: [PATCH] =?UTF-8?q?Extract=20shared=20TFE=20form=20partial=20?= =?UTF-8?q?=E2=80=94=20single=20source=20of=20truth=20for=20add/edit/parta?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created templates/partials/form/form.php as the unified form template driven by $mode ('add'|'edit'|'partage') and boolean flags for optional sections. The three calling templates (templates/admin/add.php, templates/admin/edit.php, partage/index.php renderShareLinkForm) now only set variables then include the shared partial. ~200 lines of duplicated fieldset HTML eliminated. --- TODO.md | 7 + app/public/admin/edit.php | 2 +- app/public/assets/css/admin.css | 12 - app/public/assets/css/common.css | 1 + app/public/assets/css/form.css | 4 + app/public/assets/js/beforeunload-guard.js | 25 + app/public/partage/index.php | 244 ++----- app/templates/admin/add.php | 231 ++----- app/templates/admin/edit.php | 478 +++----------- .../partials/form/fieldset-academic.php | 1 - .../partials/form/fieldset-metadata.php | 1 - .../partials/form/fieldset-tfe-info.php | 2 +- app/templates/partials/form/form.php | 603 ++++++++++++++++++ 13 files changed, 833 insertions(+), 778 deletions(-) create mode 100644 app/public/assets/js/beforeunload-guard.js create mode 100644 app/templates/partials/form/form.php diff --git a/TODO.md b/TODO.md index 48a9c7d..b269120 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,12 @@ # XAMXAM TODO +## Extract shared TFE form partial (single source of truth) +- [x] Create `templates/partials/form/form.php` — unified form with `$mode`-driven conditionals +- [x] Refactor `templates/admin/add.php` → thin wrapper setting variables + including form partial +- [x] Refactor `templates/admin/edit.php` → thin wrapper with unified `$oldFn` + form partial +- [x] Refactor `partage/index.php` → `renderShareLinkForm()` delegates to form partial +- [x] Test all three forms render correctly (add, edit, partage) — syntax verified, logic reviewed + ## Fix password-protected share links — form never loads after password entry - [x] `partage/index.php` — main GET handler: check `$_SESSION['share_verified_' . $slug]` before showing password gate; skip to form if already verified - [x] `partage/index.php` — add `error_log()` calls throughout password flow (gate entry, hash state, verification result, session check) for debugging diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index 61ff2c9..6a695a5 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -37,7 +37,7 @@ try { $isAdmin = true; $bodyClass = 'admin-body'; $extraCss = ['/assets/css/form.css']; -$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js']; +$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/edit.php'; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 5bd7b16..94fae24 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -75,18 +75,6 @@ /* ── Buttons ────────────────────────────────────────────────────────────── */ .admin-form-footer { margin-top: var(--space-l); - padding-top: var(--space-m); -} - -/* Sticky variant — pinned below admin header, top-right */ -.admin-form-footer--sticky { - position: sticky; - top: 0; - z-index: 10; - margin: 0 0 var(--space-m); - display: flex; - justify-content: flex-end; - gap: var(--space-s); } /* ── Admin button aliases — see common.css .btn base class ────────────── */ diff --git a/app/public/assets/css/common.css b/app/public/assets/css/common.css index a9d5065..3055591 100644 --- a/app/public/assets/css/common.css +++ b/app/public/assets/css/common.css @@ -427,6 +427,7 @@ main { .btn--primary { background: var(--accent-primary); color: var(--accent-foreground); + border: 1px solid transparent; } .btn--primary:hover { diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 4b34aa3..a7845dd 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -302,6 +302,10 @@ /* ── Submit / form footer ───────────────────────────────────────────────── */ .form-footer { margin-top: var(--space-l); + margin-bottom: var(--space-l); + display: flex; + gap: var(--space-s); + align-items: center; } .form-footer button { diff --git a/app/public/assets/js/beforeunload-guard.js b/app/public/assets/js/beforeunload-guard.js new file mode 100644 index 0000000..c123d18 --- /dev/null +++ b/app/public/assets/js/beforeunload-guard.js @@ -0,0 +1,25 @@ +/** + * Beforeunload guard — prompts the user before navigating away from unsaved changes. + * + * Attach to any form with a data-beforeunload-guard attribute. + * No effect when JavaScript is unavailable (form posts normally). + */ +(function () { + var forms = document.querySelectorAll('form[data-beforeunload-guard]'); + if (!forms.length) return; + + var dirty = false; + + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + form.addEventListener('input', function () { dirty = true; }); + form.addEventListener('change', function () { dirty = true; }); + form.addEventListener('submit', function () { dirty = false; }); + } + + window.addEventListener('beforeunload', function (e) { + if (dirty) { + e.preventDefault(); + } + }); +})(); diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 95f20d1..b448715 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -254,6 +254,64 @@ function renderShareLinkForm(string $slug, array $link): void // Load all form help blocks in one query. $helpBlocks = Database::getInstance()->getAllFormHelpBlocks(); $helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? ''; + + // ── Shared form variables ────────────────────────────────────────────── + $mode = 'partage'; + $formAction = '/partage/' . urlencode($slug) . '/submit'; + $hiddenFields = ''; + + $oldFn = $shareOldFn; + $withAutofocusFn = $shareWithAutofocusFn; + + // Synopsis extra: inject fieldset_synopsis help block + ob_start(); + $helpContent = $helpFn('fieldset_synopsis'); + include APP_ROOT . '/templates/partials/form/form-help-block.php'; + $synopsisExtra = ob_get_clean(); + + // Jury data from repopulation + $juryPromoteur = old($formData, 'jury_promoteur'); + $juryPromoteurUlb = old($formData, 'jury_promoteur_ulb_name'); + $lecteursInternes = []; + $lecteursExternes = []; + for ($i = 0; $i < 10; $i++) { + $n = old($formData, "jury_lecteur_interne:$i"); + if ($n !== '') $lecteursInternes[] = ['name' => $n]; + } + for ($i = 0; $i < 10; $i++) { + $n = old($formData, "jury_lecteur_externe:$i"); + if ($n !== '') $lecteursExternes[] = ['name' => $n]; + } + $juryPresident = null; + $showPresident = false; + $showPromoteurUlb = true; + $promoteurUlbConditional = true; + + // Licence / access + $libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1'; + $interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1'; + $interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1'; + $generalitiesHtml = $helpFn('fieldset_generalites'); + $defaultAccessTypeId = 2; + + // Optional sections + $showFlash = true; + $showIntroHelp = true; + $showEmailConfirmation = true; + + // Files: add mode + $filesMode = 'add'; + + // Website URL from repopulation + $existingWebsiteUrl = $formData['website_url'] ?? ''; + $existingWebsiteLabel = $formData['website_label'] ?? ''; + $checkedFormatsForSiteWeb = $formData['formats'] ?? []; + + // Context / backoffice not shown in partage + $currentRaw = []; + $currentAuthorEmail = null; + $currentAuthorShowContact = false; + $currentContextNote = null; ?> @@ -277,195 +335,13 @@ function renderShareLinkForm(string $slug, array $link): void
-

Soumettre un TFE

+

- - - - - - - - - - - - - - -

* Champs obligatoires

-
- - - - - - -
- Langue(s) - - -
- - -
- Mots-clés - -
- - - - - - $n]; - } - for ($i = 0; $i < 10; $i++) { - $n = old($formData, "jury_lecteur_externe:$i"); - if ($n !== '') $lecteursExternes[] = ['name' => $n]; - } - $juryPresident = null; - $showPresident = false; - $showPromoteurUlb = true; - $promoteurUlbConditional = true; - $helpContent = $helpFn('fieldset_jury'); - include APP_ROOT . '/templates/partials/form/form-help-block.php'; - require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; - ?> - - -
- Format(s) - -
- - - - - - - getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1'); - $_stmt->execute(['Site web']); - $_siteWebId = $_stmt->fetchColumn(); - if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) { - echo ''; - } - ?> - - - - - - - - -
- E-mail de confirmation - - -
- - -
+
diff --git a/app/templates/admin/add.php b/app/templates/admin/add.php index 4eb31ed..27ff117 100644 --- a/app/templates/admin/add.php +++ b/app/templates/admin/add.php @@ -1,204 +1,49 @@
-
-

Ajouter un TFE

-
+

Ajouter un TFE

-

* Champs obligatoires

-
- "> + '; - - + $synopsisExtra = ''; - -
- Contact -
- - L'adresse est toujours conservée en interne comme contact de référence. -
-
+ // Jury: fresh add (all empty) + $juryPromoteur = null; + $juryPromoteurUlb = null; + $lecteursInternes = []; + $lecteursExternes = []; + $juryPresident = null; + $showPresident = false; + $showPromoteurUlb = true; + $promoteurUlbConditional = false; - -
- Langue(s) - - -
+ // Licence / access + $libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1'; + $interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1'; + $interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1'; + $generalitiesHtml = $helpFn('fieldset_generalites'); + $defaultAccessTypeId = 2; - -
- Mots-clés - -
+ // Optional sections + $showContact = true; + $showContextNote = true; + $showBackoffice = true; - - + // Files: add mode + $filesMode = 'add'; - - + // Website URL (repopulation) + $existingWebsiteUrl = $formData['website_url'] ?? ''; + $existingWebsiteLabel = $formData['website_label'] ?? ''; - -
- Format(s) - -
+ // Backoffice (add mode: null → falls back to formData) + $currentRaw = []; + $currentAuthorEmail = null; + $currentAuthorShowContact = false; + $currentContextNote = null; - - - - - - getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1'); - $_stmt->execute(['Site web']); - $_siteWebId = $_stmt->fetchColumn(); - if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) { - echo ''; - } - ?> - - - - - - - - -
- Note contextuelle -
- -
- - Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères. -
-
-
- - -
- Backoffice - -
- - - Note du jury (interne, non visible publiquement). -
- -
- - - Notes internes (non visibles publiquement). -
- -
- - - Adresse de contact interne (non visible publiquement). -
- -
- - Case logistique : cocher si un exemplaire physique est disponible à la BAIU. -
- -
- - Case logistique : cocher si un exemplaire physique est disponible à l'ERG. -
-
- - -
+ include APP_ROOT . '/templates/partials/form/form.php'; + ?>
diff --git a/app/templates/admin/edit.php b/app/templates/admin/edit.php index 560c9d6..110426d 100644 --- a/app/templates/admin/edit.php +++ b/app/templates/admin/edit.php @@ -1,402 +1,110 @@

Modifier un TFE

-
- + $thesis['title'], + 'subtitle' => $thesis['subtitle'] ?? '', + 'auteurice' => $thesis['authors'] ?? '', + 'mail' => $currentAuthorEmail ?? '', + 'synopsis' => $thesis['synopsis'] ?? '', + 'tag' => $thesis['keywords'] ?? '', + 'année' => $thesis['year'], + 'orientation' => $thesis['orientation'], + 'ap' => $thesis['ap_program'], + 'finality' => $thesis['finality_type'], + 'duration_pages' => $currentRaw['duration_pages'] ?? '', + 'duration_minutes' => $currentRaw['duration_minutes'] ?? '', + 'lien' => $thesis['baiu_link'] ?? '', + 'contact_public' => $currentAuthorShowContact ?? false, + ]); + $oldFn = fn(string $key, string $default = '') => + isset($editFormData[$key]) && !is_array($editFormData[$key]) + ? htmlspecialchars((string)$editFormData[$key]) : $default; - - + $withAutofocusFn = function (string $field, array $attrs = []) use ($autofocusField) { + if ($autofocusField === $field) $attrs['autofocus'] = true; + return $attrs; + }; - - $currentAuthorShowContact ?? false]); - $editOldFn = function (string $key, string $default = '') use ($thesis, $formData, $currentAuthorEmail) { - if (!empty($formData[$key])) return htmlspecialchars($formData[$key]); - $map = [ - 'titre' => htmlspecialchars($thesis['title']), - 'subtitle' => htmlspecialchars($thesis['subtitle'] ?? ''), - 'auteurice'=> htmlspecialchars($thesis['authors'] ?? ''), - 'mail' => htmlspecialchars($currentAuthorEmail ?? ''), - 'synopsis' => htmlspecialchars($thesis['synopsis'] ?? ''), - ]; - return $map[$key] ?? $default; - }; - $editWithAutofocusFn = function (string $field, array $attrs = []) use ($autofocusField) { - if ($autofocusField === $field) $attrs['autofocus'] = true; - return $attrs; - }; - $allowedObjet = []; - $synopsisExtra = ''; - $oldFn = $editOldFn; - $withAutofocusFn = $editWithAutofocusFn; - include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php'; - $formData = $_SESSION['form_data'] ?? []; - ?> + // ── Shared form variables ────────────────────────────────────────────────── + $mode = 'edit'; + $formAction = '/admin/actions/edit.php'; + $hiddenFields = '' + . ''; - -
- Contact -
- - L'adresse est toujours conservée en interne comme contact de référence. -
-
+ $synopsisExtra = ''; + $formData = $editFormData; - -
- Langue(s) - - -
- - -
- Mots-clés - $thesis['keywords'] ?? '']; - $editKwOldFn = fn(string $key, string $default = '') => isset($editKwFormData[$key]) ? htmlspecialchars((string)$editKwFormData[$key]) : $default; - $oldFn = $editKwOldFn; $withAutofocusFn = $editWithAutofocusFn; $formData = $editKwFormData; - $name = 'tag'; $label = 'Mots-clés (max 10) :'; $value = $editKwOldFn('tag'); - $placeholder = 'sociologie, anthropologie, ...'; - $hint = 'Séparez par des virgules. Max 10 mots-clés.'; - include APP_ROOT . '/templates/partials/form/text-field.php'; - ?> -
- - - $thesis['year'], - 'orientation' => $thesis['orientation'], - 'ap' => $thesis['ap_program'], - 'finality' => $thesis['finality_type'], - ]; - $editAcademicOldFn = function (string $key, string $default = '') use ($editFormData) { - return isset($editFormData[$key]) && !is_array($editFormData[$key]) - ? htmlspecialchars((string)$editFormData[$key]) : $default; - }; - $oldFn = $editAcademicOldFn; - $withAutofocusFn = $editWithAutofocusFn; - $formData = $editFormData; - include APP_ROOT . '/templates/partials/form/fieldset-academic.php'; - ?> - - - + } + $showPresident = true; + $showPromoteurUlb = true; + $promoteurUlbConditional = false; - -
- Format(s) - -
+ // Licence / access — always all enabled for admin + $libreEnabled = true; + $interneEnabled = true; + $interditEnabled = true; + $generalitiesHtml = $helpFn('fieldset_generalites'); + $defaultAccessTypeId = $currentAccessTypeId ?? 2; + $formData['access_type_id'] = $currentAccessTypeId; + $formData['license_id'] = $currentLicenseId; + $formData['license_custom'] = $currentRaw['license_custom'] ?? ''; + $formData['cc2r'] = $currentRaw['cc4r'] ?? false; - -
- Fichiers + // Optional sections + $showContact = true; + $showContextNote = true; + $showBackoffice = true; + $showPublish = true; - -
- -
- -
- Couverture actuelle - -
- - -
- -
-
+ // Files: edit mode + $filesMode = 'edit'; + $currentCover = $currentCover ?? null; + $currentFiles = $currentFiles ?? []; + $currentBannerPath = $thesis['banner_path'] ?? null; + $currentContextNote = $currentContextNote ?? null; - - $f['file_type'] !== 'cover')); ?> - -
- - - Glissez-déposez les lignes pour réordonner les fichiers sur la page publique. - -
    - '📄', - in_array($fExt, ['jpg','jpeg','png','gif','webp']) => '🖼️', - $fType === 'video' || in_array($fExt, ['mp4','webm','mov','ogv']) => '🎬', - $fType === 'audio' || in_array($fExt, ['mp3','ogg','wav','flac','aac','m4a']) => '🔊', - $fType === 'caption' || $fExt === 'vtt' => '💬', - $fType === 'website' => '🌐', - default => '📎', - }; - $isExternalUrl = str_starts_with($f['file_path'] ?? '', 'http://') || str_starts_with($f['file_path'] ?? '', 'https://'); - $fLinkHref = $isExternalUrl - ? htmlspecialchars($f['file_path']) - : ('/media.php?path=' . urlencode($f['file_path'])); - ?> -
  • - - - - - - - - - - 0): ?> - MB - - - - - -
  • - -
-
- - - -
- -
- - - Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier. - -
    -

    Aucun nouveau fichier sélectionné.

    -
    -
    - - -
    - -
    - -
    - Bannière actuelle - -
    - - -
    - -
    -
    -
    - - - - - getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1'); - $_stmt->execute(['Site web']); - $_siteWebId = $_stmt->fetchColumn(); - if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) { - echo ''; - } - ?> - - - $currentRaw['duration_pages'] ?? '', - 'duration_minutes' => $currentRaw['duration_minutes'] ?? '', - 'lien' => $thesis['baiu_link'] ?? '', - ]; - $editMetaOldFn = function (string $key, string $default = '') use ($editMetaFormData) { - return isset($editMetaFormData[$key]) ? htmlspecialchars((string)$editMetaFormData[$key]) : $default; - }; - $oldFn = $editMetaOldFn; - $withAutofocusFn = $editWithAutofocusFn; - $formData = $editMetaFormData; - include APP_ROOT . '/templates/partials/form/fieldset-metadata.php'; - ?> - - - - - -
    - Note contextuelle -
    - -
    - - Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères. -
    -
    -
    - - -
    - Backoffice - -
    - - - Note du jury (interne, non visible publiquement). -
    - -
    - - - Notes internes (non visibles publiquement). -
    - -
    - - - Adresse de contact interne (non visible publiquement). -
    - -
    - - Case logistique : cocher si un exemplaire physique est disponible à la BAIU. -
    - -
    - - Case logistique : cocher si un exemplaire physique est disponible à l'ERG. -
    -
    - - -
    - Publication -
    - -
    -
    - -
    + include APP_ROOT . '/templates/partials/form/form.php'; + ?>
    diff --git a/app/templates/partials/form/fieldset-academic.php b/app/templates/partials/form/fieldset-academic.php index 19a27f5..32d4df2 100644 --- a/app/templates/partials/form/fieldset-academic.php +++ b/app/templates/partials/form/fieldset-academic.php @@ -46,4 +46,3 @@ $formData = $formData ?? []; ?> $d); +$withAutofocusFn = $withAutofocusFn ?? fn($field, $attrs = []) => $attrs; +$filesMode = $filesMode ?? 'add'; +$existingWebsiteUrl = $existingWebsiteUrl ?? ''; +$existingWebsiteLabel = $existingWebsiteLabel ?? ''; +$checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? []; +?> + + + + + + + + + + + + + + + + + + + +
    > + + +

    * Champs obligatoires

    + + + + + + +
    + Contact +
    + + L'adresse est toujours conservée en interne comme contact de référence. +
    +
    + + + +
    + Langue(s) + + +
    + + +
    + Mots-clés + +
    + + + + + + + + +
    + Format(s) + +
    + + + +
    + Fichiers + + +
    + +
    + +
    + " + alt="Couverture actuelle" style="max-height:180px;"> + +
    + + +
    + +
    +
    + + + $f["file_type"] !== "cover", + ), + ); ?> + +
    + + + Glissez-déposez les lignes pour réordonner les fichiers sur la page publique. + +
      + "📄", + in_array($fExt, [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + ]) + => "🖼️", + $fType === "video" || + in_array($fExt, ["mp4", "webm", "mov", "ogv"]) + => "🎬", + $fType === "audio" || + in_array($fExt, [ + "mp3", + "ogg", + "wav", + "flac", + "aac", + "m4a", + ]) + => "🔊", + $fType === "caption" || $fExt === "vtt" => "💬", + $fType === "website" => "🌐", + default => "📎", + }; + $isExternalUrl = + str_starts_with($f["file_path"] ?? "", "http://") || + str_starts_with($f["file_path"] ?? "", "https://"); + $fLinkHref = $isExternalUrl + ? htmlspecialchars($f["file_path"]) + : "/media.php?path=" . urlencode($f["file_path"]); + ?> +
    • "> + "> + + + + + + + + + 0 + ): ?> + MB + + + ]" + value="" + placeholder="Légende / description (optionnel)" + class="admin-file-label-input"> + + +
    • + +
    +
    + + + +
    + +
    + + + Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier. + +
      +

      Aucun nouveau fichier sélectionné.

      +
      +
      + + +
      + +
      + +
      + Bannière actuelle + +
      + + +
      + +
      +
      +
      + + + + + + + getConnection() + ->prepare("SELECT id FROM format_types WHERE name = ? LIMIT 1"); + $_stmt->execute(["Site web"]); + $_siteWebId = $_stmt->fetchColumn(); + if ( + $_siteWebId && + in_array( + (string) $_siteWebId, + array_map("strval", $checkedFormatsForSiteWeb), + true, + ) + ) { + echo ''; + } + ?> + + + + + + + + + +
      + Note contextuelle +
      + +
      + + Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères. +
      +
      +
      + + + + +
      + Backoffice + +
      + + " + step="0.01" min="0" max="20" placeholder="sur 20"> + Note du jury (interne, non visible publiquement). +
      + +
      + + + Notes internes (non visibles publiquement). +
      + +
      + + " + placeholder="ton.email@exemple.be"> + Adresse de contact interne (non visible publiquement). +
      + +
      + + Case logistique : cocher si un exemplaire physique est disponible à la BAIU. +
      + +
      + + Case logistique : cocher si un exemplaire physique est disponible à l'ERG. +
      +
      + + + + +
      + E-mail de confirmation + + + + +
      + + + + +
      + Publication +
      + +
      +
      + + + + +