From ba6dde750248c40cff2bd6d79a3af1daf8ad63d2 Mon Sep 17 00:00:00 2001 From: Mohammed Nasser <135416851+mohammednasser-32@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:05:41 +0300 Subject: [PATCH] Handle #delete_all (#566) * handle #delete_all * add documentation * remove rails-edge for now --------- Co-authored-by: Mathieu Jobin <99191+mathieujobin@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- README.md | 15 ++++++++ lib/paranoia.rb | 55 +++++++++++++++++++---------- test/paranoia_test.rb | 70 +++++++++++++++++++++++++++++++++++-- 4 files changed, 120 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66c036d7..44ca221f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - rails: ["edge", "~> 7.2.0", "~> 7.1.0", "~> 7.0.0", "~> 6.1.0"] + rails: ["~> 7.2.0", "~> 7.1.0", "~> 7.0.0", "~> 6.1.0"] ruby: ["3.3","3.2", "3.1", "3.0", "2.7"] exclude: - rails: "~> 7.2.0" diff --git a/README.md b/README.md index cf898b8f..a55fc9ca 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,21 @@ end # => NoMethodError: undefined method `with_deleted' for # ``` +#### delete_all: + +The gem supports `delete_all` method, however it is disabled by default, to enabled add this in your `environment` file + +``` ruby +Paranoia.delete_all_enabled = true +``` +alternatively, you can enable/disable it for specific models as follow: + +``` ruby +class User < ActiveRecord::Base + acts_as_paranoid(delete_all_enabled: true) +end +``` + ## Acts As Paranoid Migration You can replace the older `acts_as_paranoid` methods as follows: diff --git a/lib/paranoia.rb b/lib/paranoia.rb index b69b4402..1b15bf79 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -6,15 +6,11 @@ end module Paranoia - @@default_sentinel_value = nil - # Change default_sentinel_value in a rails initializer - def self.default_sentinel_value=(val) - @@default_sentinel_value = val - end - - def self.default_sentinel_value - @@default_sentinel_value + class << self + # Change default values in a rails initializer + attr_accessor :default_sentinel_value, + :delete_all_enabled end def self.included(klazz) @@ -58,6 +54,16 @@ def restore(id_or_ids, opts = {}) end ids.map { |id| only_deleted.find(id).restore!(opts) } end + + def paranoia_destroy_attributes + { + paranoia_column => current_time_from_proper_timezone + }.merge(timestamp_attributes_with_current_time) + end + + def timestamp_attributes_with_current_time + timestamp_attributes_for_update_in_model.each_with_object({}) { |attr,hash| hash[attr] = current_time_from_proper_timezone } + end end def paranoia_destroy @@ -200,18 +206,10 @@ def each_counter_cached_associations def paranoia_restore_attributes { paranoia_column => paranoia_sentinel_value - }.merge(timestamp_attributes_with_current_time) + }.merge(self.class.timestamp_attributes_with_current_time) end - def paranoia_destroy_attributes - { - paranoia_column => current_time_from_proper_timezone - }.merge(timestamp_attributes_with_current_time) - end - - def timestamp_attributes_with_current_time - timestamp_attributes_for_update_in_model.each_with_object({}) { |attr,hash| hash[attr] = current_time_from_proper_timezone } - end + delegate :paranoia_destroy_attributes, to: 'self.class' def paranoia_find_has_one_target(association) association_foreign_key = association.options[:through].present? ? association.klass.primary_key : association.foreign_key @@ -262,6 +260,14 @@ def restore_associated_records(recovery_window_range = nil) end end +module Paranoia::Relation + def paranoia_delete_all + update_all(klass.paranoia_destroy_attributes) + end + + alias_method :delete_all, :paranoia_delete_all +end + ActiveSupport.on_load(:active_record) do class ActiveRecord::Base def self.acts_as_paranoid(options={}) @@ -276,9 +282,10 @@ def self.acts_as_paranoid(options={}) alias_method :really_destroyed?, :destroyed? alias_method :really_delete, :delete alias_method :destroy_without_paranoia, :destroy + class << self; delegate :really_delete_all, to: :all end include Paranoia - class_attribute :paranoia_column, :paranoia_sentinel_value + class_attribute :paranoia_column, :paranoia_sentinel_value, :delete_all_enabled self.paranoia_column = (options[:column] || :deleted_at).to_s self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } @@ -297,6 +304,16 @@ class << self; alias_method :without_deleted, :paranoia_scope end after_restore { self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) } + + self.delete_all_enabled = options[:delete_all_enabled] || Paranoia.delete_all_enabled + + if self.delete_all_enabled + "#{self}::ActiveRecord_Relation".constantize.class_eval do + alias_method :really_delete_all, :delete_all + + include Paranoia::Relation + end + end end # Please do not use this method in production. diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index 14be4d0e..13069c2a 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -1255,6 +1255,71 @@ def test_has_one_with_scope_not_restored assert_equal 1, ParanoidHasOneWithScope.count # gamma deleted end + def test_delete_all_disabled_by_default + assert_nil ParanoidModel.delete_all_enabled + + (0...3).each{ ParanoidModel.create } + assert_equal 3, ParanoidModel.count + ParanoidModel.delete_all + assert_equal 0, ParanoidModel.count + assert_equal 0, ParanoidModel.unscoped.count + end + + def test_delete_all_called_on_class + assert Employee.delete_all_enabled + + (0...3).each{ Employee.create } + assert_equal 3, Employee.count + Employee.delete_all + assert_equal 0, Employee.count + assert_equal 3, Employee.unscoped.count + end + + def test_delete_all_called_on_relation + assert Employee.delete_all_enabled + + (0...3).each{ Employee.create } + assert_equal 3, Employee.count + Employee.where(id: 1).delete_all + assert_equal 2, Employee.count + assert_equal 3, Employee.unscoped.count + end + + def test_really_delete_all_called_on_class + assert Employee.delete_all_enabled + + (0...3).each{ Employee.create } + assert_equal 3, Employee.count + Employee.really_delete_all + assert_equal 0, Employee.count + assert_equal 0, Employee.unscoped.count + end + + def test_delete_all_called_on_relation + assert Employee.delete_all_enabled + + (0...3).each{ Employee.create } + assert_equal 3, Employee.count + Employee.where(id: 1).really_delete_all + assert_equal 2, Employee.count + assert_equal 2, Employee.unscoped.count + end + + def test_update_has_many_through_relation_delete_associations + employer = Employer.create + employee1 = Employee.create + employee2 = Employee.create + job = Job.create :employer => employer, :employee => employee1 + + assert_equal 1, employer.jobs.count + assert_equal 1, employer.jobs.with_deleted.count + + employer.update(employee_ids: [employee2.id]) + + assert_equal 1, employer.jobs.count + assert_equal 2, employer.jobs.with_deleted.count + end + private def get_featureful_model FeaturefulModel.new(:name => "not empty") @@ -1418,16 +1483,17 @@ class Employer < ActiveRecord::Base acts_as_paranoid validates_uniqueness_of :name has_many :jobs - has_many :employees, :through => :jobs + has_many :employees, :through => :jobs, dependent: :destroy end class Employee < ActiveRecord::Base - acts_as_paranoid + acts_as_paranoid(delete_all_enabled: true) has_many :jobs has_many :employers, :through => :jobs end class Job < ActiveRecord::Base + acts_as_paranoid(delete_all_enabled: true) acts_as_paranoid belongs_to :employer belongs_to :employee