jchupp-is_paranoid 0.7.1 → 0.8.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/Rakefile +22 -0
- data/VERSION.yml +2 -2
- data/lib/is_paranoid.rb +137 -101
- data/spec/is_paranoid_spec.rb +36 -1
- data/spec/models.rb +13 -1
- data/spec/schema.rb +12 -0
- metadata +10 -7
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec"
|
2
|
+
require "spec/rake/spectask"
|
3
|
+
require 'lib/is_paranoid.rb'
|
4
|
+
|
5
|
+
Spec::Rake::SpecTask.new do |t|
|
6
|
+
t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
7
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'jeweler'
|
12
|
+
Jeweler::Tasks.new do |s|
|
13
|
+
s.name = %q{is_paranoid}
|
14
|
+
s.summary = %q{ActiveRecord 2.3 compatible gem "allowing you to hide and restore records without actually deleting them." Yes, like acts_as_paranoid, only with less code and less complexity.}
|
15
|
+
s.email = %q{jeff@semanticart.com}
|
16
|
+
s.homepage = %q{http://github.com/jchupp/is_paranoid/}
|
17
|
+
s.description = ""
|
18
|
+
s.authors = ["Jeffrey Chupp"]
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
22
|
+
end
|
data/VERSION.yml
CHANGED
data/lib/is_paranoid.rb
CHANGED
@@ -3,128 +3,164 @@ require 'activerecord'
|
|
3
3
|
module IsParanoid
|
4
4
|
# Call this in your model to enable all the safety-net goodness
|
5
5
|
#
|
6
|
-
#
|
6
|
+
# Example:
|
7
7
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
8
|
+
# class Android < ActiveRecord::Base
|
9
|
+
# is_paranoid
|
10
|
+
# end
|
11
11
|
#
|
12
|
+
|
12
13
|
def is_paranoid opts = {}
|
13
14
|
opts[:field] ||= [:deleted_at, Proc.new{Time.now.utc}, nil]
|
14
15
|
class_inheritable_accessor :destroyed_field, :field_destroyed, :field_not_destroyed
|
15
16
|
self.destroyed_field, self.field_destroyed, self.field_not_destroyed = opts[:field]
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
# This is the real magic. All calls made to this model will append
|
19
|
+
# the conditions deleted_at => nil (or whatever your destroyed_field
|
20
|
+
# and field_not_destroyed are). All exceptions require using
|
21
|
+
# exclusive_scope (see self.delete_all, self.count_with_destroyed,
|
22
|
+
# and self.find_with_destroyed defined in the module ClassMethods)
|
23
|
+
default_scope :conditions => {destroyed_field => field_not_destroyed}
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# This is the real magic. All calls made to this model will append
|
24
|
-
# the conditions deleted_at => nil (or whatever your destroyed_field
|
25
|
-
# and field_not_destroyed are). All exceptions require using
|
26
|
-
# exclusive_scope (see self.delete_all, self.count_with_destroyed,
|
27
|
-
# and self.find_with_destroyed )
|
28
|
-
default_scope :conditions => {destroyed_field => field_not_destroyed}
|
29
|
-
|
30
|
-
# Actually delete the model, bypassing the safety net. Because
|
31
|
-
# this method is called internally by Model.delete(id) and on the
|
32
|
-
# delete method in each instance, we don't need to specify those
|
33
|
-
# methods separately
|
34
|
-
def self.delete_all conditions = nil
|
35
|
-
self.with_exclusive_scope { super conditions }
|
36
|
-
end
|
25
|
+
extend ClassMethods
|
26
|
+
include InstanceMethods
|
27
|
+
end
|
37
28
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
29
|
+
module ClassMethods
|
30
|
+
# Actually delete the model, bypassing the safety net. Because
|
31
|
+
# this method is called internally by Model.delete(id) and on the
|
32
|
+
# delete method in each instance, we don't need to specify those
|
33
|
+
# methods separately
|
34
|
+
def delete_all conditions = nil
|
35
|
+
self.with_exclusive_scope { super conditions }
|
36
|
+
end
|
45
37
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
38
|
+
# Use update_all with an exclusive scope to restore undo the soft-delete.
|
39
|
+
# This bypasses update-related callbacks.
|
40
|
+
#
|
41
|
+
# By default, restores cascade through associations that are
|
42
|
+
# :dependent => :destroy and under is_paranoid. You can prevent restoration
|
43
|
+
# of associated models by passing :include_destroyed_dependents => false,
|
44
|
+
# for example:
|
45
|
+
# Android.restore(:include_destroyed_dependents => false)
|
46
|
+
def restore(id, options = {})
|
47
|
+
options.reverse_merge!({:include_destroyed_dependents => true})
|
48
|
+
with_exclusive_scope do
|
49
|
+
update_all(
|
50
|
+
"#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
|
51
|
+
"id = #{id}"
|
52
|
+
)
|
53
|
+
end
|
57
54
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# Android.restore(:include_destroyed_dependents => false)
|
66
|
-
def self.restore(id, options = {})
|
67
|
-
options.reverse_merge!({:include_destroyed_dependents => true})
|
68
|
-
with_exclusive_scope do
|
69
|
-
update_all(
|
70
|
-
"#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
|
71
|
-
"id = #{id}"
|
72
|
-
)
|
73
|
-
end
|
74
|
-
if options[:include_destroyed_dependents]
|
75
|
-
self.reflect_on_all_associations.each do |association|
|
76
|
-
if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
|
77
|
-
association.klass.find_destroyed_only(:all,
|
78
|
-
:conditions => ["#{association.primary_key_name} = ?", id]
|
79
|
-
).each do |model|
|
80
|
-
model.restore
|
81
|
-
end
|
82
|
-
end
|
55
|
+
if options[:include_destroyed_dependents]
|
56
|
+
self.reflect_on_all_associations.each do |association|
|
57
|
+
if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
|
58
|
+
association.klass.find_destroyed_only(:all,
|
59
|
+
:conditions => ["#{association.primary_key_name} = ?", id]
|
60
|
+
).each do |model|
|
61
|
+
model.restore
|
83
62
|
end
|
84
63
|
end
|
85
64
|
end
|
65
|
+
end
|
66
|
+
end
|
86
67
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
68
|
+
# find_with_destroyed and other blah_with_destroyed and
|
69
|
+
# blah_destroyed_only methods are defined here
|
70
|
+
def method_missing name, *args
|
71
|
+
if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
|
72
|
+
self.extend(Module.new{
|
73
|
+
if $2 == '_with_destroyed'
|
74
|
+
# Example:
|
75
|
+
# def count_with_destroyed(*args)
|
76
|
+
# self.with_exclusive_scope{ self.send(:count, *args) }
|
77
|
+
# end
|
78
|
+
define_method name do |*args|
|
79
|
+
self.with_exclusive_scope{ self.send($1, *args) }
|
80
|
+
end
|
81
|
+
else
|
92
82
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
#
|
104
|
-
|
105
|
-
# self.with_exclusive_scope do
|
106
|
-
# with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
|
107
|
-
# self.send(:count, *args)
|
108
|
-
# end
|
109
|
-
# end
|
110
|
-
# end
|
111
|
-
define_method name do |*args|
|
112
|
-
self.with_exclusive_scope do
|
113
|
-
with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
|
114
|
-
self.send($1, *args)
|
115
|
-
end
|
116
|
-
end
|
83
|
+
# Example:
|
84
|
+
# def count_destroyed_only(*args)
|
85
|
+
# self.with_exclusive_scope do
|
86
|
+
# with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
|
87
|
+
# self.send(:count, *args)
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
define_method name do |*args|
|
92
|
+
self.with_exclusive_scope do
|
93
|
+
with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
|
94
|
+
self.send($1, *args)
|
117
95
|
end
|
118
96
|
end
|
119
|
-
|
120
|
-
|
121
|
-
else
|
122
|
-
super(name, *args)
|
97
|
+
end
|
98
|
+
|
123
99
|
end
|
124
|
-
|
100
|
+
})
|
101
|
+
self.send(name, *args)
|
102
|
+
else
|
103
|
+
super(name, *args)
|
125
104
|
end
|
126
105
|
end
|
127
106
|
end
|
107
|
+
|
108
|
+
module InstanceMethods
|
109
|
+
|
110
|
+
def method_missing name, *args
|
111
|
+
# if we're trying for a _____with_destroyed method
|
112
|
+
# and we can respond to the _____ method
|
113
|
+
# and we have an association by the name of _____
|
114
|
+
if name.to_s =~ /^(.*)(_with_destroyed)$/ and
|
115
|
+
self.respond_to?($1) and
|
116
|
+
(assoc = self.class.reflect_on_all_associations.detect{|a| a.name.to_s == $1})
|
117
|
+
|
118
|
+
parent_klass = Object.module_eval("::#{assoc.class_name}", __FILE__, __LINE__)
|
119
|
+
|
120
|
+
self.class.send(
|
121
|
+
:include,
|
122
|
+
Module.new{ # Example:
|
123
|
+
define_method name do |*args| # def android_with_destroyed
|
124
|
+
parent_klass.find_with_destroyed( # Android.find_with_destroyed(
|
125
|
+
self.send(assoc.primary_key_name) # self.send(:android_id)
|
126
|
+
) # )
|
127
|
+
end # end
|
128
|
+
}
|
129
|
+
)
|
130
|
+
self.send(name, *args)
|
131
|
+
else
|
132
|
+
super(name, *args)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Mark the model deleted_at as now.
|
137
|
+
def destroy_without_callbacks
|
138
|
+
self.class.update_all(
|
139
|
+
"#{destroyed_field} = #{self.class.connection.quote(( field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed))}",
|
140
|
+
"id = #{self.id}"
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Override the default destroy to allow us to flag deleted_at.
|
145
|
+
# This preserves the before_destroy and after_destroy callbacks.
|
146
|
+
# Because this is also called internally by Model.destroy_all and
|
147
|
+
# the Model.destroy(id), we don't need to specify those methods
|
148
|
+
# separately.
|
149
|
+
def destroy
|
150
|
+
return false if callback(:before_destroy) == false
|
151
|
+
result = destroy_without_callbacks
|
152
|
+
callback(:after_destroy)
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
# Set deleted_at flag on a model to field_not_destroyed, effectively
|
157
|
+
# undoing the soft-deletion.
|
158
|
+
def restore(options = {})
|
159
|
+
self.class.restore(id, options)
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
128
164
|
end
|
129
165
|
|
130
|
-
ActiveRecord::Base.send(:extend, IsParanoid)
|
166
|
+
ActiveRecord::Base.send(:extend, IsParanoid)
|
data/spec/is_paranoid_spec.rb
CHANGED
@@ -10,7 +10,23 @@ describe IsParanoid do
|
|
10
10
|
@luke = Person.create(:name => 'Luke Skywalker')
|
11
11
|
@r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
|
12
12
|
@c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
|
13
|
+
|
13
14
|
@r2d2.components.create(:name => 'Rotors')
|
15
|
+
|
16
|
+
@r2d2.memories.create(:name => 'A pretty sunset')
|
17
|
+
@c3p0.sticker = Sticker.create(:name => 'OMG, PONIES!')
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'non-is_paranoid models' do
|
21
|
+
it "should destroy as normal" do
|
22
|
+
lambda{
|
23
|
+
@luke.destroy
|
24
|
+
}.should change(Person, :count).by(-1)
|
25
|
+
|
26
|
+
lambda{
|
27
|
+
Person.count_with_destroyed
|
28
|
+
}.should raise_error(NoMethodError)
|
29
|
+
end
|
14
30
|
end
|
15
31
|
|
16
32
|
describe 'destroying' do
|
@@ -51,7 +67,6 @@ describe IsParanoid do
|
|
51
67
|
}.should change(Android, :count).from(2).to(0)
|
52
68
|
Android.count_with_destroyed.should == 2
|
53
69
|
end
|
54
|
-
|
55
70
|
end
|
56
71
|
end
|
57
72
|
|
@@ -152,6 +167,23 @@ describe IsParanoid do
|
|
152
167
|
end
|
153
168
|
end
|
154
169
|
|
170
|
+
describe 'accessing destroyed parent models' do
|
171
|
+
it "should be able to access destroyed parents via parent_with_destroyed" do
|
172
|
+
# Memory is has_many with a non-default primary key
|
173
|
+
# Sticker is a has_one with a default primary key
|
174
|
+
[Memory, Sticker].each do |klass|
|
175
|
+
instance = klass.last
|
176
|
+
parent = instance.android
|
177
|
+
instance.android.destroy
|
178
|
+
|
179
|
+
# reload so the model doesn't remember the parent
|
180
|
+
instance.reload
|
181
|
+
instance.android.should == nil
|
182
|
+
instance.android_with_destroyed.should == parent
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
155
187
|
describe 'alternate fields and field values' do
|
156
188
|
it "should properly function for boolean values" do
|
157
189
|
# ninjas are invisible by default. not being ninjas, we can only
|
@@ -160,12 +192,14 @@ describe IsParanoid do
|
|
160
192
|
ninja.vanish # aliased to destroy
|
161
193
|
Ninja.first.should be_blank
|
162
194
|
Ninja.find_with_destroyed(:first).should == ninja
|
195
|
+
Ninja.count.should == 0
|
163
196
|
|
164
197
|
# we're only interested in pirates who are alive by default
|
165
198
|
pirate = Pirate.create(:name => 'Reginald')
|
166
199
|
pirate.destroy
|
167
200
|
Pirate.first.should be_blank
|
168
201
|
Pirate.find_with_destroyed(:first).should == pirate
|
202
|
+
Pirate.count.should == 0
|
169
203
|
|
170
204
|
# we're only interested in pirates who are dead by default.
|
171
205
|
# zombie pirates ftw!
|
@@ -173,6 +207,7 @@ describe IsParanoid do
|
|
173
207
|
lambda{
|
174
208
|
DeadPirate.first.destroy
|
175
209
|
}.should change(Pirate, :count).from(0).to(1)
|
210
|
+
DeadPirate.count.should == 0
|
176
211
|
end
|
177
212
|
end
|
178
213
|
|
data/spec/models.rb
CHANGED
@@ -6,6 +6,8 @@ end
|
|
6
6
|
class Android < ActiveRecord::Base #:nodoc:
|
7
7
|
validates_uniqueness_of :name
|
8
8
|
has_many :components, :dependent => :destroy
|
9
|
+
has_one :sticker
|
10
|
+
has_many :memories, :foreign_key => 'parent_id'
|
9
11
|
|
10
12
|
is_paranoid
|
11
13
|
|
@@ -20,13 +22,23 @@ end
|
|
20
22
|
class Component < ActiveRecord::Base #:nodoc:
|
21
23
|
is_paranoid
|
22
24
|
NEW_NAME = 'Something Else!'
|
23
|
-
|
25
|
+
|
24
26
|
after_destroy :change_name
|
25
27
|
def change_name
|
26
28
|
self.update_attribute(:name, NEW_NAME)
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
32
|
+
class Memory < ActiveRecord::Base #:nodoc:
|
33
|
+
is_paranoid
|
34
|
+
belongs_to :android, :class_name => "Android", :foreign_key => "parent_id"
|
35
|
+
end
|
36
|
+
|
37
|
+
class Sticker < ActiveRecord::Base #:nodoc
|
38
|
+
is_paranoid
|
39
|
+
belongs_to :android
|
40
|
+
end
|
41
|
+
|
30
42
|
class AndroidWithScopedUniqueness < ActiveRecord::Base #:nodoc:
|
31
43
|
set_table_name :androids
|
32
44
|
validates_uniqueness_of :name, :scope => :deleted_at
|
data/spec/schema.rb
CHANGED
@@ -21,6 +21,18 @@ ActiveRecord::Schema.define(:version => 20090317164830) do
|
|
21
21
|
t.datetime "updated_at"
|
22
22
|
end
|
23
23
|
|
24
|
+
create_table "memories", :force => true do |t|
|
25
|
+
t.string "name"
|
26
|
+
t.integer "parent_id"
|
27
|
+
t.datetime "deleted_at"
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table "stickers", :force => true do |t|
|
31
|
+
t.string "name"
|
32
|
+
t.integer "android_id"
|
33
|
+
t.datetime "deleted_at"
|
34
|
+
end
|
35
|
+
|
24
36
|
create_table "ninjas", :force => true do |t|
|
25
37
|
t.string "name"
|
26
38
|
t.boolean "visible", :default => false
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jchupp-is_paranoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeffrey Chupp
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-05-
|
12
|
+
date: 2009-05-12 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -19,10 +19,11 @@ executables: []
|
|
19
19
|
|
20
20
|
extensions: []
|
21
21
|
|
22
|
-
extra_rdoc_files:
|
23
|
-
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.textile
|
24
24
|
files:
|
25
25
|
- README.textile
|
26
|
+
- Rakefile
|
26
27
|
- VERSION.yml
|
27
28
|
- lib/is_paranoid.rb
|
28
29
|
- spec/database.yml
|
@@ -35,7 +36,6 @@ has_rdoc: true
|
|
35
36
|
homepage: http://github.com/jchupp/is_paranoid/
|
36
37
|
post_install_message:
|
37
38
|
rdoc_options:
|
38
|
-
- --inline-source
|
39
39
|
- --charset=UTF-8
|
40
40
|
require_paths:
|
41
41
|
- lib
|
@@ -58,5 +58,8 @@ rubygems_version: 1.2.0
|
|
58
58
|
signing_key:
|
59
59
|
specification_version: 3
|
60
60
|
summary: ActiveRecord 2.3 compatible gem "allowing you to hide and restore records without actually deleting them." Yes, like acts_as_paranoid, only with less code and less complexity.
|
61
|
-
test_files:
|
62
|
-
|
61
|
+
test_files:
|
62
|
+
- spec/is_paranoid_spec.rb
|
63
|
+
- spec/models.rb
|
64
|
+
- spec/schema.rb
|
65
|
+
- spec/spec_helper.rb
|