acts_as_paranoid 0.6.0 → 0.7.1

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,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, :recovery_value => nil }
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,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 { #{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
 
@@ -116,9 +133,17 @@ module ActsAsParanoid
116
133
  with_transaction_returning_status do
117
134
  run_callbacks :destroy do
118
135
  destroy_dependent_associations!
119
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
120
- self.class.delete_all!(Hash[[Array(self.class.primary_key), Array(self.id)].transpose]) if persisted?
121
- 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!([Array(self.class.primary_key), Array(id)].transpose.to_h)
142
+ decrement_counters_on_associations
143
+ end
144
+
145
+ stale_paranoid_value
146
+ @destroyed = true
122
147
  freeze
123
148
  end
124
149
  end
@@ -128,48 +153,63 @@ module ActsAsParanoid
128
153
  if !deleted?
129
154
  with_transaction_returning_status do
130
155
  run_callbacks :destroy do
131
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
132
- @_trigger_destroy_callback = if persisted?
133
- self.class.delete_all(Hash[[Array(self.class.primary_key), Array(self.id)].transpose])
134
- else
135
- true
136
- end
137
-
138
- 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([Array(self.class.primary_key), Array(id)].transpose.to_h)
161
+ decrement_counters_on_associations
162
+ end
163
+
164
+ @_trigger_destroy_callback = true
165
+
166
+ stale_paranoid_value
139
167
  self
140
168
  end
141
169
  end
142
- else
170
+ elsif paranoid_configuration[:double_tap_destroys_fully]
143
171
  destroy_fully!
144
172
  end
145
173
  end
146
174
 
147
- alias_method :destroy, :destroy!
175
+ alias destroy destroy!
176
+
177
+ def recover(options = {})
178
+ return if !deleted?
148
179
 
149
- def recover(options={})
150
180
  options = {
151
- :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
152
- :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
153
184
  }.merge(options)
154
185
 
155
186
  self.class.transaction do
156
187
  run_callbacks :recover do
157
- recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
158
-
188
+ if options[:recursive]
189
+ recover_dependent_associations(options[:recovery_window], options)
190
+ end
191
+ increment_counters_on_associations
159
192
  self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
160
- self.save
193
+ if options[:raise_error]
194
+ save!
195
+ else
196
+ save
197
+ end
161
198
  end
162
199
  end
163
200
  end
164
201
 
202
+ def recover!(options = {})
203
+ options[:raise_error] = true
204
+
205
+ recover(options)
206
+ end
207
+
165
208
  def recover_dependent_associations(window, options)
166
209
  self.class.dependent_associations.each do |reflection|
167
210
  next unless (klass = get_reflection_class(reflection)).paranoid?
168
211
 
169
- scope = klass.only_deleted
170
-
171
- # Merge in the association's scope
172
- scope = scope.merge(association(reflection.name).association_scope)
212
+ scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
173
213
 
174
214
  # We can only recover by window if both parent and dependant have a
175
215
  # paranoid column type of :time.
@@ -187,41 +227,82 @@ module ActsAsParanoid
187
227
  self.class.dependent_associations.each do |reflection|
188
228
  next unless (klass = get_reflection_class(reflection)).paranoid?
189
229
 
190
- scope = klass.only_deleted
191
-
192
- # Merge in the association's scope
193
- scope = scope.merge(association(reflection.name).association_scope)
194
-
195
- scope.each do |object|
196
- object.destroy!
197
- end
230
+ klass
231
+ .only_deleted.merge(get_association_scope(reflection: reflection))
232
+ .each(&:destroy!)
198
233
  end
199
234
  end
200
235
 
201
236
  def deleted?
202
- !if self.class.string_type_with_deleted_value?
203
- 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]
204
241
  elsif self.class.boolean_type_not_nullable?
205
- paranoid_value == false
242
+ paranoid_value == true
206
243
  else
207
- paranoid_value.nil?
244
+ !paranoid_value.nil?
208
245
  end
209
246
  end
210
247
 
211
- alias_method :destroyed?, :deleted?
248
+ alias destroyed? deleted?
249
+
250
+ def deleted_fully?
251
+ @destroyed
252
+ end
253
+
254
+ alias destroyed_fully? deleted_fully?
212
255
 
213
256
  private
214
257
 
258
+ def get_association_scope(reflection:)
259
+ ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
260
+ end
261
+
215
262
  def get_reflection_class(reflection)
216
263
  if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
217
- self.send(reflection.foreign_type).constantize
264
+ send(reflection.foreign_type).constantize
218
265
  else
219
266
  reflection.klass
220
267
  end
221
268
  end
222
269
 
223
270
  def paranoid_value=(value)
224
- 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
+ each_counter_cached_association_reflection do |assoc_reflection|
276
+ reflection_options = assoc_reflection.options
277
+ next unless reflection_options[:counter_cache]
278
+
279
+ associated_object = send(assoc_reflection.name)
280
+ next unless associated_object
281
+
282
+ counter_cache_column = assoc_reflection.counter_cache_column
283
+ associated_object.class.send(method_sym, counter_cache_column,
284
+ associated_object.id)
285
+ associated_object.touch if reflection_options[:touch]
286
+ end
287
+ end
288
+
289
+ def each_counter_cached_association_reflection
290
+ _reflections.each do |_name, reflection|
291
+ yield reflection if reflection.belongs_to? && reflection.counter_cache_column
292
+ end
293
+ end
294
+
295
+ def increment_counters_on_associations
296
+ update_counters_on_associations :increment_counter
297
+ end
298
+
299
+ def decrement_counters_on_associations
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])
225
306
  end
226
307
  end
227
308
  end