feat(program): admin edit/delete program form responses, protecc program form responses & program offers - #1024
Merged
Merged
Conversation
japsu
marked this pull request as ready for review
September 1, 2026 21:35
Remove the FORMS-only guard from deleteSurveyResponses so it also works for program_v2 surveys; CBAC already keys off survey.app generically. Change Involvement.response to SET_NULL so deleting a response no longer cascade-deletes the Involvement it produced. Wire up the existing bulk-delete UI on the program-forms response list page, generalizing the redirect/revalidate path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…etail page Add an edit page for individual program form responses (mirroring the regular survey and program offer edit pages) and wire up canEdit/canDelete action buttons on the detail page, which previously queried these fields but rendered no actions at all. Generalize the shared edit-response server action to take an explicit basePath so it redirects/revalidates correctly for program-forms as well as regular survey routes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sponses Add ProgramV2EventMeta.protect_responses (default True), settable from the program admin preferences page, to guard against a repeat of an incident where a concom member accidentally deleted all program offers during an event. Unlike Survey.protect_responses (per-survey, used by regular forms), this new switch is event-wide since program offers and program form responses don't currently expose a per-survey toggle. Reconcile the long-standing split between Survey.can_responses_be_deleted_by and Workflow.response_can_be_deleted_by (TODO #714) by having both delegate to a new Workflow.responses_can_be_deleted_by, with Workflow.protect_responses overridden per app: the base implementation reads Survey.protect_responses (forms), while ProgramOfferWorkflow and ProgramHostInvitationWorkflow read the new event-wide switch (program offers, program form responses). Also apply the switch to ProgramV2EventMeta.can_program_offers_be_deleted_by (bulk program offer deletion) and to CancelProgramOffer's DELETE resolution, which previously bypassed protection entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An independent review of the branch found real regressions from changing Involvement.response to SET_NULL: - Involvement.profile_field_selector/get_title/admin_link crashed (NotImplementedError/TypeError) for PROGRAM_OFFER and SURVEY_RESPONSE involvements whose response had been deleted, taking down the People admin views. These now degrade gracefully instead. - DeleteProgramOffers still had a comment/no-op update() written for the old CASCADE behavior; corrected to reflect that involvements now survive deletion (pending a future Involvement expiration feature) instead of being cascade-removed. - DeleteSurveyResponses' old-version cleanup filtered current_responses (which requires superseded_by=None) by superseded_by, an impossible combination, and ran after the SET_NULL from deleting the current version had already cleared it anyway — so old versions were never actually deleted. Fixed by deleting old versions first, via all_responses. - ProgramOfferWorkflow/ProgramHostInvitationWorkflow.protect_responses failed open (unprotected) when an event has no ProgramV2EventMeta; flipped to fail closed, consistent with the new toggle's safety-first default. - The program-forms response list page checked Survey.protect_responses (always false for PROGRAM app, irrelevant since the event-wide switch governs deletion there) to explain why deletion was disabled, so the toggle was practically undiscoverable from that page. Now checks the event-wide switch and points at the program preferences page. - Stale docstring on canRemoveResponses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sesBeDeleted enum Replace the various boolean-returning permission checks (Survey.can_responses_be_deleted_by, Workflow.responses_can_be_deleted_by/ response_can_be_deleted_by, ProgramV2EventMeta.can_program_offers_be_deleted_by) with a single CanResponsesBeDeleted enum (YES, NO_PROTECTED, NO_OLD_VERSION, NO_UNAUTHORIZED) computed once at the source. This pushes the reason a deletion is disallowed right next to the logic that decides it, instead of having callers (mutations, GraphQL boolean resolvers, frontend pages) separately re-derive "why not" and risk disagreeing with the check itself -- which is exactly the class of bug fixed for program-forms responses in the previous commit. The enum overrides __bool__ to agree with `== YES`, so `if not status:` keeps working correctly for existing boolean-shaped call sites without requiring every one of them to spell out the comparison, while GraphQL `canRemoveResponses`/`canDelete`/`canDeleteProgramOffers` boolean fields are now explicitly derived via `.can_delete` from the same computation. New `responsesDeletionStatus`/`programOffersDeletionStatus` GraphQL fields expose the enum itself; the two response-list pages that show a "why can't I delete this" message now switch on it directly instead of separately inferring the reason from `protectResponses`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…leted Replace the nested if/else that mapped responsesDeletionStatus to a human-readable reason with direct indexing into a new deleteVisibleResponses.reasons translation object keyed by the enum's members, in both the regular survey and program-forms response list pages. The program-forms page overrides just the NO_PROTECTED entry with its event-wide-toggle-specific message, reusing the rest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
japsu
force-pushed
the
feat/delete-program-form-responses
branch
from
September 2, 2026 19:38
dce87b5 to
8d966d1
Compare
…form responses view (there are no anonymous program forms)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
deleteSurveyResponsesmutation — previously restricted to regular FORMS surveys.Involvement.responseis nowSET_NULLinstead ofCASCADE, so an Involvement survives deletion of the response that produced it (expiration for stale Involvements is a separate future feature).canEdit/canDeletebut rendered no actions.ProgramV2EventMeta.protect_responses, defaults to on) on the program preferences page, in response to an incident where a concom member accidentally deleted all program offers during a live event.TODO #714) betweenSurvey.can_responses_be_deleted_byandWorkflow.response_can_be_deleted_by, which could give inconsistent answers — both now delegate toWorkflow.responses_can_be_deleted_by/protect_responses, overridden per app (Forms:Survey.protect_responses; program offers/program forms: the new event-wide switch). This also closes a gap whereCancelProgramOffer's DELETE resolution bypassed protection entirely.ProgramV2EventMeta, and undiscoverable protection messaging on the program-forms response list.Test plan
docker compose -f docker-compose.test.yml run --rm test— 169 passedprek run --all-fileson touched files — cleannpm run build/npm run lintinkompassi-v2-frontend— cleanfixes #714
fixes #920
fixes #1005
improves #1020
causes #1025