ixg_platform/apps/guides/markdown.py
ixgadmin 6508cc25ef Add User Guides Viewer (Help Centre) - role-gated wiki help pages
- New apps/guides app with GuidePage model and audience mapping by wiki path
- sync_guides management command: one-way read-only sync from wikijs DB
- Read-only wikijs_ro DB role + WIKI_DB_* env config (secrets, gitignored)
- Markdown rendering with bleach sanitisation; rewrites internal guide links
  to the portal help root and wiki image paths to WIKI_PUBLIC_BASE
- /portal/help/ (buyer) and /ops/help/ (ops) index + detail views with
  role-based audience filtering (404 on cross-role guides)
- Help nav item added to both portal base templates
- Tests: audience filtering, markdown sanitisation, link rewriting, sync command
2026-08-07 14:19:49 +00:00

83 lines
2 KiB
Python

import posixpath
import re
from urllib.parse import urlparse
import bleach
from django.conf import settings
from markdown import markdown
ALLOWED_TAGS = [
"p",
"br",
"strong",
"em",
"ul",
"ol",
"li",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
"h1",
"h2",
"h3",
"h4",
"a",
"code",
"pre",
"blockquote",
"hr",
"img",
"span",
"div",
]
ALLOWED_ATTRIBUTES = {
"a": ["href", "title"],
"img": ["src", "alt", "title"],
"th": ["align"],
"td": ["align"],
}
ALLOWED_PROTOCOLS = ["http", "https", "mailto"]
EXTENSIONS = ["tables", "fenced_code", "sane_lists"]
WIKI_IMAGE_PREFIXES = ("/buyer-guide/", "/ops-guide/")
_IMAGE_SRC_RE = re.compile(r'(<img\b[^>]*?\bsrc=")([^"]*)(")', re.IGNORECASE)
_ANCHOR_HREF_RE = re.compile(r'(<a\b[^>]*?\bhref=")([^"]*)(")', re.IGNORECASE)
def _rewrite_anchor(match, base_folder, help_root):
prefix, href, suffix = match.groups()
parsed = urlparse(href)
if parsed.scheme or href.startswith(("#", "/")):
return match.group(0)
resolved = posixpath.normpath(posixpath.join(base_folder, href))
if resolved.startswith("user-guides/"):
suffix_path = resolved[len("user-guides/"):]
return f'{prefix}{help_root}{suffix_path}/{suffix}'
return match.group(0)
def _rewrite_image(match):
prefix, src, suffix = match.groups()
if src.startswith(WIKI_IMAGE_PREFIXES):
return f'{prefix}{settings.WIKI_PUBLIC_BASE}{src}{suffix}'
return match.group(0)
def render_markdown(content, page_path, help_root):
html = markdown(content or "", extensions=EXTENSIONS)
base_folder = posixpath.dirname(page_path)
html = _ANCHOR_HREF_RE.sub(lambda m: _rewrite_anchor(m, base_folder, help_root), html)
html = _IMAGE_SRC_RE.sub(_rewrite_image, html)
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
)