diff --git a/lib/paranoia.rb b/lib/paranoia.rb index 1336d6eb..a631a029 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -2,6 +2,7 @@ module Paranoia @@default_sentinel_value = nil + @@default_dependent_recovery_window = 120 # Change default_sentinel_value in a rails initilizer def self.default_sentinel_value=(val) @@ -12,6 +13,10 @@ def self.default_sentinel_value @@default_sentinel_value end + def self.default_dependent_recovery_window + @@default_dependent_recovery_window + end + def self.included(klazz) klazz.extend Query klazz.extend Callbacks @@ -66,7 +71,7 @@ def self.extended(klazz) def destroy transaction do run_callbacks(:destroy) do - result = touch_paranoia_column + result = touch_paranoia_column unless destroyed? if result && ActiveRecord::VERSION::STRING >= '4.2' each_counter_cached_associations do |association| foreign_key = association.reflection.foreign_key.to_sym @@ -87,16 +92,20 @@ def delete end def restore!(opts = {}) + opts.merge!(:recovery_window => paranoia_dependent_recovery_window) self.class.transaction do run_callbacks(:restore) do # Fixes a bug where the build would error because attributes were frozen. # This only happened on Rails versions earlier than 4.1. noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") + deleted_at = send(paranoia_column) if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen write_attribute paranoia_column, paranoia_sentinel_value update_column paranoia_column, paranoia_sentinel_value end - restore_associated_records if opts[:recursive] + if opts[:recursive] + restore_associated_records(deleted_at, opts[:recovery_window]) + end end end @@ -125,7 +134,7 @@ def touch_paranoia_column # restore associated records that have been soft deleted when # we called #destroy - def restore_associated_records + def restore_associated_records(deleted_at, window) destroyed_associations = self.class.reflect_on_all_associations.select do |association| association.options[:dependent] == :destroy end @@ -136,9 +145,12 @@ def restore_associated_records unless association_data.nil? if association_data.paranoid? if association.collection? - association_data.only_deleted.each { |record| record.restore(:recursive => true) } + x = association_data.only_deleted. + where("#{association.quoted_table_name}.#{paranoia_column} < ?", deleted_at + window). + where("#{association.quoted_table_name}.#{paranoia_column} > ?", deleted_at - window). + each { |record| record.restore(:recursive => true) } else - association_data.restore(:recursive => true) + association_data.restore(:recursive => true, :recovery_window => window) end end end @@ -194,7 +206,9 @@ def really_destroy! end include Paranoia - class_attribute :paranoia_column, :paranoia_sentinel_value + class_attribute :paranoia_column, :paranoia_sentinel_value, :paranoia_dependent_recovery_window + + self.paranoia_dependent_recovery_window = options[:dependent_recovery_window] || Paranoia.default_dependent_recovery_window self.paranoia_column = (options[:column] || :deleted_at).to_s self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index 1a5e4267..8a0c8b92 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -55,6 +55,19 @@ def setup end end + def with_stubbed_current_time(value, &block) + metaclass = Time.instance_eval{ class << self; self; end } + metaclass.send(:alias_method, :__original_time_now, :now) + metaclass.send(:define_method, :now){ value } + begin + block.call + ensure + metaclass.send(:undef_method, :now) + metaclass.send(:alias_method, :now, :__original_time_now) + metaclass.send(:undef_method, :__original_time_now) + end + end + def test_plain_model_class_is_not_paranoid assert_equal false, PlainModel.paranoid? end @@ -490,6 +503,7 @@ def test_restore_with_associations parent = ParentModel.create first_child = parent.very_related_models.create second_child = parent.non_paranoid_models.create + third_child = parent.very_related_models.create parent.destroy assert_equal false, parent.deleted_at.nil? @@ -512,6 +526,18 @@ def test_restore_with_associations assert_equal true, parent.reload.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? + + with_stubbed_current_time(Time.at(0)) do + first_child.destroy + end + with_stubbed_current_time(Time.at(3600)) do + parent.destroy + end + ParentModel.restore(parent.id, :recursive => true, :recovery_window => 5.minute) + assert_equal true, parent.reload.deleted_at.nil? + assert_equal false, first_child.reload.deleted_at.nil? + assert_equal true, second_child.destroyed? + assert_equal true, third_child.reload.deleted_at.nil? end # regression tests for #118