public
Description: ActiveRecord plugin allowing you to hide and restore records without actually deleting them.
Clone URL: git://github.com/technoweenie/acts_as_paranoid.git
acts_as_paranoid / lib / acts_as_paranoid.rb
100644 166 lines (146 sloc) 6.04 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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(:all, :with_deleted => true)
    # # SELECT * FROM widgets
    #
    # 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.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
      module ActiveRecord
        def self.included(base) # :nodoc:
          base.extend ClassMethods
          class << base
            alias_method :validate_find_options_without_deleted, :validate_find_options
            alias_method :validate_find_options, :validate_find_options_with_deleted
          end
        end
 
        module ClassMethods
          def acts_as_paranoid
            unless paranoid? # don't let AR call this twice
              alias_method :destroy_without_callbacks!, :destroy_without_callbacks
              class << self
                alias_method :original_find, :find
                alias_method :count_with_deleted, :count
                alias_method :clobbering_with_scope, :with_scope
              end
            end
            include InstanceMethods
          end
          
          def paranoid?
            self.included_modules.include?(InstanceMethods)
          end
          
          protected
          def validate_find_options_with_deleted(options)
            options.assert_valid_keys [:conditions, :group, :include, :joins, :limit, :offset, :order, :select, :readonly, :with_deleted]
          end
        end
    
        module InstanceMethods #:nodoc:
          def self.included(base) # :nodoc:
            base.extend ClassMethods
          end
      
          module ClassMethods
            def find(*args)
              options = extract_options_from_args!(args)
              call_original_find = lambda { original_find(*(args << options)) }
            
              if !options[:with_deleted]
                with_deleted_scope { return call_original_find.call }
              end
            
              call_original_find.call
            end
 
            def find_with_deleted(*args)
              original_find(*(args << extract_options_from_args!(args).merge(:with_deleted => true)))
            end
 
            def count(conditions = nil, joins = nil)
              with_deleted_scope { count_with_deleted(conditions, joins) }
            end
 
            def with_scope(method_scoping = {}, is_new_scope = true)
              # Dup first and second level of hash (method and params).
              method_scoping = method_scoping.inject({}) do |hash, (method, params)|
                hash[method] = params.dup
                hash
              end
 
              method_scoping.assert_valid_keys [:find, :create]
              if f = method_scoping[:find]
                f.assert_valid_keys [:conditions, :joins, :offset, :limit, :readonly]
                f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
              end
 
              raise ArgumentError, "Nested scopes are not yet supported: #{scoped_methods.inspect}" unless scoped_methods.nil?
 
              self.scoped_methods = method_scoping
              yield
            ensure
              self.scoped_methods = nil if is_new_scope
            end
 
            protected
            def with_deleted_scope(&block)
              deleted_cond = "#{table_name}.deleted_at IS NULL"
              if scoped_methods.nil?
                is_new_scope = true
                current_scope = {}
              else
                is_new_scope = false
                current_scope = scoped_methods.clone
                self.scoped_methods = nil
              end
            
              current_scope ||= {}
              current_scope[:find] ||= {}
              if not current_scope[:find][:conditions] =~ /#{deleted_cond}/
                current_scope[:find][:conditions] = current_scope[:find][:conditions].nil? ?
                  deleted_cond :
                  "(#{current_scope[:find][:conditions]}) AND #{deleted_cond}"
              end
            
              with_scope(current_scope, is_new_scope, &block)
            end
          end
 
          def destroy_without_callbacks
            unless new_record?
              sql = self.class.send(:sanitize_sql,
                ["UPDATE #{self.class.table_name} SET deleted_at = ? WHERE id = ?",
                  self.class.default_timezone == :utc ? Time.now.utc : Time.now, id])
              self.connection.update(sql)
            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
        end
      end
    end
  end
end
 
ActiveRecord::Base.send :include, Caboose::Acts::Paranoid::ActiveRecord