Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recovery window #229

Closed
wants to merge 1 commit into from
Closed
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
25 changes: 19 additions & 6 deletions lib/paranoia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -194,10 +206,11 @@ 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_column = (options[:column] || :deleted_at).to_s
self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value }
self.paranoia_dependent_recovery_window = options[:dependent_recovery_window] || Paranoia.default_dependent_recovery_window
def self.paranoia_scope
where(paranoia_column => paranoia_sentinel_value)
end
Expand Down
26 changes: 26 additions & 0 deletions test/paranoia_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand All @@ -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
Expand Down