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



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

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



68
69
70
# File 'lib/is_paranoid.rb', line 68

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

#exists_with_destroyed(id) ⇒ Object

TODO: needs better implementation



122
123
124
# File 'lib/is_paranoid.rb', line 122

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. Only use is_paranoid conditions when the associated class is also paranoid



53
54
55
56
57
58
59
60
61
62
# File 'lib/is_paranoid.rb', line 53

def has_many(association_id, options = {}, &extension)
  association_klass_name = options[:class_name] || options[:source] || association_id
  association_klass      = association_klass_name.to_s.classify.try(:constantize)

  if options.key?(:through) && association_klass.is_paranoid?
    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



42
43
44
45
46
47
48
# File 'lib/is_paranoid.rb', line 42

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

#is_paranoid?Boolean

Returns:

  • (Boolean)


38
39
40
# File 'lib/is_paranoid.rb', line 38

def is_paranoid?
  true
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)


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

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.



169
170
171
172
173
174
175
176
177
# File 'lib/is_paranoid.rb', line 169

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