acts_as_paranoid 0.6.0 → 0.7.1

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