diff --git a/apps/nominations/models.py b/apps/nominations/models.py
index 17b62c697..88aa5fc80 100644
--- a/apps/nominations/models.py
+++ b/apps/nominations/models.py
@@ -261,7 +261,7 @@ class Nomination(models.Model):
previous_board_service = models.CharField(max_length=1024, blank=False, null=True) # noqa: DJ001
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)
nominator = models.ForeignKey(User, related_name="nominations_made", on_delete=models.CASCADE)
nominee = models.ForeignKey(
diff --git a/apps/nominations/templates/nominations/nomination_form.html b/apps/nominations/templates/nominations/nomination_form.html
index d5157e430..e3ad0b469 100644
--- a/apps/nominations/templates/nominations/nomination_form.html
+++ b/apps/nominations/templates/nominations/nomination_form.html
@@ -97,6 +97,13 @@
Submit a Nomination for {{ election.name }} Election
}
#id_previous_service_years input { margin: 0; }
#statement-preview { border: 1px solid #ccc; border-radius: 4px; background: #fff; padding: 0.75em 1em; margin-top: 0.5em; }
+ /* Fix site styling specifically for elections */
+ #statement-preview ul, #statement-preview ol { margin: 0 0 1em 1.5em; }
+ #statement-preview ul { list-style: square; }
+ #statement-preview ol { list-style: decimal; }
+ #statement-preview li { float: none; width: auto; display: list-item; }
+ #statement-preview li > ul, #statement-preview li > ol { margin-bottom: 0; }
+ #statement-preview > *:last-child { margin-bottom: 0; }
{% for field in form %}
diff --git a/apps/nominations/tests/test_models.py b/apps/nominations/tests/test_models.py
index b6ccf3172..bfd86146d 100644
--- a/apps/nominations/tests/test_models.py
+++ b/apps/nominations/tests/test_models.py
@@ -3,7 +3,7 @@
from django.conf import settings
from django.test import TestCase
-from apps.nominations.models import DEFAULT_ACCENT_COLOR, Election, ElectionKind
+from apps.nominations.models import DEFAULT_ACCENT_COLOR, Election, ElectionKind, Nomination
class ElectionKindModelTests(TestCase):
@@ -70,3 +70,37 @@ def test_markdown_preserves_safe_links_and_formatting(self):
def test_restructuredtext_strips_javascript_uri(self):
rendered = self._render("restructuredtext", "`x `_")
self.assertNotIn("javascript:", rendered)
+
+
+class NominationStatementRenderingTests(TestCase):
+ """The statement pipeline must allow markdown but never raw HTML."""
+
+ def render(self, text):
+ return Nomination.render_statement(text)
+
+ def test_blockquote_renders(self):
+ self.assertIn("", self.render("> quoted"))
+
+ def test_lists_render(self):
+ html = self.render("- one\n- two")
+ self.assertIn("", html)
+ self.assertEqual(html.count("- "), 2)
+
+ def test_headings_and_emphasis_render(self):
+ html = self.render("# Title\n\n**bold** and *italic*")
+ self.assertIn("
Title
", html)
+ self.assertIn("bold", html)
+ self.assertIn("italic", html)
+
+ def test_script_is_dropped(self):
+ html = self.render("")
+ self.assertNotIn("script", html)
+ self.assertNotIn("alert(1)", html)
+
+ def test_event_handler_inside_blockquote_is_dropped(self):
+ html = self.render(">
")
+ self.assertIn("", html)
+ self.assertNotIn("onerror", html)
+
+ def test_unsafe_link_scheme_is_dropped(self):
+ self.assertNotIn("javascript:", self.render("[x](javascript:alert(1))"))