From 6d4104f7d2bb2cc14f8c57683e575c027e448db3 Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Tue, 3 Nov 2026 14:23:14 -0500 Subject: [PATCH] Allow adding public notes to manuscripts/folios --- manuscript/admin.py | 7 ++- .../migrations/0129_add_public_notes.py | 32 +++++++++++++ manuscript/models.py | 10 ++++ manuscript/templatetags/stanza_tags.py | 6 +++ manuscript/tests.py | 46 +++++++++++++++++++ templates/stanzas.html | 16 +++++++ 6 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 manuscript/migrations/0129_add_public_notes.py diff --git a/manuscript/admin.py b/manuscript/admin.py index 3da6c13..0bfd7ad 100644 --- a/manuscript/admin.py +++ b/manuscript/admin.py @@ -653,7 +653,7 @@ class FolioAdmin(ImportExportModelAdmin): }, ), ("Toponyms", {"fields": ("toponym_list",)}), - ("Notes", {"fields": ("folio_includes_map", "folio_notes")}), + ("Notes", {"fields": ("folio_includes_map", "folio_notes", "public_note")}), ) def get_fieldsets(self, request, obj=None): @@ -664,7 +664,10 @@ def get_fieldsets(self, request, obj=None): "Line Range", {"fields": ("line_code_range_start", "line_code_range_end")}, ), - ("Notes", {"fields": ("folio_includes_map", "folio_notes")}), + ( + "Notes", + {"fields": ("folio_includes_map", "folio_notes", "public_note")}, + ), ) return self.fieldsets diff --git a/manuscript/migrations/0129_add_public_notes.py b/manuscript/migrations/0129_add_public_notes.py new file mode 100644 index 0000000..5ae4501 --- /dev/null +++ b/manuscript/migrations/0129_add_public_notes.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.15 on 2026-11-03 17:46 + +import prose.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("manuscript", "0128_folio_iiif_canvas"), + ] + + operations = [ + migrations.AddField( + model_name="folio", + name="public_note", + field=prose.fields.RichTextField( + blank=True, + help_text="Shown to readers where this folio begins in the text.", + null=True, + ), + ), + migrations.AddField( + model_name="singlemanuscript", + name="public_note", + field=prose.fields.RichTextField( + blank=True, + help_text="Shown to readers at the start of the text.", + null=True, + ), + ), + ] diff --git a/manuscript/models.py b/manuscript/models.py index b680332..ba478aa 100644 --- a/manuscript/models.py +++ b/manuscript/models.py @@ -620,6 +620,11 @@ class Folio(models.Model): validators=[validate_line_number_code], ) folio_notes = RichTextField(blank=True, null=True) + public_note = RichTextField( + blank=True, + null=True, + help_text="Shown to readers where this folio begins in the text.", + ) manuscript = models.ForeignKey( "SingleManuscript", on_delete=models.CASCADE, blank=True, null=True ) @@ -715,6 +720,11 @@ class SingleManuscript(models.Model): help_text="The URL to the IIIF manifest for the manuscript. If there isn't one, leave blank.", verbose_name="IIIF URL", ) + public_note = RichTextField( + blank=True, + null=True, + help_text="Shown to readers at the start of the text.", + ) gazetteer_url = models.URLField( max_length=255, blank=True, diff --git a/manuscript/templatetags/stanza_tags.py b/manuscript/templatetags/stanza_tags.py index c110426..5ca07b2 100644 --- a/manuscript/templatetags/stanza_tags.py +++ b/manuscript/templatetags/stanza_tags.py @@ -78,6 +78,12 @@ def annotate_text(html_content, annotations): return mark_safe("".join(result)) +@register.filter +def has_text(html): + """Whether rich text has any text, since an emptied editor can leave markup""" + return bool(html and BeautifulSoup(html, "html.parser").get_text(strip=True)) + + @register.filter def format_ms(manuscript): """Format manuscripts for dropdowns as: diff --git a/manuscript/tests.py b/manuscript/tests.py index e080854..c3c6f83 100644 --- a/manuscript/tests.py +++ b/manuscript/tests.py @@ -789,6 +789,52 @@ def test_folio_holding_only_the_stanza_number(self): self.assertEqual(self.stanza_dividers(response), [("01.01.00", "1v")]) self.assertEqual(self.line_breaks(response), [("01.01.01", "2r")]) + def test_shows_a_folio_s_public_note_at_a_divider(self): + self.add_folios(("1r", "01.01.00", "01.02.08"), ("1v", "01.03.00", "01.04.08")) + Folio.objects.filter(folio_number="1v").update( + public_note="
Stanzas appear elsewhere
", + folio_notes="Working note", + ) + + response = self.get_page() + + self.assertContains(response, "
Stanzas appear elsewhere
", count=1) + self.assertContains(response, 'class="public-note"', count=1) + self.assertNotContains(response, "Working note") + + def test_shows_a_folio_s_public_note_at_a_mid_stanza_break(self): + self.add_folios(("1r", "01.01.00", "01.02.04"), ("1v", "01.02.05", "01.04.08")) + Folio.objects.filter(folio_number="1v").update(public_note="Mid-stanza note") + + self.assertContains(self.get_page(), "Mid-stanza note", count=1) + + def test_shows_the_manuscript_s_public_note_at_the_start_of_the_text(self): + self.manuscript.public_note = "Images may not match the text" + self.manuscript.save() + other = SingleManuscript.objects.create(item_id=2, siglum="Test2") + + response = self.get_page() + + self.assertContains(response, "Images may not match the text", count=1) + html = response.content.decode() + self.assertLess( + html.index("Images may not match the text"), html.index('id="libro-1"') + ) + self.assertNotContains( + self.client.get(reverse("manuscript_stanzas", args=[other.siglum])), + "Images may not match the text", + ) + + def test_leaves_out_public_notes_without_text(self): + self.manuscript.public_note = "" + self.manuscript.save() + self.add_folios(("1r", "01.01.00", "01.02.08"), ("1v", "01.03.00", "01.04.08")) + Folio.objects.filter(folio_number="1v").update(public_note="
 
") + + response = self.get_page() + + self.assertNotContains(response, 'class="public-note') + class RepairFolioToponymsTest(TestCase): def setUp(self): diff --git a/templates/stanzas.html b/templates/stanzas.html index 69825ba..8a60242 100644 --- a/templates/stanzas.html +++ b/templates/stanzas.html @@ -48,6 +48,13 @@ display: none !important; /* Optional: Hide title if it's redundant */ } + /* Public notes on a manuscript and on a folio */ + .public-note { + margin-bottom: 1rem; + font-size: 0.875rem; + color: #4b5563; + } + /* Hamburger menu styles */ .hamburger-menu { display: inline-flex; @@ -211,6 +218,9 @@

+ {% if default_manuscript.public_note|has_text %} +
{{ default_manuscript.public_note|safe }}
+ {% endif %} {% for book_number, stanza_pairs in paired_books.items %}
@@ -231,6 +241,9 @@

+ {% if stanza_pair.current_folio.public_note|has_text %} +
{{ stanza_pair.current_folio.public_note|safe }}
+ {% endif %} {% endif %} {% for segment in stanza_pair.segments %} {% if segment.folio_break %} @@ -241,6 +254,9 @@

+ {% if segment.folio_break.public_note|has_text %} +
{{ segment.folio_break.public_note|safe }}
+ {% endif %} {% endif %}