ixg_platform/apps/guides/management/commands/sync_guides.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

75 lines
2.5 KiB
Python

from django.core.management.base import BaseCommand
from django.db import connections
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from apps.guides.models import GuidePage
SELECT_PAGES_SQL = (
'SELECT path, title, content, "updatedAt" FROM pages '
'WHERE path LIKE %s AND "isPublished" = true AND "isPrivate" = false'
)
class Command(BaseCommand):
help = "One-way sync of Wiki.js user-guides pages into GuidePage (read-only on wikijs)."
def handle(self, *args, **options):
created = 0
updated = 0
stale = []
existing = {page.path: page for page in GuidePage.objects.all()}
known_paths = set()
with connections["wikijs"].cursor() as cursor:
cursor.execute(SELECT_PAGES_SQL, ["user-guides/%"])
rows = cursor.fetchall()
for path, title, content, updated_at_raw in rows:
audience = GuidePage.map_audience(path)
if audience is None:
continue
known_paths.add(path)
parsed = parse_datetime(updated_at_raw) if updated_at_raw else None
updated_at = parsed or timezone.now()
page = existing.pop(path, None)
if page is None:
GuidePage.objects.create(
path=path,
title=title,
audience=audience,
content=content,
updated_at=updated_at,
)
created += 1
continue
changed = (
page.title != title
or page.content != content
or page.audience != audience
or page.updated_at != updated_at
)
if changed:
page.title = title
page.content = content
page.audience = audience
page.updated_at = updated_at
page.save(update_fields=["title", "content", "audience", "updated_at", "synced_at"])
updated += 1
removed = 0
for path in existing:
if path not in known_paths:
stale.append(path)
if stale:
removed = GuidePage.objects.filter(path__in=stale).delete()[0]
self.stdout.write(
self.style.SUCCESS(
f"sync_guides complete: {created} created, {updated} updated, {removed} removed."
)
)
self.stdout.write(f"Total GuidePage rows: {GuidePage.objects.count()}.")