acts_as_paranoid 0.6.2 → 0.7.3

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,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