Guide visibility is now scoped by PORTAL first and role second: - the buyer portal lists shared and buyer guides, even for an admin, because it is the buyer portal; admin guides never surface there - the ops portal lists shared and ops guides, plus admin guides for an admin/superuser - the detail view names its context object, so guide pages get a real browser title instead of a blank one Link rewriting, so one page can serve both the wiki and the in-app help centre: - absolute /en/user-guides/... links become the in-app help path - other absolute /en/... links point at the wiki itself; the app hosts serve no /en/ routes, so leaving them untouched was a 404 for whoever clicked - relative links keep their existing behaviour Tests cover the new rewriting and the portal scoping rules. Also adds docs/USER_GUIDES_VIEWER_SPEC.md, the spec this viewer was built to.
212 lines
9 KiB
Markdown
212 lines
9 KiB
Markdown
# User Guides Viewer (Help Centre) — Implementation Spec
|
|
|
|
Status: Ready for implementation (approved by IXG)
|
|
Author: Hermes (ixg-docs) — for OpenCode to implement
|
|
Repo: /root/work/ixg_platform (Django 5.2, project package `dsp_platform`, apps under `apps/`)
|
|
Date: 07 Aug 2026
|
|
|
|
---
|
|
|
|
## 1. Goal
|
|
|
|
A **read-only, role-gated help centre** inside the IXG platform that shows the
|
|
user guides authored in Wiki.js:
|
|
|
|
- Buyers: `/portal/help/` — the buyer guides
|
|
- Ops users: `/ops/help/` — the ops guides (+ admin guides for admin role)
|
|
- Admin users: can reach both portals (admin can already access `/portal/`
|
|
and `/ops/` per RBAC), so they see everything.
|
|
|
|
**The Wiki.js pages under `user-guides/` are the single source of truth.**
|
|
This feature is a one-way read/sync: the platform never writes to the wiki.
|
|
|
|
---
|
|
|
|
## 2. Source of truth (Wiki.js)
|
|
|
|
Pages live in the `wikijs` PostgreSQL database (same server, port 5432):
|
|
|
|
- Table `pages` — columns of interest: `path` (e.g. `user-guides/buyer/getting-started`, **no locale prefix**), `title`, `content` (markdown), `description`, `"isPublished"`, `"isPrivate"`, `"updatedAt"`.
|
|
- The `user-guides` tree:
|
|
|
|
```
|
|
user-guides/index (shared landing)
|
|
user-guides/buyer/* 7 buyer guides
|
|
user-guides/ops/* 9 ops guides
|
|
user-guides/admin/* 2 admin guides
|
|
```
|
|
|
|
Audience mapping (by path prefix):
|
|
|
|
| Prefix | Audience |
|
|
|--------|----------|
|
|
| `user-guides/index` | `shared` |
|
|
| `user-guides/buyer/` | `buyer` |
|
|
| `user-guides/ops/` | `ops` |
|
|
| `user-guides/admin/` | `admin` |
|
|
|
|
---
|
|
|
|
## 3. Architecture
|
|
|
|
### 3.1 New app `apps/guides`
|
|
|
|
Register in `INSTALLED_APPS` (settings.py). Contains:
|
|
|
|
**Model `GuidePage`**
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `path` | CharField, unique | wiki path, e.g. `user-guides/buyer/getting-started` |
|
|
| `title` | CharField | from wiki title |
|
|
| `audience` | CharField(choices: shared/buyer/ops/admin) | mapped from path |
|
|
| `content` | TextField | markdown body from wiki |
|
|
| `updated_at` | DateTimeField | wiki `updatedAt` |
|
|
| `synced_at` | DateTimeField | when we last synced |
|
|
|
|
**Management command `sync_guides`** (apps/guides/management/commands/sync_guides.py):
|
|
|
|
1. Connect to the `wikijs` DB alias (read-only).
|
|
2. `SELECT path, title, content, "updatedAt" FROM pages WHERE path LIKE 'user-guides/%' AND "isPublished" = true AND "isPrivate" = false`
|
|
3. Map audience from path prefix; upsert into `GuidePage` by `path`.
|
|
4. Delete `GuidePage` rows whose path no longer exists in the wiki (removed pages disappear from the help centre).
|
|
5. Print a summary (created / updated / removed counts).
|
|
|
|
**Views** — role-scoped querysets, both mounted under the two portal namespaces:
|
|
|
|
- `GuideIndexView` — lists guides the current user's role may see.
|
|
- `GuideDetailView` — renders one guide; 404 if the page's audience is not
|
|
allowed for the current user (defence in depth — the URL routes are already
|
|
role-gated by RBACMiddleware, but never rely on that alone).
|
|
|
|
**Scoping is by PORTAL first, then by role** (revised 16 Sep 2026). Which
|
|
guides are listed depends on the portal you are standing in, not only on who
|
|
you are: the buyer portal is for buyers, so it shows buyer guides even to an
|
|
admin. Admin-only guides are ops-side material and never appear in the buyer
|
|
portal. Implemented as `apps.guides.models.allowed_audiences(portal, role,
|
|
is_superuser)` — the single place the rule lives.
|
|
|
|
| Portal | Role | Allowed guide audiences |
|
|
|--------|------|-------------------------|
|
|
| `/portal/help/` | buyer | shared, buyer |
|
|
| `/portal/help/` | admin / superuser | shared, buyer (no change) |
|
|
| `/ops/help/` | ops | shared, ops |
|
|
| `/ops/help/` | admin / superuser | shared, ops, admin |
|
|
|
|
Note the role gate on the portal itself (`BuyerRequiredMixin` /
|
|
`OpsRequiredMixin`): only `buyer` + `admin` reach `/portal/`, only `ops` +
|
|
`admin` reach `/ops/`. So the two viewers portal scoping actually
|
|
distinguishes are real buyers and admins-who-can-see-everything.
|
|
|
|
**Markdown rendering** (in the view or a small helper `apps/guides/markdown.py`):
|
|
|
|
- `markdown.markdown(content, extensions=["tables", "fenced_code", "sane_lists"])`
|
|
- Sanitise with `bleach.clean(html, tags=[...], attributes=[...], protocols=["http","https","mailto"])` — allowed tags: `p br strong em ul ol li table thead tbody tr th td h1 h2 h3 h4 a code pre blockquote hr img span div`.
|
|
- **Link rewriting**: internal guide links in the wiki are relative markdown
|
|
links like `[Compliance guide](compliance)` or `[Compliance guide](compliance)`
|
|
(same-folder) — rewrite them to the current help root
|
|
(`/portal/help/<page-path-suffix>/` or `/ops/help/...`). Wiki image paths
|
|
starting with `/buyer-guide/` or `/ops-guide/` should be rewritten to the
|
|
wiki public base URL (setting `WIKI_PUBLIC_BASE`, default
|
|
`http://194.164.95.50`).
|
|
- Note: markdown-it-anchor headings are NOT available in this Wiki.js build,
|
|
so the guides avoid anchor links — no anchor handling needed.
|
|
|
|
### 3.2 Database access to `wikijs` (read-only)
|
|
|
|
- settings.py `DATABASES` gains a second alias `wikijs`, populated from env via
|
|
django-environ (same pattern as the default DB):
|
|
`WIKI_DB_NAME`, `WIKI_DB_HOST`, `WIKI_DB_PORT`, `WIKI_DB_USER`, `WIKI_DB_PASSWORD`.
|
|
- Create a **read-only** PostgreSQL role (run once as postgres, e.g. via
|
|
`docker exec postgres psql -U postgres`):
|
|
|
|
```sql
|
|
CREATE ROLE wikijs_ro LOGIN PASSWORD '<generate-a-strong-password>';
|
|
GRANT CONNECT ON DATABASE wikijs TO wikijs_ro;
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO wikijs_ro;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO wikijs_ro;
|
|
```
|
|
|
|
- Put the password ONLY in `config/secrets/env.production` (never in code,
|
|
wiki pages, or this spec).
|
|
|
|
### 3.3 URL wiring
|
|
|
|
- `apps/buyer/urls.py` (namespace `portal`):
|
|
`path("help/", GuideIndexView, name="guide_index")`
|
|
`path("help/<str:page_path>/", GuideDetailView, name="guide_detail")`
|
|
- `apps/ops/urls.py` (namespace `ops`): same two routes.
|
|
- Add a **Help** nav item to both base templates:
|
|
- `templates/portal/base_portal.html` sidebar (`bi bi-book` icon) → `{% url 'portal:guide_index' %}`
|
|
- `templates/ops/base_ops.html` sidebar (`bi bi-book` icon) → `{% url 'ops:guide_index' %}`
|
|
|
|
### 3.4 Templates (professional manual look)
|
|
|
|
- `templates/guides/`:
|
|
- `index_portal.html` — extends `portal/base_portal.html`, includes shared partial
|
|
- `index_ops.html` — extends `ops/base_ops.html`, includes shared partial
|
|
- `detail_portal.html` / `detail_ops.html` — same split
|
|
- Shared partials `_guide_list.html` (index: cards or a clean list with
|
|
titles + first paragraph preview) and `_guide_detail.html` (two-column:
|
|
left = guide list for the role, right = rendered content, with the page
|
|
title as heading and a breadcrumb/back link).
|
|
- Use existing Bootstrap + stat-card styling so it looks native to each portal.
|
|
- Empty state: "No guides available yet" when the list is empty.
|
|
|
|
---
|
|
|
|
## 4. Sync scheduling
|
|
|
|
Primary: host crontab (no app-level scheduler needed):
|
|
|
|
```
|
|
# every 6 hours
|
|
0 */6 * * * docker exec ixg-platform-app python manage.py sync_guides >> /var/log/ixg-guides-sync.log 2>&1
|
|
```
|
|
|
|
Manual refresh: `docker exec ixg-platform-app python manage.py sync_guides`.
|
|
|
|
Optional alternative (do NOT do both): a Celery beat task calling the same
|
|
command. Pick cron.
|
|
|
|
---
|
|
|
|
## 5. Acceptance criteria
|
|
|
|
1. `sync_guides` populates `GuidePage` from the wiki (verified by running it
|
|
and querying the model).
|
|
2. `/portal/help/` lists the 7 buyer guides + the shared landing page, and
|
|
**nothing else** — for every role that can reach it, including admin.
|
|
Opening one renders tables, headings and the embedded screenshots.
|
|
3. `/ops/help/` lists the 9 ops guides + the shared landing page; an admin
|
|
user also sees the 2 admin guides (12 rows).
|
|
4. A buyer user cannot see an ops/admin guide (route + audience filter; 404),
|
|
and an admin user cannot reach an ops guide through `/portal/help/` (404,
|
|
available via `/ops/help/` instead).
|
|
5. Internal guide links resolve within the help centre; images load from the
|
|
wiki public base.
|
|
6. The sync never writes to the `wikijs` database (read-only role enforced).
|
|
7. `python manage.py test` passes (add tests: audience filtering, markdown
|
|
sanitisation strips `<script>`, link rewriting, sync command mapping).
|
|
8. Existing portal/ops pages unchanged apart from the added Help nav item.
|
|
|
|
---
|
|
|
|
## 6. Out of scope
|
|
|
|
- Any write path back to the wiki.
|
|
- Public (unauthenticated) access, subdomains, or a separate docs app.
|
|
- Translating guides, theming, or search (nice-to-haves for later).
|
|
- The admin area (`/admin/`) itself — admins use `/ops/help/` for guides.
|
|
|
|
---
|
|
|
|
## 7. Implementation notes
|
|
|
|
- Deps already present: `markdown==3.7`, `bleach==6.2.0`, `psycopg2-binary`.
|
|
- Settings uses django-environ; env file `config/secrets/env.production`.
|
|
- Project runs in Docker (`ixg-platform-app`); migrations run via
|
|
`docker exec ixg-platform-app python manage.py migrate`.
|
|
- Keep the audience mapping table in one place (model method or constant) so
|
|
index/detail views share it.
|
|
- Do not hardcode the wiki DB password anywhere; env only.
|