Fix markdown lists in the nomination statement preview - #3079
Conversation
The preview div lives inside `<form class="wide-form jobs-form">`, so the site's form-list rules applied to the markdown it renders: `form ul` in style.css dropped the bullets and indent, and `.jobs-form ul li` in mq.css floated every item into a 33%-wide column. A bulleted statement previewed as unbulleted items sitting side by side, overlapping the text above them. Those rules exist for the job form's checkbox lists, so scope the reset to `#statement-preview` rather than changing the shared stylesheets. Ordered lists were never affected -- both rules only target `ul`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`escape_html=True` ran Django's `escape()` over the raw text before the markdown parser saw it, so `>` arrived as `>` and blockquotes rendered as literal text. This hit stored statements as well as the preview, since both go through the field's `pre_save`. Pre-escaping is no longer what keeps the field safe: pydotorg.markup wraps every renderer in `nh3.clean` with a tag, attribute, and URL-scheme allowlist, so raw HTML is dropped from the rendered output instead. Dropping the flag lets markdown syntax through and leaves sanitization in one place. Statements saved before this change keep their cached HTML until next saved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adjusts the nominations “statement” rendering pipeline and preview styling so markdown constructs (notably lists and blockquotes) render correctly on election nomination pages while still preventing raw HTML from being interpreted.
Changes:
- Swap the nomination statement’s markdown renderer to a custom “HTML-neutralizing” markdown path via
markup_choices. - Add CSS in the nomination form preview area to undo site-wide list styling so rendered markdown lists display as actual lists.
- Add model-level tests to validate blockquotes/lists/headings render while raw HTML is neutralized.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/nominations/models.py | Introduces a custom markdown renderer for untrusted nomination statements by overriding the markdown renderer in markup_choices. |
| apps/nominations/templates/nominations/nomination_form.html | Adds preview-scoped CSS to ensure rendered markdown lists display with correct list semantics. |
| apps/nominations/tests/test_models.py | Adds tests for markdown rendering behavior and HTML neutralization for nomination statements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
e3b5e78 to
c3eb918
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/nominations/tests/test_models.py:77
- This docstring says the statement pipeline must “never” allow raw HTML, but with escape_html disabled the markdown renderer can pass through raw HTML that is then sanitized (some tags like
//
are allowlisted). Adjust the docstring to reflect the actual contract: markdown is allowed and the resulting HTML is sanitized.
class NominationStatementRenderingTests(TestCase):
"""The statement pipeline must allow markdown but never raw HTML."""
apps/nominations/models.py:266
- The comment says raw HTML is “dropped” by pydotorg.markup, but the sanitizer is actually an allowlist (nh3.clean) that will preserve some HTML tags/attributes (e.g. ,
,
) while stripping disallowed tags/attrs and unsafe URL schemes. Tighten this comment to describe sanitization accurately so readers don’t assume raw HTML can’t render at all.
# No escape_html: it escapes `>` too and breaks blockquotes. pydotorg.markup
# sanitizes every renderer's output, so raw HTML is dropped there instead.
nomination_statement = MarkupField(markup_type="markdown", blank=False, null=True)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
| employer = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001 | ||
| other_affiliations = models.CharField(max_length=2048, blank=True, null=True) # noqa: DJ001 | ||
| nomination_statement = MarkupField(escape_html=True, markup_type="markdown", blank=False, null=True) | ||
| nomination_statement = MarkupField(markup_type="markdown", blank=False, null=True) |
There was a problem hiding this comment.
new pydotorg.markup stuff from seth sanitizes
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/nominations/models.py:265
nomination_statementpreviously forcedescape_html=Truebut now relies onMarkupField's default. Since this change is security- and rendering-sensitive (it affects whether characters like>are preserved and how raw HTML is handled), it’s safer to make the intended behavior explicit in the field declaration rather than depending on a third-party default.
employer = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
other_affiliations = models.CharField(max_length=2048, blank=True, null=True) # noqa: DJ001
nomination_statement = MarkupField(markup_type="markdown", blank=False, null=True)
apps/nominations/tests/test_models.py:77
- The docstring says the statement pipeline must allow markdown but “never raw HTML”, but the site-wide MarkupField renderers sanitize HTML (via
nh3.clean) and can still allow some HTML tags/attributes. Consider rewording this to reflect that the pipeline sanitizes HTML rather than implying all raw HTML is rejected.
class NominationStatementRenderingTests(TestCase):
"""The statement pipeline must allow markdown but never raw HTML."""
Fix issue with markdown and blockquotes in election pages