jchupp-is_paranoid 0.0.2 → 0.3.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/README.textile +29 -2
- data/VERSION.yml +4 -0
- data/lib/is_paranoid.rb +55 -21
- data/spec/{android_spec.rb → is_paranoid_spec.rb} +59 -1
- data/spec/schema.rb +10 -0
- metadata +12 -21
- data/MIT-LICENSE +0 -19
- data/Rakefile +0 -14
data/README.textile
CHANGED
@@ -8,11 +8,11 @@ h3. and you may ask yourself, how do I work this?
|
|
8
8
|
|
9
9
|
You should read the specs, or the RDOC, or even the source itself (which is very readable), but for the lazy, here's the hand-holding:
|
10
10
|
|
11
|
-
You need ActiveRecord 2.3 and you need to properly install this gem. Then you need a model with a
|
11
|
+
You need ActiveRecord 2.3 and you need to properly install this gem. Then you need a model with a field to serve as a flag column on its database table. For this example we'll use a timestamp named "deleted_at". If that column is null, the item isn't deleted. If it has a timestamp, it should count as deleted.
|
12
12
|
|
13
13
|
So let's assume we have a model Automobile that has a deleted_at column on the automobiles table.
|
14
14
|
|
15
|
-
If you're working with Rails, in your environment.rb, add the following to your initializer block.
|
15
|
+
If you're working with Rails, in your environment.rb, add the following to your initializer block (you may want to change the version number).
|
16
16
|
|
17
17
|
<pre>
|
18
18
|
Rails::Initializer.run do |config|
|
@@ -57,10 +57,37 @@ One thing to note, destroying is always undo-able, but deleting is not.
|
|
57
57
|
# And you may say to yourself, "My god! What have I done?"
|
58
58
|
</pre>
|
59
59
|
|
60
|
+
All calculations and finds are created via a define_method call in method_missing. So you don't get a bunch of unnecessary methods defined unless you use them. Any find/count/sum/etc. _with_destroyed calls should work and you can also do find/count/sum/etc._destroyed_only.
|
61
|
+
|
62
|
+
h3. Specifying alternate rules for what should be considered destroyed
|
63
|
+
|
64
|
+
"deleted_at" as a timestamp is what acts_as_paranoid uses to define what is and isn't destroyed (see above), but you can specify alternate options with is_paranoid. In the is_paranoid line of your model you can specify the field, the value the field should have if the entry should count as destroyed, and the value the field should have if the entry is not destroyed. Consider the following models:
|
65
|
+
|
66
|
+
<pre>
|
67
|
+
class Pirate < ActiveRecord::Base
|
68
|
+
is_paranoid :field => [:alive, false, true]
|
69
|
+
end
|
70
|
+
|
71
|
+
class DeadPirate < ActiveRecord::Base
|
72
|
+
set_table_name :pirates
|
73
|
+
is_paranoid :field => [:alive, true, false]
|
74
|
+
end
|
75
|
+
</pre>
|
76
|
+
|
77
|
+
These two models share the same table, but when we are finding Pirates, we're only interested in those that are alive. To break it down, we specify :alive as our field to check, false as what the model field should be marked at when destroyed and true to what the field should be if they're not destroyed. DeadPirates are specified as the opposite. Check out the specs if you're still confused.
|
78
|
+
|
79
|
+
h3. Note:
|
80
|
+
|
81
|
+
validates_uniqueness_of does not ignore items marked with a deleted_at flag. This is a behavior difference between is_paranoid and acts_as_paranoid. I'm going to treat this as a bug until I get a chance to make it an optional feature. Be aware of it.
|
82
|
+
|
60
83
|
h3. and you may ask yourself, where does that highway go to?
|
61
84
|
|
62
85
|
If you find any bugs, have any ideas of features you think are missing, or find things you're like to see work differently, feel free to send me a message or a pull request.
|
63
86
|
|
87
|
+
Currently on the todo list:
|
88
|
+
* deal with validates_uniqueness_of issue
|
89
|
+
* add options for merging additional default_scope options (i.e. order, etc.)
|
90
|
+
|
64
91
|
h3. Thanks
|
65
92
|
|
66
93
|
Thanks to Rick Olson for acts_as_paranoid which is obviously an inspiration in concept and execution, Ryan Bates for mentioning the idea of using default_scope for this on Ryan Daigle's "post introducing default_scope":defscope, and the Talking Heads for being the Talking Heads.
|
data/VERSION.yml
ADDED
data/lib/is_paranoid.rb
CHANGED
@@ -13,44 +13,45 @@ module IsParanoid
|
|
13
13
|
# class Android < ActiveRecord::Base
|
14
14
|
# is_paranoid
|
15
15
|
# end
|
16
|
-
|
17
|
-
|
16
|
+
#
|
17
|
+
# If you want to include ActiveRecord::Calculations to include your
|
18
|
+
# destroyed models, do is_paranoid :with_calculations => true and you
|
19
|
+
# will get sum_with_deleted, count_with_deleted, etc.
|
20
|
+
def is_paranoid opts = {}
|
21
|
+
opts[:field] ||= [:deleted_at, Proc.new{Time.now.utc}, nil]
|
22
|
+
class_inheritable_accessor :destroyed_field, :field_destroyed, :field_not_destroyed
|
23
|
+
self.destroyed_field, self.field_destroyed, self.field_not_destroyed = opts[:field]
|
24
|
+
|
25
|
+
include Work
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Work
|
30
|
+
def self.included(base)
|
31
|
+
base.class_eval do
|
18
32
|
# This is the real magic. All calls made to this model will append
|
19
33
|
# the conditions deleted_at => nil. Exceptions require using
|
20
34
|
# exclusive_scope (see self.delete_all, self.count_with_destroyed,
|
21
35
|
# and self.find_with_destroyed )
|
22
|
-
default_scope :conditions => {
|
36
|
+
default_scope :conditions => {destroyed_field => field_not_destroyed}
|
23
37
|
|
24
38
|
# Actually delete the model, bypassing the safety net. Because
|
25
|
-
# this method is called internally by Model.delete and on the
|
39
|
+
# this method is called internally by Model.delete(id) and on the
|
26
40
|
# delete method in each instance, we don't need to specify those
|
27
41
|
# methods separately
|
28
42
|
def self.delete_all conditions = nil
|
29
|
-
self.with_exclusive_scope
|
30
|
-
super conditions
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Return a count that includes the soft-deleted models.
|
35
|
-
def self.count_with_destroyed *args
|
36
|
-
self.with_exclusive_scope { count(*args) }
|
37
|
-
end
|
38
|
-
|
39
|
-
# Return instances of all models matching the query regardless
|
40
|
-
# of whether or not they have been soft-deleted.
|
41
|
-
def self.find_with_destroyed *args
|
42
|
-
self.with_exclusive_scope { find(*args) }
|
43
|
+
self.with_exclusive_scope { super conditions }
|
43
44
|
end
|
44
45
|
|
45
46
|
# Mark the model deleted_at as now.
|
46
47
|
def destroy_without_callbacks
|
47
|
-
self.update_attribute(
|
48
|
+
self.update_attribute(destroyed_field, ( field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed))
|
48
49
|
end
|
49
50
|
|
50
51
|
# Override the default destroy to allow us to flag deleted_at.
|
51
52
|
# This preserves the before_destroy and after_destroy callbacks.
|
52
53
|
# Because this is also called internally by Model.destroy_all and
|
53
|
-
# the
|
54
|
+
# the Model.destroy(id), we don't need to specify those methods
|
54
55
|
# separately.
|
55
56
|
def destroy
|
56
57
|
return false if callback(:before_destroy) == false
|
@@ -62,7 +63,40 @@ module IsParanoid
|
|
62
63
|
# Set deleted_at flag on a model to nil, effectively undoing the
|
63
64
|
# soft-deletion.
|
64
65
|
def restore
|
65
|
-
self.update_attribute(
|
66
|
+
self.update_attribute(destroyed_field, field_not_destroyed)
|
67
|
+
end
|
68
|
+
|
69
|
+
# find_with_destroyed and other blah_with_destroyed and
|
70
|
+
# blah_destroyed_only methods are defined here
|
71
|
+
def self.method_missing name, *args
|
72
|
+
if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
|
73
|
+
self.extend(Module.new{
|
74
|
+
if $2 == '_with_destroyed' # Example:
|
75
|
+
define_method name do |*args| # def count_with_destroyed(*args)
|
76
|
+
self.with_exclusive_scope{ self.send($1, *args) } # self.with_exclusive_scope{ self.send(:count, *args) }
|
77
|
+
end # end
|
78
|
+
else
|
79
|
+
# Example:
|
80
|
+
# def count_destroyed_only(*args)
|
81
|
+
# self.with_exclusive_scope do
|
82
|
+
# with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
|
83
|
+
# self.send(:count, *args)
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
define_method name do |*args|
|
88
|
+
self.with_exclusive_scope do
|
89
|
+
with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
|
90
|
+
self.send($1, *args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
})
|
96
|
+
self.send(name, *args)
|
97
|
+
else
|
98
|
+
super(name, *args)
|
99
|
+
end
|
66
100
|
end
|
67
101
|
end
|
68
102
|
end
|
@@ -5,9 +5,27 @@ class Person < ActiveRecord::Base
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class Android < ActiveRecord::Base
|
8
|
+
validates_uniqueness_of :name
|
8
9
|
is_paranoid
|
9
10
|
end
|
10
11
|
|
12
|
+
class NoCalculation < ActiveRecord::Base
|
13
|
+
is_paranoid
|
14
|
+
end
|
15
|
+
|
16
|
+
class Ninja < ActiveRecord::Base
|
17
|
+
is_paranoid :field => [:visible, false, true]
|
18
|
+
end
|
19
|
+
|
20
|
+
class Pirate < ActiveRecord::Base
|
21
|
+
is_paranoid :field => [:alive, false, true]
|
22
|
+
end
|
23
|
+
|
24
|
+
class DeadPirate < ActiveRecord::Base
|
25
|
+
set_table_name :pirates
|
26
|
+
is_paranoid :field => [:alive, true, false]
|
27
|
+
end
|
28
|
+
|
11
29
|
describe Android do
|
12
30
|
before(:each) do
|
13
31
|
Android.delete_all
|
@@ -50,7 +68,13 @@ describe Android do
|
|
50
68
|
it "should be able to find deleted items via find_with_destroyed" do
|
51
69
|
@r2d2.destroy
|
52
70
|
Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
|
53
|
-
Android.
|
71
|
+
Android.first_with_destroyed(:conditions => {:name => 'R2D2'}).should_not be_blank
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should be able to find only deleted items via find_destroyed_only" do
|
75
|
+
@r2d2.destroy
|
76
|
+
Android.all_destroyed_only.size.should == 1
|
77
|
+
Android.first_destroyed_only.should == @r2d2
|
54
78
|
end
|
55
79
|
|
56
80
|
it "should have a proper count inclusively and exclusively of deleted items" do
|
@@ -73,4 +97,38 @@ describe Android do
|
|
73
97
|
@r2d2.restore
|
74
98
|
}.should change(Android, :count).from(1).to(2)
|
75
99
|
end
|
100
|
+
|
101
|
+
it "should respond to various calculations" do
|
102
|
+
@r2d2.destroy
|
103
|
+
Android.sum('id').should == @c3p0.id
|
104
|
+
Android.sum_with_destroyed('id').should == @r2d2.id + @c3p0.id
|
105
|
+
|
106
|
+
Android.average_with_destroyed('id').should == (@r2d2.id + @c3p0.id) / 2.0
|
107
|
+
end
|
108
|
+
|
109
|
+
# Note: this isn't necessarily ideal, this just serves to demostrate
|
110
|
+
# how it currently works
|
111
|
+
it "should not ignore deleted items in validation checks" do
|
112
|
+
@r2d2.destroy
|
113
|
+
lambda{
|
114
|
+
Android.create!(:name => 'R2D2')
|
115
|
+
}.should raise_error(ActiveRecord::RecordInvalid)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should allow specifying alternate fields and field values" do
|
119
|
+
ninja = Ninja.create(:name => 'Esteban')
|
120
|
+
ninja.destroy
|
121
|
+
Ninja.first.should be_blank
|
122
|
+
Ninja.find_with_destroyed(:first).should == ninja
|
123
|
+
|
124
|
+
pirate = Pirate.create(:name => 'Reginald')
|
125
|
+
pirate.destroy
|
126
|
+
Pirate.first.should be_blank
|
127
|
+
Pirate.find_with_destroyed(:first).should == pirate
|
128
|
+
|
129
|
+
DeadPirate.first.id.should == pirate.id
|
130
|
+
lambda{
|
131
|
+
DeadPirate.first.destroy
|
132
|
+
}.should change(Pirate, :count).from(0).to(1)
|
133
|
+
end
|
76
134
|
end
|
data/spec/schema.rb
CHANGED
@@ -12,4 +12,14 @@ ActiveRecord::Schema.define(:version => 20090317164830) do
|
|
12
12
|
t.datetime "created_at"
|
13
13
|
t.datetime "updated_at"
|
14
14
|
end
|
15
|
+
|
16
|
+
create_table "ninjas", :force => true do |t|
|
17
|
+
t.string "name"
|
18
|
+
t.boolean "visible", :default => true
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table "pirates", :force => true do |t|
|
22
|
+
t.string "name"
|
23
|
+
t.boolean "alive", :default => true
|
24
|
+
end
|
15
25
|
end
|
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.0
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeffrey Chupp
|
@@ -9,20 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-03-
|
12
|
+
date: 2009-03-28 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
16
|
-
|
17
|
-
type: :runtime
|
18
|
-
version_requirement:
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
20
|
-
requirements:
|
21
|
-
- - ">="
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: 2.3.0
|
24
|
-
version:
|
25
|
-
description:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: ""
|
26
17
|
email: jeff@semanticart.com
|
27
18
|
executables: []
|
28
19
|
|
@@ -31,20 +22,20 @@ extensions: []
|
|
31
22
|
extra_rdoc_files: []
|
32
23
|
|
33
24
|
files:
|
34
|
-
- lib/is_paranoid.rb
|
35
25
|
- README.textile
|
36
|
-
-
|
37
|
-
-
|
38
|
-
- spec/android_spec.rb
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/is_paranoid.rb
|
39
28
|
- spec/database.yml
|
29
|
+
- spec/is_paranoid_spec.rb
|
30
|
+
- spec/schema.rb
|
40
31
|
- spec/spec.opts
|
41
32
|
- spec/spec_helper.rb
|
42
|
-
- spec/schema.rb
|
43
33
|
has_rdoc: true
|
44
34
|
homepage: http://github.com/jchupp/is_paranoid/
|
45
35
|
post_install_message:
|
46
|
-
rdoc_options:
|
47
|
-
|
36
|
+
rdoc_options:
|
37
|
+
- --inline-source
|
38
|
+
- --charset=UTF-8
|
48
39
|
require_paths:
|
49
40
|
- lib
|
50
41
|
required_ruby_version: !ruby/object:Gem::Requirement
|
data/MIT-LICENSE
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
Copyright (c) 2009 Jeffrey Chupp
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
-
of this software and associated documentation files (the "Software"), to deal
|
5
|
-
in the Software without restriction, including without limitation the rights
|
6
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
-
copies of the Software, and to permit persons to whom the Software is
|
8
|
-
furnished to do so, subject to the following conditions:
|
9
|
-
|
10
|
-
The above copyright notice and this permission notice shall be included in
|
11
|
-
all copies or substantial portions of the Software.
|
12
|
-
|
13
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
-
THE SOFTWARE.
|
data/Rakefile
DELETED
@@ -1,14 +0,0 @@
|
|
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
|
-
task :install do
|
11
|
-
rm_rf "*.gem"
|
12
|
-
puts `gem build is_paranoid.gemspec`
|
13
|
-
puts `sudo gem install is_paranoid-0.0.1.gem`
|
14
|
-
end
|