Introduced acts_as_paranoid. Avoid deleting of suppliers and articles. (for consistency of order-results)

This commit is contained in:
Benjamin Meichsner 2009-01-20 19:37:15 +01:00
parent b820577382
commit fc45345e0d
30 changed files with 1238 additions and 151 deletions

View file

@ -0,0 +1,14 @@
module Caboose # :nodoc:
module Acts # :nodoc:
class BelongsToWithDeletedAssociation < ActiveRecord::Associations::BelongsToAssociation
private
def find_target
@reflection.klass.find_with_deleted(
@owner[@reflection.primary_key_name],
:conditions => conditions,
:include => @reflection.options[:include]
)
end
end
end
end

View file

@ -0,0 +1,27 @@
module Caboose # :nodoc:
module Acts # :nodoc:
class HasManyThroughWithoutDeletedAssociation < ActiveRecord::Associations::HasManyThroughAssociation
protected
def current_time
ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
end
def construct_conditions
return super unless @reflection.through_reflection.klass.paranoid?
table_name = @reflection.through_reflection.table_name
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
"#{table_name}.#{attr} = #{value}"
end
deleted_attribute = @reflection.through_reflection.klass.deleted_attribute
quoted_current_time = @reflection.through_reflection.klass.quote_value(
current_time,
@reflection.through_reflection.klass.columns_hash[deleted_attribute.to_s])
conditions << "#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > #{quoted_current_time}"
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
end
end
end
end

View file

@ -0,0 +1,196 @@
module Caboose #:nodoc:
module Acts #:nodoc:
# Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.
# This assumes the table has a deleted_at date/time field. Most normal model operations will work, but there will be some oddities.
#
# class Widget < ActiveRecord::Base
# acts_as_paranoid
# end
#
# Widget.find(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find(:first, :conditions => ['title = ?', 'test'], :order => 'title')
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test' ORDER BY title LIMIT 1
#
# Widget.find_with_deleted(:all)
# # SELECT * FROM widgets
#
# Widget.find_only_deleted(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find_with_deleted(1).deleted?
# # Returns true if the record was previously destroyed, false if not
#
# Widget.count
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.count ['title = ?', 'test']
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test'
#
# Widget.count_with_deleted
# # SELECT COUNT(*) FROM widgets
#
# Widget.count_only_deleted
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.delete_all
# # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36'
#
# Widget.delete_all!
# # DELETE FROM widgets
#
# @widget.destroy
# # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36' WHERE id = 1
#
# @widget.destroy!
# # DELETE FROM widgets WHERE id = 1
#
module Paranoid
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def acts_as_paranoid(options = {})
unless paranoid? # don't let AR call this twice
cattr_accessor :deleted_attribute
self.deleted_attribute = options[:with] || :deleted_at
alias_method :destroy_without_callbacks!, :destroy_without_callbacks
class << self
alias_method :find_every_with_deleted, :find_every
alias_method :calculate_with_deleted, :calculate
alias_method :delete_all!, :delete_all
end
end
include InstanceMethods
end
def paranoid?
self.included_modules.include?(InstanceMethods)
end
end
module InstanceMethods #:nodoc:
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def find_with_deleted(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)
options[:with_deleted] = true # yuck!
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
def find_only_deleted(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)
options[:only_deleted] = true # yuck!
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
def exists?(*args)
with_deleted_scope { exists_with_deleted?(*args) }
end
def exists_only_deleted?(*args)
with_only_deleted_scope { exists_with_deleted?(*args) }
end
def count_with_deleted(*args)
calculate_with_deleted(:count, *construct_count_options_from_args(*args))
end
def count_only_deleted(*args)
with_only_deleted_scope { count_with_deleted(*args) }
end
def count(*args)
with_deleted_scope { count_with_deleted(*args) }
end
def calculate(*args)
with_deleted_scope { calculate_with_deleted(*args) }
end
def delete_all(conditions = nil)
self.update_all ["#{self.deleted_attribute} = ?", current_time], conditions
end
protected
def current_time
default_timezone == :utc ? Time.now.utc : Time.now
end
def with_deleted_scope(&block)
with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > ?", current_time] } }, :merge, &block)
end
def with_only_deleted_scope(&block)
with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NOT NULL AND #{table_name}.#{deleted_attribute} <= ?", current_time] } }, :merge, &block)
end
private
# all find calls lead here
def find_every(options)
options.delete(:with_deleted) ?
find_every_with_deleted(options) :
options.delete(:only_deleted) ?
with_only_deleted_scope { find_every_with_deleted(options) } :
with_deleted_scope { find_every_with_deleted(options) }
end
end
def destroy_without_callbacks
unless new_record?
self.class.update_all self.class.send(:sanitize_sql, ["#{self.class.deleted_attribute} = ?", (self.deleted_at = self.class.send(:current_time))]), ["#{self.class.primary_key} = ?", id]
end
freeze
end
def destroy_with_callbacks!
return false if callback(:before_destroy) == false
result = destroy_without_callbacks!
callback(:after_destroy)
result
end
def destroy!
transaction { destroy_with_callbacks! }
end
def deleted?
!!read_attribute(:deleted_at)
end
def recover!
self.deleted_at = nil
save!
end
def recover_with_associations!(*associations)
self.recover!
associations.to_a.each do |assoc|
self.send(assoc).find_with_deleted(:all).each do |a|
a.recover! if a.class.paranoid?
end
end
end
end
end
end
end

View file

@ -0,0 +1,94 @@
module Caboose #:nodoc:
module Acts #:nodoc:
# Adds a wrapper find method which can identify :with_deleted or :only_deleted options
# and would call the corresponding acts_as_paranoid finders find_with_deleted or
# find_only_deleted methods.
#
# With this wrapper you can easily change from using this pattern:
#
# if some_condition_enabling_access_to_deleted_records?
# @post = Post.find_with_deleted(params[:id])
# else
# @post = Post.find(params[:id])
# end
#
# to this:
#
# @post = Post.find(params[:id], :with_deleted => some_condition_enabling_access_to_deleted_records?)
#
# Examples
#
# class Widget < ActiveRecord::Base
# acts_as_paranoid
# end
#
# Widget.find(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find(:all, :with_deleted => false)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find_with_deleted(:all)
# # SELECT * FROM widgets
#
# Widget.find(:all, :with_deleted => true)
# # SELECT * FROM widgets
#
# Widget.find_only_deleted(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find(:all, :only_deleted => true)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find(:all, :only_deleted => false)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
module ParanoidFindWrapper
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def acts_as_paranoid_with_find_wrapper(options = {})
unless paranoid? # don't let AR call this twice
acts_as_paranoid_without_find_wrapper(options)
class << self
alias_method :find_without_find_wrapper, :find
alias_method :validate_find_options_without_find_wrapper, :validate_find_options
end
end
include InstanceMethods
end
end
module InstanceMethods #:nodoc:
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
# This is a wrapper for the regular "find" so you can pass acts_as_paranoid related
# options and determine which finder to call.
def find(*args)
options = args.extract_options!
# Determine who to call.
finder_option = VALID_PARANOID_FIND_OPTIONS.detect { |key| options.delete(key) } || :without_find_wrapper
finder_method = "find_#{finder_option}".to_sym
# Put back the options in the args now that they don't include the extended keys.
args << options
send(finder_method, *args)
end
protected
VALID_PARANOID_FIND_OPTIONS = [:with_deleted, :only_deleted]
def validate_find_options(options) #:nodoc:
cleaned_options = options.reject { |k, v| VALID_PARANOID_FIND_OPTIONS.include?(k) }
validate_find_options_without_find_wrapper(cleaned_options)
end
end
end
end
end
end