- 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
57 lines
1.8 KiB
Python
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),
|
|
}
|