soft_destroyable 0.1.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.
- data/MIT-LICENSE +20 -0
- data/README +18 -0
- data/Rakefile +43 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/soft_destroyable/is_soft_destroyable.rb +30 -0
- data/lib/soft_destroyable/table_definition.rb +15 -0
- data/lib/soft_destroyable.rb +299 -0
- data/spec/support/soft_destroy_spec_helper.rb +111 -0
- data/test/basic_test.rb +33 -0
- data/test/callback_test.rb +52 -0
- data/test/class_method_test.rb +87 -0
- data/test/dependent_delete_all_test.rb +55 -0
- data/test/dependent_delete_test.rb +49 -0
- data/test/dependent_destroy_test.rb +95 -0
- data/test/dependent_nullify_test.rb +96 -0
- data/test/dependent_restrict_test.rb +105 -0
- data/test/non_dependent_test.rb +30 -0
- data/test/test_helper.rb +395 -0
- data/test/through_associations_test.rb +85 -0
- data/uninstall.rb +1 -0
- metadata +129 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-11 Michael Kintzer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
SoftDestroyable
|
2
|
+
===============
|
3
|
+
|
4
|
+
Description goes here.
|
5
|
+
|
6
|
+
== Note on Patches/Pull Requests
|
7
|
+
|
8
|
+
* Fork the project.
|
9
|
+
* Make your feature addition or bug fix.
|
10
|
+
* Add tests for it. This is important so I don't break it in a
|
11
|
+
future version unintentionally.
|
12
|
+
* Commit, do not mess with rakefile, version, or history.
|
13
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
14
|
+
* Send me a pull request. Bonus points for topic branches.
|
15
|
+
|
16
|
+
== Copyright
|
17
|
+
|
18
|
+
Copyright (c) 2010-11 Michael Kintzer, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Test the soft_destroyable plugin.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.libs << 'test'
|
13
|
+
t.pattern = 'test/**/*_test.rb'
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generate documentation for the soft_destroyable plugin.'
|
18
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = "SoftDestroyable #{version}"
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'jeweler'
|
29
|
+
Jeweler::Tasks.new do |gem|
|
30
|
+
gem.name = "soft_destroyable"
|
31
|
+
gem.summary = "Rails 3 ActiveRecord compatible soft destroy implementation"
|
32
|
+
gem.description = "Rails 3 ActiveRecord compatible soft destroy implementation supporting dependent associations"
|
33
|
+
gem.email = "rockrep@yahoo.com"
|
34
|
+
gem.homepage = "http://github.com/rockrep/soft_destroyable"
|
35
|
+
gem.authors = ["Michael Kintzer"]
|
36
|
+
gem.add_development_dependency "rails", ">=3.0"
|
37
|
+
gem.add_development_dependency "sqlite3", ">=1.3.3"
|
38
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
39
|
+
end
|
40
|
+
Jeweler::GemcutterTasks.new
|
41
|
+
rescue LoadError
|
42
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
43
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "soft_destroyable"
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SoftDestroyable
|
2
|
+
# Simply adds a flag to determine whether a model class is soft_destroyable.
|
3
|
+
module IsSoftDestroyable
|
4
|
+
def self.extended(base) # :nodoc:
|
5
|
+
base.class_eval do
|
6
|
+
class << self
|
7
|
+
alias_method_chain :soft_destroyable, :flag
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Overrides the +soft_destroyable+ method to first define the +soft_destroyable?+ class method before
|
13
|
+
# deferring to the original +soft_destroyable+.
|
14
|
+
def soft_destroyable_with_flag(*args)
|
15
|
+
soft_destroyable_without_flag(*args)
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def soft_destroyable?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# For all ActiveRecord::Base models that do not call the +soft_destroyable+ method, the +soft_destroyable?+
|
25
|
+
# method will return false.
|
26
|
+
def soft_destroyable?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SoftDestroyable
|
2
|
+
module TableDefinition
|
3
|
+
|
4
|
+
# provide a migration short-cut for defining the required soft-destroyable columns
|
5
|
+
# can be used inside of a create_table or change_table (to add the columns)
|
6
|
+
#
|
7
|
+
# If you want to index on either of these fields, you need to handle that separately
|
8
|
+
def soft_destroyable
|
9
|
+
column "deleted", :boolean, :default => false
|
10
|
+
column "deleted_at", :datetime
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,299 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/soft_destroyable/table_definition"
|
2
|
+
require "#{File.dirname(__FILE__)}/soft_destroyable/is_soft_destroyable"
|
3
|
+
|
4
|
+
# Allows one to annotate an ActiveRecord module as being soft_destroyable.
|
5
|
+
#
|
6
|
+
# This changes the behavior of the +destroy+ method to being a soft-destroy, which
|
7
|
+
# will set the +deleted_at+ attribute to <tt>Time.now</tt>, and the +deleted+ attribute to <tt>true</tt>
|
8
|
+
# It exposes the +revive+ method to reverse the effects of +destroy+.
|
9
|
+
# It also exposes the +destroy!+ method which can be used to <b>really</b> destroy an object and it's associations.
|
10
|
+
#
|
11
|
+
# Standard ActiveRecord destroy callbacks are _not_ called, however you can override +before_soft_destroy+, +after_soft_destroy+,
|
12
|
+
# and +before_destroy!+ on your soft_destroyable models.
|
13
|
+
#
|
14
|
+
# Standard ActiveRecord dependent options :destroy, :restrict, :nullify, :delete_all, and :delete are supported.
|
15
|
+
# +revive+ will _not_ undo the effects of +nullify+, +delete_all+, and +delete+. +restrict+ is _not_ effected by the
|
16
|
+
# +deleted?+ state. In other words, deleted child models will still restrict destroying the parent.
|
17
|
+
#
|
18
|
+
# The +delete+ operation is _not_ modified by this module.
|
19
|
+
#
|
20
|
+
# The operations: +destroy+, +destroy!+, and +revive+ are automatically delegated to the dependent association records.
|
21
|
+
# in a single transaction.
|
22
|
+
#
|
23
|
+
# Examples:
|
24
|
+
# class Parent
|
25
|
+
# has_many :children, :dependent => :restrict
|
26
|
+
# has_many :animals, :dependent => :nullify
|
27
|
+
# soft_destroyable
|
28
|
+
#
|
29
|
+
#
|
30
|
+
# Author: Michael Kintzer
|
31
|
+
#
|
32
|
+
|
33
|
+
module SoftDestroyable
|
34
|
+
|
35
|
+
def self.included(base)
|
36
|
+
base.class_eval do
|
37
|
+
extend ClassMethods
|
38
|
+
extend IsSoftDestroyable
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
|
44
|
+
def soft_destroyable(options = {})
|
45
|
+
return if soft_destroyable?
|
46
|
+
|
47
|
+
scope :not_deleted, where(:deleted => false)
|
48
|
+
scope :deleted, where(:deleted => true)
|
49
|
+
|
50
|
+
include InstanceMethods
|
51
|
+
extend SingletonMethods
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module SingletonMethods
|
56
|
+
|
57
|
+
# returns an array of association symbols that must be managed by soft_destroyable on
|
58
|
+
# destroy and destroy!
|
59
|
+
def soft_dependencies
|
60
|
+
has_many_dependencies + has_one_dependencies
|
61
|
+
end
|
62
|
+
|
63
|
+
def restrict_dependencies
|
64
|
+
with_restrict_option(:has_many).map(&:name) + with_restrict_option(:has_one).map(&:name)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def non_through_dependent_associations(type)
|
70
|
+
reflect_on_all_associations(type).reject { |k, v|
|
71
|
+
k.class == ActiveRecord::Reflection::ThroughReflection }.reject { |k, v| k.options[:dependent].nil? }
|
72
|
+
end
|
73
|
+
|
74
|
+
def has_many_dependencies
|
75
|
+
non_through_dependent_associations(:has_many).map(&:name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_one_dependencies
|
79
|
+
non_through_dependent_associations(:has_one).map(&:name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def with_restrict_option(type)
|
83
|
+
non_through_dependent_associations(type).reject { |k, v| k.options[:dependent] != :restrict }
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
module InstanceMethods
|
89
|
+
|
90
|
+
# overrides the normal ActiveRecord::Transactions#destroy.
|
91
|
+
# can be recovered with +revive+
|
92
|
+
def destroy
|
93
|
+
before_soft_destroy
|
94
|
+
result = soft_destroy
|
95
|
+
after_soft_destroy
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
# not a recoverable operation
|
100
|
+
def destroy!
|
101
|
+
transaction do
|
102
|
+
before_destroy!
|
103
|
+
cascade_destroy!
|
104
|
+
delete
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# un-does the effect of +destroy+. Does not undo nullify on dependents
|
109
|
+
def revive
|
110
|
+
transaction do
|
111
|
+
cascade_revive
|
112
|
+
update_attributes(:deleted_at => nil, :deleted => false)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def soft_dependencies
|
117
|
+
self.class.soft_dependencies
|
118
|
+
end
|
119
|
+
|
120
|
+
def restrict_dependencies
|
121
|
+
self.class.restrict_dependencies
|
122
|
+
end
|
123
|
+
|
124
|
+
# override
|
125
|
+
def before_soft_destroy
|
126
|
+
# empty
|
127
|
+
end
|
128
|
+
|
129
|
+
# override
|
130
|
+
def after_soft_destroy
|
131
|
+
# empty
|
132
|
+
end
|
133
|
+
|
134
|
+
# override
|
135
|
+
def before_destroy!
|
136
|
+
# empty
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def non_restrict_dependencies
|
142
|
+
soft_dependencies.reject { |assoc_sym| restrict_dependencies.include?(assoc_sym) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def soft_destroy
|
146
|
+
transaction do
|
147
|
+
cascade_soft_destroy
|
148
|
+
update_attributes(:deleted_at => Time.now, :deleted => true)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def cascade_soft_destroy
|
153
|
+
cascade_to_soft_dependents { |assoc_obj|
|
154
|
+
if assoc_obj.respond_to?(:destroy) && assoc_obj.respond_to?(:revive)
|
155
|
+
wrap_with_callbacks(assoc_obj, "soft_destroy") do
|
156
|
+
assoc_obj.destroy
|
157
|
+
end
|
158
|
+
else
|
159
|
+
wrap_with_callbacks(assoc_obj, "soft_destroy") do
|
160
|
+
# no-op
|
161
|
+
end
|
162
|
+
end
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def cascade_destroy!
|
167
|
+
cascade_to_soft_dependents { |assoc_obj|
|
168
|
+
# cascade destroy! to soft dependencies objects
|
169
|
+
if assoc_obj.respond_to?(:destroy!)
|
170
|
+
wrap_with_callbacks(assoc_obj, "destroy!") do
|
171
|
+
assoc_obj.destroy!
|
172
|
+
end
|
173
|
+
else
|
174
|
+
wrap_with_callbacks(assoc_obj, "destroy!") do
|
175
|
+
assoc_obj.destroy
|
176
|
+
end
|
177
|
+
end
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
def cascade_revive
|
182
|
+
cascade_to_soft_dependents { |assoc_obj|
|
183
|
+
assoc_obj.revive if assoc_obj.respond_to?(:revive)
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
def cascade_to_soft_dependents(&block)
|
188
|
+
return unless block_given?
|
189
|
+
|
190
|
+
# fail fast on :dependent => :restrict
|
191
|
+
restrict_dependencies.each { |assoc_sym| handle_restrict(assoc_sym) }
|
192
|
+
|
193
|
+
non_restrict_dependencies.each do |assoc_sym|
|
194
|
+
reflection = reflection_for(assoc_sym)
|
195
|
+
association = send(reflection.name)
|
196
|
+
|
197
|
+
case reflection.options[:dependent]
|
198
|
+
when :destroy
|
199
|
+
handle_destroy(reflection, association, &block)
|
200
|
+
when :nullify
|
201
|
+
handle_nullify(reflection, association)
|
202
|
+
when :delete_all
|
203
|
+
handle_delete_all(reflection, association)
|
204
|
+
when :delete
|
205
|
+
handle_delete(reflection, association)
|
206
|
+
else
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
# reload as dependent associations may have updated
|
211
|
+
reload if self.id
|
212
|
+
end
|
213
|
+
|
214
|
+
def handle_destroy(reflection, association, &block)
|
215
|
+
case reflection.macro
|
216
|
+
when :has_many
|
217
|
+
association.each { |assoc_obj| yield(assoc_obj) }
|
218
|
+
when :has_one
|
219
|
+
# handle non-nil has_one
|
220
|
+
yield(association) if association
|
221
|
+
else
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def handle_restrict(assoc_sym)
|
226
|
+
reflection = reflection_for(assoc_sym)
|
227
|
+
association = send(reflection.name)
|
228
|
+
case reflection.macro
|
229
|
+
when :has_many
|
230
|
+
restrict_on_non_empty_has_many(reflection, association)
|
231
|
+
when :has_one
|
232
|
+
restrict_on_nil_has_one(reflection, association)
|
233
|
+
else
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def handle_nullify(reflection, association)
|
238
|
+
return unless association
|
239
|
+
case reflection.macro
|
240
|
+
when :has_many
|
241
|
+
self.class.send(:nullify_has_many_dependencies,
|
242
|
+
self,
|
243
|
+
reflection.name,
|
244
|
+
reflection.klass,
|
245
|
+
reflection.primary_key_name,
|
246
|
+
reflection.dependent_conditions(self, self.class, nil))
|
247
|
+
when :has_one
|
248
|
+
association.update_attributes(reflection.primary_key_name => nil)
|
249
|
+
else
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
def handle_delete_all(reflection, association)
|
255
|
+
return unless association
|
256
|
+
self.class.send(:delete_all_has_many_dependencies,
|
257
|
+
self,
|
258
|
+
reflection.name,
|
259
|
+
reflection.klass,
|
260
|
+
reflection.dependent_conditions(self, self.class, nil))
|
261
|
+
end
|
262
|
+
|
263
|
+
def handle_delete(reflection, association)
|
264
|
+
return unless association
|
265
|
+
association.update_attribute(reflection.primary_key_name, nil)
|
266
|
+
end
|
267
|
+
|
268
|
+
def wrap_with_callbacks(assoc_obj, action)
|
269
|
+
return unless block_given?
|
270
|
+
assoc_obj.send("before_#{action}".to_sym) if assoc_obj.respond_to?("before_#{action}".to_sym)
|
271
|
+
yield
|
272
|
+
assoc_obj.send("after_#{action}".to_sym) if assoc_obj.respond_to?("after_#{action}".to_sym)
|
273
|
+
end
|
274
|
+
|
275
|
+
def reflection_for(assoc_sym)
|
276
|
+
self.class.reflect_on_association(assoc_sym)
|
277
|
+
end
|
278
|
+
|
279
|
+
def restrict_on_non_empty_has_many(reflection, association)
|
280
|
+
return unless association
|
281
|
+
raise ActiveRecord::DeleteRestrictionError.new(reflection) if !association.empty?
|
282
|
+
end
|
283
|
+
|
284
|
+
def restrict_on_nil_has_one(reflection, association)
|
285
|
+
raise ActiveRecord::DeleteRestrictionError.new(reflection) if !association.nil?
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
ActiveRecord::Base.send :include, SoftDestroyable
|
291
|
+
[ActiveRecord::ConnectionAdapters::TableDefinition, ActiveRecord::ConnectionAdapters::Table].each { |base|
|
292
|
+
base.send(:include, SoftDestroyable::TableDefinition)
|
293
|
+
}
|
294
|
+
|
295
|
+
class SoftDestroyError < StandardError
|
296
|
+
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Utility module for Specing Database constraints
|
2
|
+
#
|
3
|
+
# Would like to rewrite these at some point to be more RSpec 'matcherish' so they would read more BDD-like.
|
4
|
+
#
|
5
|
+
# Author: Michael Kintzer
|
6
|
+
# August 31, 2010
|
7
|
+
|
8
|
+
module SoftDestroySpecHelper
|
9
|
+
|
10
|
+
# Ensures that the model class is annotated as +soft_destroyable+
|
11
|
+
# and that the model behaves appropriately to destroy and revive
|
12
|
+
def asserts_soft_destroy?(model_klass, new_record_args={})
|
13
|
+
model_klass = model_klass.constantize if model_klass.is_a?(String)
|
14
|
+
|
15
|
+
model_klass.respond_to?(:not_deleted).should be_true
|
16
|
+
|
17
|
+
current_count = model_klass.count
|
18
|
+
current_deleted_count = model_klass.deleted.count
|
19
|
+
|
20
|
+
# create a new record
|
21
|
+
obj = model_klass.create!(new_record_args)
|
22
|
+
|
23
|
+
# verify the database migration
|
24
|
+
asserts_soft_destroy_migration?(obj)
|
25
|
+
|
26
|
+
model_klass.count.should == 1 + current_count
|
27
|
+
|
28
|
+
# verify soft destroy behaves correctly, and the record still exists and is correctly marked
|
29
|
+
obj.destroy.should be_true
|
30
|
+
obj.reload
|
31
|
+
obj.deleted_at.should_not be_nil
|
32
|
+
obj.deleted?.should be_true
|
33
|
+
|
34
|
+
# verify counts are correct
|
35
|
+
model_klass.count.should == 1 + current_count
|
36
|
+
model_klass.not_deleted.count.should == 0 + current_count
|
37
|
+
model_klass.deleted.count.should == 1 + current_deleted_count
|
38
|
+
|
39
|
+
# verify revive behaves correctly
|
40
|
+
obj.revive
|
41
|
+
obj.reload
|
42
|
+
obj.deleted_at.should be_nil
|
43
|
+
obj.deleted?.should be_false
|
44
|
+
|
45
|
+
# verify counts are correct
|
46
|
+
model_klass.count.should == 1 + current_count
|
47
|
+
model_klass.not_deleted.count.should == 1 + current_count
|
48
|
+
model_klass.deleted.count.should == 0 + current_deleted_count
|
49
|
+
end
|
50
|
+
|
51
|
+
# Ensures that the model class soft destroys the associated dependent association on +destroy+
|
52
|
+
# This helper only valid if :dependent => :destroy
|
53
|
+
def asserts_soft_destroy_associations?(model_obj, association_symbol, new_association_record)
|
54
|
+
# save model_obj in case it hasn't been yet
|
55
|
+
model_obj.save
|
56
|
+
association_reflection = model_obj.class.reflect_on_association(association_symbol.to_sym)
|
57
|
+
association_reflection.options[:dependent].should == :destroy
|
58
|
+
assign_association(model_obj, association_reflection, new_association_record)
|
59
|
+
|
60
|
+
unless new_association_record.respond_to?(:revive)
|
61
|
+
# if associated_klass is NOT soft_destroyable, then calling destroy on parent is a NO-OP so associated_klass should
|
62
|
+
# NOT receive a destroy call
|
63
|
+
new_association_record.expects(:destroy).never
|
64
|
+
end
|
65
|
+
|
66
|
+
model_obj.destroy
|
67
|
+
|
68
|
+
if new_association_record.respond_to?(:revive)
|
69
|
+
# if associated_klass IS soft_destroyable, then the new_association_record should be deleted
|
70
|
+
new_association_record.deleted?.should be_true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Ensures that the model class hard destroys the associated dependent association on +destroy!+
|
75
|
+
# This helper only valid if :dependent => :destroy
|
76
|
+
def asserts_hard_destroy_associations?(model_obj, association_symbol, new_association_record)
|
77
|
+
# save model obj in case it hasn't been yet
|
78
|
+
model_obj.save
|
79
|
+
association_reflection = model_obj.class.reflect_on_association(association_symbol.to_sym)
|
80
|
+
association_reflection.options[:dependent].should == :destroy
|
81
|
+
assign_association(model_obj, association_reflection, new_association_record)
|
82
|
+
association_reflection.klass.where(association_reflection.primary_key_name => model_obj.id).count.should > 0
|
83
|
+
model_obj.destroy!
|
84
|
+
association_reflection.klass.where(association_reflection.primary_key_name => model_obj.id).count.should == 0
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# verifies the table contains the expected soft destroy fields,
|
90
|
+
# and they have the appropriate defaults
|
91
|
+
def asserts_soft_destroy_migration?(obj)
|
92
|
+
obj.respond_to?(:deleted_at).should be_true
|
93
|
+
obj.respond_to?(:deleted).should be_true
|
94
|
+
obj.deleted_at.should be_nil
|
95
|
+
obj.deleted?.should be_false
|
96
|
+
end
|
97
|
+
|
98
|
+
# Assigns the new_association_record to the model_obj
|
99
|
+
def assign_association(model_obj, association_reflection, new_association_record)
|
100
|
+
case association_reflection.macro
|
101
|
+
when :has_many
|
102
|
+
model_obj.send(association_reflection.name) << new_association_record
|
103
|
+
when :has_one
|
104
|
+
model_obj.send("#{association_reflection.name}=", new_association_record)
|
105
|
+
else
|
106
|
+
raise NotImplementedError.new("Association #{association_reflection.macro} not handled")
|
107
|
+
end
|
108
|
+
new_association_record.id.should_not be_nil
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/test/basic_test.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/test_helper"
|
2
|
+
|
3
|
+
class BasicTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@fred = Parent.create!(:name => "fred")
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
Parent.delete_all
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_destroy
|
14
|
+
@fred.destroy
|
15
|
+
fred = Parent.where(:name => "fred").first
|
16
|
+
assert_not_nil fred
|
17
|
+
assert_equal fred.deleted, true
|
18
|
+
assert_not_nil fred.deleted_at
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_revive
|
22
|
+
@fred.destroy
|
23
|
+
assert Parent.deleted.include?(@fred)
|
24
|
+
@fred.revive
|
25
|
+
assert !Parent.deleted.include?(@fred)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_destroy!
|
29
|
+
@fred.destroy!
|
30
|
+
assert_nil Parent.where(:name => "fred").first
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class CallbackTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@fred = CallbackParent.create!(:name => "fred")
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
CallbackChild.delete_all
|
12
|
+
SoftCallbackChild.delete_all
|
13
|
+
CallbackParent.delete_all
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_callback_before_soft_destroy_for_soft_children
|
17
|
+
@fred.soft_callback_children << pebbles = SoftCallbackChild.new(:name => "pebbles")
|
18
|
+
assert_raise PreventSoftDestroyError do
|
19
|
+
@fred.destroy
|
20
|
+
end
|
21
|
+
assert_equal @fred.reload.deleted?, false
|
22
|
+
assert_equal pebbles.reload.deleted?, false
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_callback_before_destroy_bang_for_soft_children
|
26
|
+
@fred.soft_callback_children << pebbles = SoftCallbackChild.new(:name => "pebbles")
|
27
|
+
assert_raise PreventDestroyBangError do
|
28
|
+
@fred.destroy!
|
29
|
+
end
|
30
|
+
assert_equal @fred.reload.deleted?, false
|
31
|
+
assert_equal pebbles.reload.deleted?, false
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_callback_before_soft_destroy
|
35
|
+
@fred.callback_children << pebbles = CallbackChild.new(:name => "pebbles")
|
36
|
+
assert_raise PreventSoftDestroyError do
|
37
|
+
@fred.destroy
|
38
|
+
end
|
39
|
+
assert_equal @fred.reload.deleted?, false
|
40
|
+
assert_not_nil pebbles.reload
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_callback_before_destroy!
|
44
|
+
@fred.callback_children << pebbles = CallbackChild.new(:name => "pebbles")
|
45
|
+
assert_raise PreventDestroyBangError do
|
46
|
+
@fred.destroy!
|
47
|
+
end
|
48
|
+
assert_equal @fred.reload.deleted?, false
|
49
|
+
assert_not_nil pebbles.reload
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|