acts_as_paranoid 0.6.2 → 0.7.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -10
- data/CONTRIBUTING.md +59 -0
- data/README.md +151 -57
- data/lib/acts_as_paranoid.rb +29 -29
- data/lib/acts_as_paranoid/associations.rb +21 -17
- data/lib/acts_as_paranoid/core.rb +151 -95
- data/lib/acts_as_paranoid/relation.rb +9 -0
- data/lib/acts_as_paranoid/validations.rb +8 -74
- data/lib/acts_as_paranoid/version.rb +3 -1
- data/test/test_associations.rb +118 -46
- data/test/test_core.rb +264 -66
- data/test/test_default_scopes.rb +20 -7
- data/test/test_dependent_recovery.rb +54 -0
- data/test/test_deprecated_behavior.rb +30 -0
- data/test/test_helper.rb +130 -95
- data/test/test_inheritance.rb +3 -1
- data/test/test_relations.rb +98 -16
- data/test/test_table_namespace.rb +40 -0
- data/test/test_validations.rb +27 -7
- metadata +78 -32
- data/lib/acts_as_paranoid/preloader_association.rb +0 -16
- data/test/test_preloader_association.rb +0 -27
data/lib/acts_as_paranoid.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
11
|
+
included_modules.include?(ActsAsParanoid::Core)
|
11
12
|
end
|
12
13
|
|
13
14
|
def validates_as_paranoid
|
@@ -15,19 +16,28 @@ module ActsAsParanoid
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def acts_as_paranoid(options = {})
|
18
|
-
|
19
|
+
if !options.is_a?(Hash) && !options.empty?
|
20
|
+
raise ArgumentError, "Hash expected, got #{options.class.name}"
|
21
|
+
end
|
19
22
|
|
20
23
|
class_attribute :paranoid_configuration
|
21
24
|
|
22
|
-
self.paranoid_configuration = {
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
self.paranoid_configuration = {
|
26
|
+
column: "deleted_at",
|
27
|
+
column_type: "time",
|
28
|
+
recover_dependent_associations: true,
|
29
|
+
dependent_recovery_window: 2.minutes,
|
30
|
+
double_tap_destroys_fully: true
|
31
|
+
}
|
32
|
+
if options[:column_type] == "string"
|
33
|
+
paranoid_configuration.merge!(deleted_value: "deleted")
|
34
|
+
end
|
26
35
|
|
27
|
-
|
36
|
+
paranoid_configuration.merge!(options) # user options
|
28
37
|
|
29
|
-
|
30
|
-
"
|
38
|
+
unless %w[time boolean string].include? paranoid_configuration[:column_type]
|
39
|
+
raise ArgumentError, "'time', 'boolean' or 'string' expected" \
|
40
|
+
" for :column_type option, got #{paranoid_configuration[:column_type]}"
|
31
41
|
end
|
32
42
|
|
33
43
|
return if paranoid?
|
@@ -37,28 +47,18 @@ module ActsAsParanoid
|
|
37
47
|
# Magic!
|
38
48
|
default_scope { where(paranoid_default_scope) }
|
39
49
|
|
40
|
-
if
|
41
|
-
scope :deleted_inside_time_window, lambda {|time, window|
|
42
|
-
deleted_after_time((time - window)).deleted_before_time((time + window))
|
43
|
-
}
|
44
|
-
|
45
|
-
scope :deleted_after_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} > ?", time) }
|
46
|
-
scope :deleted_before_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} < ?", time) }
|
47
|
-
end
|
50
|
+
define_deleted_time_scopes if paranoid_column_type == :time
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
51
54
|
# Extend ActiveRecord's functionality
|
52
|
-
ActiveRecord::Base.
|
55
|
+
ActiveRecord::Base.extend ActsAsParanoid
|
53
56
|
|
54
57
|
# Extend ActiveRecord::Base with paranoid associations
|
55
|
-
ActiveRecord::Base.
|
58
|
+
ActiveRecord::Base.include ActsAsParanoid::Associations
|
56
59
|
|
57
60
|
# Override ActiveRecord::Relation's behavior
|
58
|
-
ActiveRecord::Relation.
|
61
|
+
ActiveRecord::Relation.include ActsAsParanoid::Relation
|
59
62
|
|
60
63
|
# Push the recover callback onto the activerecord callback list
|
61
64
|
ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
|
62
|
-
|
63
|
-
# Use with_deleted in preloader build_scope
|
64
|
-
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
|
23
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
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)
|
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
|
-
|
45
|
-
or(
|
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
|
-
|
51
|
+
all.table[paranoid_column].eq(false)
|
48
52
|
else
|
49
|
-
|
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
|
-
|
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,42 @@ module ActsAsParanoid
|
|
78
88
|
end
|
79
89
|
end
|
80
90
|
|
81
|
-
|
91
|
+
def recovery_value
|
92
|
+
if paranoid_configuration.key? :recovery_value
|
93
|
+
ActiveSupport::Deprecation.warn \
|
94
|
+
"The recovery_value setting is deprecated and will be removed in" \
|
95
|
+
" ActsAsParanoid 0.8.0"
|
96
|
+
paranoid_configuration[:recovery_value]
|
97
|
+
elsif boolean_type_not_nullable?
|
98
|
+
false
|
99
|
+
else
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
def define_deleted_time_scopes
|
107
|
+
scope :deleted_inside_time_window, lambda { |time, window|
|
108
|
+
deleted_after_time((time - window)).deleted_before_time((time + window))
|
109
|
+
}
|
110
|
+
|
111
|
+
scope :deleted_after_time, lambda { |time|
|
112
|
+
only_deleted
|
113
|
+
.where("#{table_name}.#{paranoid_column} > ?", time)
|
114
|
+
}
|
115
|
+
scope :deleted_before_time, lambda { |time|
|
116
|
+
only_deleted
|
117
|
+
.where("#{table_name}.#{paranoid_column} < ?", time)
|
118
|
+
}
|
119
|
+
end
|
82
120
|
|
83
121
|
def without_paranoid_default_scope
|
84
|
-
scope =
|
122
|
+
scope = all
|
85
123
|
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
124
|
+
scope = scope.unscope(where: paranoid_column)
|
125
|
+
# Fix problems with unscope group chain
|
126
|
+
scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
|
95
127
|
|
96
128
|
scope
|
97
129
|
end
|
@@ -102,13 +134,13 @@ module ActsAsParanoid
|
|
102
134
|
end
|
103
135
|
|
104
136
|
def paranoid_value
|
105
|
-
|
137
|
+
send(self.class.paranoid_column)
|
106
138
|
end
|
107
139
|
|
108
140
|
# Straight from ActiveRecord 5.1!
|
109
141
|
def delete
|
110
142
|
self.class.delete(id) if persisted?
|
111
|
-
|
143
|
+
stale_paranoid_value
|
112
144
|
freeze
|
113
145
|
end
|
114
146
|
|
@@ -118,13 +150,15 @@ module ActsAsParanoid
|
|
118
150
|
destroy_dependent_associations!
|
119
151
|
|
120
152
|
if persisted?
|
121
|
-
# Handle composite keys, otherwise we would just use
|
122
|
-
self.class.
|
123
|
-
|
153
|
+
# Handle composite keys, otherwise we would just use
|
154
|
+
# `self.class.primary_key.to_sym => self.id`.
|
155
|
+
self.class
|
156
|
+
.delete_all!([Array(self.class.primary_key), Array(id)].transpose.to_h)
|
124
157
|
decrement_counters_on_associations
|
125
158
|
end
|
126
159
|
|
127
|
-
|
160
|
+
stale_paranoid_value
|
161
|
+
@destroyed = true
|
128
162
|
freeze
|
129
163
|
end
|
130
164
|
end
|
@@ -134,140 +168,162 @@ module ActsAsParanoid
|
|
134
168
|
if !deleted?
|
135
169
|
with_transaction_returning_status do
|
136
170
|
run_callbacks :destroy do
|
137
|
-
|
138
171
|
if persisted?
|
139
|
-
# Handle composite keys, otherwise we would just use
|
140
|
-
self.class.
|
141
|
-
|
172
|
+
# Handle composite keys, otherwise we would just use
|
173
|
+
# `self.class.primary_key.to_sym => self.id`.
|
174
|
+
self.class
|
175
|
+
.delete_all([Array(self.class.primary_key), Array(id)].transpose.to_h)
|
142
176
|
decrement_counters_on_associations
|
143
177
|
end
|
144
178
|
|
145
179
|
@_trigger_destroy_callback = true
|
146
180
|
|
147
|
-
|
181
|
+
stale_paranoid_value
|
148
182
|
self
|
149
183
|
end
|
150
184
|
end
|
151
|
-
|
152
|
-
|
153
|
-
destroy_fully!
|
154
|
-
end
|
185
|
+
elsif paranoid_configuration[:double_tap_destroys_fully]
|
186
|
+
destroy_fully!
|
155
187
|
end
|
156
188
|
end
|
157
189
|
|
158
|
-
|
190
|
+
alias destroy destroy!
|
191
|
+
|
192
|
+
def recover(options = {})
|
193
|
+
return if !deleted?
|
159
194
|
|
160
|
-
def recover(options={})
|
161
|
-
return if !self.deleted?
|
162
195
|
options = {
|
163
|
-
:
|
164
|
-
:
|
196
|
+
recursive: self.class.paranoid_configuration[:recover_dependent_associations],
|
197
|
+
recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
|
198
|
+
raise_error: false
|
165
199
|
}.merge(options)
|
166
200
|
|
167
201
|
self.class.transaction do
|
168
202
|
run_callbacks :recover do
|
169
|
-
recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
|
170
203
|
increment_counters_on_associations
|
171
|
-
|
172
|
-
self.
|
204
|
+
deleted_value = paranoid_value
|
205
|
+
self.paranoid_value = self.class.recovery_value
|
206
|
+
result = if options[:raise_error]
|
207
|
+
save!
|
208
|
+
else
|
209
|
+
save
|
210
|
+
end
|
211
|
+
recover_dependent_associations(deleted_value, options) if options[:recursive]
|
212
|
+
result
|
173
213
|
end
|
174
214
|
end
|
175
215
|
end
|
176
216
|
|
177
|
-
def
|
178
|
-
|
179
|
-
next unless (klass = get_reflection_class(reflection)).paranoid?
|
180
|
-
|
181
|
-
scope = klass.only_deleted
|
217
|
+
def recover!(options = {})
|
218
|
+
options[:raise_error] = true
|
182
219
|
|
183
|
-
|
184
|
-
|
185
|
-
scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
|
186
|
-
else
|
187
|
-
scope.merge(association(reflection.name).association_scope)
|
188
|
-
end
|
189
|
-
|
190
|
-
# We can only recover by window if both parent and dependant have a
|
191
|
-
# paranoid column type of :time.
|
192
|
-
if self.class.paranoid_column_type == :time && klass.paranoid_column_type == :time
|
193
|
-
scope = scope.deleted_inside_time_window(paranoid_value, window)
|
194
|
-
end
|
220
|
+
recover(options)
|
221
|
+
end
|
195
222
|
|
196
|
-
|
197
|
-
|
198
|
-
|
223
|
+
def recover_dependent_associations(deleted_value, options)
|
224
|
+
self.class.dependent_associations.each do |reflection|
|
225
|
+
recover_dependent_association(reflection, deleted_value, options)
|
199
226
|
end
|
200
227
|
end
|
201
228
|
|
202
229
|
def destroy_dependent_associations!
|
203
230
|
self.class.dependent_associations.each do |reflection|
|
204
|
-
|
205
|
-
|
206
|
-
scope = klass.only_deleted
|
231
|
+
assoc = association(reflection.name)
|
232
|
+
next unless (klass = assoc.klass).paranoid?
|
207
233
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
else
|
212
|
-
scope.merge(association(reflection.name).association_scope)
|
213
|
-
end
|
214
|
-
|
215
|
-
scope.each do |object|
|
216
|
-
object.destroy!
|
217
|
-
end
|
234
|
+
klass
|
235
|
+
.only_deleted.merge(get_association_scope(assoc))
|
236
|
+
.each(&:destroy!)
|
218
237
|
end
|
219
238
|
end
|
220
239
|
|
221
240
|
def deleted?
|
222
|
-
|
223
|
-
|
241
|
+
return true if @destroyed
|
242
|
+
|
243
|
+
if self.class.string_type_with_deleted_value?
|
244
|
+
paranoid_value == paranoid_configuration[:deleted_value]
|
224
245
|
elsif self.class.boolean_type_not_nullable?
|
225
|
-
paranoid_value ==
|
246
|
+
paranoid_value == true
|
226
247
|
else
|
227
|
-
paranoid_value.nil?
|
248
|
+
!paranoid_value.nil?
|
228
249
|
end
|
229
250
|
end
|
230
251
|
|
231
|
-
|
252
|
+
alias destroyed? deleted?
|
253
|
+
|
254
|
+
def deleted_fully?
|
255
|
+
@destroyed
|
256
|
+
end
|
257
|
+
|
258
|
+
alias destroyed_fully? deleted_fully?
|
232
259
|
|
233
260
|
private
|
234
261
|
|
235
|
-
def
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
262
|
+
def recover_dependent_association(reflection, deleted_value, options)
|
263
|
+
assoc = association(reflection.name)
|
264
|
+
return unless (klass = assoc.klass).paranoid?
|
265
|
+
|
266
|
+
if reflection.belongs_to? && attributes[reflection.association_foreign_key].nil?
|
267
|
+
return
|
240
268
|
end
|
269
|
+
|
270
|
+
scope = klass.only_deleted.merge(get_association_scope(assoc))
|
271
|
+
|
272
|
+
# We can only recover by window if both parent and dependant have a
|
273
|
+
# paranoid column type of :time.
|
274
|
+
if self.class.paranoid_column_type == :time && klass.paranoid_column_type == :time
|
275
|
+
scope = scope.deleted_inside_time_window(deleted_value, options[:recovery_window])
|
276
|
+
end
|
277
|
+
|
278
|
+
recovered = false
|
279
|
+
scope.each do |object|
|
280
|
+
object.recover(options)
|
281
|
+
recovered = true
|
282
|
+
end
|
283
|
+
|
284
|
+
assoc.reload if recovered && reflection.has_one? && assoc.loaded?
|
285
|
+
end
|
286
|
+
|
287
|
+
def get_association_scope(dependent_association)
|
288
|
+
ActiveRecord::Associations::AssociationScope.scope(dependent_association)
|
241
289
|
end
|
242
290
|
|
243
291
|
def paranoid_value=(value)
|
244
|
-
|
292
|
+
write_attribute(self.class.paranoid_column, value)
|
245
293
|
end
|
246
294
|
|
247
|
-
def update_counters_on_associations
|
295
|
+
def update_counters_on_associations(method_sym)
|
296
|
+
each_counter_cached_association_reflection do |assoc_reflection|
|
297
|
+
reflection_options = assoc_reflection.options
|
298
|
+
next unless reflection_options[:counter_cache]
|
248
299
|
|
249
|
-
|
300
|
+
associated_object = send(assoc_reflection.name)
|
301
|
+
next unless associated_object
|
250
302
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
end
|
303
|
+
counter_cache_column = assoc_reflection.counter_cache_column
|
304
|
+
associated_object.class.send(method_sym, counter_cache_column,
|
305
|
+
associated_object.id)
|
306
|
+
associated_object.touch if reflection_options[:touch]
|
256
307
|
end
|
257
308
|
end
|
258
309
|
|
259
310
|
def each_counter_cached_association_reflection
|
260
|
-
_reflections.each do |
|
311
|
+
_reflections.each do |_name, reflection|
|
261
312
|
yield reflection if reflection.belongs_to? && reflection.counter_cache_column
|
262
313
|
end
|
263
314
|
end
|
264
315
|
|
265
316
|
def increment_counters_on_associations
|
266
|
-
|
317
|
+
update_counters_on_associations :increment_counter
|
267
318
|
end
|
268
319
|
|
269
320
|
def decrement_counters_on_associations
|
270
|
-
|
321
|
+
update_counters_on_associations :decrement_counter
|
322
|
+
end
|
323
|
+
|
324
|
+
def stale_paranoid_value
|
325
|
+
self.paranoid_value = self.class.delete_now_value
|
326
|
+
clear_attribute_changes([self.class.paranoid_column])
|
271
327
|
end
|
272
328
|
end
|
273
329
|
end
|