HOLD: Add archive and deletion options for people#1686
Conversation
AWBW carries test data on production that touches many records, and there was no safe way to remove people or to wind down an account. This adds a graduated set of actions so admins can pick the least destructive one that fits: - Archive (default): soft-deletes the person and their user via the discard gem, blocking login and hiding them from default listings while staying fully reversible. Chosen over paranoia because it does not override destroy or add a default scope, so existing hard-delete cascades keep working unchanged. - Delete: hard-deletes a person plus their user (and the user's ahoy visits/events) only when nothing of substance is attached. - Permanently delete (?admin=true): purges the person, user, and all associated data for cleaning test records off production; guarded by a browser confirmation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| # so none are left orphaned. | ||
| def purge_ahoy_records(user) | ||
| visit_ids = Ahoy::Visit.where(user_id: user.id).pluck(:id) | ||
| Ahoy::Event.where(user_id: user.id).or(Ahoy::Event.where(visit_id: visit_ids)).delete_all |
There was a problem hiding this comment.
🤖 From Claude: Ahoy events are deleted by both user_id and parent visit_id so events on the user's visits that were never attributed to a user (e.g. pre-login) are not left orphaned.
| record.user.present? || | ||
| record.affiliations.exists? || | ||
| record.stories_as_spotlighted_facilitator.exists? | ||
| def has_significant_associated_data? |
There was a problem hiding this comment.
🤖 From Claude: A bare user account is intentionally not "significant" here — that's exactly the "person only has a user" case we want plain Delete to allow. Affiliations/registrations/scholarships/etc. still block it.
| @@ -118,7 +120,11 @@ def remote_search_label | |||
| end | |||
|
|
|||
| def active_for_authentication? | |||
There was a problem hiding this comment.
🤖 From Claude: Discarded (archived) users are blocked from authenticating here in addition to the existing inactive check, so archiving an account immediately revokes login.
| def destroy | ||
| authorize! @person | ||
| @person.destroy | ||
| if ActiveModel::Type::Boolean.new.cast(params[:admin]) |
There was a problem hiding this comment.
🤖 From Claude: ?admin=true selects the full-purge path (person + user + all associated data) and authorizes against full_destroy?; the UI gates it behind a browser confirmation.
What is the goal of this PR and why is this important?
How did you approach the change?
Soft-delete via the
discardgem (notparanoia):discardaddsdiscarded_at+kept/discardedscopes without overridingdestroyor installing adefault_scope, so the app's existingdependent: :destroyhard-delete cascades keep working unchanged.paranoia's own README marks it "not recommended for new projects."Three operations, gated by
PersonPolicy:?admin=true)turbo_confirmDetails:
Discard::ModelonPersonandUser;User#active_for_authentication?+inactive_messageblock discarded users;has_accessscoped.kept.PersonArchivalService(archive!/restore!) andPersonDeletionService(destroy_with_user!/full_destroy!). Full delete nullifies spotlighting stories (which userestrict_with_error) so the destroy isn't blocked, and purges ahoy records byuser_idand parent visit so none are orphaned.PersonPolicy:archive?,full_destroy?(any admin),destroy?now permits delete when only a user exists (has_significant_associated_data?= affiliations, spotlighted stories, registrations, event-staff, scholarships, grants, form submissions);relation_scopefilters.kept.archive/unarchivemember routes;destroybranches onadmin=true. People & users indexes hide archived by default with a?archived=truetoggle.UI Testing Checklist
Anything else to add?
discarded_attopeopleandusers(reversible up/down).🤖 Generated with Claude Code