feat(surrenders): public rabbit surrender form + admin review queue #28

Merged
cpressland merged 2 commits from feat/surrender-form into main 2026-09-11 02:05:29 +01:00
Collaborator

Closes #19.

Summary

Adds a public, anonymous rabbit surrender form and an admin-only review
queue. Nothing is added to the live rabbit records until an admin
explicitly accepts a submission — mirrors the existing adopt.py/
adoptions.py public-form + admin-management split.

Public form (/surrender, unauthenticated)

  • Rabbit details + optional submitter contact — fully anonymous
    submission is supported.
  • Mandatory legal agreement checkbox: ownership transfers irrevocably on
    acceptance. Captures the acceptance timestamp + a version string
    (SURRENDER_AGREEMENT_VERSION) so wording can change later without
    losing what a given submitter actually agreed to.
  • Honeypot field (website, hidden off-screen) — a filled honeypot
    silently "succeeds" without writing anything to the database.
  • Creates a SurrenderSubmission row with status=pending. No Rabbit
    row is created at this point.

Admin queue (/surrenders, admin only)

  • List filterable by status (pending/accepted/rejected) with counts.
  • Detail page: full submitted info, submitter contact, agreement
    timestamp/version.
  • Accept: creates a real Rabbit (status=Resident, no hutch
    assigned — staff place it later from the Rabbits page), links back via
    rabbit_id, audit-logs both the rabbit creation and the status change.
  • Reject: sets status=rejected + optional admin note, no rabbit
    created, record kept.
  • Delete: hard delete, for spam/test junk.
  • All submission data retained indefinitely regardless of outcome, per
    the earlier design discussion on #19.

Data model

New SurrenderSubmission table — no migration needed, picked up by the
existing Base.metadata.create_all() on boot.

Nav + audit integration

  • "Surrenders" link added next to Adoptions in the admin nav.
  • Dashboard activity feed resolves surrender_submissions audit entries
    to the rabbit's name.

Testing

No local Postgres/Docker in this sandbox, so I wrote a throwaway smoke
test exercising the real FastAPI routers (public + admin) against an
in-memory SQLite engine — form load, missing-agreement rejection,
honeypot silently dropping a submission, a real submission creating a
pending row with no Rabbit, admin list/detail, accept creating an
unplaced Rabbit + audit logs, double-accept blocked, reject with a note,
and delete. All 10 checks passed.

Caught and fixed a real bug this way: escaped quotes nested inside an
onsubmit JS string aren't valid Jinja template syntax
(TemplateSyntaxError: unexpected char '\\'), which would have 500'd
the detail page for every pending submission. Simplified the confirm()
text instead of trying to interpolate the rabbit name into it (the name
is already shown in the page heading right above the button).

Please test against the live app and let me know if anything needs
adjusting.

Closes #19. ## Summary Adds a public, anonymous rabbit surrender form and an admin-only review queue. Nothing is added to the live rabbit records until an admin explicitly accepts a submission — mirrors the existing `adopt.py`/ `adoptions.py` public-form + admin-management split. ## Public form (`/surrender`, unauthenticated) - Rabbit details + optional submitter contact — fully anonymous submission is supported. - Mandatory legal agreement checkbox: ownership transfers irrevocably on acceptance. Captures the acceptance timestamp + a version string (`SURRENDER_AGREEMENT_VERSION`) so wording can change later without losing what a given submitter actually agreed to. - Honeypot field (`website`, hidden off-screen) — a filled honeypot silently "succeeds" without writing anything to the database. - Creates a `SurrenderSubmission` row with `status=pending`. **No Rabbit row is created at this point.** ## Admin queue (`/surrenders`, admin only) - List filterable by status (pending/accepted/rejected) with counts. - Detail page: full submitted info, submitter contact, agreement timestamp/version. - **Accept**: creates a real `Rabbit` (status=`Resident`, no hutch assigned — staff place it later from the Rabbits page), links back via `rabbit_id`, audit-logs both the rabbit creation and the status change. - **Reject**: sets `status=rejected` + optional admin note, no rabbit created, record kept. - **Delete**: hard delete, for spam/test junk. - All submission data retained indefinitely regardless of outcome, per the earlier design discussion on #19. ## Data model New `SurrenderSubmission` table — no migration needed, picked up by the existing `Base.metadata.create_all()` on boot. ## Nav + audit integration - "Surrenders" link added next to Adoptions in the admin nav. - Dashboard activity feed resolves `surrender_submissions` audit entries to the rabbit's name. ## Testing No local Postgres/Docker in this sandbox, so I wrote a throwaway smoke test exercising the real FastAPI routers (public + admin) against an in-memory SQLite engine — form load, missing-agreement rejection, honeypot silently dropping a submission, a real submission creating a pending row with no Rabbit, admin list/detail, accept creating an unplaced Rabbit + audit logs, double-accept blocked, reject with a note, and delete. All 10 checks passed. Caught and fixed a real bug this way: escaped quotes nested inside an `onsubmit` JS string aren't valid Jinja template syntax (`TemplateSyntaxError: unexpected char '\\'`), which would have 500'd the detail page for every pending submission. Simplified the confirm() text instead of trying to interpolate the rabbit name into it (the name is already shown in the page heading right above the button). Please test against the live app and let me know if anything needs adjusting.
Adds a public, anonymous 'surrender a rabbit' form and an admin-only
review queue, per the design in #19. Nothing is added to the live
rabbit records until an admin explicitly accepts a submission.

## Public form (/surrender, unauthenticated)
- Rabbit details (name, breed/colour, approx age, gender, neutered/
  spayed, health notes, reason), optional submitter contact (name/
  phone/email — submission works fully anonymously).
- Mandatory legal agreement checkbox with caveat text
  (SURRENDER_AGREEMENT_TEXT in models.py) stating ownership transfers
  irrevocably to Casa Del Bunnies on acceptance. Captures acceptance
  timestamp + SURRENDER_AGREEMENT_VERSION, so wording can change later
  without losing what a given submitter actually agreed to.
- Honeypot field ('website', hidden off-screen via CSS) for spam:
  a filled honeypot silently 'succeeds' without writing anything.
- On submit: creates a SurrenderSubmission row with status=pending.
  No Rabbit row is created at this point.

## Admin queue (/surrenders, admin only)
- List filterable by status (pending/accepted/rejected) with counts.
- Detail page shows full submitted info + submitter contact +
  agreement timestamp/version.
- Accept: creates a real Rabbit (status=Resident, no hutch assigned —
  staff place it later from the Rabbits page as normal), links
  rabbit_id back to the submission for traceability, audit-logs both
  the rabbit creation and the submission status change.
- Reject: sets status=rejected + optional admin note, no rabbit
  created, record kept (not deleted).
- Delete: hard delete for spam/test junk.
- All submission data (including contact info) is retained
  indefinitely regardless of outcome, per the earlier design
  discussion.

## Data model
New table SurrenderSubmission: rabbit fields, submitter contact
fields, agreement timestamp + version, status, admin_note, rabbit_id
FK (nullable, set on accept), reviewed_by_id/reviewed_at, timestamps.
No migration needed — new table picked up by the existing
Base.metadata.create_all() on boot.

## Nav + audit integration
- 'Surrenders' link added next to Adoptions in the admin nav section.
- Dashboard activity feed resolves surrender_submissions audit entries
  to the rabbit's name, same pattern as adoptions/health records.

## Testing
No local Postgres/Docker in the sandbox this was built in. Wrote a
throwaway smoke test exercising the real FastAPI routers (public +
admin) against an in-memory SQLite engine: form load, missing-
agreement rejection, honeypot silently dropping a submission, a real
submission creating a pending row with no Rabbit, the admin list/
detail pages, accept creating an unplaced Rabbit + audit logs,
double-accept being blocked, reject with an admin note, and delete.
All 10 checks passed. Caught and fixed a real Jinja bug this way (see
next commit) — escaped quotes inside a nested onsubmit JS string
aren't valid Jinja syntax; simplified the confirm() text instead.

Closes #19
surrenders/detail.html re-implemented its own success/error query-param
banner, but base.html's shared content block already renders the exact
same alert for every page (see the block content wrapper in base.html).
Result: accepting or rejecting a submission showed the same 'Accepted —
rabbit created' message twice, stacked on top of each other.

Caught this visually via a real browser screenshot of the accepted-state
detail page, not from the earlier router-level smoke test (which only
asserted the text was present, not that it appeared once). Removed the
redundant local success/error handling; base.html's already covers it.
Author
Collaborator

Screenshots

Rendered the branch locally (SQLite demo instance) and drove it with a real
headless Chrome to capture the actual pages end to end: public form -> fill
-> submit -> thank you -> admin queue -> admin detail -> accept -> outcome.

1. Public surrender form (blank)
blank form

2. Public form filled in
filled form

3. Thank-you page after submit
thank you

4. Admin surrenders queue (pending filter)
admin queue

5. Admin submission detail (pending, review buttons visible)
admin detail pending

6. After Accept — rabbit created, outcome shown
admin detail accepted

Bug found + fixed while capturing these

Screenshot 6 originally showed the "Accepted — rabbit created" success banner
twice, stacked on top of each other — surrenders/detail.html re-implemented
its own success/error query-param banner, not realising base.html's shared
content block already renders the identical alert for every page in the app.
The router-level smoke test in the PR description didn't catch this because it
only asserted the text was present, not that it appeared exactly once.

Fixed in the latest commit by removing the redundant local banner handling —
confirmed with a re-screenshot (image 6 above is from after the fix).

## Screenshots Rendered the branch locally (SQLite demo instance) and drove it with a real headless Chrome to capture the actual pages end to end: public form -> fill -> submit -> thank you -> admin queue -> admin detail -> accept -> outcome. **1. Public surrender form (blank)** ![blank form](https://git.cpressland.io/attachments/310649cf-fa03-45a3-a931-3c00b536db1f) **2. Public form filled in** ![filled form](https://git.cpressland.io/attachments/ae8ce667-96c7-4990-9120-c41820cb96f0) **3. Thank-you page after submit** ![thank you](https://git.cpressland.io/attachments/dda64194-68a2-432a-9d6b-ca2bfd8ee576) **4. Admin surrenders queue (pending filter)** ![admin queue](https://git.cpressland.io/attachments/7ea8f5c8-95a3-4eb8-abdd-ceef19a7e99b) **5. Admin submission detail (pending, review buttons visible)** ![admin detail pending](https://git.cpressland.io/attachments/cc2f9e23-d52f-4751-9350-bbc097f02e0b) **6. After Accept — rabbit created, outcome shown** ![admin detail accepted](https://git.cpressland.io/attachments/c44ddf01-c6bc-45be-9ae4-ae51e9001575) ## Bug found + fixed while capturing these Screenshot 6 originally showed the "Accepted — rabbit created" success banner **twice**, stacked on top of each other — `surrenders/detail.html` re-implemented its own success/error query-param banner, not realising `base.html`'s shared content block already renders the identical alert for every page in the app. The router-level smoke test in the PR description didn't catch this because it only asserted the text was present, not that it appeared exactly once. Fixed in the latest commit by removing the redundant local banner handling — confirmed with a re-screenshot (image 6 above is from *after* the fix).
cpressland deleted branch feat/surrender-form 2026-09-11 02:05:29 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
cpressland/rabbitdb!28
No description provided.