acts_as_paranoid 0.6.1 → 0.7.2

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,34 @@ 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
19
+ if !options.is_a?(Hash) && !options.empty?
20
+ raise ArgumentError, "Hash expected, got #{options.class.name}"
21
+ end
21
22
 
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
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
+ elsif options[:column_type] == "boolean" && !options[:allow_nulls]
36
+ paranoid_configuration.merge!(recovery_value: false)
37
+ elsif options[:column_type] == "boolean"
38
+ paranoid_configuration.merge!(allow_nulls: true)
39
+ end
26
40
 
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]
41
+ paranoid_configuration.merge!(options) # user options
28
42
 
29
- self.paranoid_column_reference = "#{self.table_name}.#{paranoid_configuration[:column]}"
43
+ unless %w[time boolean string].include? paranoid_configuration[:column_type]
44
+ raise ArgumentError, "'time', 'boolean' or 'string' expected" \
45
+ " for :column_type option, got #{paranoid_configuration[:column_type]}"
46
+ end
30
47
 
31
48
  return if paranoid?
32
49
 
@@ -35,28 +52,18 @@ module ActsAsParanoid
35
52
  # Magic!
36
53
  default_scope { where(paranoid_default_scope) }
37
54
 
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
55
+ define_deleted_time_scopes if paranoid_column_type == :time
46
56
  end
47
57
  end
48
58
 
49
59
  # Extend ActiveRecord's functionality
50
- ActiveRecord::Base.send :extend, ActsAsParanoid
60
+ ActiveRecord::Base.extend ActsAsParanoid
51
61
 
52
62
  # Extend ActiveRecord::Base with paranoid associations
53
- ActiveRecord::Base.send :include, ActsAsParanoid::Associations
63
+ ActiveRecord::Base.include ActsAsParanoid::Associations
54
64
 
55
65
  # Override ActiveRecord::Relation's behavior
56
- ActiveRecord::Relation.send :include, ActsAsParanoid::Relation
66
+ ActiveRecord::Relation.include ActsAsParanoid::Relation
57
67
 
58
68
  # Push the recover callback onto the activerecord callback list
59
69
  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,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,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,13 +119,13 @@ 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)
106
123
  end
107
124
 
108
125
  # Straight from ActiveRecord 5.1!
109
126
  def delete
110
127
  self.class.delete(id) if persisted?
111
- @destroyed = true
128
+ stale_paranoid_value
112
129
  freeze
113
130
  end
114
131
 
@@ -118,13 +135,15 @@ module ActsAsParanoid
118
135
  destroy_dependent_associations!
119
136
 
120
137
  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
-
138
+ # Handle composite keys, otherwise we would just use
139
+ # `self.class.primary_key.to_sym => self.id`.
140
+ self.class
141
+ .delete_all!([Array(self.class.primary_key), Array(id)].transpose.to_h)
124
142
  decrement_counters_on_associations
125
143
  end
126
144
 
127
- self.paranoid_value = self.class.delete_now_value
145
+ stale_paranoid_value
146
+ @destroyed = true
128
147
  freeze
129
148
  end
130
149
  end
@@ -134,57 +153,63 @@ module ActsAsParanoid
134
153
  if !deleted?
135
154
  with_transaction_returning_status do
136
155
  run_callbacks :destroy do
137
-
138
156
  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
-
157
+ # Handle composite keys, otherwise we would just use
158
+ # `self.class.primary_key.to_sym => self.id`.
159
+ self.class
160
+ .delete_all([Array(self.class.primary_key), Array(id)].transpose.to_h)
142
161
  decrement_counters_on_associations
143
162
  end
144
163
 
145
164
  @_trigger_destroy_callback = true
146
165
 
147
- self.paranoid_value = self.class.delete_now_value
166
+ stale_paranoid_value
148
167
  self
149
168
  end
150
169
  end
151
- else
152
- if paranoid_configuration[:double_tap_destroys_fully]
153
- destroy_fully!
154
- end
170
+ elsif paranoid_configuration[:double_tap_destroys_fully]
171
+ destroy_fully!
155
172
  end
156
173
  end
157
174
 
158
- alias_method :destroy, :destroy!
175
+ alias destroy destroy!
176
+
177
+ def recover(options = {})
178
+ return if !deleted?
159
179
 
160
- def recover(options={})
161
180
  options = {
162
- :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
163
- :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
164
184
  }.merge(options)
165
185
 
166
186
  self.class.transaction do
167
187
  run_callbacks :recover do
168
- recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
188
+ if options[:recursive]
189
+ recover_dependent_associations(options[:recovery_window], options)
190
+ end
169
191
  increment_counters_on_associations
170
192
  self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
171
- self.save
193
+ if options[:raise_error]
194
+ save!
195
+ else
196
+ save
197
+ end
172
198
  end
173
199
  end
174
200
  end
175
201
 
202
+ def recover!(options = {})
203
+ options[:raise_error] = true
204
+
205
+ recover(options)
206
+ end
207
+
176
208
  def recover_dependent_associations(window, options)
177
209
  self.class.dependent_associations.each do |reflection|
178
210
  next unless (klass = get_reflection_class(reflection)).paranoid?
179
211
 
180
- scope = klass.only_deleted
181
-
182
- # Merge in the association's scope
183
- scope = if ActiveRecord::VERSION::MAJOR >= 6
184
- scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
185
- else
186
- scope.merge(association(reflection.name).association_scope)
187
- end
212
+ scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
188
213
 
189
214
  # We can only recover by window if both parent and dependant have a
190
215
  # paranoid column type of :time.
@@ -202,70 +227,82 @@ module ActsAsParanoid
202
227
  self.class.dependent_associations.each do |reflection|
203
228
  next unless (klass = get_reflection_class(reflection)).paranoid?
204
229
 
205
- scope = klass.only_deleted
206
-
207
- # Merge in the association's scope
208
- scope = if ActiveRecord::VERSION::MAJOR >= 6
209
- scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
210
- else
211
- scope.merge(association(reflection.name).association_scope)
212
- end
213
-
214
- scope.each do |object|
215
- object.destroy!
216
- end
230
+ klass
231
+ .only_deleted.merge(get_association_scope(reflection: reflection))
232
+ .each(&:destroy!)
217
233
  end
218
234
  end
219
235
 
220
236
  def deleted?
221
- !if self.class.string_type_with_deleted_value?
222
- 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]
223
241
  elsif self.class.boolean_type_not_nullable?
224
- paranoid_value == false
242
+ paranoid_value == true
225
243
  else
226
- paranoid_value.nil?
244
+ !paranoid_value.nil?
227
245
  end
228
246
  end
229
247
 
230
- alias_method :destroyed?, :deleted?
248
+ alias destroyed? deleted?
249
+
250
+ def deleted_fully?
251
+ @destroyed
252
+ end
253
+
254
+ alias destroyed_fully? deleted_fully?
231
255
 
232
256
  private
233
257
 
258
+ def get_association_scope(reflection:)
259
+ ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
260
+ end
261
+
234
262
  def get_reflection_class(reflection)
235
263
  if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
236
- self.send(reflection.foreign_type).constantize
264
+ send(reflection.foreign_type).constantize
237
265
  else
238
266
  reflection.klass
239
267
  end
240
268
  end
241
269
 
242
270
  def paranoid_value=(value)
243
- self.send("#{self.class.paranoid_column}=", value)
271
+ write_attribute(self.class.paranoid_column, value)
244
272
  end
245
273
 
246
- def update_counters_on_associations method_sym
247
-
248
- return unless [:decrement_counter, :increment_counter].include? method_sym
249
-
274
+ def update_counters_on_associations(method_sym)
250
275
  each_counter_cached_association_reflection do |assoc_reflection|
276
+ reflection_options = assoc_reflection.options
277
+ next unless reflection_options[:counter_cache]
278
+
251
279
  associated_object = send(assoc_reflection.name)
280
+ next unless associated_object
281
+
252
282
  counter_cache_column = assoc_reflection.counter_cache_column
253
- associated_object.class.send(method_sym, counter_cache_column, associated_object.id)
283
+ associated_object.class.send(method_sym, counter_cache_column,
284
+ associated_object.id)
285
+ associated_object.touch if reflection_options[:touch]
254
286
  end
255
287
  end
256
288
 
257
289
  def each_counter_cached_association_reflection
258
- _reflections.each do |name, reflection|
259
- yield reflection if reflection.belongs_to? && reflection.counter_cache_column == "#{self.class.name.underscore.pluralize}_count"
290
+ _reflections.each do |_name, reflection|
291
+ yield reflection if reflection.belongs_to? && reflection.counter_cache_column
260
292
  end
261
293
  end
262
294
 
263
295
  def increment_counters_on_associations
264
- update_counters_on_associations :increment_counter
296
+ update_counters_on_associations :increment_counter
265
297
  end
266
298
 
267
299
  def decrement_counters_on_associations
268
- update_counters_on_associations :decrement_counter
300
+ update_counters_on_associations :decrement_counter
301
+ end
302
+
303
+ def stale_paranoid_value
304
+ self.paranoid_value = self.class.delete_now_value
305
+ clear_attribute_changes([self.class.paranoid_column])
269
306
  end
270
307
  end
271
308
  end