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
+34 -1
View File
@@ -3,6 +3,7 @@ import os
from datetime import datetime
from logging.handlers import RotatingFileHandler
import bleach
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect
@@ -29,6 +30,11 @@ class Section(db.Model):
cascade="all, delete-orphan",
)
@property
def published_topics(self):
# Topics are already ordered by sort_order via the relationship.
return [t for t in self.topics if t.is_published]
class Topic(db.Model):
__tablename__ = "topic"
@@ -45,6 +51,7 @@ class Topic(db.Model):
media_url = db.Column(db.String(500))
media_caption = db.Column(db.String(255))
sort_order = db.Column(db.Integer, nullable=False, default=0)
is_published = db.Column(db.Boolean, nullable=False, default=True)
@property
def body(self):
@@ -79,6 +86,30 @@ def log_action(actor, action, entity, entity_id=None, detail=None):
db.session.commit()
# Tags/attributes the rich-text editor (Quill) can emit. Everything else is
# stripped on save so a WYSIWYG paste can't inject markup into the public page.
ALLOWED_TAGS = [
"p", "br", "strong", "b", "em", "i", "u", "s", "strike",
"ul", "ol", "li", "a", "h2", "h3", "blockquote",
]
ALLOWED_ATTRS = {"a": ["href", "title", "target", "rel"]}
def sanitize_html(raw):
"""Clean editor HTML against the allowlist. Returns None for empty content
so blank bodies stay NULL. bleach also restricts link protocols to
http/https/mailto, blocking javascript: URLs."""
if not raw:
return None
cleaned = bleach.clean(
raw, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True
).strip()
# Quill leaves an empty paragraph for a blank editor.
if cleaned in ("", "<p></p>", "<p><br></p>"):
return None
return cleaned
def _configure_auth_logger(app):
"""A dedicated 'jqc.auth' logger writing one line per login attempt to a
file fail2ban watches. Kept separate from the app log so the filter regex
@@ -123,9 +154,11 @@ def create_app():
@app.route("/")
def index():
sections = (
all_sections = (
Section.query.order_by(Section.sort_order, Section.num).all()
)
# Hide sections whose topics are all drafts (or which have no topics).
sections = [s for s in all_sections if s.published_topics]
return render_template(
"index.html",
sections=sections,