Skip to content
This repository has been archived by the owner on Dec 10, 2022. It is now read-only.

Commit

Permalink
ported from local
Browse files Browse the repository at this point in the history
  • Loading branch information
rockrep committed Mar 5, 2011
1 parent 2fc7fb3 commit c085006
Show file tree
Hide file tree
Showing 21 changed files with 1,596 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2010 [name of plugin creator]

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 changes: 13 additions & 0 deletions README
@@ -0,0 +1,13 @@
SoftDestroyable
===============

Introduction goes here.


Example
=======

Example goes here.


Copyright (c) 2010 [name of plugin creator], released under the MIT license
23 changes: 23 additions & 0 deletions Rakefile
@@ -0,0 +1,23 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the soft_destroyable plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the soft_destroyable plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'SoftDestroyable'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
1 change: 1 addition & 0 deletions init.rb
@@ -0,0 +1 @@
require "soft_destroyable"
1 change: 1 addition & 0 deletions install.rb
@@ -0,0 +1 @@
# Install hook code here
299 changes: 299 additions & 0 deletions lib/soft_destroyable.rb
@@ -0,0 +1,299 @@
require "#{File.dirname(__FILE__)}/soft_destroyable/table_definition"
require "#{File.dirname(__FILE__)}/soft_destroyable/is_soft_destroyable"

# Allows one to annotate an ActiveRecord module as being soft_destroyable.
#
# This changes the behavior of the +destroy+ method to being a soft-destroy, which
# will set the +deleted_at+ attribute to <tt>Time.now</tt>, and the +deleted+ attribute to <tt>true</tt>
# It exposes the +revive+ method to reverse the effects of +destroy+.
# It also exposes the +destroy!+ method which can be used to <b>really</b> destroy an object and it's associations.
#
# Standard ActiveRecord destroy callbacks are _not_ called, however you can override +before_soft_destroy+, +after_soft_destroy+,
# and +before_destroy!+ on your soft_destroyable models.
#
# Standard ActiveRecord dependent options :destroy, :restrict, :nullify, :delete_all, and :delete are supported.
# +revive+ will _not_ undo the effects of +nullify+, +delete_all+, and +delete+. +restrict+ is _not_ effected by the
# +deleted?+ state. In other words, deleted child models will still restrict destroying the parent.
#
# The +delete+ operation is _not_ modified by this module.
#
# The operations: +destroy+, +destroy!+, and +revive+ are automatically delegated to the dependent association records.
# in a single transaction.
#
# Examples:
# class Parent
# has_many :children, :dependent => :restrict
# has_many :animals, :dependent => :nullify
# soft_destroyable
#
#
# Author: Michael Kintzer
#

module SoftDestroyable

def self.included(base)
base.class_eval do
extend ClassMethods
extend IsSoftDestroyable
end
end

module ClassMethods

def soft_destroyable(options = {})
return if soft_destroyable?

scope :not_deleted, where(:deleted => false)
scope :deleted, where(:deleted => true)

include InstanceMethods
extend SingletonMethods
end
end

module SingletonMethods

# returns an array of association symbols that must be managed by soft_destroyable on
# destroy and destroy!
def soft_dependencies
has_many_dependencies + has_one_dependencies
end

def restrict_dependencies
with_restrict_option(:has_many).map(&:name) + with_restrict_option(:has_one).map(&:name)
end

private

def non_through_dependent_associations(type)
reflect_on_all_associations(type).reject { |k, v|
k.class == ActiveRecord::Reflection::ThroughReflection }.reject { |k, v| k.options[:dependent].nil? }
end

def has_many_dependencies
non_through_dependent_associations(:has_many).map(&:name)
end

def has_one_dependencies
non_through_dependent_associations(:has_one).map(&:name)
end

def with_restrict_option(type)
non_through_dependent_associations(type).reject { |k, v| k.options[:dependent] != :restrict }
end

end

module InstanceMethods

# overrides the normal ActiveRecord::Transactions#destroy.
# can be recovered with +revive+
def destroy
before_soft_destroy
result = soft_destroy
after_soft_destroy
result
end

# not a recoverable operation
def destroy!
transaction do
before_destroy!
cascade_destroy!
delete
end
end

# un-does the effect of +destroy+. Does not undo nullify on dependents
def revive
transaction do
cascade_revive
update_attributes(:deleted_at => nil, :deleted => false)
end
end

def soft_dependencies
self.class.soft_dependencies
end

def restrict_dependencies
self.class.restrict_dependencies
end

# override
def before_soft_destroy
# empty
end

# override
def after_soft_destroy
# empty
end

# override
def before_destroy!
# empty
end

private

def non_restrict_dependencies
soft_dependencies.reject { |assoc_sym| restrict_dependencies.include?(assoc_sym) }
end

def soft_destroy
transaction do
cascade_soft_destroy
update_attributes(:deleted_at => Time.now, :deleted => true)
end
end

def cascade_soft_destroy
cascade_to_soft_dependents { |assoc_obj|
if assoc_obj.respond_to?(:destroy) && assoc_obj.respond_to?(:revive)
wrap_with_callbacks(assoc_obj, "soft_destroy") do
assoc_obj.destroy
end
else
wrap_with_callbacks(assoc_obj, "soft_destroy") do
# no-op
end
end
}
end

def cascade_destroy!
cascade_to_soft_dependents { |assoc_obj|
# cascade destroy! to soft dependencies objects
if assoc_obj.respond_to?(:destroy!)
wrap_with_callbacks(assoc_obj, "destroy!") do
assoc_obj.destroy!
end
else
wrap_with_callbacks(assoc_obj, "destroy!") do
assoc_obj.destroy
end
end
}
end

def cascade_revive
cascade_to_soft_dependents { |assoc_obj|
assoc_obj.revive if assoc_obj.respond_to?(:revive)
}
end

def cascade_to_soft_dependents(&block)
return unless block_given?

# fail fast on :dependent => :restrict
restrict_dependencies.each { |assoc_sym| handle_restrict(assoc_sym) }

non_restrict_dependencies.each do |assoc_sym|
reflection = reflection_for(assoc_sym)
association = send(reflection.name)

case reflection.options[:dependent]
when :destroy
handle_destroy(reflection, association, &block)
when :nullify
handle_nullify(reflection, association)
when :delete_all
handle_delete_all(reflection, association)
when :delete
handle_delete(reflection, association)
else
end

end
# reload as dependent associations may have updated
reload if self.id
end

def handle_destroy(reflection, association, &block)
case reflection.macro
when :has_many
association.each { |assoc_obj| yield(assoc_obj) }
when :has_one
# handle non-nil has_one
yield(association) if association
else
end
end

def handle_restrict(assoc_sym)
reflection = reflection_for(assoc_sym)
association = send(reflection.name)
case reflection.macro
when :has_many
restrict_on_non_empty_has_many(reflection, association)
when :has_one
restrict_on_nil_has_one(reflection, association)
else
end
end

def handle_nullify(reflection, association)
return unless association
case reflection.macro
when :has_many
self.class.send(:nullify_has_many_dependencies,
self,
reflection.name,
reflection.klass,
reflection.primary_key_name,
reflection.dependent_conditions(self, self.class, nil))
when :has_one
association.update_attributes(reflection.primary_key_name => nil)
else
end

end

def handle_delete_all(reflection, association)
return unless association
self.class.send(:delete_all_has_many_dependencies,
self,
reflection.name,
reflection.klass,
reflection.dependent_conditions(self, self.class, nil))
end

def handle_delete(reflection, association)
return unless association
association.update_attribute(reflection.primary_key_name, nil)
end

def wrap_with_callbacks(assoc_obj, action)
return unless block_given?
assoc_obj.send("before_#{action}".to_sym) if assoc_obj.respond_to?("before_#{action}".to_sym)
yield
assoc_obj.send("after_#{action}".to_sym) if assoc_obj.respond_to?("after_#{action}".to_sym)
end

def reflection_for(assoc_sym)
self.class.reflect_on_association(assoc_sym)
end

def restrict_on_non_empty_has_many(reflection, association)
return unless association
raise ActiveRecord::DeleteRestrictionError.new(reflection) if !association.empty?
end

def restrict_on_nil_has_one(reflection, association)
raise ActiveRecord::DeleteRestrictionError.new(reflection) if !association.nil?
end

end

ActiveRecord::Base.send :include, SoftDestroyable
[ActiveRecord::ConnectionAdapters::TableDefinition, ActiveRecord::ConnectionAdapters::Table].each { |base|
base.send(:include, SoftDestroyable::TableDefinition)
}

class SoftDestroyError < StandardError

end

end
30 changes: 30 additions & 0 deletions lib/soft_destroyable/is_soft_destroyable.rb
@@ -0,0 +1,30 @@
module SoftDestroyable
# Simply adds a flag to determine whether a model class is soft_destroyable.
module IsSoftDestroyable
def self.extended(base) # :nodoc:
base.class_eval do
class << self
alias_method_chain :soft_destroyable, :flag
end
end
end

# Overrides the +soft_destroyable+ method to first define the +soft_destroyable?+ class method before
# deferring to the original +soft_destroyable+.
def soft_destroyable_with_flag(*args)
soft_destroyable_without_flag(*args)

class << self
def soft_destroyable?
true
end
end
end

# For all ActiveRecord::Base models that do not call the +soft_destroyable+ method, the +soft_destroyable?+
# method will return false.
def soft_destroyable?
false
end
end
end

0 comments on commit c085006

Please sign in to comment.