acts_as_paranoid 0.6.1 → 0.7.2

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