roster: households, scouts, a per-year checklist, lead import

Decided by Mike 2026-09-04. my.scouting stays the record of registration;
this holds what a den leader needs on a Tuesday: the families, which scout
is in which den, and a per-program-year checklist of things collected -
dues paid, health form handed in - recording THAT a thing was collected,
by whom and when, never the thing. Health forms are never stored here;
that is a policy, not a gap. A scout is a first name, last name, unit,
den and an optional BSA member ID (the recharter join key), and nothing
else: no date of birth, no address, nothing medical, and a test asserts
no such column exists.

A join lead can be imported as a household: contact copied, the children
field carried as a note to sort by hand, the lead linked and untouched.
Importing twice is 409.

roster:write for leader and above, never on a script key. Every write
lands in the action log. scout-website-backup.timer already copies the
database nightly, which was the doc's first condition for naming scouts.

tests/smoke_admin.py 122 -> 141.
This commit is contained in:
2026-09-04 18:56:30 -04:00
parent 59559ad909
commit 504538567f
4 changed files with 403 additions and 1 deletions
+4 -1
View File
@@ -77,6 +77,7 @@ CAPS = {
"nearby:write",
"calendar:write",
"unit:write_own",
"roster:write",
"apikeys:own",
"api:docs",
"email:draft",
@@ -89,6 +90,7 @@ CAPS = {
"nearby:write",
"calendar:write",
"unit:write_own",
"roster:write",
"units:write",
"settings:write",
"apikeys:own",
@@ -985,7 +987,8 @@ KEY_MAX_DAYS = 365
# Scopes a key may never carry, whatever the owner holds. Minting keys from a
# key is a loop; the rest are owner powers that belong to a person at a screen.
KEY_UNSCOPABLE = {"account:self", "apikeys:own", "api:docs", "history:read", "people:manage",
"secrets:rotate", "people:invite_leader", "people:invite_admin"}
"secrets:rotate", "people:invite_leader", "people:invite_admin",
"roster:write"} # minors' names never ride a script key
def scopable_caps(person):