Add a document shelf at /documents, served through the app
Files live outside the repo at /srv/scout-website-assets/docs (bind-mounted
read-only at /docs), the same arrangement as the photos, published by a
manifest.json beside them. Adding a document is a file drop plus a manifest
entry - the app re-reads the manifest on mtime change, so no rebuild and no
redeploy.
The manifest maps a stable slug to a filename, so next year's permission slip
can replace this year's without breaking a link already printed on a flyer.
Documents are served through /documents/{slug} rather than from a static
mount, and NOT placed under /app/static, which is public forever. That is the
point: when member login exists it plugs into documents.visible() and no
public URL moves. The visibility field already carries 'members', which today
is hidden from the index and 404s rather than 403s, since a 403 would
advertise a document we cannot yet gate.
This commit is contained in:
+69
-2
@@ -1,13 +1,14 @@
|
||||
import json, os, time, datetime, urllib.request, urllib.parse
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
# Internal record store + admin API. The store is the source of truth for
|
||||
# anything a family submits; see store.py for why.
|
||||
import store
|
||||
import admin_api
|
||||
import documents
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pack & Troop 73 - greenlanescouts73.org
|
||||
@@ -259,6 +260,14 @@ footer a{color:#D8DFEA;text-decoration:none;font-size:14px}
|
||||
footer a:hover{color:#fff}
|
||||
footer a.gold{color:#F7C556;font-weight:700}
|
||||
footer a.fb{display:inline-flex;align-items:center;gap:8px}
|
||||
.docrow{display:flex;gap:16px;align-items:center;background:#fff;border:1px solid #EBE4D4;border-radius:14px;padding:16px 18px;text-decoration:none;color:inherit;transition:transform .12s,border-color .12s}
|
||||
.docrow:hover{transform:translateY(-2px);border-color:#CBD2DC;color:inherit}
|
||||
.docicon{font-family:'Archivo';font-weight:800;font-size:12px;letter-spacing:.06em;color:#fff;background:#1E2F52;border-radius:10px;padding:13px 0;width:58px;min-width:58px;text-align:center}
|
||||
.docmain{flex:1;min-width:0}
|
||||
.doct{font-family:'Archivo';font-weight:700;font-size:16px;color:#1E2F52;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||||
.docd{display:block;margin-top:3px;color:#6A7280;font-size:14px}
|
||||
.docmeta{font-size:12.5px;font-weight:600;color:#8B93A3;white-space:nowrap}
|
||||
@media(max-width:560px){.docrow{flex-wrap:wrap}.docmeta{width:100%;padding-left:74px}}
|
||||
"""
|
||||
|
||||
def page(title, body, active=""):
|
||||
@@ -296,7 +305,7 @@ def page(title, body, active=""):
|
||||
<div style="font-size:14px;line-height:1.7">Chartered by St. Luke's Lutheran Church · Zieglerville, PA<br>Cradle of Liberty Council · A family program. Every kid welcome.</div></div>
|
||||
<div><div class="fh">VISIT</div><div style="font-size:14px;line-height:1.9">Tuesdays · Pack 6:00 · Troop 7:30<br>St. Luke's Lutheran Church<br>Zieglerville, PA</div></div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">EXPLORE</div>
|
||||
<a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">2026-2027 Calendar</a><a class="gold" href="/join">Join us</a></div>
|
||||
<a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">2026-2027 Calendar</a><a href="/documents">Forms & documents</a><a class="gold" href="/join">Join us</a></div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">FOLLOW</div>
|
||||
<a class="fb" href="https://www.facebook.com/Pack73GreenLane" target="_blank" rel="noopener" aria-label="Pack 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Pack 73</a><a class="fb" href="https://www.facebook.com/Troop73GreenLane" target="_blank" rel="noopener" aria-label="Troop 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Troop 73</a></div>
|
||||
</div></footer>
|
||||
@@ -531,6 +540,64 @@ Dates occasionally shift. Registered families get the live TeamSnap calendar wit
|
||||
</div></div></section>"""
|
||||
return page("2026-2027 Calendar · Pack & Troop 73", body, "cal")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Documents. Files come from the manifest in /docs (see documents.py); they are
|
||||
# deliberately served through this route rather than a static mount, so member
|
||||
# login can gate them later without a single public URL changing.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def doc_row(d):
|
||||
label, _ = documents.kind(d)
|
||||
u = d["unit"]
|
||||
chip = f' <span class="uchip uchip-{u}">{unit_label(u)}</span>' if u != "both" else ""
|
||||
desc = f'<span class="docd">{d["description"]}</span>' if d["description"] else ""
|
||||
meta = " · ".join(x for x in [documents.size_text(d), documents.updated_text(d)] if x)
|
||||
return (f'<a class="docrow" href="/documents/{d["slug"]}">'
|
||||
f'<span class="docicon">{label}</span>'
|
||||
f'<span class="docmain"><span class="doct">{d["title"]}{chip}</span>{desc}</span>'
|
||||
f'<span class="docmeta">{meta}</span></a>')
|
||||
|
||||
@app.get("/documents", response_class=HTMLResponse)
|
||||
def documents_index():
|
||||
groups = documents.listing()
|
||||
if groups:
|
||||
cards = []
|
||||
for cat, rows in groups:
|
||||
blurb = f'<p class="lead" style="margin:0 0 18px;font-size:15px">{cat["blurb"]}</p>' if cat["blurb"] else ""
|
||||
cards.append(f'<div style="margin-bottom:44px"><h2 class="sec" style="font-size:1.5rem;margin-bottom:6px">{cat["title"]}</h2>{blurb}'
|
||||
f'<div style="display:flex;flex-direction:column;gap:12px">{"".join(doc_row(d) for d in rows)}</div></div>')
|
||||
inner = "".join(cards)
|
||||
else:
|
||||
inner = '<div class="noev">Nothing posted here yet. Forms and handouts will land on this page as the program year gets going.</div>'
|
||||
body = f"""
|
||||
<div class="calhead"><div class="wrap">
|
||||
<div class="eyebrow gold">Pack & Troop 73</div>
|
||||
<h1>Forms and documents.</h1>
|
||||
<p class="sub">Everything worth handing out, in one place. These links are permanent, so a QR code or a printed handout keeps working even after the file behind it is replaced.</p>
|
||||
</div></div>
|
||||
<section style="padding:52px 0 70px"><div class="wrap">{inner}</div></section>
|
||||
<section style="padding:0 0 80px"><div class="wrap"><div class="note">
|
||||
Can't find something, or need a form in another format? Message us on Facebook or catch a leader on a Tuesday night.
|
||||
</div></div></section>"""
|
||||
return page("Forms & Documents · Pack & Troop 73", body, "docs")
|
||||
|
||||
@app.get("/documents/{slug}")
|
||||
def document_file(slug: str):
|
||||
d = documents.find(slug)
|
||||
if d is None:
|
||||
body = """
|
||||
<div class="calhead"><div class="wrap">
|
||||
<div class="eyebrow gold">Pack & Troop 73</div>
|
||||
<h1>That document isn't here.</h1>
|
||||
<p class="sub">The link may be out of date, or the file may have been taken down. Everything currently posted is on the documents page.</p>
|
||||
<a class="cta" href="/documents">See all documents</a>
|
||||
</div></div>"""
|
||||
return HTMLResponse(page("Not found · Pack & Troop 73", body, "docs"), status_code=404)
|
||||
_, mime = documents.kind(d)
|
||||
return FileResponse(d["path"], media_type=mime, filename=d["path"].name,
|
||||
content_disposition_type=documents.disposition(d))
|
||||
|
||||
|
||||
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."),
|
||||
("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."),
|
||||
|
||||
Reference in New Issue
Block a user