202 lines
7.5 KiB
Python
202 lines
7.5 KiB
Python
"""
|
|
nearby.py - the courtesy directory behind /find-a-unit.
|
|
|
|
Other Scouting units in the Continental District. Rows live in `nearby_units`
|
|
in scout73.db (schema in store.py); this module only reads and renders them.
|
|
|
|
Two things shape everything here.
|
|
|
|
It is somebody else's data. It comes from a hand-typed district document that
|
|
goes stale without telling us, so `verified_at` is shown to the reader instead
|
|
of kept as bookkeeping, and BeAScout is offered as the authoritative lookup
|
|
rather than buried. A family driving to the wrong address on a Tuesday night is
|
|
the failure this page exists to avoid.
|
|
|
|
It is not about us. Pack 73 and Troop 73 are deliberately absent from the table
|
|
and from this page. The rest of the site is our pitch; this one is a door out
|
|
for a family we are not the right fit for.
|
|
"""
|
|
|
|
import store
|
|
|
|
# unit_type -> the badge a parent actually recognises.
|
|
TYPE_LABEL = {
|
|
"pack": "Cub Scouts",
|
|
"troop": "Scouts BSA",
|
|
"ship": "Sea Scouts",
|
|
"club": "Exploring",
|
|
}
|
|
|
|
# The unit's own words for who it serves. "family" is the common case and gets
|
|
# the warmer chip; a single-gender unit is stated plainly rather than softened,
|
|
# because that is the thing a parent is scanning for.
|
|
SERVES_LABEL = {
|
|
"family": "Boys & girls",
|
|
"boys": "Boys",
|
|
"girls": "Girls",
|
|
"coed": "Co-ed",
|
|
}
|
|
|
|
BEASCOUT = "https://beascout.scouting.org"
|
|
|
|
# The district's own short link to its unit list. This is the page the district
|
|
# hands out, so it is the right thing to credit and the right thing to link.
|
|
# One URL for the whole page rather than a per-row column: when the admin panel
|
|
# exists this belongs in a settings row, not in seventeen copies.
|
|
SOURCE_NAME = "Continental District unit list"
|
|
SOURCE_URL = "https://tinyurl.com/ContinentalScouts"
|
|
|
|
MONTHS = ["January", "February", "March", "April", "May", "June", "July",
|
|
"August", "September", "October", "November", "December"]
|
|
|
|
|
|
def pretty_date(iso):
|
|
"""2026-09-04 -> September 4, 2026. A log line is not a sentence."""
|
|
try:
|
|
y, m, d = (int(x) for x in str(iso).split("-"))
|
|
return f"{MONTHS[m - 1]} {d}, {y}"
|
|
except Exception:
|
|
return str(iso)
|
|
|
|
|
|
def listing():
|
|
"""Rows grouped by area, in sort_order. Empty list if the table is unusable."""
|
|
try:
|
|
con = store.connect()
|
|
except Exception:
|
|
return []
|
|
try:
|
|
rows = con.execute(
|
|
"SELECT * FROM nearby_units WHERE active = 1 "
|
|
"ORDER BY sort_order, unit_number"
|
|
).fetchall()
|
|
except Exception:
|
|
return []
|
|
finally:
|
|
con.close()
|
|
|
|
groups, order = {}, []
|
|
for r in rows:
|
|
area = r["area"] or "Elsewhere in the district"
|
|
if area not in groups:
|
|
groups[area] = []
|
|
order.append(area)
|
|
groups[area].append(dict(r))
|
|
return [(a, groups[a]) for a in order]
|
|
|
|
|
|
def checked_on():
|
|
"""The most recent verified_at across live rows, for the honesty line."""
|
|
try:
|
|
con = store.connect()
|
|
except Exception:
|
|
return None
|
|
try:
|
|
row = con.execute(
|
|
"SELECT MAX(verified_at) AS v FROM nearby_units WHERE active = 1"
|
|
).fetchone()
|
|
return row["v"] if row else None
|
|
except Exception:
|
|
return None
|
|
finally:
|
|
con.close()
|
|
|
|
|
|
def _esc(s):
|
|
return (str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
|
if s else "")
|
|
|
|
|
|
def unit_name(u):
|
|
n = _esc(u["unit_number"])
|
|
kind = {"pack": "Pack", "troop": "Troop", "ship": "Ship", "club": "Club"}.get(
|
|
u["unit_type"], "Unit")
|
|
return f"{kind} {n}"
|
|
|
|
|
|
def card(u):
|
|
"""One unit.
|
|
|
|
The link sits on the unit's name, never printed raw. Most of these are
|
|
tinyurls and a wall of them reads like a spam folder.
|
|
"""
|
|
name = unit_name(u)
|
|
if u["link_url"]:
|
|
title = (f'<a class="nu-name" href="{_esc(u["link_url"])}" '
|
|
f'target="_blank" rel="noopener">{name}</a>')
|
|
else:
|
|
title = f'<span class="nu-name nu-plain">{name}</span>'
|
|
|
|
chips = [f'<span class="badge">{TYPE_LABEL.get(u["unit_type"], "Scouting")}</span>']
|
|
if u["serves"] and u["serves"] in SERVES_LABEL:
|
|
# Tone tracks who the unit is OPEN to, not the word it uses. Exploring
|
|
# posts and clubs are co-ed by design and have no "family" variant to
|
|
# be compared against, so colouring "coed" like a single-gender unit
|
|
# would make Club 254 look narrower than Pack 244 when it is not.
|
|
tone = "gold" if u["serves"] in ("family", "coed") else "blue"
|
|
chips.append(f'<span class="chip {tone}">{SERVES_LABEL[u["serves"]]}</span>')
|
|
|
|
lines = []
|
|
if u["chartered_org"]:
|
|
lines.append(_esc(u["chartered_org"]))
|
|
where = ", ".join(x for x in [_esc(u["street"]), _esc(u["town"])] if x)
|
|
if where:
|
|
lines.append(where)
|
|
body = "<br>".join(lines)
|
|
|
|
meets = (f'<div class="nu-meets">{_esc(u["meets"])}</div>'
|
|
if u["meets"] else "")
|
|
notes = f'<div class="nu-note">{_esc(u["notes"])}</div>' if u["notes"] else ""
|
|
contact = ""
|
|
if u["contact"]:
|
|
c = _esc(u["contact"])
|
|
contact = (f'<div class="nu-note">Contact <a class="nu-mail" '
|
|
f'href="mailto:{c}">{c}</a></div>')
|
|
|
|
return (f'<div class="mcard nu-card">'
|
|
f'<div class="nu-head">{title}<span class="nu-chips">{"".join(chips)}</span></div>'
|
|
f'<div class="nu-where">{body}</div>{meets}{notes}{contact}</div>')
|
|
|
|
|
|
def section(area, units):
|
|
cards = "".join(card(u) for u in units)
|
|
return (f'<div class="nu-group">'
|
|
f'<h2 class="sec nu-area">{_esc(area)}</h2>'
|
|
f'<div class="gridcards">{cards}</div></div>')
|
|
|
|
|
|
def body_html(hero_html):
|
|
"""The whole page body, hero included, ready to hand to page()."""
|
|
groups = listing()
|
|
if groups:
|
|
inner = "".join(section(a, u) for a, u in groups)
|
|
else:
|
|
inner = ('<div class="noev">This list is being rebuilt. '
|
|
f'<a href="{BEASCOUT}" target="_blank" rel="noopener">BeAScout.org</a> '
|
|
'has every unit in the area.</div>')
|
|
|
|
checked = checked_on()
|
|
src = (f'<a class="nu-src" href="{SOURCE_URL}" target="_blank" '
|
|
f'rel="noopener">{SOURCE_NAME}</a>')
|
|
stamp = (f'Pulled from the {src} on {pretty_date(checked)}.' if checked
|
|
else f'Pulled from the {src}.')
|
|
|
|
return f"""{hero_html}
|
|
<section style="padding:56px 0 20px"><div class="wrap">
|
|
<p class="lead">Scouting works best close to home. If we are the wrong side of the county, or
|
|
Tuesday nights are already spoken for, one of these units may fit your family better. Every one
|
|
of them welcomes visitors, and none of them mind being asked.</p>
|
|
</div></section>
|
|
<section style="padding:0 0 24px"><div class="wrap">{inner}</div></section>
|
|
<section style="padding:0 0 40px"><div class="wrap"><div class="note">
|
|
<strong>Reach out before you go.</strong> {stamp} Meeting nights and locations change through
|
|
the year, so contact the unit first and make sure the details still match their program. Each
|
|
unit's own page, linked from its name above, is more current than we are.
|
|
</div></div></section>
|
|
<div class="band" style="background:linear-gradient(180deg,rgba(13,22,40,.55),rgba(13,22,40,.78)),url('/static/img/campfire-dusk.jpg') center/cover no-repeat">
|
|
<h2>Looking somewhere we haven't listed?</h2>
|
|
<p>BeAScout is Scouting America's official lookup. Put in your zip code and it will show you
|
|
every pack, troop, ship and post near you, with current contact details for each one.</p>
|
|
<a class="cta" href="{BEASCOUT}" target="_blank" rel="noopener">Search BeAScout.org</a>
|
|
</div>"""
|