Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions calendarium/liturgics/day.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
Tradition.Greek: GreekYear,
}

# How many commemorations Day.minimal_saints keeps before truncating --
# purely a display-space constraint (the monthly calendar grid cell,
# summary_title's fallback), not a statement about which commemorations
# matter more. Adjust freely if the grid's cell size changes.
MINIMAL_SAINTS_LIMIT = 3


def _join_and(items):
"""'meat' / 'meat and fish' / 'meat, fish, and dairy'."""
Expand Down Expand Up @@ -297,22 +303,21 @@ async def _add_supplemental_commemorations(self):
additive.append(dc)
elif dc.day_native and dc.ordering >= 0:
day_native_by_day.setdefault(dc.day_id, []).append(dc)
elif not dc.day_native:
elif not dc.day_native and dc.ordering >= 0:
additive.append(dc)
elif dc.day_id not in winning_day_ids:
# day_native with ordering < 0 (feast_name-matched), but its
# own Day row lost _prefer_tradition_days's preference for
# this request -- e.g. a saint shared via
# DayCommemoration.tradition='common' whose day_native
# entry lives on a different tradition's Day row than the
# one whose feast_name is actually being shown. Its
# feast_name isn't surfacing via self.feasts at all in that
# case, so fall back to showing it plainly rather than
# silently dropping it.
# ordering < 0 (feast_name-matched, day_native or not), but
# its own Day row lost _prefer_tradition_days's preference
# for this request -- e.g. a saint shared via
# DayCommemoration.tradition='common' whose entry lives on a
# different tradition's Day row than the one whose
# feast_name is actually being shown. Its feast_name isn't
# surfacing via self.feasts at all in that case, so fall
# back to showing it plainly rather than silently dropping it.
additive.append(dc)
# else: day_native with ordering < 0, and its own Day row is the
# one whose feast_name is being shown -- already represented via
# self.feasts; story-only, excluded here.
# else: ordering < 0 (day_native or not), and its own Day row is
# the one whose feast_name is being shown -- already
# represented via self.feasts; excluded here.

self.saints = []
# Parallel to self.saints, but pairs each title with the story's id
Expand All @@ -321,7 +326,6 @@ async def _add_supplemental_commemorations(self):
# itself -- it's consumed as plain strings elsewhere (ical.py, RSS).
self.saint_links = []
self.spoken_saints = []
self.minimal_saints = []
for dcs in day_native_by_day.values():
# Grouped by whichever Day row the entries originally came from
# (dc.day_id may not be in self.days -- see the class docstring
Expand All @@ -332,13 +336,19 @@ async def _add_supplemental_commemorations(self):
self.saints.extend(titles)
self.saint_links.extend((dc.title, dc.id if dc.story else None) for dc in dcs)
self.spoken_saints.extend(dc.title for dc in dcs if _speech_worthy(dc))
if titles:
self.minimal_saints.append('; '.join(titles))

self.saints.extend(dc.title for dc in additive)
self.saint_links.extend((dc.title, dc.id if dc.story else None) for dc in additive)
self.spoken_saints.extend(dc.title for dc in additive if _speech_worthy(dc))

# A length-capped view of self.saints for space-constrained displays
# (the monthly calendar grid, and summary_title's fallback below) --
# deliberately just a truncation, not a day_native/story-provenance
# distinction (that was the old design, and it broke down as soon as
# a "story-only" commemoration needed to be the thing shown, e.g.
# after a feast_name/DayCommemoration de-duplication).
self.minimal_saints = self.saints[:MINIMAL_SAINTS_LIMIT]

# spoken_saints excludes only the story-less tradition='greek' overlay
# (the bulk antiochian.org-harvested commemorations, see
# docs/saint-model-refactor.md) -- those would otherwise bloat the
Expand Down
17 changes: 17 additions & 0 deletions calendarium/migrations/0009_remove_day_saints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 6.0.7 on 2026-08-07 14:41

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('calendarium', '0008_day_story'),
]

operations = [
migrations.RemoveField(
model_name='day',
name='saints',
),
]
1 change: 0 additions & 1 deletion calendarium/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class Day(models.Model):
feast_level = models.SmallIntegerField()
service = models.SmallIntegerField()
service_note = models.CharField(max_length=64)
saints = models.JSONField(default=list)
story = models.TextField(null=True, blank=True) # feast-level narrative, non-saint content
fast = models.SmallIntegerField()
fast_exception = models.SmallIntegerField()
Expand Down
4 changes: 2 additions & 2 deletions calendarium/templates/calendar_day.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
</ul>
{% endif %}

{% if day.saints %}
{% if day.minimal_saints %}
<ul class="saints">
{% for saint in day.saints %}
{% for saint in day.minimal_saints %}
<li>{{ saint }}</li>
{% endfor %}
</ul>
Expand Down
13 changes: 1 addition & 12 deletions calendarium/templates/readings.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

{% block main %}
<header>
<h1>{{ day.gregorian_date|date|widont }}<span><br>{{ day.titles.0|widont }}</span></h1>
<h1>{{ day.gregorian_date|date:"l, F j, Y"|widont }}<span><br>{% if day.feasts %}{{ day.feasts|join:"; "|widont }}{% else %}{{ day.titles.0|widont }}{% endif %}</span></h1>

<p class="fasting">
{% filter widont %}
Expand All @@ -59,17 +59,6 @@ <h2>Service Notes</h2>
</section>
{% endif %}

{% if day.feasts %}
<section class="feasts">
<h2>Feasts</h2>
<ul>
{% for feast in day.feasts %}
<li>{{ feast|widont }}</li>
{% endfor %}
</ul>
</section>
{% endif %}

{% if day.saints %}
<section class="commemorations">
<h2>Commemorations</h2>
Expand Down
34 changes: 17 additions & 17 deletions calendarium/tests/data/january.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
"titles": [
"Saturday of the 28th week after Pentecost"
],
"summary_title": "Saturday before Theophany; Circumcision of Our Lord; St Basil the Great",
"summary_title": "Saturday before Theophany; Circumcision of Our Lord",
"feast_level": 6,
"feast_level_description": "Red cross circle (great feast typikon symbol)",
"feasts": [
"Saturday before Theophany",
"Circumcision of Our Lord; St Basil the Great"
"Circumcision of Our Lord"
],
"fast_level": 0,
"fast_level_desc": "No Fast",
"fast_exception": 11,
"fast_exception_desc": "Fast Free",
"fast_abstentions": [],
"saints": [
"Our Father among the Saints Basil the Great (379).",
"St Gregory, Bishop of Nazianzus (374), father of St Gregory the Theologian"
],
"service_notes": null,
Expand Down Expand Up @@ -965,18 +966,17 @@
"titles": [
"Tuesday of the 30th week after Pentecost"
],
"summary_title": "Ven. Theodosius the Great",
"summary_title": "Our Holy Father Theodosius the Cenobiarch (519); The Venerable Vitalis (5th c.); Venerable Michael of Klops, Fool for Christ (1456)",
"feast_level": 4,
"feast_level_description": "Red cross (polyeleos typikon symbol)",
"feasts": [
"Ven. Theodosius the Great"
],
"feasts": null,
"fast_level": 0,
"fast_level_desc": "No Fast",
"fast_exception": 0,
"fast_exception_desc": "",
"fast_abstentions": [],
"saints": [
"Our Holy Father Theodosius the Cenobiarch (519)",
"The Venerable Vitalis (5th c.)",
"Venerable Michael of Klops, Fool for Christ (1456)"
],
Expand Down Expand Up @@ -1063,7 +1063,7 @@
"titles": [
"Wednesday of the 30th week after Pentecost"
],
"summary_title": "St Sava, Archbishop of Serbia",
"summary_title": "St Sava, Archbishop of Serbia; Holy Martyr Tatiana (ca. 230); Venerable Benedict Biscop, Abbot of Wearmouth (689-690)",
"feast_level": 3,
"feast_level_description": "Red squigg (doxology typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -1119,7 +1119,7 @@
"titles": [
"Thursday of the 30th week after Pentecost"
],
"summary_title": "Martyrs Hermylus and Stratonicus",
"summary_title": "Martyrs Hermylus and Stratonicus; Our Holy Father Maximos Kavsokalybites (the Hut-burner) (1365); Our Holy Father Hilary, Bishop of Poitiers (369)",
"feast_level": 0,
"feast_level_description": "Liturgy",
"feasts": null,
Expand Down Expand Up @@ -1226,7 +1226,7 @@
"titles": [
"Saturday of the 30th week after Pentecost"
],
"summary_title": "Ven. Paul of Thebes and John Calabytes; Ven. Pansophius of Alexandria, the Martyr",
"summary_title": "Ven. Paul of Thebes and John Calabytes; Ven. Pansophius of Alexandria, the Martyr; Our Holy Father John Kalyvites (the hut-dweller) (ca. 450)",
"feast_level": 2,
"feast_level_description": "Black squigg (6-stich typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -1476,7 +1476,7 @@
"titles": [
"Wednesday of the 31st week after Pentecost"
],
"summary_title": "Ven. Macarius the Great",
"summary_title": "Ven. Macarius the Great; Our Holy Father Mark Eugenikos, Metropolitan of Ephesus and Confessor of the Orthodox Faith (1443)",
"feast_level": 2,
"feast_level_description": "Black squigg (6-stich typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -1630,7 +1630,7 @@
"titles": [
"Friday of the 31st week after Pentecost"
],
"summary_title": "Ven. Maximus the Confessor",
"summary_title": "Ven. Maximus the Confessor; Holy Martyr Agnes of Rome (ca. 304); Our Holy Father Maximos the Greek (1556)",
"feast_level": 0,
"feast_level_description": "Liturgy",
"feasts": null,
Expand Down Expand Up @@ -1686,7 +1686,7 @@
"titles": [
"Saturday of the 31st week after Pentecost"
],
"summary_title": "Apostle Timothy of the Seventy",
"summary_title": "Apostle Timothy of the Seventy; Holy Martyr Anastasius of Persia (628)",
"feast_level": 0,
"feast_level_description": "Liturgy",
"feasts": null,
Expand Down Expand Up @@ -1791,7 +1791,7 @@
"titles": [
"Monday of the 32nd week after Pentecost"
],
"summary_title": "Ven. Xenia of Rome; Bl. Xenia of St Petersburg",
"summary_title": "Ven. Xenia of Rome; Bl. Xenia of St Petersburg; Our Holy Father Macedonian (ca. 430)",
"feast_level": 4,
"feast_level_description": "Red cross (polyeleos typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -1935,7 +1935,7 @@
"titles": [
"Wednesday of the 32nd week after Pentecost"
],
"summary_title": "Ven. Xenophon and Mary; St Simeon the Elder of Mount Sinai",
"summary_title": "Ven. Xenophon and Mary; St Simeon the Elder of Mount Sinai; St Ammonas of Egypt, disciple of St Anthony the Great (350)",
"feast_level": 0,
"feast_level_description": "Liturgy",
"feasts": null,
Expand Down Expand Up @@ -2087,7 +2087,7 @@
"titles": [
"Friday of the 32nd week after Pentecost"
],
"summary_title": "Ven. Ephrem the Syrian",
"summary_title": "Ven. Ephrem the Syrian; Our Holy Father Isaac the Syrian, bishop of Nineveh (7th c.)",
"feast_level": 2,
"feast_level_description": "Black squigg (6-stich typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -2142,7 +2142,7 @@
"titles": [
"Saturday of the 32nd week after Pentecost"
],
"summary_title": "Trans. Rel. Ignatius the Godbearer; St Laurence, Recluse of the Kiev Caves",
"summary_title": "Trans. Rel. Ignatius the Godbearer; St Laurence, Recluse of the Kiev Caves; St Andrei Rublev, iconographer (1430)",
"feast_level": 2,
"feast_level_description": "Black squigg (6-stich typikon symbol)",
"feasts": null,
Expand Down Expand Up @@ -2290,7 +2290,7 @@
"titles": [
"Monday of the 33rd week after Pentecost"
],
"summary_title": "Unmercenaries Cyrus and John",
"summary_title": "Unmercenaries Cyrus and John; Venerable Nicetas, hermit of the Kiev Caves and Bishop of Novgorod (1108); Marcella of Rome (410)",
"feast_level": 3,
"feast_level_description": "Red squigg (doxology typikon symbol)",
"feasts": null,
Expand Down
13 changes: 10 additions & 3 deletions calendarium/tests/test_ical.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


class CalendarTest(TestCase):
fixtures = ['calendarium.json']
fixtures = ['calendarium.json', 'commemorations.json']

def test_ical(self):
"""ical endpoint should return 200."""
Expand Down Expand Up @@ -82,7 +82,13 @@ def test_ical_julian_urls(self):
self.assertEqual(match.kwargs['cal'], Calendar.Julian)

async def test_ical_content(self):
"""ical with timestamp of Jan 7, 2022 should have Synaxis of St. John."""
"""ical with timestamp of Jan 7, 2022 should have Synaxis of St. John.

Synaxis of St John the Baptist is Jan 7's sole significant
commemoration, so its original Day.feast_name text (restored after
the feast_name/DayCommemoration de-duplication audit briefly
blanked it, 2026-08) drives summary_title directly rather than
falling back to a join of that day's other commemorations."""

def build_absolute_uri(url):
return urljoin('http://testserver', url)
Expand All @@ -92,7 +98,8 @@ def build_absolute_uri(url):
for event in cal.walk('vevent'):
if event['dtstart'].dt == timestamp.date():
summary = event.decoded('summary')
self.assertEqual(summary, 'Synaxis of St John the Baptist')
self.assertIn('Synaxis', summary)
self.assertIn('John the Baptist', summary)
break
else:
self.fail('No event for timestamp found')
Expand Down
Loading