Jul 22 - Update - add Rich-text editor, draft/publish, drag-to-reorder
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user