Module: IsParanoid::ClassMethods

Defined in:
lib/is_paranoid.rb

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &block) ⇒ Object

find_with_destroyed and other blah_with_destroyed and blah_destroyed_only methods are defined here



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
# File 'lib/is_paranoid.rb', line 117

def method_missing name, *args, &block
  if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
    self.extend(Module.new{
      if $2 == '_with_destroyed'
        # Example:
        # def count_with_destroyed(*args)
        #   self.with_exclusive_scope{ self.send(:count, *args) }
        # end
        define_method name do |*args|
          self.with_exclusive_scope{ self.send($1, *args) }
        end
      else

        # Example:
        # def count_destroyed_only(*args)
        #   self.with_exclusive_scope do
        #     with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
        #       self.send(:count, *args)
        #     end
        #   end
        # end
        define_method name do |*args|
          self.with_exclusive_scope do
            with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
              self.send($1, *args, &block)
            end
          end
        end

      end
    })
  self.send(name, *args, &block)
  else
    super(name, *args, &block)
  end
end

Instance Method Details

#delete_all(conditions = nil) ⇒ Object

Actually delete the model, bypassing the safety net. Because this method is called internally by Model.delete(id) and on the delete method in each instance, we don’t need to specify those methods separately



57
58
59
# File 'lib/is_paranoid.rb', line 57

def delete_all conditions = nil
  self.with_exclusive_scope { super conditions }
end

#exists_with_destroyed(id) ⇒ Object

TODO: needs better implementation



111
112
113
# File 'lib/is_paranoid.rb', line 111

def exists_with_destroyed id
  self.with_exclusive_scope{ exists?(id)}
end

#has_many(association_id, options = {}, &extension) ⇒ Object

ensure that we respect the is_paranoid conditions when being loaded as a has_many :through NOTE: this only works if is_paranoid is declared before has_many relationships.



44
45
46
47
48
49
50
# File 'lib/is_paranoid.rb', line 44

def has_many(association_id, options = {}, &extension)
   if options.key?(:through)
    conditions = "#{options[:through].to_s.pluralize}.#{destroyed_field} #{is_or_equals_not_destroyed}"
    options[:conditions] = "(" + [options[:conditions], conditions].compact.join(") AND (") + ")"
  end
  super
end

#is_or_equals_not_destroyedObject



34
35
36
37
38
39
40
# File 'lib/is_paranoid.rb', line 34

def is_or_equals_not_destroyed
  if [nil, 'NULL'].include?(field_not_destroyed)
    'IS NULL'
  else
    "= #{field_not_destroyed}"
  end
end

#restore(id, options = {}) ⇒ Object

Use update_all with an exclusive scope to restore undo the soft-delete. This bypasses update-related callbacks.

By default, restores cascade through associations that are belongs_to :dependent => :destroy and under is_paranoid. You can prevent restoration of associated models by passing :include_destroyed_dependents => false, for example:

Android.restore(:include_destroyed_dependents => false)

Alternatively you can specify which relationships to restore via :include, for example:

Android.restore(:include => [:parts, memories])

Please note that specifying :include means you’re not using :include_destroyed_dependents by default, though you can explicitly use both if you want all has_* relationships and specific belongs_to relationships, for example

Android.restore(:include => [:home, :planet], :include_destroyed_dependents => true)


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
# File 'lib/is_paranoid.rb', line 82

def restore(id, options = {})
  options.reverse_merge!({:include_destroyed_dependents => true}) unless options[:include]
  with_exclusive_scope do
    update_all(
      "#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
      primary_key.to_sym => id
    )
  end

  self.reflect_on_all_associations.each do |association|
    if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
      dependent_relationship = association.macro.to_s =~ /^has/
      if should_restore?(association.name, dependent_relationship, options)
        if dependent_relationship
          restore_related(association.klass, association.primary_key_name, id, options)
        else
          restore_related(
            association.klass,
            association.klass.primary_key,
            self.first(id).send(association.primary_key_name),
            options
          )
        end
      end
    end
  end
end

#with_exclusive_scope(method_scoping = {}, &block) ⇒ Object

with_exclusive_scope is used internally by ActiveRecord when preloading associations. Unfortunately this is problematic for is_paranoid since we want preloaded is_paranoid items to still be scoped to their deleted conditions. so we override that here.



158
159
160
161
162
163
164
165
166
# File 'lib/is_paranoid.rb', line 158

def with_exclusive_scope(method_scoping = {}, &block)
  # this is rather hacky, suggestions for improvements appreciated... the idea
  # is that when the caller includes the method preload_associations, we want
  # to apply our is_paranoid conditions
  if caller.any?{|c| c =~ /\d+:in `preload_associations'$/}
    method_scoping.deep_merge!(:find => {:conditions => {destroyed_field => field_not_destroyed} })
  end
  super method_scoping, &block
end