Add /find-a-unit: a courtesy directory of other Continental District units
This commit is contained in:
+33
@@ -8,6 +8,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
# anything a family submits; see store.py for why.
|
# anything a family submits; see store.py for why.
|
||||||
import store
|
import store
|
||||||
import admin_api
|
import admin_api
|
||||||
|
import nearby
|
||||||
import documents
|
import documents
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -317,6 +318,18 @@ footer a.fb{display:inline-flex;align-items:center;gap:8px}
|
|||||||
.anncitem+.anncitem{border-top:1px solid rgba(0,0,0,.09);padding-top:10px;margin-top:10px}
|
.anncitem+.anncitem{border-top:1px solid rgba(0,0,0,.09);padding-top:10px;margin-top:10px}
|
||||||
.anncitem a{color:inherit;text-decoration:underline;font-size:13px}
|
.anncitem a{color:inherit;text-decoration:underline;font-size:13px}
|
||||||
.ancclink{flex:none;color:inherit;text-decoration:underline;font-size:13px;white-space:nowrap}
|
.ancclink{flex:none;color:inherit;text-decoration:underline;font-size:13px;white-space:nowrap}
|
||||||
|
.nu-group{margin-bottom:44px}
|
||||||
|
.nu-area{font-size:1.45rem;margin-bottom:14px}
|
||||||
|
.nu-card{padding:22px 24px;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.nu-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
|
||||||
|
.nu-name{font-family:'Archivo';font-weight:800;font-size:1.15rem;color:#1E2F52;text-decoration:none;border-bottom:2px solid #F0AD2D}
|
||||||
|
.nu-name:hover{color:#B07B12}
|
||||||
|
.nu-plain{border-bottom:none;color:#1E2F52}
|
||||||
|
.nu-chips{display:flex;flex-wrap:wrap;gap:6px}
|
||||||
|
.nu-where{color:#5A6272;font-size:14.5px;line-height:1.6}
|
||||||
|
.nu-meets{font-weight:700;color:#1E2F52;font-size:14.5px}
|
||||||
|
.nu-note{color:#6A7280;font-size:13.5px;line-height:1.6}
|
||||||
|
.nu-mail{color:#B07B12;font-weight:600;text-decoration:none}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ICO_INFO = ('<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor"'
|
ICO_INFO = ('<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor"'
|
||||||
@@ -730,6 +743,26 @@ def document_file(slug: str):
|
|||||||
headers=headers)
|
headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Find a unit. Other people's units, from nearby_units (see nearby.py). There
|
||||||
|
# are deliberately NO entry points elsewhere on the site yet - reachable by
|
||||||
|
# direct link only until the placement is settled. noindex, so a courtesy
|
||||||
|
# directory never outranks another unit for its own name.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.get("/find-a-unit", response_class=HTMLResponse)
|
||||||
|
def find_a_unit():
|
||||||
|
h = hero("morning-trail.jpg", "center 62%",
|
||||||
|
"Continental District \u00b7 Scouting America",
|
||||||
|
"Find a unit near you.",
|
||||||
|
"Packs, troops, a ship and an Exploring club within a short drive of "
|
||||||
|
"Zieglerville, so you can pick the one that fits your family.",
|
||||||
|
minh=440)
|
||||||
|
return HTMLResponse(page("Find a Unit Near You \u00b7 Pack & Troop 73",
|
||||||
|
nearby.body_html(h), "nearby"),
|
||||||
|
headers={"X-Robots-Tag": "noindex, nofollow"})
|
||||||
|
|
||||||
|
|
||||||
FAQ = [
|
FAQ = [
|
||||||
("What does it cost?", "Annual registration plus modest pack or troop dues. Fundraisers like our chocolate booth keep costs down, and quiet help is available. Cost never keeps a kid out of Pack 73; just talk to us."),
|
("What does it cost?", "Annual registration plus modest pack or troop dues. Fundraisers like our chocolate booth keep costs down, and quiet help is available. Cost never keeps a kid out of Pack 73; just talk to us."),
|
||||||
("Do parents stay at meetings?", "Yes: a parent or guardian stays at every Cub Scout meeting and event. Lions and Tigers pair up with their grown-up for the hands-on parts; for older dens, being in the room is enough. Troop scouts (11 to 17) can be dropped off."),
|
("Do parents stay at meetings?", "Yes: a parent or guardian stays at every Cub Scout meeting and event. Lions and Tigers pair up with their grown-up for the hands-on parts; for older dens, being in the room is enough. Troop scouts (11 to 17) can be dropped off."),
|
||||||
|
|||||||
+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