meeting words come from the units table; the constants are now defaults
MEETING_DAY, MEETING_DAYS, MEETING_DAY_ABBR, PACK_TIME, TROOP_TIME, PACK_CLOCK and TROOP_CLOCK were the last thing a leader could not change without a commit. identity.meeting_words() derives all seven from the units rows; app.py refreshes the module globals from it on a 30-second TTL in a middleware, and the page templates - f-strings that read those globals when a route runs - are untouched. A row that cannot supply a word keeps the default, so a half-filled unit degrades to today's copy rather than a blank. The day words come from the pack row: the site's copy assumes both units meet the same night, and if that changes the copy needs rewriting, not a bigger constant. Proven on a throwaway container with a DB copy: patching pack73 to Thursday 18:15 changed the homepage, cubs, troop and meta description; patching back restored them. tests/smoke_identity.py 59 -> 66.
This commit is contained in:
+39
@@ -59,9 +59,47 @@ TROOP_TIME = "7:30 PM"
|
|||||||
PACK_CLOCK = "6:00"
|
PACK_CLOCK = "6:00"
|
||||||
TROOP_CLOCK = "7:30"
|
TROOP_CLOCK = "7:30"
|
||||||
|
|
||||||
|
# Since 2026-09-04 the seven meeting words above are DEFAULTS. The live values
|
||||||
|
# come from the units table (edited by leaders through the console, P2) via
|
||||||
|
# identity.meeting_words(), applied by the middleware below before every
|
||||||
|
# request. A unit row that cannot supply a word leaves the default standing,
|
||||||
|
# so the constants are still the right place to read what the site falls back
|
||||||
|
# to - and PROGRAM_YEAR stays a plain constant.
|
||||||
|
_MEETING_DEFAULTS = dict(MEETING_DAY=MEETING_DAY, MEETING_DAYS=MEETING_DAYS,
|
||||||
|
MEETING_DAY_ABBR=MEETING_DAY_ABBR, PACK_TIME=PACK_TIME,
|
||||||
|
TROOP_TIME=TROOP_TIME, PACK_CLOCK=PACK_CLOCK, TROOP_CLOCK=TROOP_CLOCK)
|
||||||
|
_meeting_cache = {"at": 0.0}
|
||||||
|
MEETING_TTL = 30
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_meeting_words(force=False):
|
||||||
|
"""Re-derive the meeting words from the units table, at most every
|
||||||
|
MEETING_TTL seconds. The page templates are f-strings that read these
|
||||||
|
module globals when a route runs, so updating the globals is the whole
|
||||||
|
wiring - no template changes. Any failure keeps the defaults."""
|
||||||
|
global MEETING_DAY, MEETING_DAYS, MEETING_DAY_ABBR, PACK_TIME, TROOP_TIME, PACK_CLOCK, TROOP_CLOCK
|
||||||
|
now = time.monotonic()
|
||||||
|
if not force and now - _meeting_cache["at"] < MEETING_TTL:
|
||||||
|
return
|
||||||
|
_meeting_cache["at"] = now
|
||||||
|
try:
|
||||||
|
w = identity.meeting_words(identity.list_units(), _MEETING_DEFAULTS)
|
||||||
|
except Exception as e:
|
||||||
|
print("meeting words: keeping defaults: %s" % e, flush=True)
|
||||||
|
return
|
||||||
|
MEETING_DAY, MEETING_DAYS, MEETING_DAY_ABBR = w["MEETING_DAY"], w["MEETING_DAYS"], w["MEETING_DAY_ABBR"]
|
||||||
|
PACK_TIME, TROOP_TIME = w["PACK_TIME"], w["TROOP_TIME"]
|
||||||
|
PACK_CLOCK, TROOP_CLOCK = w["PACK_CLOCK"], w["TROOP_CLOCK"]
|
||||||
|
|
||||||
app = FastAPI(title="Pack & Troop 73")
|
app = FastAPI(title="Pack & Troop 73")
|
||||||
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def _meeting_words_middleware(request, call_next):
|
||||||
|
refresh_meeting_words()
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
# Creates the schema if absent and backfills the legacy leads.jsonl once.
|
# Creates the schema if absent and backfills the legacy leads.jsonl once.
|
||||||
store.init(backfill_jsonl=LEADS)
|
store.init(backfill_jsonl=LEADS)
|
||||||
app.include_router(admin_api.router)
|
app.include_router(admin_api.router)
|
||||||
@@ -1089,6 +1127,7 @@ def bootstrap_notify(url):
|
|||||||
|
|
||||||
|
|
||||||
identity.init()
|
identity.init()
|
||||||
|
refresh_meeting_words(force=True)
|
||||||
auth.PAGE = page
|
auth.PAGE = page
|
||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,43 @@ def get_unit(slug_or_id):
|
|||||||
# code change.
|
# code change.
|
||||||
UNIT_MEETS_FIELDS = ("meets_weekday", "meets_time", "meets_at")
|
UNIT_MEETS_FIELDS = ("meets_weekday", "meets_time", "meets_at")
|
||||||
|
|
||||||
|
WEEKDAY_NAMES = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||||
|
|
||||||
|
|
||||||
|
def clock_words(hhmm):
|
||||||
|
""""18:00" -> ("6:00 PM", "6:00"). Returns None on anything unparseable so
|
||||||
|
the caller keeps its fallback rather than rendering a blank."""
|
||||||
|
try:
|
||||||
|
t = datetime.datetime.strptime(str(hhmm), "%H:%M")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
h12 = t.hour % 12 or 12
|
||||||
|
clock = "%d:%02d" % (h12, t.minute)
|
||||||
|
return ("%s %s" % (clock, "AM" if t.hour < 12 else "PM"), clock)
|
||||||
|
|
||||||
|
|
||||||
|
def meeting_words(units, defaults):
|
||||||
|
"""The seven seasonal strings app.py renders, derived from the units rows.
|
||||||
|
|
||||||
|
`defaults` is the dict of the code constants; any word a row cannot supply
|
||||||
|
keeps its default, so a half-filled unit row degrades to today's copy and
|
||||||
|
never to a blank page. The day words come from the PACK row: the site's
|
||||||
|
copy assumes both units meet the same night ("right after the pack"), and
|
||||||
|
if that ever changes the copy needs rewriting, not a bigger constant.
|
||||||
|
"""
|
||||||
|
out = dict(defaults)
|
||||||
|
by = {u.get("slug"): u for u in units}
|
||||||
|
pack, troop = by.get("pack73"), by.get("troop73")
|
||||||
|
day = (pack or troop or {}).get("meets_weekday")
|
||||||
|
if isinstance(day, int) and 1 <= day <= 7:
|
||||||
|
name = WEEKDAY_NAMES[day - 1]
|
||||||
|
out["MEETING_DAY"], out["MEETING_DAYS"], out["MEETING_DAY_ABBR"] = name, name + "s", name[:3]
|
||||||
|
for row, tkey, ckey in ((pack, "PACK_TIME", "PACK_CLOCK"), (troop, "TROOP_TIME", "TROOP_CLOCK")):
|
||||||
|
w = clock_words((row or {}).get("meets_time"))
|
||||||
|
if w:
|
||||||
|
out[tkey], out[ckey] = w
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def update_unit_meets(slug_or_id, fields):
|
def update_unit_meets(slug_or_id, fields):
|
||||||
"""Change when and where a unit meets.
|
"""Change when and where a unit meets.
|
||||||
|
|||||||
@@ -168,6 +168,25 @@ con.close()
|
|||||||
for k in ("invite.created", "invite.consumed", "login.ok", "login.failed", "login.throttled"):
|
for k in ("invite.created", "invite.consumed", "login.ok", "login.failed", "login.throttled"):
|
||||||
check("auth_events records %s" % k, k in kinds)
|
check("auth_events records %s" % k, k in kinds)
|
||||||
|
|
||||||
|
print("\nmeeting words")
|
||||||
|
D = dict(MEETING_DAY="Tuesday", MEETING_DAYS="Tuesdays", MEETING_DAY_ABBR="Tue", PACK_TIME="6:00 PM",
|
||||||
|
TROOP_TIME="7:30 PM", PACK_CLOCK="6:00", TROOP_CLOCK="7:30")
|
||||||
|
check("clock words", I.clock_words("18:00") == ("6:00 PM", "6:00") and I.clock_words("07:05") == ("7:05 AM", "7:05")
|
||||||
|
and I.clock_words("00:30") == ("12:30 AM", "12:30") and I.clock_words("12:00") == ("12:00 PM", "12:00"))
|
||||||
|
check("clock words refuse junk", I.clock_words(None) is None and I.clock_words("6pm") is None)
|
||||||
|
w = I.meeting_words([{"slug": "pack73", "meets_weekday": 4, "meets_time": "18:15"},
|
||||||
|
{"slug": "troop73", "meets_weekday": 4, "meets_time": "19:45"}], D)
|
||||||
|
check("day and both times derived", w["MEETING_DAY"] == "Thursday" and w["MEETING_DAYS"] == "Thursdays"
|
||||||
|
and w["MEETING_DAY_ABBR"] == "Thu" and w["PACK_TIME"] == "6:15 PM" and w["PACK_CLOCK"] == "6:15"
|
||||||
|
and w["TROOP_TIME"] == "7:45 PM" and w["TROOP_CLOCK"] == "7:45")
|
||||||
|
w2 = I.meeting_words([{"slug": "pack73", "meets_weekday": None, "meets_time": None},
|
||||||
|
{"slug": "troop73", "meets_weekday": 2, "meets_time": "19:30"}], D)
|
||||||
|
check("missing pack values keep the defaults, day falls to the troop row",
|
||||||
|
w2["MEETING_DAY"] == "Tuesday" and w2["PACK_TIME"] == "6:00 PM" and w2["TROOP_TIME"] == "7:30 PM")
|
||||||
|
check("no rows at all is the defaults", I.meeting_words([], D) == D)
|
||||||
|
check("defaults dict is not mutated", D["PACK_TIME"] == "6:00 PM")
|
||||||
|
check("live seed rows reproduce the constants", I.meeting_words(I.list_units(), D) == D)
|
||||||
|
|
||||||
print("\nsafe_next")
|
print("\nsafe_next")
|
||||||
check("relative path passes", I.safe_next("/leaders/") == "/leaders/")
|
check("relative path passes", I.safe_next("/leaders/") == "/leaders/")
|
||||||
check("query string kept", I.safe_next("/leaders/nearby?x=1") == "/leaders/nearby?x=1")
|
check("query string kept", I.safe_next("/leaders/nearby?x=1") == "/leaders/nearby?x=1")
|
||||||
|
|||||||
Reference in New Issue
Block a user