From aefbaa6f01bf26b31b0197994b3de0c4b4b9adfd Mon Sep 17 00:00:00 2001 From: maebeale Date: Mon, 3 Aug 2026 21:47:02 -0400 Subject: [PATCH 1/4] Block deleting people with financial records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A person who was a payer, scholarship recipient, or grant donor could still be deleted whenever they had no user, affiliations, or authored content — the destroy gate ignored money entirely — cascade-destroying or orphaning that financial history. Fold the checks into a Person#deletable? source of truth, have the policy delegate to it, and surface a "Can't be deleted — …" reason on the edit form (mirroring the EventRegistration pattern) instead of silently hiding the button. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/person_decorator.rb | 14 ++++++ app/models/person.rb | 28 ++++++++++++ app/policies/person_policy.rb | 13 +----- app/views/people/_form.html.erb | 5 +++ spec/decorators/person_decorator_spec.rb | 24 ++++++++++ spec/models/person_spec.rb | 31 +++++++++++++ spec/policies/person_policy_spec.rb | 56 ++++++++++++++++++++++++ 7 files changed, 159 insertions(+), 12 deletions(-) diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 09bb4d5421..3cc6ec56ce 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -83,6 +83,20 @@ def facilitator_since_date end end + # Human-readable explanation of why the Delete button is unavailable, or nil + # when the person is deletable. Mirrors the model's deletable? checks so the + # edit form can tell an admin exactly what's holding the record in place. + def deletion_blocked_reason + return if deletable? + + reasons = [] + reasons << "a linked user account" if user.present? + reasons << "organization affiliations" if affiliations.exists? + reasons << "authored content (stories, workshops, news, or resources)" if authored_content? + reasons << "financial records (payments, scholarships, or grants)" if financial_records? + "Can't be deleted — this person has #{reasons.to_sentence}." + end + def affiliated_since_date # Compute in Ruby from the (eager-loaded) association so list pages that # preload affiliations don't fire a MIN(start_date) query per row. diff --git a/app/models/person.rb b/app/models/person.rb index 0d2032be50..5d9dcb243f 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -38,6 +38,7 @@ class Person < ApplicationRecord has_many :event_staffs, dependent: :destroy has_many :scholarships, foreign_key: :recipient_id, dependent: :destroy has_many :grants, as: :donor, dependent: :destroy + has_many :payments, dependent: :restrict_with_error has_many :events, through: :event_registrations has_many :staffed_events, through: :event_staffs, source: :event has_many :categories, through: :categorizable_items @@ -327,6 +328,33 @@ def age_range_items_ordered age_range_categorizable_items.sort_by { |item| [ item.category&.position || 0, item.category&.name.to_s ] } end + # True only when removing this person would neither cascade-destroy nor orphan + # any associated record. Gates PersonPolicy#destroy?; the decorator's + # deletion_blocked_reason turns a false result into a human explanation. + def deletable? + user.blank? && + !affiliations.exists? && + !authored_content? && + !financial_records? + end + + # Content this person is credited on. These associations are + # dependent: :restrict_with_error, so they'd have to be reassigned first. + def authored_content? + stories_as_spotlighted_facilitator.exists? || + stories_as_author.exists? || + workshop_variations_as_author.exists? || + workshops_as_author.exists? || + community_news_as_author.exists? || + resources_as_author.exists? + end + + # Money tied to this person as payer, scholarship recipient, or grant donor. + # Deleting the person would erase this financial history, so it blocks deletion. + def financial_records? + payments.exists? || scholarships.exists? || grants.exists? + end + private # Count the in-memory set (not a DB query): nested attributes build the items in diff --git a/app/policies/person_policy.rb b/app/policies/person_policy.rb index e6d21f8cf7..3fb8242ed6 100644 --- a/app/policies/person_policy.rb +++ b/app/policies/person_policy.rb @@ -30,7 +30,7 @@ def update? end def destroy? - admin? && record.persisted? && !has_associated_data? + admin? && record.persisted? && record.deletable? end def search? @@ -51,15 +51,4 @@ def owner? return false unless authenticated? record.user == user end - - def has_associated_data? - record.user.present? || - record.affiliations.exists? || - record.stories_as_spotlighted_facilitator.exists? || - record.stories_as_author.exists? || - record.workshop_variations_as_author.exists? || - record.workshops_as_author.exists? || - record.community_news_as_author.exists? || - record.resources_as_author.exists? - end end diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index f79f3f5ceb..6fb5408930 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -566,6 +566,11 @@ <% if allowed_to?(:destroy?, f.object) %> <%= link_to "Delete", @person, class: "btn btn-danger-outline", data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete?" } %> + <% elsif f.object.persisted? %> + + + <%= f.object.decorate.deletion_blocked_reason %> + <% end %> <%= link_to "Cancel", people_path, class: "btn btn-secondary-outline", diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index 307b06189b..e2ab9670ab 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -52,4 +52,28 @@ expect(person.decorate.affiliated_since_date).to be_nil end end + + describe "#deletion_blocked_reason" do + it "is nil when the person has no blocking associations" do + person = create(:person, user: nil) + + expect(person.decorate.deletion_blocked_reason).to be_nil + end + + it "names financial records when the person has payments" do + person = create(:person, user: nil) + create(:payment, person: person) + + expect(person.decorate.deletion_blocked_reason) + .to eq("Can't be deleted — this person has financial records (payments, scholarships, or grants).") + end + + it "names a linked user account and financial records together" do + person = create(:person) + create(:scholarship, recipient: person) + + expect(person.decorate.deletion_blocked_reason) + .to eq("Can't be deleted — this person has a linked user account and financial records (payments, scholarships, or grants).") + end + end end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index 2ba7b56755..49c8b1ecce 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -523,4 +523,35 @@ def answer(identifier, value) expect(person.completed_facilitator_trainings).to contain_exactly(training) end end + + describe "#deletable?" do + it "is true for a person with no user, affiliations, content, or financial records" do + expect(create(:person, user: nil)).to be_deletable + end + + it "is false when the person has a linked user account" do + expect(create(:person)).not_to be_deletable + end + + it "is false when the person has payments" do + person = create(:person, user: nil) + create(:payment, person: person) + + expect(person).not_to be_deletable + end + + it "is false when the person is a scholarship recipient" do + person = create(:person, user: nil) + create(:scholarship, recipient: person) + + expect(person).not_to be_deletable + end + + it "is false when the person is a grant donor" do + person = create(:person, user: nil) + create(:grant, donor: person) + + expect(person).not_to be_deletable + end + end end diff --git a/spec/policies/person_policy_spec.rb b/spec/policies/person_policy_spec.rb index 70b4ccb6fc..439c6fd51f 100644 --- a/spec/policies/person_policy_spec.rb +++ b/spec/policies/person_policy_spec.rb @@ -212,6 +212,62 @@ def policy_for(record: nil, user:) expect(policy).not_to be_allowed_to(:destroy?) end end + + context "when person has payments" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + create(:payment, person: person) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + + context "when person has scholarships" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + create(:scholarship, recipient: person) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + + context "when person has grants" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + create(:grant, donor: person) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + + context "when person has no associated data" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + it "is allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).to be_allowed_to(:destroy?) + end + end end describe "relation_scope" do From e30bae6669ac70fe461057d3a6d99b55efaf001e Mon Sep 17 00:00:00 2001 From: maebeale Date: Mon, 3 Aug 2026 21:56:07 -0400 Subject: [PATCH 2/4] Block on paid registrations + event staffing; name each blocker Extend the person delete gate to event registrations that carry payments (allocations) and event staff assignments, and refactor the checks into a single Person#deletion_blockers key list. The edit-form notice now names the actual kinds of records the person has ("...has authored stories, payments, and grants.") instead of lumping them under generic categories. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/person_decorator.rb | 33 +++++++++++------ app/models/person.rb | 45 ++++++++++++------------ spec/decorators/person_decorator_spec.rb | 18 +++++++--- spec/models/person_spec.rb | 15 ++++++++ spec/policies/person_policy_spec.rb | 16 +++++++++ 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 3cc6ec56ce..483f3b3b1b 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -83,18 +83,31 @@ def facilitator_since_date end end + # Label shown for each deletion-blocker key (see Person#deletion_blockers), so + # the "Can't be deleted" notice names the actual kinds of records the person + # has rather than a generic catch-all. + DELETION_BLOCKER_LABELS = { + user_account: "a user account", + affiliations: "organization affiliations", + stories: "authored stories", + workshops: "authored workshops", + workshop_variations: "authored workshop variations", + community_news: "authored community news", + resources: "authored resources", + payments: "payments", + scholarships: "scholarships", + grants: "grants", + paid_event_registrations: "event registrations with payments", + event_staffing: "event staff assignments" + }.freeze + # Human-readable explanation of why the Delete button is unavailable, or nil - # when the person is deletable. Mirrors the model's deletable? checks so the - # edit form can tell an admin exactly what's holding the record in place. + # when the person is deletable — naming each kind of record that's blocking it. def deletion_blocked_reason - return if deletable? - - reasons = [] - reasons << "a linked user account" if user.present? - reasons << "organization affiliations" if affiliations.exists? - reasons << "authored content (stories, workshops, news, or resources)" if authored_content? - reasons << "financial records (payments, scholarships, or grants)" if financial_records? - "Can't be deleted — this person has #{reasons.to_sentence}." + labels = deletion_blockers.map { |key| DELETION_BLOCKER_LABELS.fetch(key) } + return if labels.empty? + + "Can't be deleted — this person has #{labels.to_sentence}." end def affiliated_since_date diff --git a/app/models/person.rb b/app/models/person.rb index 5d9dcb243f..53c67715c3 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -328,31 +328,30 @@ def age_range_items_ordered age_range_categorizable_items.sort_by { |item| [ item.category&.position || 0, item.category&.name.to_s ] } end - # True only when removing this person would neither cascade-destroy nor orphan - # any associated record. Gates PersonPolicy#destroy?; the decorator's - # deletion_blocked_reason turns a false result into a human explanation. - def deletable? - user.blank? && - !affiliations.exists? && - !authored_content? && - !financial_records? - end - - # Content this person is credited on. These associations are - # dependent: :restrict_with_error, so they'd have to be reassigned first. - def authored_content? - stories_as_spotlighted_facilitator.exists? || - stories_as_author.exists? || - workshop_variations_as_author.exists? || - workshops_as_author.exists? || - community_news_as_author.exists? || - resources_as_author.exists? + # Keys for each kind of record that deleting this person would cascade-destroy + # or orphan — the things worth keeping (identity, authored content, financial + # and event history), not disposable profile detail like addresses or tags. + # Empty means the person is safe to delete. Gates PersonPolicy#destroy?; the + # decorator maps each key to the label shown in the "Can't be deleted" notice. + def deletion_blockers + blockers = [] + blockers << :user_account if user.present? + blockers << :affiliations if affiliations.exists? + blockers << :stories if stories_as_author.exists? || stories_as_spotlighted_facilitator.exists? + blockers << :workshops if workshops_as_author.exists? + blockers << :workshop_variations if workshop_variations_as_author.exists? + blockers << :community_news if community_news_as_author.exists? + blockers << :resources if resources_as_author.exists? + blockers << :payments if payments.exists? + blockers << :scholarships if scholarships.exists? + blockers << :grants if grants.exists? + blockers << :paid_event_registrations if event_registrations.joins(:allocations).exists? + blockers << :event_staffing if event_staffs.exists? + blockers end - # Money tied to this person as payer, scholarship recipient, or grant donor. - # Deleting the person would erase this financial history, so it blocks deletion. - def financial_records? - payments.exists? || scholarships.exists? || grants.exists? + def deletable? + deletion_blockers.empty? end private diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index e2ab9670ab..d52f7f1c76 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -60,20 +60,30 @@ expect(person.decorate.deletion_blocked_reason).to be_nil end - it "names financial records when the person has payments" do + it "names payments when the person has them" do person = create(:person, user: nil) create(:payment, person: person) expect(person.decorate.deletion_blocked_reason) - .to eq("Can't be deleted — this person has financial records (payments, scholarships, or grants).") + .to eq("Can't be deleted — this person has payments.") end - it "names a linked user account and financial records together" do + it "names event registrations with payments" do + person = create(:person, user: nil) + registration = create(:event_registration, registrant: person) + create(:allocation, allocatable: registration) + + expect(person.decorate.deletion_blocked_reason) + .to eq("Can't be deleted — this person has event registrations with payments.") + end + + it "names each kind of blocking record the person actually has" do person = create(:person) create(:scholarship, recipient: person) + create(:grant, donor: person) expect(person.decorate.deletion_blocked_reason) - .to eq("Can't be deleted — this person has a linked user account and financial records (payments, scholarships, or grants).") + .to eq("Can't be deleted — this person has a user account, scholarships, and grants.") end end end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index 49c8b1ecce..938084d306 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -553,5 +553,20 @@ def answer(identifier, value) expect(person).not_to be_deletable end + + it "is false when the person has an event registration with a payment" do + person = create(:person, user: nil) + registration = create(:event_registration, registrant: person) + create(:allocation, allocatable: registration) + + expect(person).not_to be_deletable + end + + it "is true when the person has only an unpaid event registration" do + person = create(:person, user: nil) + create(:event_registration, registrant: person) + + expect(person).to be_deletable + end end end diff --git a/spec/policies/person_policy_spec.rb b/spec/policies/person_policy_spec.rb index 439c6fd51f..f87b463b9b 100644 --- a/spec/policies/person_policy_spec.rb +++ b/spec/policies/person_policy_spec.rb @@ -258,6 +258,22 @@ def policy_for(record: nil, user:) end end + context "when person has an event registration with a payment" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + registration = create(:event_registration, registrant: person) + create(:allocation, allocatable: registration) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + context "when person has no associated data" do let(:admin) { create(:user, :admin) } let(:person) { create(:person, user: nil) } From 994dc9c151fee076eda7a8d493e65370fe0b6e7d Mon Sep 17 00:00:00 2001 From: maebeale Date: Mon, 3 Aug 2026 22:02:10 -0400 Subject: [PATCH 3/4] Also block on any event registration + form submissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any event registration (paid or not) and any form submission now blocks person deletion, alongside event staffing — this history is worth keeping, and the Delete button is only for cleaning up records with no associations at all. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/person_decorator.rb | 5 ++-- app/models/person.rb | 3 +- spec/decorators/person_decorator_spec.rb | 8 +++--- spec/models/person_spec.rb | 18 ++++++++---- spec/policies/person_policy_spec.rb | 35 ++++++++++++++++++++++-- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 483f3b3b1b..5eaeede67f 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -97,8 +97,9 @@ def facilitator_since_date payments: "payments", scholarships: "scholarships", grants: "grants", - paid_event_registrations: "event registrations with payments", - event_staffing: "event staff assignments" + event_registrations: "event registrations", + event_staffing: "event staff assignments", + form_submissions: "form submissions" }.freeze # Human-readable explanation of why the Delete button is unavailable, or nil diff --git a/app/models/person.rb b/app/models/person.rb index 53c67715c3..1f975c5607 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -345,8 +345,9 @@ def deletion_blockers blockers << :payments if payments.exists? blockers << :scholarships if scholarships.exists? blockers << :grants if grants.exists? - blockers << :paid_event_registrations if event_registrations.joins(:allocations).exists? + blockers << :event_registrations if event_registrations.exists? blockers << :event_staffing if event_staffs.exists? + blockers << :form_submissions if form_submissions.exists? blockers end diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index d52f7f1c76..0154a84da2 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -68,13 +68,13 @@ .to eq("Can't be deleted — this person has payments.") end - it "names event registrations with payments" do + it "names event registrations and form submissions" do person = create(:person, user: nil) - registration = create(:event_registration, registrant: person) - create(:allocation, allocatable: registration) + create(:event_registration, registrant: person) + create(:form_submission, person: person) expect(person.decorate.deletion_blocked_reason) - .to eq("Can't be deleted — this person has event registrations with payments.") + .to eq("Can't be deleted — this person has event registrations and form submissions.") end it "names each kind of blocking record the person actually has" do diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index 938084d306..a43b53c67b 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -554,19 +554,25 @@ def answer(identifier, value) expect(person).not_to be_deletable end - it "is false when the person has an event registration with a payment" do + it "is false when the person has an event registration" do person = create(:person, user: nil) - registration = create(:event_registration, registrant: person) - create(:allocation, allocatable: registration) + create(:event_registration, registrant: person) expect(person).not_to be_deletable end - it "is true when the person has only an unpaid event registration" do + it "is false when the person has staffed an event" do person = create(:person, user: nil) - create(:event_registration, registrant: person) + create(:event_staff, person: person) + + expect(person).not_to be_deletable + end + + it "is false when the person has a form submission" do + person = create(:person, user: nil) + create(:form_submission, person: person) - expect(person).to be_deletable + expect(person).not_to be_deletable end end end diff --git a/spec/policies/person_policy_spec.rb b/spec/policies/person_policy_spec.rb index f87b463b9b..055f60e0ef 100644 --- a/spec/policies/person_policy_spec.rb +++ b/spec/policies/person_policy_spec.rb @@ -258,13 +258,42 @@ def policy_for(record: nil, user:) end end - context "when person has an event registration with a payment" do + context "when person has an event registration" do let(:admin) { create(:user, :admin) } let(:person) { create(:person, user: nil) } before do - registration = create(:event_registration, registrant: person) - create(:allocation, allocatable: registration) + create(:event_registration, registrant: person) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + + context "when person has staffed an event" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + create(:event_staff, person: person) + end + + it "is not allowed" do + policy = policy_for(record: person, user: admin) + + expect(policy).not_to be_allowed_to(:destroy?) + end + end + + context "when person has a form submission" do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person, user: nil) } + + before do + create(:form_submission, person: person) end it "is not allowed" do From 1ebda1a7074fbd8a66f3d073e2f1de7aa4dba3ba Mon Sep 17 00:00:00 2001 From: maebeale Date: Mon, 3 Aug 2026 22:21:42 -0400 Subject: [PATCH 4/4] Name user-authored workshop logs and ideas as blockers When a person's user authored workshop logs, story ideas, workshop ideas, or workshop variation ideas, name them in the "Can't be deleted" notice rather than only the generic user-account reason. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/person_decorator.rb | 4 ++++ app/models/person.rb | 8 +++++++- spec/decorators/person_decorator_spec.rb | 12 ++++++++++-- spec/models/person_spec.rb | 7 +++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 5eaeede67f..cc572d4c6a 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -88,6 +88,10 @@ def facilitator_since_date # has rather than a generic catch-all. DELETION_BLOCKER_LABELS = { user_account: "a user account", + workshop_logs: "authored workshop logs", + story_ideas: "authored story ideas", + workshop_ideas: "authored workshop ideas", + workshop_variation_ideas: "authored workshop variation ideas", affiliations: "organization affiliations", stories: "authored stories", workshops: "authored workshops", diff --git a/app/models/person.rb b/app/models/person.rb index 1f975c5607..7e07ff519c 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -335,7 +335,13 @@ def age_range_items_ordered # decorator maps each key to the label shown in the "Can't be deleted" notice. def deletion_blockers blockers = [] - blockers << :user_account if user.present? + if user.present? + blockers << :user_account + blockers << :workshop_logs if user.workshop_logs.exists? + blockers << :story_ideas if user.story_ideas_as_creator.exists? + blockers << :workshop_ideas if user.workshop_ideas_as_creator.exists? + blockers << :workshop_variation_ideas if user.workshop_variation_ideas_creator.exists? + end blockers << :affiliations if affiliations.exists? blockers << :stories if stories_as_author.exists? || stories_as_spotlighted_facilitator.exists? blockers << :workshops if workshops_as_author.exists? diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index 0154a84da2..f90e528381 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -77,13 +77,21 @@ .to eq("Can't be deleted — this person has event registrations and form submissions.") end - it "names each kind of blocking record the person actually has" do + it "names authored records owned through the person's user" do person = create(:person) + create(:workshop_log, created_by: person.user) + + expect(person.decorate.deletion_blocked_reason) + .to eq("Can't be deleted — this person has a user account and authored workshop logs.") + end + + it "names each kind of blocking record the person actually has" do + person = create(:person, user: nil) create(:scholarship, recipient: person) create(:grant, donor: person) expect(person.decorate.deletion_blocked_reason) - .to eq("Can't be deleted — this person has a user account, scholarships, and grants.") + .to eq("Can't be deleted — this person has scholarships and grants.") end end end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index a43b53c67b..0974748020 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -574,5 +574,12 @@ def answer(identifier, value) expect(person).not_to be_deletable end + + it "is false when the person's user authored workshop logs or ideas" do + person = create(:person) + create(:workshop_log, created_by: person.user) + + expect(person).not_to be_deletable + end end end