acts_as_paranoid 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,14 @@
1
- require 'acts_as_paranoid/core'
2
- require 'acts_as_paranoid/associations'
3
- require 'acts_as_paranoid/validations'
4
- require 'acts_as_paranoid/relation'
5
- require 'acts_as_paranoid/preloader_association'
1
+ # frozen_string_literal: true
6
2
 
7
- module ActsAsParanoid
3
+ require "active_record"
4
+ require "acts_as_paranoid/core"
5
+ require "acts_as_paranoid/associations"
6
+ require "acts_as_paranoid/validations"
7
+ require "acts_as_paranoid/relation"
8
8
 
9
+ module ActsAsParanoid
9
10
  def paranoid?
10
- self.included_modules.include?(ActsAsParanoid::Core)
11
+ included_modules.include?(ActsAsParanoid::Core)
11
12
  end
12
13
 
13
14
  def validates_as_paranoid
@@ -15,18 +16,30 @@ module ActsAsParanoid
15
16
  end
16
17
 
17
18
  def acts_as_paranoid(options = {})
18
- raise ArgumentError, "Hash expected, got #{options.class.name}" if not options.is_a?(Hash) and not options.empty?
19
-
20
- class_attribute :paranoid_configuration, :paranoid_column_reference
21
-
22
- self.paranoid_configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 2.minutes }
23
- self.paranoid_configuration.merge!({ :deleted_value => "deleted" }) if options[:column_type] == "string"
24
- self.paranoid_configuration.merge!({ :allow_nulls => true }) if options[:column_type] == "boolean"
25
- self.paranoid_configuration.merge!(options) # user options
19
+ if !options.is_a?(Hash) && !options.empty?
20
+ raise ArgumentError, "Hash expected, got #{options.class.name}"
21
+ end
26
22
 
27
- raise ArgumentError, "'time', 'boolean' or 'string' expected for :column_type option, got #{paranoid_configuration[:column_type]}" unless ['time', 'boolean', 'string'].include? paranoid_configuration[:column_type]
23
+ class_attribute :paranoid_configuration
24
+
25
+ self.paranoid_configuration = {
26
+ column: "deleted_at",
27
+ column_type: "time",
28
+ recover_dependent_associations: true,
29
+ dependent_recovery_window: 2.minutes,
30
+ recovery_value: nil,
31
+ double_tap_destroys_fully: true
32
+ }
33
+ if options[:column_type] == "string"
34
+ paranoid_configuration.merge!(deleted_value: "deleted")
35
+ end
36
+ paranoid_configuration.merge!(allow_nulls: true) if options[:column_type] == "boolean"
37
+ paranoid_configuration.merge!(options) # user options
28
38
 
29
- self.paranoid_column_reference = "#{self.table_name}.#{paranoid_configuration[:column]}"
39
+ unless %w[time boolean string].include? paranoid_configuration[:column_type]
40
+ raise ArgumentError, "'time', 'boolean' or 'string' expected" \
41
+ " for :column_type option, got #{paranoid_configuration[:column_type]}"
42
+ end
30
43
 
31
44
  return if paranoid?
32
45
 
@@ -35,28 +48,18 @@ module ActsAsParanoid
35
48
  # Magic!
36
49
  default_scope { where(paranoid_default_scope) }
37
50
 
38
- if paranoid_configuration[:column_type] == 'time'
39
- scope :deleted_inside_time_window, lambda {|time, window|
40
- deleted_after_time((time - window)).deleted_before_time((time + window))
41
- }
42
-
43
- scope :deleted_after_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} > ?", time) }
44
- scope :deleted_before_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} < ?", time) }
45
- end
51
+ define_deleted_time_scopes if paranoid_column_type == :time
46
52
  end
47
53
  end
48
54
 
49
55
  # Extend ActiveRecord's functionality
50
- ActiveRecord::Base.send :extend, ActsAsParanoid
56
+ ActiveRecord::Base.extend ActsAsParanoid
51
57
 
52
58
  # Extend ActiveRecord::Base with paranoid associations
53
- ActiveRecord::Base.send :include, ActsAsParanoid::Associations
59
+ ActiveRecord::Base.include ActsAsParanoid::Associations
54
60
 
55
61
  # Override ActiveRecord::Relation's behavior
56
- ActiveRecord::Relation.send :include, ActsAsParanoid::Relation
62
+ ActiveRecord::Relation.include ActsAsParanoid::Relation
57
63
 
58
64
  # Push the recover callback onto the activerecord callback list
59
65
  ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
60
-
61
- # Use with_deleted in preloader build_scope
62
- ActiveRecord::Associations::Preloader::Association.send :include, ActsAsParanoid::PreloaderAssociation
@@ -1,37 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsParanoid
2
4
  module Associations
3
5
  def self.included(base)
4
6
  base.extend ClassMethods
5
7
  class << base
6
- alias_method_chain :belongs_to, :deleted
8
+ alias_method :belongs_to_without_deleted, :belongs_to
9
+ alias_method :belongs_to, :belongs_to_with_deleted
7
10
  end
8
11
  end
9
12
 
10
13
  module ClassMethods
11
14
  def belongs_to_with_deleted(target, scope = nil, options = {})
12
- with_deleted = (scope.is_a?(Hash) ? scope : options).delete(:with_deleted)
13
- result = belongs_to_without_deleted(target, scope, options)
15
+ if scope.is_a?(Hash)
16
+ options = scope
17
+ scope = nil
18
+ end
14
19
 
20
+ with_deleted = options.delete(:with_deleted)
15
21
  if with_deleted
16
- if result.is_a? Hash
17
- result.values.last.options[:with_deleted] = with_deleted
22
+ if scope
23
+ old_scope = scope
24
+ scope = proc do |*args|
25
+ if old_scope.arity == 0
26
+ instance_exec(&old_scope).with_deleted
27
+ else
28
+ old_scope.call(*args).with_deleted
29
+ end
30
+ end
18
31
  else
19
- result.options[:with_deleted] = with_deleted
20
- end
21
-
22
- unless method_defined? "#{target}_with_unscoped"
23
- class_eval <<-RUBY, __FILE__, __LINE__
24
- def #{target}_with_unscoped(*args)
25
- association = association(:#{target})
26
- return nil if association.options[:polymorphic] && association.klass.nil?
27
- return #{target}_without_unscoped(*args) unless association.klass.paranoid?
28
- association.klass.with_deleted.scoping { #{target}_without_unscoped(*args) }
32
+ scope = proc do
33
+ if respond_to? :with_deleted
34
+ self.with_deleted
35
+ else
36
+ all
29
37
  end
30
- alias_method_chain :#{target}, :unscoped
31
- RUBY
38
+ end
32
39
  end
33
40
  end
34
41
 
42
+ result = belongs_to_without_deleted(target, scope, **options)
43
+
44
+ result.values.last.options[:with_deleted] = with_deleted if with_deleted
45
+
35
46
  result
36
47
  end
37
48
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsParanoid
2
4
  module Core
3
5
  def self.included(base)
@@ -23,7 +25,8 @@ module ActsAsParanoid
23
25
 
24
26
  def only_deleted
25
27
  if string_type_with_deleted_value?
26
- without_paranoid_default_scope.where(paranoid_column_reference => paranoid_configuration[:deleted_value])
28
+ without_paranoid_default_scope
29
+ .where(paranoid_column_reference => paranoid_configuration[:deleted_value])
27
30
  elsif boolean_type_not_nullable?
28
31
  without_paranoid_default_scope.where(paranoid_column_reference => true)
29
32
  else
@@ -36,17 +39,18 @@ module ActsAsParanoid
36
39
  end
37
40
 
38
41
  def delete_all(conditions = nil)
39
- where(conditions).update_all(["#{paranoid_configuration[:column]} = ?", delete_now_value])
42
+ where(conditions)
43
+ .update_all(["#{paranoid_configuration[:column]} = ?", delete_now_value])
40
44
  end
41
45
 
42
46
  def paranoid_default_scope
43
47
  if string_type_with_deleted_value?
44
- self.all.table[paranoid_column].eq(nil).
45
- or(self.all.table[paranoid_column].not_eq(paranoid_configuration[:deleted_value]))
48
+ all.table[paranoid_column].eq(nil)
49
+ .or(all.table[paranoid_column].not_eq(paranoid_configuration[:deleted_value]))
46
50
  elsif boolean_type_not_nullable?
47
- self.all.table[paranoid_column].eq(false)
51
+ all.table[paranoid_column].eq(false)
48
52
  else
49
- self.all.table[paranoid_column].eq(nil)
53
+ all.table[paranoid_column].eq(nil)
50
54
  end
51
55
  end
52
56
 
@@ -66,8 +70,14 @@ module ActsAsParanoid
66
70
  paranoid_configuration[:column_type].to_sym
67
71
  end
68
72
 
73
+ def paranoid_column_reference
74
+ "#{table_name}.#{paranoid_column}"
75
+ end
76
+
69
77
  def dependent_associations
70
- self.reflect_on_all_associations.select {|a| [:destroy, :delete_all].include?(a.options[:dependent]) }
78
+ reflect_on_all_associations.select do |a|
79
+ [:destroy, :delete_all].include?(a.options[:dependent])
80
+ end
71
81
  end
72
82
 
73
83
  def delete_now_value
@@ -78,20 +88,27 @@ module ActsAsParanoid
78
88
  end
79
89
  end
80
90
 
81
- protected
91
+ protected
92
+
93
+ def define_deleted_time_scopes
94
+ scope :deleted_inside_time_window, lambda { |time, window|
95
+ deleted_after_time((time - window)).deleted_before_time((time + window))
96
+ }
97
+
98
+ scope :deleted_after_time, lambda { |time|
99
+ where("#{table_name}.#{paranoid_column} > ?", time)
100
+ }
101
+ scope :deleted_before_time, lambda { |time|
102
+ where("#{table_name}.#{paranoid_column} < ?", time)
103
+ }
104
+ end
82
105
 
83
106
  def without_paranoid_default_scope
84
- scope = self.all
107
+ scope = all
85
108
 
86
- if ActiveRecord::VERSION::MAJOR < 5
87
- # ActiveRecord 4.0.*
88
- scope = scope.with_default_scope if ActiveRecord::VERSION::MINOR < 1
89
- scope.where_values.delete(paranoid_default_scope)
90
- else
91
- scope = scope.unscope(where: paranoid_default_scope)
92
- # Fix problems with unscope group chain
93
- scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
94
- end
109
+ scope = scope.unscope(where: paranoid_column)
110
+ # Fix problems with unscope group chain
111
+ scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
95
112
 
96
113
  scope
97
114
  end
@@ -102,16 +119,31 @@ module ActsAsParanoid
102
119
  end
103
120
 
104
121
  def paranoid_value
105
- self.send(self.class.paranoid_column)
122
+ send(self.class.paranoid_column)
123
+ end
124
+
125
+ # Straight from ActiveRecord 5.1!
126
+ def delete
127
+ self.class.delete(id) if persisted?
128
+ stale_paranoid_value
129
+ freeze
106
130
  end
107
131
 
108
132
  def destroy_fully!
109
133
  with_transaction_returning_status do
110
134
  run_callbacks :destroy do
111
135
  destroy_dependent_associations!
112
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
113
- self.class.delete_all!(Hash[[Array(self.class.primary_key), Array(self.id)].transpose]) if persisted?
114
- self.paranoid_value = self.class.delete_now_value
136
+
137
+ if persisted?
138
+ # Handle composite keys, otherwise we would just use
139
+ # `self.class.primary_key.to_sym => self.id`.
140
+ self.class
141
+ .delete_all!(Hash[[Array(self.class.primary_key), Array(id)].transpose])
142
+ decrement_counters_on_associations
143
+ end
144
+
145
+ stale_paranoid_value
146
+ @destroyed = true
115
147
  freeze
116
148
  end
117
149
  end
@@ -121,43 +153,63 @@ module ActsAsParanoid
121
153
  if !deleted?
122
154
  with_transaction_returning_status do
123
155
  run_callbacks :destroy do
124
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
125
- self.class.delete_all(Hash[[Array(self.class.primary_key), Array(self.id)].transpose]) if persisted?
126
- self.paranoid_value = self.class.delete_now_value
156
+ if persisted?
157
+ # Handle composite keys, otherwise we would just use
158
+ # `self.class.primary_key.to_sym => self.id`.
159
+ self.class
160
+ .delete_all(Hash[[Array(self.class.primary_key), Array(id)].transpose])
161
+ decrement_counters_on_associations
162
+ end
163
+
164
+ @_trigger_destroy_callback = true
165
+
166
+ stale_paranoid_value
127
167
  self
128
168
  end
129
169
  end
130
- else
170
+ elsif paranoid_configuration[:double_tap_destroys_fully]
131
171
  destroy_fully!
132
172
  end
133
173
  end
134
174
 
135
- alias_method :destroy, :destroy!
175
+ alias destroy destroy!
176
+
177
+ def recover(options = {})
178
+ return if !deleted?
136
179
 
137
- def recover(options={})
138
180
  options = {
139
- :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
140
- :recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
181
+ recursive: self.class.paranoid_configuration[:recover_dependent_associations],
182
+ recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
183
+ raise_error: false
141
184
  }.merge(options)
142
185
 
143
186
  self.class.transaction do
144
187
  run_callbacks :recover do
145
- recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
146
-
147
- self.paranoid_value = nil
148
- self.save
188
+ if options[:recursive]
189
+ recover_dependent_associations(options[:recovery_window], options)
190
+ end
191
+ increment_counters_on_associations
192
+ self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
193
+ if options[:raise_error]
194
+ save!
195
+ else
196
+ save
197
+ end
149
198
  end
150
199
  end
151
200
  end
152
201
 
202
+ def recover!(options = {})
203
+ options[:raise_error] = true
204
+
205
+ recover(options)
206
+ end
207
+
153
208
  def recover_dependent_associations(window, options)
154
209
  self.class.dependent_associations.each do |reflection|
155
210
  next unless (klass = get_reflection_class(reflection)).paranoid?
156
211
 
157
- scope = klass.only_deleted
158
-
159
- # Merge in the association's scope
160
- scope = scope.merge(association(reflection.name).association_scope)
212
+ scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
161
213
 
162
214
  # We can only recover by window if both parent and dependant have a
163
215
  # paranoid column type of :time.
@@ -175,41 +227,80 @@ module ActsAsParanoid
175
227
  self.class.dependent_associations.each do |reflection|
176
228
  next unless (klass = get_reflection_class(reflection)).paranoid?
177
229
 
178
- scope = klass.only_deleted
179
-
180
- # Merge in the association's scope
181
- scope = scope.merge(association(reflection.name).association_scope)
182
-
183
- scope.each do |object|
184
- object.destroy!
185
- end
230
+ klass
231
+ .only_deleted.merge(get_association_scope(reflection: reflection))
232
+ .each(&:destroy!)
186
233
  end
187
234
  end
188
235
 
189
236
  def deleted?
190
- !if self.class.string_type_with_deleted_value?
191
- paranoid_value != self.class.delete_now_value || paranoid_value.nil?
237
+ return true if @destroyed
238
+
239
+ if self.class.string_type_with_deleted_value?
240
+ paranoid_value == paranoid_configuration[:deleted_value]
192
241
  elsif self.class.boolean_type_not_nullable?
193
- paranoid_value == false
242
+ paranoid_value == true
194
243
  else
195
- paranoid_value.nil?
244
+ !paranoid_value.nil?
196
245
  end
197
246
  end
198
247
 
199
- alias_method :destroyed?, :deleted?
248
+ alias destroyed? deleted?
249
+
250
+ def deleted_fully?
251
+ @destroyed
252
+ end
253
+
254
+ alias destroyed_fully? deleted_fully?
200
255
 
201
256
  private
202
257
 
258
+ def get_association_scope(reflection:)
259
+ ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
260
+ end
261
+
203
262
  def get_reflection_class(reflection)
204
263
  if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
205
- self.send(reflection.foreign_type).constantize
264
+ send(reflection.foreign_type).constantize
206
265
  else
207
266
  reflection.klass
208
267
  end
209
268
  end
210
269
 
211
270
  def paranoid_value=(value)
212
- self.send("#{self.class.paranoid_column}=", value)
271
+ write_attribute(self.class.paranoid_column, value)
272
+ end
273
+
274
+ def update_counters_on_associations(method_sym)
275
+ return unless [:decrement_counter, :increment_counter].include? method_sym
276
+
277
+ each_counter_cached_association_reflection do |assoc_reflection|
278
+ associated_object = send(assoc_reflection.name)
279
+ next unless associated_object
280
+
281
+ counter_cache_column = assoc_reflection.counter_cache_column
282
+ associated_object.class.send(method_sym, counter_cache_column,
283
+ associated_object.id)
284
+ end
285
+ end
286
+
287
+ def each_counter_cached_association_reflection
288
+ _reflections.each do |_name, reflection|
289
+ yield reflection if reflection.belongs_to? && reflection.counter_cache_column
290
+ end
291
+ end
292
+
293
+ def increment_counters_on_associations
294
+ update_counters_on_associations :increment_counter
295
+ end
296
+
297
+ def decrement_counters_on_associations
298
+ update_counters_on_associations :decrement_counter
299
+ end
300
+
301
+ def stale_paranoid_value
302
+ self.paranoid_value = self.class.delete_now_value
303
+ clear_attribute_changes([self.class.paranoid_column])
213
304
  end
214
305
  end
215
306
  end