acts_as_paranoid 0.5.0 → 0.7.0

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 }
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,37 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsParanoid
2
4
  module Associations
3
5
  def self.included(base)
4
6
  base.extend ClassMethods
5
7
  class << base
6
- alias_method_chain :belongs_to, :deleted
8
+ alias_method :belongs_to_without_deleted, :belongs_to
9
+ alias_method :belongs_to, :belongs_to_with_deleted
7
10
  end
8
11
  end
9
12
 
10
13
  module ClassMethods
11
14
  def belongs_to_with_deleted(target, scope = nil, options = {})
12
- with_deleted = (scope.is_a?(Hash) ? scope : options).delete(:with_deleted)
13
- result = belongs_to_without_deleted(target, scope, options)
15
+ if scope.is_a?(Hash)
16
+ options = scope
17
+ scope = nil
18
+ end
14
19
 
20
+ with_deleted = options.delete(:with_deleted)
15
21
  if with_deleted
16
- if result.is_a? Hash
17
- 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
18
31
  else
19
- result.options[:with_deleted] = with_deleted
20
- end
21
-
22
- unless method_defined? "#{target}_with_unscoped"
23
- class_eval <<-RUBY, __FILE__, __LINE__
24
- def #{target}_with_unscoped(*args)
25
- association = association(:#{target})
26
- return nil if association.options[:polymorphic] && association.klass.nil?
27
- return #{target}_without_unscoped(*args) unless association.klass.paranoid?
28
- 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
29
37
  end
30
- alias_method_chain :#{target}, :unscoped
31
- RUBY
38
+ end
32
39
  end
33
40
  end
34
41
 
42
+ result = belongs_to_without_deleted(target, scope, **options)
43
+
44
+ result.values.last.options[:with_deleted] = with_deleted if with_deleted
45
+
35
46
  result
36
47
  end
37
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,16 +119,31 @@ 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)
123
+ end
124
+
125
+ # Straight from ActiveRecord 5.1!
126
+ def delete
127
+ self.class.delete(id) if persisted?
128
+ stale_paranoid_value
129
+ freeze
106
130
  end
107
131
 
108
132
  def destroy_fully!
109
133
  with_transaction_returning_status do
110
134
  run_callbacks :destroy do
111
135
  destroy_dependent_associations!
112
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
113
- self.class.delete_all!(Hash[[Array(self.class.primary_key), Array(self.id)].transpose]) if persisted?
114
- 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!(Hash[[Array(self.class.primary_key), Array(id)].transpose])
142
+ decrement_counters_on_associations
143
+ end
144
+
145
+ stale_paranoid_value
146
+ @destroyed = true
115
147
  freeze
116
148
  end
117
149
  end
@@ -121,43 +153,63 @@ module ActsAsParanoid
121
153
  if !deleted?
122
154
  with_transaction_returning_status do
123
155
  run_callbacks :destroy do
124
- # Handle composite keys, otherwise we would just use `self.class.primary_key.to_sym => self.id`.
125
- self.class.delete_all(Hash[[Array(self.class.primary_key), Array(self.id)].transpose]) if persisted?
126
- 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(Hash[[Array(self.class.primary_key), Array(id)].transpose])
161
+ decrement_counters_on_associations
162
+ end
163
+
164
+ @_trigger_destroy_callback = true
165
+
166
+ stale_paranoid_value
127
167
  self
128
168
  end
129
169
  end
130
- else
170
+ elsif paranoid_configuration[:double_tap_destroys_fully]
131
171
  destroy_fully!
132
172
  end
133
173
  end
134
174
 
135
- alias_method :destroy, :destroy!
175
+ alias destroy destroy!
176
+
177
+ def recover(options = {})
178
+ return if !deleted?
136
179
 
137
- def recover(options={})
138
180
  options = {
139
- :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
140
- :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
141
184
  }.merge(options)
142
185
 
143
186
  self.class.transaction do
144
187
  run_callbacks :recover do
145
- recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
146
-
147
- self.paranoid_value = nil
148
- self.save
188
+ if options[:recursive]
189
+ recover_dependent_associations(options[:recovery_window], options)
190
+ end
191
+ increment_counters_on_associations
192
+ self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
193
+ if options[:raise_error]
194
+ save!
195
+ else
196
+ save
197
+ end
149
198
  end
150
199
  end
151
200
  end
152
201
 
202
+ def recover!(options = {})
203
+ options[:raise_error] = true
204
+
205
+ recover(options)
206
+ end
207
+
153
208
  def recover_dependent_associations(window, options)
154
209
  self.class.dependent_associations.each do |reflection|
155
210
  next unless (klass = get_reflection_class(reflection)).paranoid?
156
211
 
157
- scope = klass.only_deleted
158
-
159
- # Merge in the association's scope
160
- scope = scope.merge(association(reflection.name).association_scope)
212
+ scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
161
213
 
162
214
  # We can only recover by window if both parent and dependant have a
163
215
  # paranoid column type of :time.
@@ -175,41 +227,80 @@ module ActsAsParanoid
175
227
  self.class.dependent_associations.each do |reflection|
176
228
  next unless (klass = get_reflection_class(reflection)).paranoid?
177
229
 
178
- scope = klass.only_deleted
179
-
180
- # Merge in the association's scope
181
- scope = scope.merge(association(reflection.name).association_scope)
182
-
183
- scope.each do |object|
184
- object.destroy!
185
- end
230
+ klass
231
+ .only_deleted.merge(get_association_scope(reflection: reflection))
232
+ .each(&:destroy!)
186
233
  end
187
234
  end
188
235
 
189
236
  def deleted?
190
- !if self.class.string_type_with_deleted_value?
191
- 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]
192
241
  elsif self.class.boolean_type_not_nullable?
193
- paranoid_value == false
242
+ paranoid_value == true
194
243
  else
195
- paranoid_value.nil?
244
+ !paranoid_value.nil?
196
245
  end
197
246
  end
198
247
 
199
- alias_method :destroyed?, :deleted?
248
+ alias destroyed? deleted?
249
+
250
+ def deleted_fully?
251
+ @destroyed
252
+ end
253
+
254
+ alias destroyed_fully? deleted_fully?
200
255
 
201
256
  private
202
257
 
258
+ def get_association_scope(reflection:)
259
+ ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
260
+ end
261
+
203
262
  def get_reflection_class(reflection)
204
263
  if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
205
- self.send(reflection.foreign_type).constantize
264
+ send(reflection.foreign_type).constantize
206
265
  else
207
266
  reflection.klass
208
267
  end
209
268
  end
210
269
 
211
270
  def paranoid_value=(value)
212
- 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
+ return unless [:decrement_counter, :increment_counter].include? method_sym
276
+
277
+ each_counter_cached_association_reflection do |assoc_reflection|
278
+ associated_object = send(assoc_reflection.name)
279
+ next unless associated_object
280
+
281
+ counter_cache_column = assoc_reflection.counter_cache_column
282
+ associated_object.class.send(method_sym, counter_cache_column,
283
+ associated_object.id)
284
+ end
285
+ end
286
+
287
+ def each_counter_cached_association_reflection
288
+ _reflections.each do |_name, reflection|
289
+ yield reflection if reflection.belongs_to? && reflection.counter_cache_column
290
+ end
291
+ end
292
+
293
+ def increment_counters_on_associations
294
+ update_counters_on_associations :increment_counter
295
+ end
296
+
297
+ def decrement_counters_on_associations
298
+ update_counters_on_associations :decrement_counter
299
+ end
300
+
301
+ def stale_paranoid_value
302
+ self.paranoid_value = self.class.delete_now_value
303
+ clear_attribute_changes([self.class.paranoid_column])
213
304
  end
214
305
  end
215
306
  end