ixg_platform/apps/guides/models.py
ixgadmin d74f0814da Finish the wiki root move, zone map view and spec approval
- Follow home/user-guides/ through one GUIDE_PATH_PREFIX constant.
- Add a read-only Origin Zone detail view with the map as the main panel.
- Name due diligence explicitly and add its section headings.
- Add admin-only, four-eyes specification approval (portal and API).
- Empty the New-zone geometry placeholder instead of showing null.
2026-10-27 17:41:08 +00:00

80 lines
2.8 KiB
Python

from django.db import models
# One structural prefix for every wiki guide path. Wiki.js now keeps all pages
# under a single `home/` root; change this line if that ever moves again.
GUIDE_PATH_PREFIX = "home/user-guides/"
class GuidePage(models.Model):
class Audience(models.TextChoices):
SHARED = "shared", "Shared"
BUYER = "buyer", "Buyer"
OPS = "ops", "Operations"
ADMIN = "admin", "Administrator"
AUDIENCE_PREFIXES = (
(f"{GUIDE_PATH_PREFIX}index", Audience.SHARED),
(f"{GUIDE_PATH_PREFIX}buyer/", Audience.BUYER),
(f"{GUIDE_PATH_PREFIX}ops/", Audience.OPS),
(f"{GUIDE_PATH_PREFIX}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):
if self.path.startswith(GUIDE_PATH_PREFIX):
return self.path[len(GUIDE_PATH_PREFIX):]
return 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
# The help centre is scoped by PORTAL first, then by role. Which guides are
# listed depends on which portal you are standing in, not only on who you are:
# an admin browsing the buyer portal sees the buyer guides, because it is the
# buyer portal. The admin-only guides never surface there.
AUDIENCES_BY_PORTAL = {
"portal": frozenset({GuidePage.Audience.SHARED, GuidePage.Audience.BUYER}),
"ops": frozenset({GuidePage.Audience.SHARED, GuidePage.Audience.OPS}),
}
# Extra audiences unlocked by ROLE, per portal. Admin guides are ops-side
# material, so they are only ever added inside the ops portal.
ADMIN_EXTRA_AUDIENCES = {
"ops": frozenset({GuidePage.Audience.ADMIN}),
}
def allowed_audiences(portal, role=None, is_superuser=False):
"""Guide audiences visible in `portal` ("portal" or "ops") for this user."""
allowed = set(AUDIENCES_BY_PORTAL.get(portal, frozenset()))
if is_superuser or role == "admin":
allowed |= ADMIN_EXTRA_AUDIENCES.get(portal, frozenset())
return allowed