Skip to content

Commit

Permalink
Updated gem versions
Browse files Browse the repository at this point in the history
Renamed is_paranoid? to paranoid? and removed from Object class
Rewrote acts_as_paranoid to include/extend Instance/Class method modules instead of class_eval
Added support for the destroy callback chain w/ tests
  • Loading branch information
phene authored and goncalossilva committed Apr 1, 2011
1 parent bfb589e commit 6c7956f
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 126 deletions.
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ GEM
arel (1.0.1)
activesupport (~> 3.0.0)
builder (2.1.2)
i18n (0.4.1)
i18n (0.4.2)
rake (0.8.7)
sqlite3-ruby (1.3.2)
tzinfo (0.3.23)
tzinfo (0.3.25)

PLATFORMS
ruby
Expand Down
235 changes: 126 additions & 109 deletions lib/rails3_acts_as_paranoid.rb
Original file line number Diff line number Diff line change
@@ -1,151 +1,168 @@
require 'active_record'
require 'validations/uniqueness_without_deleted'

class Object
class << self
def is_paranoid?
false
end
end
end

module ActsAsParanoid

def paranoid?
self.included_modules.include?(InstanceMethods)
end

def validates_as_paranoid
extend ParanoidValidations::ClassMethods
end

def acts_as_paranoid(options = {})
raise ArgumentError, "Hash expected, got #{options.class.name}" if not options.is_a?(Hash) and not options.empty?

configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 5.minutes }
configuration.update(options) unless options.nil?

type = case configuration[:column_type]
when "time" then "Time.now"
when "boolean" then "true"
else
raise ArgumentError, "'time' or 'boolean' expected for :column_type option, got #{configuration[:column_type]}"

return if paranoid?

class << self
attr_accessor :paranoid_configuration, :paranoid_column_reference
end

@paranoid_configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 5.minutes }.merge(options)

column_reference = "#{self.table_name}.#{configuration[:column]}"
raise ArgumentError, "'time' or 'boolean' expected for :column_type option, got #{paranoid_configuration[:column_type]}" unless ['time', 'boolean'].include? paranoid_configuration[:column_type]

class_eval <<-EOV
default_scope where("#{column_reference} IS ?", nil)
@paranoid_column_reference = "#{self.table_name}.#{paranoid_configuration[:column]}"

class << self
def is_paranoid?
true
end
def with_deleted
self.unscoped.where("") #self.unscoped.reload
ActiveRecord::Relation.class_eval do
alias_method :delete_all!, :delete_all
alias_method :destroy!, :destroy
end

default_scope where("#{paranoid_column_reference} IS ?", nil)

scope :deleted_around, lambda {|value, window|
if self.class.respond_to?(:paranoid?) && self.class.paranoid?
if self.class.paranoid_column_type == 'time' && ![true, false].include?(value)
self.where("#{self.class.paranoid_column} > ? AND #{self.class.paranoid_column} < ?", (value - window), (value + window))
else
self.only_deleted
end
end
}

include InstanceMethods
extend ClassMethods
end

def only_deleted
self.unscoped.where("#{column_reference} IS NOT ?", nil)
end
module ClassMethods

def with_deleted
self.unscoped.reload
end

def delete_all!(conditions = nil)
self.unscoped.delete_all!(conditions)
end
def only_deleted
self.unscoped.where("#{paranoid_column_reference} IS NOT ?", nil)
end

def delete_all(conditions = nil)
update_all ["#{configuration[:column]} = ?", #{type}], conditions
end
def delete_all!(conditions = nil)
self.unscoped.delete_all!(conditions)
end

def paranoid_column
:"#{configuration[:column]}"
end
def delete_all(conditions = nil)
update_all ["#{paranoid_configuration[:column]} = ?", delete_now_value], conditions
end

def paranoid_column_type
:"#{configuration[:column_type]}"
end
def paranoid_column
paranoid_configuration[:column].to_sym
end

def dependent_associations
self.reflect_on_all_associations.select {|a| [:delete_all, :destroy].include?(a.options[:dependent]) }
end
end
def paranoid_column_type
paranoid_configuration[:column_type].to_sym
end

def paranoid_value
self.send(self.class.paranoid_column)
def dependent_associations
self.reflect_on_all_associations.select {|a| [:delete_all, :destroy].include?(a.options[:dependent]) }
end

def delete_now_value
case paranoid_configuration[:column_type]
when "time" then Time.now
when "boolean" then true
end
def destroy!
before_destroy() if respond_to?(:before_destroy)
#{self.name}.delete_all!(:id => self)
after_destroy() if respond_to?(:after_destroy)
end

end

module InstanceMethods

def paranoid_value
self.send(self.class.paranoid_column)
end

def destroy!
with_transaction_returning_status do
run_callbacks :destroy do
self.class.delete_all!(:id => self)
self.paranoid_value = self.class.delete_now_value
freeze
end
end
end

def destroy
def destroy
with_transaction_returning_status do
run_callbacks :destroy do
if paranoid_value == nil
#{self.name}.delete_all(:id => self.id)
self.class.delete_all(:id => self.id)
else
#{self.name}.delete_all!(:id => self.id)
self.class.delete_all!(:id => self.id)
end
self.paranoid_value = self.class.delete_now_value
end
end
end

def recover(options = {})
options = {
:recursive => #{configuration[:recover_dependent_associations]},
:recovery_window => #{configuration[:dependent_recovery_window]}
}.merge(options)
def recover(options = {})
options = {
:recursive => self.class.paranoid_configuration[:recover_dependent_associations],
:recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
}.merge(options)

self.class.transaction do
recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
self.class.transaction do
recover_dependent_associations(options[:recovery_window], options) if options[:recursive]

self.update_attributes(self.class.paranoid_column.to_sym => nil)
end
self.update_attributes(self.class.paranoid_column.to_sym => nil)
end
end

def recover_dependent_associations(window, options)
self.class.dependent_associations.each do |association|
if association.collection? && self.send(association.name).is_paranoid?
self.send(association.name).unscoped do
self.send(association.name).deleted_around(paranoid_value, window).each do |object|
object.recover(options) if object.respond_to?(:recover)
end
end
elsif association.macro == :has_one && association.klass.is_paranoid?
association.klass.unscoped do
object = association.klass.deleted_around(paranoid_value, window).send('find_by_'+association.primary_key_name, self.id)
object.recover(options) if object && object.respond_to?(:recover)
end
elsif association.klass.is_paranoid?
association.klass.unscoped do
id = self.send(association.primary_key_name)
object = association.klass.deleted_around(paranoid_value, window).find_by_id(id)
object.recover(options) if object && object.respond_to?(:recover)
def recover_dependent_associations(window, options)
self.class.dependent_associations.each do |association|
if association.collection? && self.send(association.name).paranoid?
self.send(association.name).unscoped do
self.send(association.name).deleted_around(paranoid_value, window).each do |object|
object.recover(options) if object.respond_to?(:recover)
end
end
end
end
def deleted?
!self.#{configuration[:column]}.nil?
end
scope :deleted_around, lambda {|value, window|
if self.class.is_paranoid?
if self.class.paranoid_column_type == 'time' && ![true, false].include?(value)
self.where("\#{self.class.paranoid_column} > ? AND \#{self.class.paranoid_column} < ?", (value - window), (value + window))
else
self.only_deleted
elsif association.macro == :has_one && association.klass.paranoid?
association.klass.unscoped do
object = association.klass.deleted_around(paranoid_value, window).send('find_by_'+association.primary_key_name, self.id)
object.recover(options) if object && object.respond_to?(:recover)
end
elsif association.klass.paranoid?
association.klass.unscoped do
id = self.send(association.primary_key_name)
object = association.klass.deleted_around(paranoid_value, window).find_by_id(id)
object.recover(options) if object && object.respond_to?(:recover)
end
end
}
ActiveRecord::Relation.class_eval do
alias_method :delete_all!, :delete_all
alias_method :destroy!, :destroy
end
EOV
end

def deleted?
!paranoid_value.nil?
end
alias_method :destroyed?, :deleted?

private
def paranoid_value=(value)
self.send("#{self.class.paranoid_column}=", value)
end

end

def validates_as_paranoid
class_eval <<-EOV
send :extend, ParanoidValidations::ClassMethods
EOV
end
end

# Extend ActiveRecord's functionality
Expand Down
45 changes: 30 additions & 15 deletions test/rails3_acts_as_paranoid_test.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
require 'test_helper'

#["paranoid", "really paranoid", "extremely paranoid"].each do |name|
# Parent.create! :name => name
# Son.create! :name => name
#end
#Parent.first.destroy
#Son.delete_all("name = 'paranoid' OR name = 'really paranoid'")
#Parent.count
#Son.count
#Parent.only_deleted.count
#Son.only_deleted.count
#Parent.with_deleted.count
#Son.with_deleted.count

class ParanoidBase < ActiveSupport::TestCase
def assert_empty(collection)
assert(collection.respond_to?(:empty?) && collection.empty?)
end

def setup
setup_db

Expand All @@ -23,6 +14,7 @@ def setup
end

NotParanoid.create! :name => "no paranoid goals"
ParanoidWithCallback.create! :name => 'paranoid with callbacks'
end

def teardown
Expand Down Expand Up @@ -119,7 +111,7 @@ def setup_recursive_recovery_tests
assert_equal 0, ParanoidHasManyDependant.count
assert_equal 0, ParanoidBelongsDependant.count
assert_equal 0, ParanoidHasOneDependant.count
puts NotParanoid.all.inspect

assert_equal 1, NotParanoid.count
assert_equal 0, HasOneNotParanoid.count
assert_equal @paranoid_boolean_count, ParanoidBoolean.count
Expand Down Expand Up @@ -151,13 +143,36 @@ def test_non_recursive_recovery
assert_equal 1, NotParanoid.count
assert_equal 0, HasOneNotParanoid.count
assert_equal @paranoid_boolean_count, ParanoidBoolean.count

end

def test_deleted?
ParanoidTime.first.destroy
assert ParanoidTime.with_deleted.first.deleted?
end

def test_paranoid_destroy_callbacks
@paranoid_with_callback = ParanoidWithCallback.first
ParanoidWithCallback.transaction do
@paranoid_with_callback.destroy
end

assert @paranoid_with_callback.called_before_destroy
assert @paranoid_with_callback.called_after_destroy
assert @paranoid_with_callback.called_after_commit_on_destroy
end

def test_hard_destroy_callbacks
@paranoid_with_callback = ParanoidWithCallback.first

ParanoidWithCallback.transaction do
@paranoid_with_callback.destroy!
end

assert @paranoid_with_callback.called_before_destroy
assert @paranoid_with_callback.called_after_destroy
assert @paranoid_with_callback.called_after_commit_on_destroy
end

end

class ValidatesUniquenessTest < ParanoidBase
Expand Down
Loading

0 comments on commit 6c7956f

Please sign in to comment.