ixg_platform/apps/guides/models.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

57 lines
1.8 KiB
Python

from django.db import models
class GuidePage(models.Model):
class Audience(models.TextChoices):
SHARED = "shared", "Shared"
BUYER = "buyer", "Buyer"
OPS = "ops", "Operations"
ADMIN = "admin", "Administrator"
AUDIENCE_PREFIXES = (
("user-guides/index", Audience.SHARED),
("user-guides/buyer/", Audience.BUYER),
("user-guides/ops/", Audience.OPS),
("user-guides/admin/", Audience.ADMIN),
)
path = models.CharField(max_length=255, unique=True)
title = models.CharField(max_length=255)
audience = models.CharField(max_length=20, choices=Audience.choices)
content = models.TextField(blank=True, default="")
updated_at = models.DateTimeField()
synced_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["path"]
def __str__(self):
return self.title
@property
def page_slug(self):
return self.path[len("user-guides/"):] if self.path.startswith("user-guides/") else self.path
def preview(self, max_chars=180):
for line in self.content.splitlines():
line = line.strip()
if not line or line.startswith(("#", "-", ">", "|", "*", "![", "```", "<")):
continue
text = line.replace("**", "").replace("`", "").replace("*", "").strip()
if text:
return text[:max_chars]
return ""
@classmethod
def map_audience(cls, path):
for prefix, audience in cls.AUDIENCE_PREFIXES:
if path == prefix or path.startswith(prefix):
return audience
return None
AUDIENCES_BY_ROLE = {
"buyer": {GuidePage.Audience.SHARED, GuidePage.Audience.BUYER},
"ops": {GuidePage.Audience.SHARED, GuidePage.Audience.OPS},
"admin": set(GuidePage.Audience.values),
}