Jul 22 - Update - add Rich-text editor, draft/publish, drag-to-reorder

This commit is contained in:
2026-07-22 17:13:26 -04:00
parent a39fa091fa
commit 69b97d51fe
15 changed files with 1295 additions and 23 deletions
+33 -3
View File
@@ -40,10 +40,11 @@ Ubuntu 24.04. Fonts via Google Fonts (Bricolage Grotesque, IBM Plex Sans/Mono).
- `section(id, num UNIQUE, title, subtitle, sort_order)`
- `topic(id, section_id FK cascade, slug UNIQUE, title, body_html, link_url,
link_label, media_type[none|image|video|embed], media_url, media_caption,
sort_order)`
sort_order, is_published)`
- `audit_log(id, actor, action, entity, entity_id, detail, created_at)`
Public page orders sections by `sort_order, num`; topics by `sort_order`.
Public page orders sections by `sort_order, num`; topics by `sort_order`, and
shows only `is_published` topics (sections with no published topics are hidden).
## Conventions (follow every change)
@@ -103,12 +104,41 @@ Public page orders sections by `sort_order, num`; topics by `sort_order`.
- CSRF-less POST floods get HTTP 400 before the view, so they don't reach the
auth log — add nginx `limit_req` on `/admin/login` if that traffic matters.
## Content editing (rich text / publish / reorder)
- **Rich text:** Quill 1.3.7 vendored at `static/vendor/` (no CDN — survives a
locked-down server or strict CSP). It's a *progressive enhancement over a real
`<textarea name="body_html" id="body_src">`*: `body.quill-on` is added only
after `new Quill()` succeeds (hides the textarea, shows the editor). If Quill
fails to load, the textarea stays usable and a save never wipes the body.
- **Sanitize on save:** `sanitize_html()` (bleach) runs on every `body_html`
write — allowlist `ALLOWED_TAGS`/`ALLOWED_ATTRS`, `strip=True`, and bleach
restricts link protocols to http/https/mailto (blocks `javascript:`). Empty /
`<p><br></p>` editor content is stored as NULL. The public page still renders
`body_html` via `Markup`, but the content is now sanitized at the source.
- **Draft/publish:** `topic.is_published` (default 1, so existing rows stay
live). Public route filters to `Section.published_topics` and drops sections
with none. Admin: per-topic `/topic/<id>/toggle` (quick button) + a Published
checkbox on the form; dashboard shows a `draft` badge. New topics default
published (least surprise); uncheck to stage a draft.
- **Drag reorder:** SortableJS 1.15.2 vendored. Dashboard drags post JSON to
`/admin/reorder` with the CSRF token from `<meta name="csrf-token">` sent as
the `X-CSRFToken` header (Flask-WTF accepts that header for AJAX). Payloads:
`{type:'topic',section_id,order:[ids]}` (within-section only) or
`{type:'section',order:[ids]}`; sort values rewritten 10,20,30…. The topic
branch filters `Topic.section_id == section_id` so a spoofed cross-section id
is ignored. Cross-section moves are done via the section dropdown on the form.
- **CDN caveat (testing):** the build sandbox proxy 403s all external CDNs incl.
Google Fonts — that's why libs are vendored. Fonts still load fine on the real
server; they degrade to system fonts if blocked.
## Deploy delta cheatsheet
```bash
sudo mysql < add_admin.sql # audit_log (existing DBs)
sudo mysql < add_publish.sql # topic.is_published (existing DBs)
# .env: SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD_HASH, SESSION_COOKIE_SECURE=1
sudo ./venv/bin/pip install -r requirements.txt
sudo ./venv/bin/pip install -r requirements.txt # includes bleach
sudo systemctl restart jqc-features
# fail2ban:
sudo cp deploy/fail2ban/filter.d/jqc-admin.conf /etc/fail2ban/filter.d/