Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ class User < ApplicationRecord
accepts_nested_attributes_for :user_forms
accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: proc { |attrs| attrs["body"].blank? }

# Tables with a RESTRICT foreign key to users that block destruction.
# bookmarks and user_forms are omitted: they're removed via dependent: :destroy.
OWNED_RECORD_REFERENCES = {
Banner => %i[created_by_id updated_by_id],
Comment => %i[created_by_id updated_by_id],
CommunityNews => %i[author_id created_by_id updated_by_id],
Event => %i[created_by_id],
Notification => %i[sender_id],
Person => %i[created_by_id updated_by_id],
Report => %i[created_by_id],
Resource => %i[created_by_id],
Story => %i[created_by_id updated_by_id],
StoryIdea => %i[created_by_id updated_by_id],
Affiliation => %i[user_id],
User => %i[created_by_id updated_by_id],
Workshop => %i[created_by_id],
WorkshopIdea => %i[created_by_id updated_by_id],
WorkshopLog => %i[created_by_id],
WorkshopVariation => %i[created_by_id],
WorkshopVariationIdea => %i[created_by_id updated_by_id]
}.freeze

# Legacy tables (no ActiveRecord model) with a RESTRICT foreign key to users.
LEGACY_OWNED_RECORD_REFERENCES = {
"user_permissions" => "user_id",
"blazer_audits" => "user_id",
"blazer_checks" => "creator_id",
"blazer_dashboards" => "creator_id",
"blazer_queries" => "creator_id"
}.freeze

before_validation :strip_whitespace

Expand Down Expand Up @@ -178,16 +208,11 @@ def organization_workshop_logs(date, windows_type, organization_id)
end
end

# A user can only be destroyed when nothing references them via a RESTRICT
# foreign key. We check every such reference so the UI reflects deletability
# accurately, rather than relying on the controller's InvalidForeignKey rescue.
def deletable?
!reports.exists? &&
!workshop_logs.exists? &&
!resources.exists? &&
!workshops.exists? &&
!stories_as_creator.exists? &&
!story_ideas_as_creator.exists? &&
!workshop_ideas_as_creator.exists? &&
!workshop_variations_as_creator.exists? &&
!workshop_variation_ideas_creator.exists?
!owns_model_records? && !owns_legacy_records?
end

def name
Expand Down Expand Up @@ -268,6 +293,25 @@ def person_id_must_be_present_if_previously_set
errors.add(:person_id, "cannot be removed once set")
end

def owns_model_records?
OWNED_RECORD_REFERENCES.any? do |model, columns|
columns.any? { |column| model.where(column => id).exists? }
end
end

def owns_legacy_records?
LEGACY_OWNED_RECORD_REFERENCES.any? do |table, column|
legacy_reference_exists?(table, column)
end
end

def legacy_reference_exists?(table, column)
self.class.connection.select_value(
ActiveRecord::Base.sanitize_sql_array(
[ "SELECT 1 FROM #{table} WHERE #{column} = ? LIMIT 1", id ]
)
).present?
end

def after_confirmation
super
Expand Down
34 changes: 34 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,40 @@
resource = create(:resource)
expect(resource.created_by.deletable?).to be false
end

it "returns false when user created a banner" do
banner = create(:banner)
expect(banner.created_by.deletable?).to be false
end

it "returns false when user authored community news" do
news = create(:community_news)
expect(news.author.deletable?).to be false
end

it "returns false when user created an event" do
event = create(:event)
expect(event.created_by.deletable?).to be false
end

it "returns false when user created a person" do
person = create(:person)
expect(person.created_by.deletable?).to be false
end

it "returns false when user authored a comment" do
user = create(:user)
create(:comment, created_by: user)
expect(user.deletable?).to be false
end

it "returns false when user is referenced by a legacy table" do
user = create(:user)
User.connection.execute(
"INSERT INTO user_permissions (user_id, created_at, updated_at) VALUES (#{user.id}, NOW(), NOW())"
)
expect(user.deletable?).to be false
end
end

describe '.search_by_params' do
Expand Down