feat(legal): polished UX for 5 legal pages + AGPL external link (sticky TOC, prev/next, breadcrumb)
Refonte visuelle et accessibilité (WCAG 2.2 AA) de la section /legal/ sans toucher au contenu juridique signé (dc4ac97). Templates : - templates/legal/index.html : grille 6 cartes (5 internes + AGPL externe) avec icônes SVG sémantiques, hero gradient, bloc info sous-processeurs, carte AGPL ↗ (target=_blank, rel=noopener noreferrer). - templates/legal/_layout.html : breadcrumb sticky, TOC sticky desktop + collapsible mobile (Alpine.js + IntersectionObserver), prev/next nav entre les 6 docs, skip link, landmarks (main / aside / nav), typographie améliorée (h2 avec accent gradient, tables zebrées, blockquotes), print stylesheet (cache header/breadcrumb/TOC/prev-next). Routes (src/legal/routes.py) : - DISPLAY_ORDER + EXTERNAL_LINKS + PAGE_ICONS exposés. - legal_page() calcule prev/next via _neighbour() helper. - legal_index() concatène pages internes + EXTERNAL_LINKS dans `pages`. Footer : lien AGPL déjà présent depuisdc4ac97(col 4 Compte, ligne 49). Tests (tests/test_legal_pages.py) : 9 anciens + 9 nouveaux = 18/18 PASS - AGPL external link (target+rel) - 5 internes + 1 externe sur l'index - Skip link présent partout - Prev/next existe sur chaque page - Conditions (1ère) sans prev / Mentions (dernière) sans next - Landmarks aside aria-label="Table des matières" - Landmark main role + id="main-content" - Breadcrumb avec aria-current="page" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from src.legal import LEGAL_VERSION, legal_bp
|
|||||||
|
|
||||||
CONTENT_DIR = Path(__file__).parent / 'content'
|
CONTENT_DIR = Path(__file__).parent / 'content'
|
||||||
|
|
||||||
|
# All slugs that have a markdown file rendered by this blueprint.
|
||||||
VALID_PAGES = (
|
VALID_PAGES = (
|
||||||
'conditions',
|
'conditions',
|
||||||
'confidentialite',
|
'confidentialite',
|
||||||
@@ -21,6 +22,17 @@ VALID_PAGES = (
|
|||||||
'mentions',
|
'mentions',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Display order on the index page (mentions kept in VALID_PAGES but
|
||||||
|
# rendered last on /legal/ — purely a presentation choice).
|
||||||
|
DISPLAY_ORDER = (
|
||||||
|
'conditions',
|
||||||
|
'confidentialite',
|
||||||
|
'cookies',
|
||||||
|
'remboursement',
|
||||||
|
'accessibilite',
|
||||||
|
'mentions',
|
||||||
|
)
|
||||||
|
|
||||||
PAGE_TITLES = {
|
PAGE_TITLES = {
|
||||||
'conditions': "Conditions d'utilisation",
|
'conditions': "Conditions d'utilisation",
|
||||||
'confidentialite': "Politique de confidentialité (Loi 25)",
|
'confidentialite': "Politique de confidentialité (Loi 25)",
|
||||||
@@ -39,6 +51,90 @@ PAGE_DESCRIPTIONS = {
|
|||||||
'mentions': "Mentions légales — DictIA Inc. (filiale d'InnovA AI S.E.N.C.).",
|
'mentions': "Mentions légales — DictIA Inc. (filiale d'InnovA AI S.E.N.C.).",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Inline SVG icons (semantic, brand-colored). Each opens with a 24x24 viewBox.
|
||||||
|
PAGE_ICONS = {
|
||||||
|
'conditions': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
|
||||||
|
'<polyline points="14 2 14 8 20 8"/>'
|
||||||
|
'<line x1="9" y1="13" x2="15" y2="13"/>'
|
||||||
|
'<line x1="9" y1="17" x2="15" y2="17"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'confidentialite': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>'
|
||||||
|
'<polyline points="9 12 11 14 15 10"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'cookies': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"/>'
|
||||||
|
'<circle cx="8.5" cy="10.5" r="0.6" fill="currentColor"/>'
|
||||||
|
'<circle cx="13" cy="14" r="0.6" fill="currentColor"/>'
|
||||||
|
'<circle cx="16" cy="9" r="0.6" fill="currentColor"/>'
|
||||||
|
'<circle cx="9" cy="15.5" r="0.6" fill="currentColor"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'remboursement': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<path d="M21 12V8a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-1"/>'
|
||||||
|
'<path d="M16 12h5"/>'
|
||||||
|
'<circle cx="17" cy="12" r="2"/>'
|
||||||
|
'<path d="M3 9l4-4 2 2"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'accessibilite': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<circle cx="12" cy="12" r="10"/>'
|
||||||
|
'<circle cx="12" cy="6.5" r="1.2" fill="currentColor"/>'
|
||||||
|
'<path d="M5 9h14"/>'
|
||||||
|
'<path d="M12 9v4l-3 6"/>'
|
||||||
|
'<path d="M12 13l3 6"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'mentions': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<rect x="3" y="4" width="18" height="16" rx="2"/>'
|
||||||
|
'<line x1="7" y1="9" x2="17" y2="9"/>'
|
||||||
|
'<line x1="7" y1="13" x2="17" y2="13"/>'
|
||||||
|
'<line x1="7" y1="17" x2="13" y2="17"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
'agpl': (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||||
|
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||||
|
'stroke-linejoin="round" aria-hidden="true">'
|
||||||
|
'<polyline points="16 18 22 12 16 6"/>'
|
||||||
|
'<polyline points="8 6 2 12 8 18"/>'
|
||||||
|
'<line x1="14" y1="4" x2="10" y2="20"/>'
|
||||||
|
'</svg>'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# External links surfaced on the legal index alongside the markdown pages.
|
||||||
|
EXTERNAL_LINKS = (
|
||||||
|
{
|
||||||
|
'slug': 'agpl',
|
||||||
|
'title': 'Code source AGPL',
|
||||||
|
'description': "Code source de DictIA publié sous licence AGPL-3.0 — conformité art. 13 de la licence.",
|
||||||
|
'url': 'https://gitea.dictia.ca',
|
||||||
|
'external': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _render_markdown(page: str) -> str:
|
def _render_markdown(page: str) -> str:
|
||||||
"""Read the markdown file for `page` and return rendered HTML."""
|
"""Read the markdown file for `page` and return rendered HTML."""
|
||||||
@@ -53,11 +149,24 @@ def _render_markdown(page: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _neighbour(slug: str, offset: int):
|
||||||
|
"""Return the (slug, title) of the previous/next page in DISPLAY_ORDER."""
|
||||||
|
if slug not in DISPLAY_ORDER:
|
||||||
|
return None, None
|
||||||
|
idx = DISPLAY_ORDER.index(slug) + offset
|
||||||
|
if idx < 0 or idx >= len(DISPLAY_ORDER):
|
||||||
|
return None, None
|
||||||
|
neighbour_slug = DISPLAY_ORDER[idx]
|
||||||
|
return neighbour_slug, PAGE_TITLES[neighbour_slug]
|
||||||
|
|
||||||
|
|
||||||
@legal_bp.route('/<page>')
|
@legal_bp.route('/<page>')
|
||||||
def legal_page(page):
|
def legal_page(page):
|
||||||
"""Render one of the 6 legal pages by slug."""
|
"""Render one of the 6 legal pages by slug."""
|
||||||
if page not in VALID_PAGES:
|
if page not in VALID_PAGES:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
prev_slug, prev_title = _neighbour(page, -1)
|
||||||
|
next_slug, next_title = _neighbour(page, +1)
|
||||||
return render_template(
|
return render_template(
|
||||||
'legal/_layout.html',
|
'legal/_layout.html',
|
||||||
title=PAGE_TITLES[page],
|
title=PAGE_TITLES[page],
|
||||||
@@ -65,20 +174,40 @@ def legal_page(page):
|
|||||||
content=_render_markdown(page),
|
content=_render_markdown(page),
|
||||||
page=page,
|
page=page,
|
||||||
legal_version=LEGAL_VERSION,
|
legal_version=LEGAL_VERSION,
|
||||||
|
prev_page=prev_slug,
|
||||||
|
prev_title=prev_title,
|
||||||
|
next_page=next_slug,
|
||||||
|
next_title=next_title,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@legal_bp.route('/')
|
@legal_bp.route('/')
|
||||||
def legal_index():
|
def legal_index():
|
||||||
"""Index page listing all 6 legal pages."""
|
"""Index page listing all internal legal pages plus external links."""
|
||||||
pages = [
|
pages = [
|
||||||
{'slug': slug, 'title': PAGE_TITLES[slug], 'description': PAGE_DESCRIPTIONS[slug]}
|
{
|
||||||
for slug in VALID_PAGES
|
'slug': slug,
|
||||||
|
'title': PAGE_TITLES[slug],
|
||||||
|
'description': PAGE_DESCRIPTIONS[slug],
|
||||||
|
'icon': PAGE_ICONS.get(slug, ''),
|
||||||
|
'external': False,
|
||||||
|
}
|
||||||
|
for slug in DISPLAY_ORDER
|
||||||
|
] + [
|
||||||
|
{
|
||||||
|
'slug': link['slug'],
|
||||||
|
'title': link['title'],
|
||||||
|
'description': link['description'],
|
||||||
|
'icon': PAGE_ICONS.get(link['slug'], ''),
|
||||||
|
'url': link['url'],
|
||||||
|
'external': True,
|
||||||
|
}
|
||||||
|
for link in EXTERNAL_LINKS
|
||||||
]
|
]
|
||||||
return render_template(
|
return render_template(
|
||||||
'legal/index.html',
|
'legal/index.html',
|
||||||
title="Documents légaux DictIA",
|
title="Documents légaux DictIA",
|
||||||
description="Index des documents légaux DictIA — conditions, confidentialité, cookies, remboursement, accessibilité, mentions.",
|
description="Index des documents légaux DictIA — conditions, confidentialité, cookies, remboursement, accessibilité, mentions, code source AGPL.",
|
||||||
pages=pages,
|
pages=pages,
|
||||||
legal_version=LEGAL_VERSION,
|
legal_version=LEGAL_VERSION,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,24 +5,38 @@
|
|||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<style>
|
<style>
|
||||||
/* Typographic styles for markdown-rendered legal content.
|
/* ---------------------------------------------------------------------------
|
||||||
Inlined here so we don't need to rebuild static/css/marketing.css. */
|
Typographie pour le markdown rendu (héritée de B-2.9, étendue B-2.10).
|
||||||
|
Inlinée pour ne pas avoir à reconstruire static/css/marketing.css.
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
.legal-content h2 {
|
.legal-content h2 {
|
||||||
|
position: relative;
|
||||||
font-size: 1.5rem; /* 24px */
|
font-size: 1.5rem; /* 24px */
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #060d1a; /* brand-navy */
|
color: #060d1a; /* brand-navy */
|
||||||
margin-top: 2rem;
|
margin-top: 2.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 1rem;
|
||||||
letter-spacing: -0.022em;
|
letter-spacing: -0.022em;
|
||||||
|
scroll-margin-top: 90px; /* pour ancres sous le header sticky */
|
||||||
|
}
|
||||||
|
.legal-content h2::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(118deg, #0062ff, #00bdd8 52%, #00c896);
|
||||||
}
|
}
|
||||||
.legal-content h3 {
|
.legal-content h3 {
|
||||||
font-size: 1.25rem; /* 20px */
|
font-size: 1.25rem; /* 20px */
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #060d1a;
|
color: #060d1a;
|
||||||
margin-top: 1.5rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.75rem;
|
||||||
|
scroll-margin-top: 90px;
|
||||||
}
|
}
|
||||||
.legal-content h4 {
|
.legal-content h4 {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
@@ -34,17 +48,17 @@
|
|||||||
.legal-content p {
|
.legal-content p {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
.legal-content ul,
|
.legal-content ul,
|
||||||
.legal-content ol {
|
.legal-content ol {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
line-height: 1.7;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
.legal-content ul { list-style-type: disc; list-style-position: outside; }
|
.legal-content ul { list-style-type: disc; list-style-position: outside; }
|
||||||
.legal-content ol { list-style-type: decimal; list-style-position: outside; }
|
.legal-content ol { list-style-type: decimal; list-style-position: outside; }
|
||||||
.legal-content li { margin-bottom: 0.25rem; }
|
.legal-content li { margin-bottom: 0.35rem; }
|
||||||
.legal-content a {
|
.legal-content a {
|
||||||
background: linear-gradient(118deg, #0062ff, #00bdd8 52%, #00c896);
|
background: linear-gradient(118deg, #0062ff, #00bdd8 52%, #00c896);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
@@ -61,26 +75,33 @@
|
|||||||
}
|
}
|
||||||
.legal-content table {
|
.legal-content table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1rem;
|
margin: 1rem 0 1.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
.legal-content th,
|
.legal-content th,
|
||||||
.legal-content td {
|
.legal-content td {
|
||||||
border: 1px solid #e6ebf2;
|
border: 1px solid #e6ebf2;
|
||||||
padding: 0.5rem;
|
padding: 0.6rem 0.75rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.legal-content th {
|
.legal-content th {
|
||||||
background-color: #f7f9fc;
|
background-color: #f7f9fc;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #060d1a;
|
||||||
|
}
|
||||||
|
.legal-content tbody tr:nth-child(even) td {
|
||||||
|
background-color: #fafbfd;
|
||||||
}
|
}
|
||||||
.legal-content blockquote {
|
.legal-content blockquote {
|
||||||
border-left: 4px solid #0062ff;
|
border-left: 4px solid #0062ff;
|
||||||
padding-left: 1rem;
|
background-color: rgba(247, 249, 252, 0.6);
|
||||||
margin: 1rem 0;
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: rgba(6, 13, 26, 0.7);
|
color: rgba(6, 13, 26, 0.75);
|
||||||
}
|
}
|
||||||
.legal-content code {
|
.legal-content code {
|
||||||
padding: 0.15rem 0.4rem;
|
padding: 0.15rem 0.4rem;
|
||||||
@@ -89,6 +110,19 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-family: 'JetBrains Mono Variable', 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono Variable', 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
.legal-content pre {
|
||||||
|
background-color: #f7f9fc;
|
||||||
|
border: 1px solid #e6ebf2;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.legal-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.legal-content hr {
|
.legal-content hr {
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -107,28 +141,239 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #78350f;
|
color: #78350f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
Sticky TOC + breadcrumb (desktop ≥ lg).
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
.legal-toc {
|
||||||
|
position: sticky;
|
||||||
|
top: 86px; /* sous header 62px + marge */
|
||||||
|
max-height: calc(100vh - 110px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.legal-toc a {
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
|
||||||
|
}
|
||||||
|
.legal-toc a:hover {
|
||||||
|
background-color: rgba(0, 98, 255, 0.05);
|
||||||
|
}
|
||||||
|
.legal-toc a.is-active {
|
||||||
|
border-left-color: #0062ff;
|
||||||
|
color: #0062ff !important;
|
||||||
|
background-color: rgba(0, 98, 255, 0.06);
|
||||||
|
}
|
||||||
|
.legal-breadcrumb {
|
||||||
|
position: sticky;
|
||||||
|
top: 62px;
|
||||||
|
z-index: 30;
|
||||||
|
background-color: rgba(247, 249, 252, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid #e6ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
Print stylesheet — hide nav chrome, keep article + header.
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
@media print {
|
||||||
|
header.fixed,
|
||||||
|
.legal-breadcrumb,
|
||||||
|
.legal-toc-wrapper,
|
||||||
|
.legal-prev-next,
|
||||||
|
footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
main { padding-top: 0 !important; }
|
||||||
|
.legal-content a {
|
||||||
|
color: #000 !important;
|
||||||
|
background: none !important;
|
||||||
|
-webkit-text-fill-color: #000 !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
body { background: white !important; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="bg-brand-bg py-16 px-4" aria-labelledby="legal-title">
|
{# Skip link (WCAG 2.4.1) — visible uniquement au focus clavier. #}
|
||||||
<article class="max-w-3xl mx-auto bg-white p-8 md:p-10 rounded-[18px] border border-brand-border shadow-cta">
|
<a href="#main-content"
|
||||||
|
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-brand-navy focus:text-white focus:rounded-lg focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
|
||||||
|
Aller au contenu principal
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{# Breadcrumb sticky #}
|
||||||
|
<nav class="legal-breadcrumb px-4 py-3" aria-label="Fil d'Ariane">
|
||||||
|
<ol class="max-w-[1200px] mx-auto flex flex-wrap items-center gap-2 text-xs md:text-sm text-brand-navy/70">
|
||||||
|
<li><a href="/" class="hover:text-brand-b1 focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded">Accueil</a></li>
|
||||||
|
<li aria-hidden="true" class="text-brand-navy/40">›</li>
|
||||||
|
<li><a href="{{ url_for('legal.legal_index') }}" class="hover:text-brand-b1 focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded">Documents légaux</a></li>
|
||||||
|
<li aria-hidden="true" class="text-brand-navy/40">›</li>
|
||||||
|
<li class="text-brand-navy font-semibold truncate" aria-current="page">{{ title }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="bg-brand-bg pt-8 pb-16 px-4">
|
||||||
|
<div class="max-w-[1200px] mx-auto lg:grid lg:grid-cols-[1fr_240px] lg:gap-10">
|
||||||
|
|
||||||
|
{# Article principal #}
|
||||||
|
<article id="main-content"
|
||||||
|
role="main"
|
||||||
|
aria-labelledby="legal-title"
|
||||||
|
class="bg-white p-6 md:p-10 rounded-[18px] border border-brand-border shadow-cta order-1">
|
||||||
<header class="mb-8 pb-6 border-b border-brand-border">
|
<header class="mb-8 pb-6 border-b border-brand-border">
|
||||||
<p class="text-xs uppercase tracking-wider text-brand-navy/70 mb-2">Document légal DictIA</p>
|
<p class="text-xs uppercase tracking-wider text-brand-navy/70 mb-2">Document légal DictIA</p>
|
||||||
<h1 id="legal-title" class="text-3xl md:text-4xl font-black text-brand-navy mb-3">{{ title }}</h1>
|
<h1 id="legal-title" class="text-3xl md:text-4xl font-black text-brand-navy mb-4 tracking-tight">{{ title }}</h1>
|
||||||
<p class="text-sm text-brand-navy/70">
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-brand-navy/70">
|
||||||
Version {{ legal_version }}{{ ' ' }}·{{ ' ' }}Dernière révision : {{ legal_version }}.
|
<span class="inline-flex items-center gap-1.5">
|
||||||
Pour toute question, écrivez à <a href="mailto:rprp@dictia.ca" class="grad-text font-semibold">rprp@dictia.ca</a>.
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
</p>
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
</svg>
|
||||||
|
<span>Version <strong class="text-brand-navy">{{ legal_version }}</strong></span>
|
||||||
|
</span>
|
||||||
|
<span class="text-brand-navy/40" aria-hidden="true">·</span>
|
||||||
|
<span>Dernière mise à jour : {{ legal_version }}</span>
|
||||||
|
<span class="text-brand-navy/40" aria-hidden="true">·</span>
|
||||||
|
<span>RPRP : <a href="mailto:rprp@dictia.ca" class="grad-text font-semibold underline">rprp@dictia.ca</a></span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{# TOC mobile (collapsible) — visible < lg seulement #}
|
||||||
|
<details class="lg:hidden mb-6 border border-brand-border rounded-[12px] bg-brand-bg/50">
|
||||||
|
<summary class="cursor-pointer px-4 py-3 text-sm font-semibold text-brand-navy flex items-center justify-between focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded-[12px]">
|
||||||
|
<span>Sur cette page</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<ul id="legal-toc-mobile" class="px-4 pb-3 pt-1 space-y-1 text-sm">
|
||||||
|
{# Rempli côté JS (Alpine via init du desktop). #}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="legal-content text-brand-navy/90 leading-relaxed">
|
<div class="legal-content text-brand-navy/90 leading-relaxed">
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-10 pt-6 border-t border-brand-border text-sm text-brand-navy/70">
|
{# Prev / Next navigation #}
|
||||||
<p>← <a href="{{ url_for('legal.legal_index') }}" class="grad-text font-semibold">Index des documents légaux</a></p>
|
{% if prev_page or next_page %}
|
||||||
|
<nav class="legal-prev-next mt-12 pt-6 border-t border-brand-border grid sm:grid-cols-2 gap-3"
|
||||||
|
aria-label="Navigation entre documents légaux">
|
||||||
|
{% if prev_page %}
|
||||||
|
<a href="{{ url_for('legal.legal_page', page=prev_page) }}"
|
||||||
|
rel="prev"
|
||||||
|
class="block p-4 bg-brand-bg/60 border border-brand-border rounded-[12px] hover:border-brand-b1 hover:bg-white transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
|
||||||
|
<span class="block text-xs uppercase tracking-wider text-brand-navy/60 mb-1">
|
||||||
|
<span aria-hidden="true">←</span> Précédent
|
||||||
|
</span>
|
||||||
|
<span class="block text-sm font-semibold text-brand-navy">{{ prev_title }}</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next_page %}
|
||||||
|
<a href="{{ url_for('legal.legal_page', page=next_page) }}"
|
||||||
|
rel="next"
|
||||||
|
class="block p-4 bg-brand-bg/60 border border-brand-border rounded-[12px] hover:border-brand-b1 hover:bg-white transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 sm:text-right">
|
||||||
|
<span class="block text-xs uppercase tracking-wider text-brand-navy/60 mb-1">
|
||||||
|
Suivant <span aria-hidden="true">→</span>
|
||||||
|
</span>
|
||||||
|
<span class="block text-sm font-semibold text-brand-navy">{{ next_title }}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<footer class="mt-8 pt-6 border-t border-brand-border text-sm text-brand-navy/70">
|
||||||
|
<p>
|
||||||
|
<span aria-hidden="true">←</span>
|
||||||
|
<a href="{{ url_for('legal.legal_index') }}" class="grad-text font-semibold">Index des documents légaux</a>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{# TOC desktop — sidebar sticky #}
|
||||||
|
<aside class="legal-toc-wrapper hidden lg:block order-2"
|
||||||
|
aria-label="Table des matières">
|
||||||
|
<div x-data="legalToc()"
|
||||||
|
x-init="init()"
|
||||||
|
class="legal-toc bg-white border border-brand-border rounded-[14px] p-5 mt-0">
|
||||||
|
<h2 class="text-xs font-bold uppercase tracking-wider text-brand-navy/70 mb-3">
|
||||||
|
Sur cette page
|
||||||
|
</h2>
|
||||||
|
<ul role="list">
|
||||||
|
<template x-for="item in items" :key="item.id">
|
||||||
|
<li>
|
||||||
|
<a :href="'#' + item.id"
|
||||||
|
:class="active === item.id ? 'is-active font-semibold' : ''"
|
||||||
|
:aria-current="active === item.id ? 'true' : null"
|
||||||
|
class="block py-1.5 pl-3 pr-2 text-sm text-brand-navy/70 hover:text-brand-b1 focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded"
|
||||||
|
x-text="item.text"></a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template x-if="items.length === 0">
|
||||||
|
<li class="text-xs text-brand-navy/50 italic py-1">
|
||||||
|
Aucune section à afficher.
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Construit la TOC en scannant les <h2> du contenu rendu, met l'élément actif
|
||||||
|
// à jour via IntersectionObserver. Synchronise aussi la liste mobile.
|
||||||
|
function legalToc() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
active: '',
|
||||||
|
init() {
|
||||||
|
const populate = () => {
|
||||||
|
const headings = Array.from(document.querySelectorAll('.legal-content h2'));
|
||||||
|
this.items = headings
|
||||||
|
.filter(h => h.id) // markdown.toc auto-id; skip if missing
|
||||||
|
.map(h => ({ id: h.id, text: h.textContent.trim() }));
|
||||||
|
|
||||||
|
// Mirror dans le <details> mobile.
|
||||||
|
const mobileList = document.getElementById('legal-toc-mobile');
|
||||||
|
if (mobileList) {
|
||||||
|
mobileList.innerHTML = '';
|
||||||
|
this.items.forEach(it => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '#' + it.id;
|
||||||
|
a.textContent = it.text;
|
||||||
|
a.className = 'block py-1.5 text-brand-navy/80 hover:text-brand-b1 focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded';
|
||||||
|
li.appendChild(a);
|
||||||
|
mobileList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headings.length === 0) return;
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (e.isIntersecting) this.active = e.target.id;
|
||||||
|
});
|
||||||
|
}, { rootMargin: '-100px 0px -60% 0px' });
|
||||||
|
headings.forEach(el => observer.observe(el));
|
||||||
|
};
|
||||||
|
// Lance après que Alpine ait rendu et que le DOM soit posé.
|
||||||
|
if (document.readyState === 'complete') populate();
|
||||||
|
else window.addEventListener('load', populate, { once: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,25 +3,117 @@
|
|||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% block description %}{{ description }}{% endblock %}
|
{% block description %}{{ description }}{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
/* Card hover/lift uniquement pour les cartes légales (sobre, accessible). */
|
||||||
|
.legal-card {
|
||||||
|
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
||||||
|
}
|
||||||
|
.legal-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.legal-card:focus-visible {
|
||||||
|
outline: 2px solid #0062ff;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
/* Icône circulaire avec dégradé de marque, contraste suffisant. */
|
||||||
|
.legal-card-icon {
|
||||||
|
background: linear-gradient(135deg, rgba(0,98,255,0.10), rgba(0,200,150,0.10));
|
||||||
|
color: #0062ff;
|
||||||
|
}
|
||||||
|
.legal-card.is-external .legal-card-icon {
|
||||||
|
background: linear-gradient(135deg, rgba(0,189,216,0.12), rgba(0,200,150,0.12));
|
||||||
|
}
|
||||||
|
/* Print : pas de bouton CTA, pas d'animations. */
|
||||||
|
@media print {
|
||||||
|
.legal-card { box-shadow: none !important; transform: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="bg-brand-bg py-16 px-4 min-h-[60vh]" aria-labelledby="legal-index-title">
|
<a href="#main-content"
|
||||||
<div class="max-w-3xl mx-auto">
|
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-brand-navy focus:text-white focus:rounded-lg focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
|
||||||
<header class="mb-8 text-center">
|
Aller au contenu principal
|
||||||
<p class="text-xs uppercase tracking-wider text-brand-navy/70 mb-2">DictIA Inc.</p>
|
</a>
|
||||||
<h1 id="legal-index-title" class="text-3xl md:text-4xl font-black text-brand-navy mb-3">Documents légaux</h1>
|
|
||||||
<p class="text-sm text-brand-navy/70">Version {{ legal_version }} de l'ensemble des documents.</p>
|
<section class="bg-brand-bg pt-12 pb-20 px-4" aria-labelledby="legal-index-title">
|
||||||
|
<div id="main-content" class="max-w-[1100px] mx-auto">
|
||||||
|
|
||||||
|
{# Hero #}
|
||||||
|
<header class="text-center max-w-3xl mx-auto mb-12">
|
||||||
|
<p class="eyebrow text-brand-navy/60 mb-3">DictIA Inc.</p>
|
||||||
|
<h1 id="legal-index-title" class="text-4xl md:text-5xl font-black text-brand-navy mb-4 tracking-tight">
|
||||||
|
Documents <span class="grad-text">légaux</span> DictIA
|
||||||
|
</h1>
|
||||||
|
<p class="text-base md:text-lg text-brand-navy/70 leading-relaxed mb-5">
|
||||||
|
Transparence totale conforme à la {{ 'Loi 25' | safe }} du Québec : tous nos documents juridiques,
|
||||||
|
notre code source et notre politique de confidentialité sont publics et indexables.
|
||||||
|
</p>
|
||||||
|
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-brand-border text-xs font-semibold text-brand-navy/80">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12 6 12 12 16 14"/>
|
||||||
|
</svg>
|
||||||
|
<span>Version {{ legal_version }} · dernière mise à jour {{ legal_version }}</span>
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul class="space-y-3" role="list">
|
{# Grille 6 cartes — 5 internes + 1 externe AGPL #}
|
||||||
|
<ul class="grid sm:grid-cols-2 gap-4 md:gap-5" role="list">
|
||||||
{% for page in pages %}
|
{% for page in pages %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('legal.legal_page', page=page.slug) }}" class="block p-5 bg-white border border-brand-border rounded-[12px] hover:border-brand-b1 hover:shadow-cta transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
|
{% if page.external %}
|
||||||
<h2 class="text-lg font-semibold text-brand-navy mb-1">{{ page.title }}</h2>
|
<a href="{{ page.url }}"
|
||||||
<p class="text-sm text-brand-navy/70">{{ page.description }}</p>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="legal-card is-external group block h-full p-5 md:p-6 bg-white border border-brand-border rounded-[14px] hover:border-brand-b1 hover:shadow-cta focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 relative">
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('legal.legal_page', page=page.slug) }}"
|
||||||
|
class="legal-card group block h-full p-5 md:p-6 bg-white border border-brand-border rounded-[14px] hover:border-brand-b1 hover:shadow-cta focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 relative">
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="legal-card-icon shrink-0 inline-flex items-center justify-center w-12 h-12 rounded-xl">
|
||||||
|
{{ page.icon | safe }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h2 class="text-base md:text-lg font-bold text-brand-navy mb-1.5 group-hover:grad-text transition-colors">
|
||||||
|
{{ page.title }}{% if page.external %}<span class="ml-1.5 text-brand-b1" aria-hidden="true">↗</span>{% endif %}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-brand-navy/70 leading-relaxed">{{ page.description }}</p>
|
||||||
|
{% if page.external %}
|
||||||
|
<p class="mt-2 text-xs text-brand-navy/50 font-medium">
|
||||||
|
<span aria-hidden="true">→</span>
|
||||||
|
<span>{{ page.url }}</span>
|
||||||
|
<span class="sr-only">(s'ouvre dans un nouvel onglet)</span>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{# Bloc info pied — signature, contact, sous-processeurs #}
|
||||||
|
<aside class="mt-12 max-w-3xl mx-auto bg-white border border-brand-border rounded-[14px] p-6 md:p-7"
|
||||||
|
aria-label="Informations complémentaires sur les documents légaux">
|
||||||
|
<p class="text-sm text-brand-navy/80 leading-relaxed mb-3">
|
||||||
|
Documents signés numériquement par <strong class="text-brand-navy">Allison Rioux</strong>,
|
||||||
|
présidente et responsable de la protection des renseignements personnels (RPRP) —
|
||||||
|
version {{ legal_version }}.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-brand-navy/80 leading-relaxed mb-3">
|
||||||
|
Pour toute question, demande d'accès, rectification ou plainte :
|
||||||
|
<a href="mailto:rprp@dictia.ca" class="grad-text font-semibold underline">rprp@dictia.ca</a>.
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-brand-navy/60 leading-relaxed">
|
||||||
|
<strong>5 sous-processeurs</strong> : OVH (Beauharnois, QC) ·
|
||||||
|
Google Cloud (Toronto, Ontario) · Cloudflare (US) · HubSpot (US) · Stripe (US) —
|
||||||
|
détails dans la <a href="{{ url_for('legal.legal_page', page='confidentialite') }}" class="grad-text font-semibold">Politique de confidentialité</a>.
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -164,3 +164,168 @@ def test_legal_pages_have_loi25_draft_callout():
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.session.rollback(); db.drop_all()
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# B-2.10 — UX upgrade tests : AGPL external link, skip link, breadcrumb,
|
||||||
|
# landmarks, prev/next navigation, sticky TOC.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_legal_index_includes_agpl_external_link():
|
||||||
|
"""The /legal/ index must surface the AGPL source code as an external link."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
resp = client.get('/legal/')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'https://gitea.dictia.ca' in body
|
||||||
|
assert 'target="_blank"' in body
|
||||||
|
assert 'rel="noopener noreferrer"' in body
|
||||||
|
assert 'AGPL' in body
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_index_lists_5_internal_pages_plus_agpl():
|
||||||
|
"""Index must show internal pages + AGPL external card (count >=6)."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
resp = client.get('/legal/')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
for slug in ('conditions', 'confidentialite', 'cookies',
|
||||||
|
'remboursement', 'accessibilite'):
|
||||||
|
assert f'/legal/{slug}' in body, f'Missing internal card: {slug}'
|
||||||
|
# External AGPL link
|
||||||
|
assert 'gitea.dictia.ca' in body
|
||||||
|
# Count cards via the legal-card class
|
||||||
|
assert body.count('legal-card') >= 6, (
|
||||||
|
f'Expected at least 6 legal-card occurrences, found {body.count("legal-card")}'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_pages_have_skip_link():
|
||||||
|
"""Every legal page must include a WCAG skip link to #main-content."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
for page in VALID_PAGES:
|
||||||
|
resp = client.get(f'/legal/{page}')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'href="#main-content"' in body, (
|
||||||
|
f'/legal/{page} missing skip link to #main-content'
|
||||||
|
)
|
||||||
|
assert 'Aller au contenu principal' in body, (
|
||||||
|
f'/legal/{page} missing French skip link label'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_pages_have_prev_next_navigation():
|
||||||
|
"""Each legal page (except first/last) must have prev OR next link to neighbours."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
for page in VALID_PAGES:
|
||||||
|
resp = client.get(f'/legal/{page}')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
# The wrapping nav must always be present (rel="prev" OR rel="next").
|
||||||
|
assert 'rel="prev"' in body or 'rel="next"' in body, (
|
||||||
|
f'/legal/{page} has neither prev nor next neighbour link'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_first_page_no_prev_link():
|
||||||
|
"""The first page (conditions) must not expose a 'prev' link."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
resp = client.get('/legal/conditions')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'rel="prev"' not in body, "conditions page should not have a prev link"
|
||||||
|
assert 'rel="next"' in body, "conditions page should have a next link"
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_last_page_no_next_link():
|
||||||
|
"""The last page (mentions) must not expose a 'next' link."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
resp = client.get('/legal/mentions')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'rel="next"' not in body, "mentions page should not have a next link"
|
||||||
|
assert 'rel="prev"' in body, "mentions page should have a prev link"
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_pages_aside_toc_landmark():
|
||||||
|
"""Every legal page must expose an <aside aria-label='Table des matières'> landmark."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
for page in VALID_PAGES:
|
||||||
|
resp = client.get(f'/legal/{page}')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'aria-label="Table des matières"' in body, (
|
||||||
|
f'/legal/{page} missing TOC aside landmark'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_pages_main_landmark():
|
||||||
|
"""Every legal page must wrap its article in role='main' with id='main-content'."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
for page in VALID_PAGES:
|
||||||
|
resp = client.get(f'/legal/{page}')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert 'id="main-content"' in body, (
|
||||||
|
f'/legal/{page} missing id="main-content"'
|
||||||
|
)
|
||||||
|
assert 'role="main"' in body, (
|
||||||
|
f'/legal/{page} missing role="main"'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_pages_breadcrumb_present():
|
||||||
|
"""Every legal page must include a Fil d'Ariane breadcrumb."""
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
try:
|
||||||
|
client = app.test_client()
|
||||||
|
for page in VALID_PAGES:
|
||||||
|
resp = client.get(f'/legal/{page}')
|
||||||
|
body = resp.data.decode('utf-8')
|
||||||
|
assert "aria-label=\"Fil d'Ariane\"" in body, (
|
||||||
|
f'/legal/{page} missing breadcrumb landmark'
|
||||||
|
)
|
||||||
|
assert 'aria-current="page"' in body, (
|
||||||
|
f'/legal/{page} missing aria-current="page" on breadcrumb'
|
||||||
|
)
|
||||||
|
# Breadcrumb chain should reference Accueil and Documents légaux
|
||||||
|
assert 'Accueil' in body and 'Documents légaux' in body, (
|
||||||
|
f'/legal/{page} breadcrumb chain incomplete'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.session.rollback(); db.drop_all()
|
||||||
|
|||||||
Reference in New Issue
Block a user