Add /find-a-unit: a courtesy directory of other Continental District units
This commit is contained in:
+176
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
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 = "gold" if u["serves"] == "family" 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()
|
||||
stamp = (f'We checked these details on {_esc(checked)}.' if checked
|
||||
else 'These details come from the district unit list.')
|
||||
|
||||
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>Before you drive out, call ahead.</strong> {stamp} Meeting nights and locations change,
|
||||
and this list is a courtesy rather than an official record. The unit's own page, linked from its
|
||||
name above, is always 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>"""
|
||||
Reference in New Issue
Block a user