From b9fdb45171144280c15f7065c02e80aebbd84b17 Mon Sep 17 00:00:00 2001 From: "J. M. F. Tsang" Date: Sun, 10 Sep 2023 19:41:37 -0700 Subject: [PATCH] Support for multiple secondary feasts (#18) If more than one secondary feast is specified then all the collects are used. UI is a bit ugly at the moment. Ho hum. --- filters.py | 8 +++++--- forms.py | 16 +++++++--------- models.py | 18 ++++++++++-------- static/serviceForm.js | 21 +++++++++++++-------- templates/serviceForm.html | 16 ++++++++-------- tests/test_pypew.py | 33 ++++++++++++++++++++++++++++----- utils.py | 9 +++++++-- views/pew_sheet_views.py | 8 ++++---- 8 files changed, 82 insertions(+), 47 deletions(-) diff --git a/filters.py b/filters.py index d788095..9efda6f 100644 --- a/filters.py +++ b/filters.py @@ -22,7 +22,8 @@ def as_richtext(item: ServiceItem) -> RichText: def service_supertitle(service: Service) -> str: s = english_date(service.date) if service.time: - s += f", " + service.time.strftime("%-I:%M%p").lower() + fmt = "%I:%M%p" + s += ", " + service.time.strftime(fmt).lower() return s @@ -58,8 +59,9 @@ def service_header(service: Service) -> str: def service_subtitle(service: Service) -> str: # Feastday (Secondary), Fr XX YY (Preacher: AN Other) - if service.secondary_feast: - s = service.primary_feast.name + ' (' + service.secondary_feast.name + '),' + if service.secondary_feasts: + parens = ", ".join(sf.name for sf in service.secondary_feasts) + s = service.primary_feast.name + ' (' + parens + '),' else: s = service.primary_feast.name + ',' diff --git a/forms.py b/forms.py index bcbb446..0aa6af5 100644 --- a/forms.py +++ b/forms.py @@ -1,10 +1,8 @@ from flask import request from flask_wtf import FlaskForm +from wtforms import DateField, HiddenField, SelectField, SelectMultipleField, \ + StringField from wtforms import ( - DateField, - HiddenField, - SelectField, - StringField, TimeField, ) from wtforms.validators import DataRequired @@ -20,13 +18,13 @@ class PewSheetForm(FlaskForm): title = HiddenField('Title') feasts = Feast.upcoming() feast_choices = [(feast.slug, feast.name) for feast in feasts] - primary_feast_name = SelectField( + primary_feast = SelectField( 'Primary Feast', - choices=feast_choices + choices=feast_choices, ) - secondary_feast_name = SelectField( - 'Secondary Feast', - choices=[('', '')] + feast_choices + secondary_feasts = SelectMultipleField( + 'Secondary Feasts', + choices=[('', '')] + feast_choices, ) date = DateField('Date', validators=[DataRequired()]) time = TimeField('Time', validators=[DataRequired()]) diff --git a/models.py b/models.py index b4c6f91..37bd8d5 100644 --- a/models.py +++ b/models.py @@ -312,8 +312,8 @@ class Service: title: str = field() date: dt.date = field() primary_feast: Feast = field() - secondary_feast: Optional[Feast] = field(default=None) time: dt.time = field(default=dt.time(11, 0)) + secondary_feasts: [Feast] = field(factory=list) celebrant: str = field(default='') preacher: str = field(default='') introit_hymn: Optional[Music] = field(default=None) @@ -330,8 +330,9 @@ def collects(self) -> List[str]: out = [] if self.primary_feast.collect: out.append(self.primary_feast.collect) - if self.secondary_feast and self.secondary_feast.collect: - out.append(self.secondary_feast.collect) + for sf in self.secondary_feasts: + if sf.collect: + out.append(sf.collect) # Collects for Advent I and Ash Wednesday are repeated # throughout Advent and Lent respectively. @@ -429,11 +430,12 @@ def items(self) -> List[PewSheetItem]: @classmethod def from_form(cls, form: 'PewSheetForm') -> 'Service': - primary_feast = Feast.get(slug=form.primary_feast_name.data) - if form.secondary_feast_name.data: - secondary_feast = Feast.get(slug=form.secondary_feast_name.data) + primary_feast = Feast.get(slug=form.primary_feast.data) + if form.secondary_feasts.data: + secondary_feasts = [Feast.get(slug=slug) for slug in + form.secondary_feasts.data] else: - secondary_feast = None + secondary_feasts = None if form.anthem_title.data or form.anthem_composer.data or form.anthem_lyrics.data: anthem = Music( @@ -453,7 +455,7 @@ def from_form(cls, form: 'PewSheetForm') -> 'Service': celebrant=form.celebrant.data, preacher=form.preacher.data, primary_feast=primary_feast, - secondary_feast=secondary_feast, + secondary_feasts=secondary_feasts, introit_hymn=Music.get_neh_hymn_by_ref(form.introit_hymn.data), offertory_hymn=Music.get_neh_hymn_by_ref(form.offertory_hymn.data), recessional_hymn=Music.get_neh_hymn_by_ref( diff --git a/static/serviceForm.js b/static/serviceForm.js index 49c6ec8..2be5501 100644 --- a/static/serviceForm.js +++ b/static/serviceForm.js @@ -1,16 +1,16 @@ const titleH3 = document.getElementById('titleH3'); const titleField = document.getElementById('title'); -const primaryFeastNameField = document.getElementById('primary_feast_name'); -const secondaryFeastNameField = document.getElementById('secondary_feast_name'); +const primaryFeastField = document.getElementById('primary_feast'); +const secondaryFeastsField = document.getElementById('secondary_feasts'); const dateField = document.getElementById('date'); const timeField = document.getElementById('time'); const setTitle = () => { let txt; - const primaryFeastName = primaryFeastNameField.options[primaryFeastNameField.selectedIndex].innerText; - const secondaryFeastName = secondaryFeastNameField.options[secondaryFeastNameField.selectedIndex].innerText; - if (secondaryFeastNameField.value) - txt = primaryFeastName + ' (' + secondaryFeastName + ')'; + const primaryFeastName = primaryFeastField.selectedOptions[0].innerText; + const secondaryFeastsNames = Array.from(secondaryFeastsField.selectedOptions).map(e => e.innerText) + if (secondaryFeastsNames.length !== 0) + txt = primaryFeastName + ' (' + secondaryFeastsNames.join(", ") + ')'; else txt = primaryFeastName; @@ -18,15 +18,20 @@ const setTitle = () => { titleField.value = txt; }; +setTitle(); +primaryFeastField.addEventListener('change', setTitle); +secondaryFeastsField.addEventListener('change', setTitle); + const updateDateFromPrimaryFeast = async () => { - const url = '/feast/api/' + primaryFeastNameField.value + '/date'; + const url = '/feast/api/' + primaryFeastField.value + '/date'; const r = await fetch(url); const j = await r.json(); if (j !== null) dateField.value = j; }; -primaryFeastNameField.addEventListener('change', updateDateFromPrimaryFeast); +updateDateFromPrimaryFeast().then(); +primaryFeastField.addEventListener('change', updateDateFromPrimaryFeast); const today = new Date() const day = 1000 * 60 * 60 * 24; // milliseconds in a day diff --git a/templates/serviceForm.html b/templates/serviceForm.html index 7597181..54d55e8 100644 --- a/templates/serviceForm.html +++ b/templates/serviceForm.html @@ -16,11 +16,11 @@

- {{ form.primary_feast_name.label(class='col-sm-2 col-form-label') }} + {{ form.primary_feast.label(class='col-sm-2 col-form-label') }}
- {{ form.primary_feast_name(class='form-select') }} - {% if form.primary_feast_name.errors %} - {% for error in form.primary_feast_name.errors %} + {{ form.primary_feast(class='form-select') }} + {% if form.primary_feast.errors %} + {% for error in form.primary_feast.errors %} {{ error }} @@ -30,11 +30,11 @@

- {{ form.secondary_feast_name.label(class='col-sm-2 col-form-label') }} + {{ form.secondary_feasts.label(class='col-sm-2 col-form-label') }}
- {{ form.secondary_feast_name(class='form-select') }} - {% if form.secondary_feast_name.errors %} - {% for error in form.secondary_feast_name.errors %} + {{ form.secondary_feasts(class='form-select') }} + {% if form.secondary_feasts.errors %} + {% for error in form.secondary_feasts.errors %} {{ error }} diff --git a/tests/test_pypew.py b/tests/test_pypew.py index f1a932f..b639361 100644 --- a/tests/test_pypew.py +++ b/tests/test_pypew.py @@ -85,8 +85,31 @@ def test_collects_normal(self): def test_collects_with_secondary_feast(self): primary = get(Feast.all(), name='Septuagesima') secondary = get(Feast.all(), name='Christmas Day') - service = Service(title='', date=today(), primary_feast=primary, secondary_feast=secondary) - self.assertListEqual(service.collects, [primary.collect, secondary.collect]) + service = Service( + title='', + date=today(), + primary_feast=primary, + secondary_feasts=[secondary] + ) + self.assertListEqual( + service.collects, + [primary.collect, secondary.collect] + ) + + def test_collects_with_two_secondary_feasts(self): + primary = get(Feast.all(), name='Septuagesima') + secondary1 = get(Feast.all(), name='Christmas Day') + secondary2 = get(Feast.all(), name='St. Stephen') + service = Service( + title='', + date=today(), + primary_feast=primary, + secondary_feasts=[secondary1, secondary2] + ) + self.assertListEqual( + service.collects, + [primary.collect, secondary1.collect, secondary2.collect] + ) def test_collect_on_advent1(self): advent1 = get(Feast.all(), name='Advent I') @@ -188,11 +211,11 @@ def test_pew_sheet_docx_view(self, m_create_docx): 'title': 'Feast of Foo', 'date': '2022-01-01', 'time': '11:00', - 'primary_feast_name': 'advent-i', - 'secondary_feast_name': '', + 'primary_feast': 'advent-i', + 'secondary_feasts': 'trinity-iii', 'introit_hymn': '', 'offertory_hymn': '', - 'recessional_hymn': '' + 'recessional_hymn': '', } ) ) diff --git a/utils.py b/utils.py index a8fa333..bcfc31f 100644 --- a/utils.py +++ b/utils.py @@ -2,6 +2,7 @@ import os from datetime import timedelta, date from functools import lru_cache +from pathlib import Path from typing import Optional import pandas as pd @@ -31,9 +32,13 @@ def str2date(s: Optional[str]) -> date: @lru_cache() def get_neh_df(): if pd is None: - raise NoPandasError('Pandas not available, can\'t load hymn information') + raise NoPandasError( + 'Pandas not available, can\'t load hymn information' + ) - df = pd.read_csv(os.path.join('data', 'neh.csv')) + df = pd.read_csv( + Path(__file__).parent / 'data/neh.csv' + ) assert 'number' in df.columns assert 'firstLine' in df.columns diff --git a/views/pew_sheet_views.py b/views/pew_sheet_views.py index fc62b83..bbe0710 100644 --- a/views/pew_sheet_views.py +++ b/views/pew_sheet_views.py @@ -8,8 +8,8 @@ from werkzeug.datastructures import ImmutableMultiDict from forms import PewSheetForm -from models import Service, Feast -from utils import logger, cache_dir +from models import Feast, Service +from utils import cache_dir, logger __all__ = ['pew_sheet_create_view', 'pew_sheet_clear_history_endpoint', 'pew_sheet_docx_view'] @@ -19,8 +19,8 @@ def pew_sheet_create_view(): form = PewSheetForm(request.args) - if not form.primary_feast_name.data: - form.primary_feast_name.data = Feast.next().slug + if not form.primary_feast.data: + form.primary_feast.data = Feast.next().slug service = None if form.validate_on_submit():