- 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
75 lines
2.5 KiB
Python
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()}.")
|