acts_as_paranoid 0.6.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,19 +16,28 @@ 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
+ if !options.is_a?(Hash) && !options.empty?
20
+ raise ArgumentError, "Hash expected, got #{options.class.name}"
21
+ end
19
22
 
20
23
  class_attribute :paranoid_configuration
21
24
 
22
- self.paranoid_configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 2.minutes, :recovery_value => nil, double_tap_destroys_fully: true }
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
25
+ self.paranoid_configuration = {
26
+ column: "deleted_at",
27
+ column_type: "time",
28
+ recover_dependent_associations: true,
29
+ dependent_recovery_window: 2.minutes,
30
+ double_tap_destroys_fully: true
31
+ }
32
+ if options[:column_type] == "string"
33
+ paranoid_configuration.merge!(deleted_value: "deleted")
34
+ end
26
35
 
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]
36
+ paranoid_configuration.merge!(options) # user options
28
37
 
29
- def self.paranoid_column_reference
30
- "#{self.table_name}.#{paranoid_configuration[:column]}"
38
+ unless %w[time boolean string].include? paranoid_configuration[:column_type]
39
+ raise ArgumentError, "'time', 'boolean' or 'string' expected" \
40
+ " for :column_type option, got #{paranoid_configuration[:column_type]}"
31
41
  end
32
42
 
33
43
  return if paranoid?
@@ -37,28 +47,18 @@ module ActsAsParanoid
37
47
  # Magic!
38
48
  default_scope { where(paranoid_default_scope) }
39
49
 
40
- if paranoid_configuration[:column_type] == 'time'
41
- scope :deleted_inside_time_window, lambda {|time, window|
42
- deleted_after_time((time - window)).deleted_before_time((time + window))
43
- }
44
-
45
- scope :deleted_after_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} > ?", time) }
46
- scope :deleted_before_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} < ?", time) }
47
- end
50
+ define_deleted_time_scopes if paranoid_column_type == :time
48
51
  end
49
52
  end
50
53
 
51
54
  # Extend ActiveRecord's functionality
52
- ActiveRecord::Base.send :extend, ActsAsParanoid
55
+ ActiveRecord::Base.extend ActsAsParanoid
53
56
 
54
57
  # Extend ActiveRecord::Base with paranoid associations
55
- ActiveRecord::Base.send :include, ActsAsParanoid::Associations
58
+ ActiveRecord::Base.include ActsAsParanoid::Associations
56
59
 
57
60
  # Override ActiveRecord::Relation's behavior
58
- ActiveRecord::Relation.send :include, ActsAsParanoid::Relation
61
+ ActiveRecord::Relation.include ActsAsParanoid::Relation
59
62
 
60
63
  # Push the recover callback onto the activerecord callback list
61
64
  ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
62
-
63
- # Use with_deleted in preloader build_scope
64
- ActiveRecord::Associations::Preloader::Association.send :include, ActsAsParanoid::PreloaderAssociation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsParanoid
2
4
  module Associations
3
5
  def self.included(base)
@@ -16,29 +18,31 @@ module ActsAsParanoid
16
18
  end
17
19
 
18
20
  with_deleted = options.delete(:with_deleted)
19
- result = belongs_to_without_deleted(target, scope, options)
20
-
21
21
  if with_deleted
22
- if result.is_a? Hash
23
- 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
24
31
  else
25
- result.options[:with_deleted] = with_deleted
26
- end
27
-
28
- unless method_defined? "#{target}_with_unscoped"
29
- class_eval <<-RUBY, __FILE__, __LINE__
30
- def #{target}_with_unscoped(*args)
31
- association = association(:#{target})
32
- return nil if association.options[:polymorphic] && association.klass.nil?
33
- return #{target}_without_unscoped(*args) unless association.klass.paranoid?
34
- association.klass.with_deleted.scoping { association.klass.unscoped { #{target}_without_unscoped(*args) } }
32
+ scope = proc do
33
+ if respond_to? :with_deleted
34
+ self.with_deleted
35
+ else
36
+ all
35
37
  end
36
- alias_method :#{target}_without_unscoped, :#{target}
37
- alias_method :#{target}, :#{target}_with_unscoped
38
- RUBY
38
+ end
39
39
  end
40
40
  end
41
41
 
42
+ result = belongs_to_without_deleted(target, scope, **options)
43
+
44
+ result.values.last.options[:with_deleted] = with_deleted if with_deleted
45
+
42
46
  result
43
47
  end
44
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,42 @@ module ActsAsParanoid
78
88
  end
79
89
  end
80
90
 
81
- protected
91
+ def recovery_value
92
+ if paranoid_configuration.key? :recovery_value
93
+ ActiveSupport::Deprecation.warn \
94
+ "The recovery_value setting is deprecated and will be removed in" \
95
+ " ActsAsParanoid 0.8.0"
96
+ paranoid_configuration[:recovery_value]
97
+ elsif boolean_type_not_nullable?
98
+ false
99
+ else
100
+ nil
101
+ end
102
+ end
103
+
104
+ protected
105
+
106
+ def define_deleted_time_scopes
107
+ scope :deleted_inside_time_window, lambda { |time, window|
108
+ deleted_after_time((time - window)).deleted_before_time((time + window))
109
+ }
110
+
111
+ scope :deleted_after_time, lambda { |time|
112
+ only_deleted
113
+ .where("#{table_name}.#{paranoid_column} > ?", time)
114
+ }
115
+ scope :deleted_before_time, lambda { |time|
116
+ only_deleted
117
+ .where("#{table_name}.#{paranoid_column} < ?", time)
118
+ }
119
+ end
82
120
 
83
121
  def without_paranoid_default_scope
84
- scope = self.all
122
+ scope = all
85
123
 
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
124
+ scope = scope.unscope(where: paranoid_column)
125
+ # Fix problems with unscope group chain
126
+ scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
95
127
 
96
128
  scope
97
129
  end
@@ -102,13 +134,13 @@ module ActsAsParanoid
102
134
  end
103
135
 
104
136
  def paranoid_value
105
- self.send(self.class.paranoid_column)
137
+ send(self.class.paranoid_column)
106
138
  end
107
139
 
108
140
  # Straight from ActiveRecord 5.1!
109
141
  def delete
110
142
  self.class.delete(id) if persisted?
111
- @destroyed = true
143
+ stale_paranoid_value
112
144
  freeze
113
145
  end
114
146
 
@@ -118,13 +150,15 @@ module ActsAsParanoid
118
150
  destroy_dependent_associations!
119
151
 
120
152
  if persisted?
121
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
122
- self.class.delete_all!(Hash[[Array(self.class.primary_key), Array(self.id)].transpose])
123
-
153
+ # Handle composite keys, otherwise we would just use
154
+ # `self.class.primary_key.to_sym => self.id`.
155
+ self.class
156
+ .delete_all!([Array(self.class.primary_key), Array(id)].transpose.to_h)
124
157
  decrement_counters_on_associations
125
158
  end
126
159
 
127
- self.paranoid_value = self.class.delete_now_value
160
+ stale_paranoid_value
161
+ @destroyed = true
128
162
  freeze
129
163
  end
130
164
  end
@@ -134,140 +168,162 @@ module ActsAsParanoid
134
168
  if !deleted?
135
169
  with_transaction_returning_status do
136
170
  run_callbacks :destroy do
137
-
138
171
  if persisted?
139
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
140
- self.class.delete_all(Hash[[Array(self.class.primary_key), Array(self.id)].transpose])
141
-
172
+ # Handle composite keys, otherwise we would just use
173
+ # `self.class.primary_key.to_sym => self.id`.
174
+ self.class
175
+ .delete_all([Array(self.class.primary_key), Array(id)].transpose.to_h)
142
176
  decrement_counters_on_associations
143
177
  end
144
178
 
145
179
  @_trigger_destroy_callback = true
146
180
 
147
- self.paranoid_value = self.class.delete_now_value
181
+ stale_paranoid_value
148
182
  self
149
183
  end
150
184
  end
151
- else
152
- if paranoid_configuration[:double_tap_destroys_fully]
153
- destroy_fully!
154
- end
185
+ elsif paranoid_configuration[:double_tap_destroys_fully]
186
+ destroy_fully!
155
187
  end
156
188
  end
157
189
 
158
- alias_method :destroy, :destroy!
190
+ alias destroy destroy!
191
+
192
+ def recover(options = {})
193
+ return if !deleted?
159
194
 
160
- def recover(options={})
161
- return if !self.deleted?
162
195
  options = {
163
- :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
164
- :recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
196
+ recursive: self.class.paranoid_configuration[:recover_dependent_associations],
197
+ recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
198
+ raise_error: false
165
199
  }.merge(options)
166
200
 
167
201
  self.class.transaction do
168
202
  run_callbacks :recover do
169
- recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
170
203
  increment_counters_on_associations
171
- self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
172
- self.save
204
+ deleted_value = paranoid_value
205
+ self.paranoid_value = self.class.recovery_value
206
+ result = if options[:raise_error]
207
+ save!
208
+ else
209
+ save
210
+ end
211
+ recover_dependent_associations(deleted_value, options) if options[:recursive]
212
+ result
173
213
  end
174
214
  end
175
215
  end
176
216
 
177
- def recover_dependent_associations(window, options)
178
- self.class.dependent_associations.each do |reflection|
179
- next unless (klass = get_reflection_class(reflection)).paranoid?
180
-
181
- scope = klass.only_deleted
217
+ def recover!(options = {})
218
+ options[:raise_error] = true
182
219
 
183
- # Merge in the association's scope
184
- scope = if ActiveRecord::VERSION::MAJOR >= 6
185
- scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
186
- else
187
- scope.merge(association(reflection.name).association_scope)
188
- end
189
-
190
- # We can only recover by window if both parent and dependant have a
191
- # paranoid column type of :time.
192
- if self.class.paranoid_column_type == :time && klass.paranoid_column_type == :time
193
- scope = scope.deleted_inside_time_window(paranoid_value, window)
194
- end
220
+ recover(options)
221
+ end
195
222
 
196
- scope.each do |object|
197
- object.recover(options)
198
- end
223
+ def recover_dependent_associations(deleted_value, options)
224
+ self.class.dependent_associations.each do |reflection|
225
+ recover_dependent_association(reflection, deleted_value, options)
199
226
  end
200
227
  end
201
228
 
202
229
  def destroy_dependent_associations!
203
230
  self.class.dependent_associations.each do |reflection|
204
- next unless (klass = get_reflection_class(reflection)).paranoid?
205
-
206
- scope = klass.only_deleted
231
+ assoc = association(reflection.name)
232
+ next unless (klass = assoc.klass).paranoid?
207
233
 
208
- # Merge in the association's scope
209
- scope = if ActiveRecord::VERSION::MAJOR >= 6
210
- scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
211
- else
212
- scope.merge(association(reflection.name).association_scope)
213
- end
214
-
215
- scope.each do |object|
216
- object.destroy!
217
- end
234
+ klass
235
+ .only_deleted.merge(get_association_scope(assoc))
236
+ .each(&:destroy!)
218
237
  end
219
238
  end
220
239
 
221
240
  def deleted?
222
- !if self.class.string_type_with_deleted_value?
223
- paranoid_value != self.class.delete_now_value || paranoid_value.nil?
241
+ return true if @destroyed
242
+
243
+ if self.class.string_type_with_deleted_value?
244
+ paranoid_value == paranoid_configuration[:deleted_value]
224
245
  elsif self.class.boolean_type_not_nullable?
225
- paranoid_value == false
246
+ paranoid_value == true
226
247
  else
227
- paranoid_value.nil?
248
+ !paranoid_value.nil?
228
249
  end
229
250
  end
230
251
 
231
- alias_method :destroyed?, :deleted?
252
+ alias destroyed? deleted?
253
+
254
+ def deleted_fully?
255
+ @destroyed
256
+ end
257
+
258
+ alias destroyed_fully? deleted_fully?
232
259
 
233
260
  private
234
261
 
235
- def get_reflection_class(reflection)
236
- if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
237
- self.send(reflection.foreign_type).constantize
238
- else
239
- reflection.klass
262
+ def recover_dependent_association(reflection, deleted_value, options)
263
+ assoc = association(reflection.name)
264
+ return unless (klass = assoc.klass).paranoid?
265
+
266
+ if reflection.belongs_to? && attributes[reflection.association_foreign_key].nil?
267
+ return
240
268
  end
269
+
270
+ scope = klass.only_deleted.merge(get_association_scope(assoc))
271
+
272
+ # We can only recover by window if both parent and dependant have a
273
+ # paranoid column type of :time.
274
+ if self.class.paranoid_column_type == :time && klass.paranoid_column_type == :time
275
+ scope = scope.deleted_inside_time_window(deleted_value, options[:recovery_window])
276
+ end
277
+
278
+ recovered = false
279
+ scope.each do |object|
280
+ object.recover(options)
281
+ recovered = true
282
+ end
283
+
284
+ assoc.reload if recovered && reflection.has_one? && assoc.loaded?
285
+ end
286
+
287
+ def get_association_scope(dependent_association)
288
+ ActiveRecord::Associations::AssociationScope.scope(dependent_association)
241
289
  end
242
290
 
243
291
  def paranoid_value=(value)
244
- self.send("#{self.class.paranoid_column}=", value)
292
+ write_attribute(self.class.paranoid_column, value)
245
293
  end
246
294
 
247
- def update_counters_on_associations method_sym
295
+ def update_counters_on_associations(method_sym)
296
+ each_counter_cached_association_reflection do |assoc_reflection|
297
+ reflection_options = assoc_reflection.options
298
+ next unless reflection_options[:counter_cache]
248
299
 
249
- return unless [:decrement_counter, :increment_counter].include? method_sym
300
+ associated_object = send(assoc_reflection.name)
301
+ next unless associated_object
250
302
 
251
- each_counter_cached_association_reflection do |assoc_reflection|
252
- if associated_object = send(assoc_reflection.name)
253
- counter_cache_column = assoc_reflection.counter_cache_column
254
- associated_object.class.send(method_sym, counter_cache_column, associated_object.id)
255
- end
303
+ counter_cache_column = assoc_reflection.counter_cache_column
304
+ associated_object.class.send(method_sym, counter_cache_column,
305
+ associated_object.id)
306
+ associated_object.touch if reflection_options[:touch]
256
307
  end
257
308
  end
258
309
 
259
310
  def each_counter_cached_association_reflection
260
- _reflections.each do |name, reflection|
311
+ _reflections.each do |_name, reflection|
261
312
  yield reflection if reflection.belongs_to? && reflection.counter_cache_column
262
313
  end
263
314
  end
264
315
 
265
316
  def increment_counters_on_associations
266
- update_counters_on_associations :increment_counter
317
+ update_counters_on_associations :increment_counter
267
318
  end
268
319
 
269
320
  def decrement_counters_on_associations
270
- update_counters_on_associations :decrement_counter
321
+ update_counters_on_associations :decrement_counter
322
+ end
323
+
324
+ def stale_paranoid_value
325
+ self.paranoid_value = self.class.delete_now_value
326
+ clear_attribute_changes([self.class.paranoid_column])
271
327
  end
272
328
  end
273
329
  end