Feedback reply: light Markdown formatting (bold / bullets / heading)
Per Codex: a constrained Markdown-ish composer rather than contenteditable. * goodnews/markup.render_reply_html — escapes everything first, then introduces only a tiny whitelist (**bold**, - bullets, #/##/### headings, paragraphs, line breaks). No links, attributes, inline styles, or raw HTML passthrough. * feedback_replies.message_html column (+ live migration); replies store both the Markdown text and the rendered HTML. * email_send.send_feedback_reply now sends multipart text/plain + text/html (the sanitized render, wrapped in a trusted email template). * Frontend: textarea + a small toolbar (Bold / • List / H) that inserts Markdown; the reply thread renders the server-sanitized HTML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,19 +102,22 @@ def test_feedback_reply_sends_stores_marks_read(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(api.email_send, "send_feedback", lambda *a, **k: None)
|
||||
sent = {}
|
||||
monkeypatch.setattr(api.email_send, "send_feedback_reply",
|
||||
lambda to, msg, orig: sent.update(to=to, msg=msg, orig=orig))
|
||||
lambda to, msg, html, orig: sent.update(to=to, msg=msg, html=html, orig=orig))
|
||||
TestClient(app).post("/api/feedback", json={"message": "is there an app?", "email": "reader@x.com", "visitor": "v"})
|
||||
tc = TestClient(app); link = {}
|
||||
monkeypatch.setattr(api.email_send, "send_magic_link", lambda to, l: link.update(l=l))
|
||||
tc.post("/api/auth/email/start", json={"email": "boss@x.com"})
|
||||
tc.post("/api/auth/email/verify", json={"token": link["l"].split("token=")[1]})
|
||||
fid = tc.get("/api/admin/feedback").json()[0]["id"]
|
||||
r = tc.post(f"/api/admin/feedback/{fid}/reply", json={"message": "Yes — a companion app is coming."})
|
||||
r = tc.post(f"/api/admin/feedback/{fid}/reply", json={"message": "Yes — a **companion app** is coming."})
|
||||
assert r.status_code == 200
|
||||
assert sent["to"] == "reader@x.com" and "companion app" in sent["msg"] and "is there an app?" in sent["orig"]
|
||||
assert "<strong>companion app</strong>" in sent["html"] # rendered markdown sent as HTML
|
||||
fb = tc.get("/api/admin/feedback").json()[0]
|
||||
assert fb["read_at"] is not None # marked read on reply
|
||||
assert len(fb["replies"]) == 1 and fb["replies"][0]["sent_to"] == "reader@x.com"
|
||||
rep = fb["replies"][0]
|
||||
assert len(fb["replies"]) == 1 and rep["sent_to"] == "reader@x.com"
|
||||
assert "<strong>companion app</strong>" in rep["message_html"] # stored HTML
|
||||
|
||||
|
||||
def test_feedback_reply_requires_address_and_message(tmp_path, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user