jomz-is_paranoid 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG +43 -0
- data/MIT-LICENSE +19 -0
- data/README.textile +117 -0
- data/Rakefile +24 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/is_paranoid.gemspec +57 -0
- data/lib/is_paranoid.rb +285 -0
- data/spec/database.yml +3 -0
- data/spec/is_paranoid_spec.rb +337 -0
- data/spec/models.rb +138 -0
- data/spec/schema.rb +91 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- metadata +84 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
This will only document major changes. Please see the commit log for minor changes.
|
2
|
+
|
3
|
+
-2009-09-25
|
4
|
+
* fixing bug with has_many :through not respecting is_paranoid conditions (thanks, Ben Johnson)
|
5
|
+
|
6
|
+
-2009-09-18
|
7
|
+
* making is_paranoid play nice with habtm relationships
|
8
|
+
|
9
|
+
-2009-09-17
|
10
|
+
* fixed when primary key was not "id" (ie: "uuid") via Thibaud Guillaume-Gentil
|
11
|
+
|
12
|
+
-2009-09-15
|
13
|
+
* added support for has_many associations (i.e. @r2d2.components_with_destroyed) via Amiel Martin
|
14
|
+
|
15
|
+
-2009-06-13
|
16
|
+
* added support for is_paranoid conditions being maintained on preloaded associations
|
17
|
+
* destroy and restore now return self (to be more in keeping with other ActiveRecord methods) via Brent Dillingham
|
18
|
+
|
19
|
+
-2009-05-19
|
20
|
+
* added support for specifying relationships to restore via instance_model.restore(:include => [:parent_1, :child_1, :child_2]), etc.
|
21
|
+
* method_missing is no longer overridden on instances provided you declare your custom method_missing *before* specifying the model is_paranoid
|
22
|
+
|
23
|
+
-2009-05-12
|
24
|
+
* added support for parent_with_destroyed methods
|
25
|
+
|
26
|
+
-2009-04-27
|
27
|
+
* restoring models now cascades to child dependent => destroy models via Matt Todd
|
28
|
+
|
29
|
+
-2009-04-22
|
30
|
+
* destroying and restoring records no longer triggers saving/updating callbacks
|
31
|
+
|
32
|
+
-2009-03-28
|
33
|
+
* removing syntax for calculation require (all find and ActiveRecord calculations are done on-the-fly now via method_missing)
|
34
|
+
* adding ability to specify alternate fields and values for destroyed objects
|
35
|
+
* adding in support for _destroyed_only methods (with inspiration from David Krmpotic)
|
36
|
+
* adding init.rb via David Krmpotic
|
37
|
+
* adding jewler tasks via Scott Woods
|
38
|
+
|
39
|
+
-2009-03-24
|
40
|
+
* requiring specific syntax to include calculations
|
41
|
+
|
42
|
+
-2009-03-21
|
43
|
+
* initial release
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
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/README.textile
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
h1. NOTICE: this library is no longer supported or actively developed by the original author. It never made it to a 1.0 stable version. Use it at your own risk and write lots of tests.
|
2
|
+
|
3
|
+
You can read more here: http://blog.semanticart.com/killing_is_paranoid/
|
4
|
+
|
5
|
+
h1. is_paranoid ( same as it ever was )
|
6
|
+
|
7
|
+
h3. advice and disclaimer
|
8
|
+
|
9
|
+
You should always declare is_paranoid before any associations in your model unless you have a good reason for doing otherwise. Some relationships might not behave properly if you fail to do so. If you know what you're doing (and have written tests) and want to supress the warning then you can pass :suppress_load_order_warning => true as an option.
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
is_paranoid :suppress_load_order_warning => true
|
13
|
+
</pre>
|
14
|
+
|
15
|
+
You should never expect _any_ library to work or behave exactly how you want it to: test, test, test and file an issue if you have any problems. Bonus points if you include sample failing code. Extra bonus points if you send a pull request that implements a feature/fixes a bug.
|
16
|
+
|
17
|
+
h3. and you may ask yourself, well, how did I get here?
|
18
|
+
|
19
|
+
Sometimes you want to delete something in ActiveRecord, but you realize you might need it later (for an undo feature, or just as a safety net, etc.). There are a plethora of plugins that accomplish this, the most famous of which is the venerable acts_as_paranoid which is great but not really actively developed any more. What's more, acts_as_paranoid was written for an older version of ActiveRecord and, with default_scope in 2.3, it is now possible to do the same thing with significantly less complexity. Thus, *is_paranoid*.
|
20
|
+
|
21
|
+
h3. and you may ask yourself, how do I work this?
|
22
|
+
|
23
|
+
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:
|
24
|
+
|
25
|
+
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.
|
26
|
+
|
27
|
+
So let's assume we have a model Automobile that has a deleted_at column on the automobiles table.
|
28
|
+
|
29
|
+
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).
|
30
|
+
|
31
|
+
<pre>
|
32
|
+
Rails::Initializer.run do |config|
|
33
|
+
# ...
|
34
|
+
config.gem "semanticart-is_paranoid", :lib => 'is_paranoid', :version => ">= 0.0.1"
|
35
|
+
end
|
36
|
+
</pre>
|
37
|
+
|
38
|
+
Then in your ActiveRecord model
|
39
|
+
|
40
|
+
<pre>
|
41
|
+
class Automobile < ActiveRecord::Base
|
42
|
+
is_paranoid
|
43
|
+
end
|
44
|
+
</pre>
|
45
|
+
|
46
|
+
Now our automobiles are now soft-deleteable.
|
47
|
+
|
48
|
+
<pre>
|
49
|
+
that_large_automobile = Automobile.create()
|
50
|
+
Automobile.count # => 1
|
51
|
+
|
52
|
+
that_large_automobile.destroy
|
53
|
+
Automobile.count # => 0
|
54
|
+
Automobile.count_with_destroyed # => 1
|
55
|
+
|
56
|
+
# where is that large automobile?
|
57
|
+
that_large_automobile = Automobile.find_with_destroyed(:all).first
|
58
|
+
that_large_automobile.restore
|
59
|
+
Automobile.count # => 1
|
60
|
+
</pre>
|
61
|
+
|
62
|
+
One thing to note, destroying is always undo-able, but deleting is not. This is a behavior difference between acts_as_paranoid and is_paranoid.
|
63
|
+
|
64
|
+
<pre>
|
65
|
+
Automobile.destroy_all
|
66
|
+
Automobile.count # => 0
|
67
|
+
Automobile.count_with_destroyed # => 1
|
68
|
+
|
69
|
+
Automobile.delete_all
|
70
|
+
Automobile.count_with_destroyed # => 0
|
71
|
+
# And you may say to yourself, "My god! What have I done?"
|
72
|
+
</pre>
|
73
|
+
|
74
|
+
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.
|
75
|
+
|
76
|
+
h3. Specifying alternate rules for what should be considered destroyed
|
77
|
+
|
78
|
+
"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:
|
79
|
+
|
80
|
+
<pre>
|
81
|
+
class Pirate < ActiveRecord::Base
|
82
|
+
is_paranoid :field => [:alive, false, true]
|
83
|
+
end
|
84
|
+
|
85
|
+
class DeadPirate < ActiveRecord::Base
|
86
|
+
set_table_name :pirates
|
87
|
+
is_paranoid :field => [:alive, true, false]
|
88
|
+
end
|
89
|
+
</pre>
|
90
|
+
|
91
|
+
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.
|
92
|
+
|
93
|
+
h3. Note about validates_uniqueness_of:
|
94
|
+
|
95
|
+
validates_uniqueness_of does not, by default, ignore items marked with a deleted_at (or other field name) flag. This is a behavior difference between is_paranoid and acts_as_paranoid. You can overcome this by specifying the field name you are using to mark destroyed items as your scope. Example:
|
96
|
+
|
97
|
+
<pre>
|
98
|
+
class Android < ActiveRecord::Base
|
99
|
+
validates_uniqueness_of :name, :scope => :deleted_at
|
100
|
+
is_paranoid
|
101
|
+
end
|
102
|
+
</pre>
|
103
|
+
|
104
|
+
And now the validates_uniqueness_of will ignore items that are destroyed.
|
105
|
+
|
106
|
+
h3. and you may ask yourself, where does that highway go to?
|
107
|
+
|
108
|
+
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 file an issue or send a pull request.
|
109
|
+
|
110
|
+
Currently on the todo list:
|
111
|
+
* add options for merging additional default_scope options (i.e. order, etc.)
|
112
|
+
|
113
|
+
h3. Thanks
|
114
|
+
|
115
|
+
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.
|
116
|
+
|
117
|
+
[defscope]http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
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
|
23
|
+
|
24
|
+
task :default => :spec
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'is_paranoid'
|
data/is_paranoid.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{jomz-is_paranoid}
|
8
|
+
s.version = "0.9.7"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jeffrey Chupp"]
|
12
|
+
s.date = %q{2009-11-23}
|
13
|
+
s.description = %q{}
|
14
|
+
s.email = %q{jeff@semanticart.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.textile"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"CHANGELOG",
|
21
|
+
"MIT-LICENSE",
|
22
|
+
"README.textile",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION.yml",
|
25
|
+
"init.rb",
|
26
|
+
"is_paranoid.gemspec",
|
27
|
+
"lib/is_paranoid.rb",
|
28
|
+
"spec/database.yml",
|
29
|
+
"spec/is_paranoid_spec.rb",
|
30
|
+
"spec/models.rb",
|
31
|
+
"spec/schema.rb",
|
32
|
+
"spec/spec.opts",
|
33
|
+
"spec/spec_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/jchupp/is_paranoid/}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.5}
|
39
|
+
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.}
|
40
|
+
s.test_files = [
|
41
|
+
"spec/is_paranoid_spec.rb",
|
42
|
+
"spec/models.rb",
|
43
|
+
"spec/schema.rb",
|
44
|
+
"spec/spec_helper.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
49
|
+
s.specification_version = 3
|
50
|
+
|
51
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
52
|
+
else
|
53
|
+
end
|
54
|
+
else
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/lib/is_paranoid.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'activerecord'
|
2
|
+
|
3
|
+
module IsParanoid
|
4
|
+
# Call this in your model to enable all the safety-net goodness
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# class Android < ActiveRecord::Base
|
9
|
+
# is_paranoid
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
|
13
|
+
def is_paranoid?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_paranoid opts = {}
|
18
|
+
opts[:field] ||= [:deleted_at, Proc.new{Time.now.utc}, nil]
|
19
|
+
class_inheritable_accessor :destroyed_field, :field_destroyed, :field_not_destroyed
|
20
|
+
self.destroyed_field, self.field_destroyed, self.field_not_destroyed = opts[:field]
|
21
|
+
|
22
|
+
if self.reflect_on_all_associations.size > 0 && ! opts[:suppress_load_order_warning]
|
23
|
+
warn "is_paranoid warning in class #{self}: You should declare is_paranoid before your associations"
|
24
|
+
end
|
25
|
+
|
26
|
+
# This is the real magic. All calls made to this model will append
|
27
|
+
# the conditions deleted_at => nil (or whatever your destroyed_field
|
28
|
+
# and field_not_destroyed are). All exceptions require using
|
29
|
+
# exclusive_scope (see self.delete_all, self.count_with_destroyed,
|
30
|
+
# and self.find_with_destroyed defined in the module ClassMethods)
|
31
|
+
default_scope :conditions => {destroyed_field => field_not_destroyed}
|
32
|
+
|
33
|
+
extend ClassMethods
|
34
|
+
include InstanceMethods
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def is_paranoid?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_or_equals_not_destroyed
|
43
|
+
if [nil, 'NULL'].include?(field_not_destroyed)
|
44
|
+
'IS NULL'
|
45
|
+
else
|
46
|
+
"= #{field_not_destroyed}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# ensure that we respect the is_paranoid conditions when being loaded as a has_many :through
|
51
|
+
# NOTE: this only works if is_paranoid is declared before has_many relationships.
|
52
|
+
# Only use is_paranoid conditions when the associated class is also paranoid
|
53
|
+
def has_many(association_id, options = {}, &extension)
|
54
|
+
association_klass_name = options[:class_name] || options[:source] || association_id
|
55
|
+
association_klass = association_klass_name.to_s.classify.try(:constantize)
|
56
|
+
|
57
|
+
if options.key?(:through) && association_klass.is_paranoid?
|
58
|
+
conditions = "#{options[:through].to_s.pluralize}.#{destroyed_field} #{is_or_equals_not_destroyed}"
|
59
|
+
options[:conditions] = "(" + [options[:conditions], conditions].compact.join(") AND (") + ")"
|
60
|
+
end
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
# Actually delete the model, bypassing the safety net. Because
|
65
|
+
# this method is called internally by Model.delete(id) and on the
|
66
|
+
# delete method in each instance, we don't need to specify those
|
67
|
+
# methods separately
|
68
|
+
def delete_all conditions = nil
|
69
|
+
self.with_exclusive_scope { super conditions }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Use update_all with an exclusive scope to restore undo the soft-delete.
|
73
|
+
# This bypasses update-related callbacks.
|
74
|
+
#
|
75
|
+
# By default, restores cascade through associations that are belongs_to
|
76
|
+
# :dependent => :destroy and under is_paranoid. You can prevent restoration
|
77
|
+
# of associated models by passing :include_destroyed_dependents => false,
|
78
|
+
# for example:
|
79
|
+
#
|
80
|
+
# Android.restore(:include_destroyed_dependents => false)
|
81
|
+
#
|
82
|
+
# Alternatively you can specify which relationships to restore via :include,
|
83
|
+
# for example:
|
84
|
+
#
|
85
|
+
# Android.restore(:include => [:parts, memories])
|
86
|
+
#
|
87
|
+
# Please note that specifying :include means you're not using
|
88
|
+
# :include_destroyed_dependents by default, though you can explicitly use
|
89
|
+
# both if you want all has_* relationships and specific belongs_to
|
90
|
+
# relationships, for example
|
91
|
+
#
|
92
|
+
# Android.restore(:include => [:home, :planet], :include_destroyed_dependents => true)
|
93
|
+
def restore(id, options = {})
|
94
|
+
options.reverse_merge!({:include_destroyed_dependents => true}) unless options[:include]
|
95
|
+
with_exclusive_scope do
|
96
|
+
update_all(
|
97
|
+
"#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
|
98
|
+
primary_key.to_sym => id
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
self.reflect_on_all_associations.each do |association|
|
103
|
+
if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
|
104
|
+
dependent_relationship = association.macro.to_s =~ /^has/
|
105
|
+
if should_restore?(association.name, dependent_relationship, options)
|
106
|
+
if dependent_relationship
|
107
|
+
restore_related(association.klass, association.primary_key_name, id, options)
|
108
|
+
else
|
109
|
+
restore_related(
|
110
|
+
association.klass,
|
111
|
+
association.klass.primary_key,
|
112
|
+
self.first(id).send(association.primary_key_name),
|
113
|
+
options
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# TODO: needs better implementation
|
122
|
+
def exists_with_destroyed id
|
123
|
+
self.with_exclusive_scope{ exists?(id)}
|
124
|
+
end
|
125
|
+
|
126
|
+
# find_with_destroyed and other blah_with_destroyed and
|
127
|
+
# blah_destroyed_only methods are defined here
|
128
|
+
def method_missing name, *args, &block
|
129
|
+
if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
|
130
|
+
self.extend(Module.new{
|
131
|
+
if $2 == '_with_destroyed'
|
132
|
+
# Example:
|
133
|
+
# def count_with_destroyed(*args)
|
134
|
+
# self.with_exclusive_scope{ self.send(:count, *args) }
|
135
|
+
# end
|
136
|
+
define_method name do |*args|
|
137
|
+
self.with_exclusive_scope{ self.send($1, *args) }
|
138
|
+
end
|
139
|
+
else
|
140
|
+
|
141
|
+
# Example:
|
142
|
+
# def count_destroyed_only(*args)
|
143
|
+
# self.with_exclusive_scope do
|
144
|
+
# with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
|
145
|
+
# self.send(:count, *args)
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
define_method name do |*args|
|
150
|
+
self.with_exclusive_scope do
|
151
|
+
with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
|
152
|
+
self.send($1, *args, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
})
|
159
|
+
self.send(name, *args, &block)
|
160
|
+
else
|
161
|
+
super(name, *args, &block)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# with_exclusive_scope is used internally by ActiveRecord when preloading
|
166
|
+
# associations. Unfortunately this is problematic for is_paranoid since we
|
167
|
+
# want preloaded is_paranoid items to still be scoped to their deleted conditions.
|
168
|
+
# so we override that here.
|
169
|
+
def with_exclusive_scope(method_scoping = {}, &block)
|
170
|
+
# this is rather hacky, suggestions for improvements appreciated... the idea
|
171
|
+
# is that when the caller includes the method preload_associations, we want
|
172
|
+
# to apply our is_paranoid conditions
|
173
|
+
if caller.any?{|c| c =~ /\d+:in `preload_associations'$/}
|
174
|
+
method_scoping.deep_merge!(:find => {:conditions => {destroyed_field => field_not_destroyed} })
|
175
|
+
end
|
176
|
+
super method_scoping, &block
|
177
|
+
end
|
178
|
+
|
179
|
+
protected
|
180
|
+
|
181
|
+
def should_restore?(association_name, dependent_relationship, options) #:nodoc:
|
182
|
+
([*options[:include]] || []).include?(association_name) or
|
183
|
+
(options[:include_destroyed_dependents] and dependent_relationship)
|
184
|
+
end
|
185
|
+
|
186
|
+
def restore_related klass, key_name, id, options #:nodoc:
|
187
|
+
klass.find_destroyed_only(:all,
|
188
|
+
:conditions => ["#{key_name} = ?", id]
|
189
|
+
).each do |model|
|
190
|
+
model.restore(options)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
module InstanceMethods
|
196
|
+
def destroyed?
|
197
|
+
destroyed_field != nil
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.included(base)
|
201
|
+
base.class_eval do
|
202
|
+
unless method_defined? :method_missing
|
203
|
+
def method_missing(meth, *args, &block); super; end
|
204
|
+
end
|
205
|
+
alias_method :old_method_missing, :method_missing
|
206
|
+
alias_method :method_missing, :is_paranoid_method_missing
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def is_paranoid_method_missing name, *args, &block
|
211
|
+
# if we're trying for a _____with_destroyed method
|
212
|
+
# and we can respond to the _____ method
|
213
|
+
# and we have an association by the name of _____
|
214
|
+
if name.to_s =~ /^(.*)(_with_destroyed)$/ and
|
215
|
+
self.respond_to?($1) and
|
216
|
+
(assoc = self.class.reflect_on_all_associations.detect{|a| a.name.to_s == $1})
|
217
|
+
|
218
|
+
parent_klass = Object.module_eval("::#{assoc.class_name}", __FILE__, __LINE__)
|
219
|
+
|
220
|
+
self.class.send(
|
221
|
+
:include,
|
222
|
+
Module.new {
|
223
|
+
if assoc.macro.to_s =~ /^has/
|
224
|
+
parent_method = assoc.macro.to_s =~ /^has_one/ ? 'first_with_destroyed' : 'all_with_destroyed'
|
225
|
+
# Example:
|
226
|
+
define_method name do |*args| # def android_with_destroyed
|
227
|
+
parent_klass.send("#{parent_method}", # Android.all_with_destroyed(
|
228
|
+
:conditions => { # :conditions => {
|
229
|
+
assoc.primary_key_name => # :person_id =>
|
230
|
+
self.send(parent_klass.primary_key) # self.send(:id)
|
231
|
+
} # }
|
232
|
+
) # )
|
233
|
+
end # end
|
234
|
+
|
235
|
+
else
|
236
|
+
# Example:
|
237
|
+
define_method name do |*args| # def android_with_destroyed
|
238
|
+
parent_klass.first_with_destroyed( # Android.first_with_destroyed(
|
239
|
+
:conditions => { # :conditions => {
|
240
|
+
parent_klass.primary_key => # :id =>
|
241
|
+
self.send(assoc.primary_key_name) # self.send(:android_id)
|
242
|
+
} # }
|
243
|
+
) # )
|
244
|
+
end # end
|
245
|
+
end
|
246
|
+
}
|
247
|
+
)
|
248
|
+
self.send(name, *args, &block)
|
249
|
+
else
|
250
|
+
old_method_missing(name, *args, &block)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Mark the model deleted_at as now.
|
255
|
+
def alt_destroy_without_callbacks
|
256
|
+
self.class.update_all(
|
257
|
+
"#{destroyed_field} = #{self.class.connection.quote(( field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed))}",
|
258
|
+
self.class.primary_key.to_sym => self.id
|
259
|
+
)
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
# Override the default destroy to allow us to flag deleted_at.
|
264
|
+
# This preserves the before_destroy and after_destroy callbacks.
|
265
|
+
# Because this is also called internally by Model.destroy_all and
|
266
|
+
# the Model.destroy(id), we don't need to specify those methods
|
267
|
+
# separately.
|
268
|
+
def destroy
|
269
|
+
return false if callback(:before_destroy) == false
|
270
|
+
result = alt_destroy_without_callbacks
|
271
|
+
callback(:after_destroy)
|
272
|
+
self
|
273
|
+
end
|
274
|
+
|
275
|
+
# Set deleted_at flag on a model to field_not_destroyed, effectively
|
276
|
+
# undoing the soft-deletion.
|
277
|
+
def restore(options = {})
|
278
|
+
self.class.restore(id, options)
|
279
|
+
self
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
ActiveRecord::Base.send(:extend, IsParanoid)
|
data/spec/database.yml
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/models')
|
3
|
+
|
4
|
+
LUKE = 'Luke Skywalker'
|
5
|
+
|
6
|
+
describe IsParanoid do
|
7
|
+
before(:each) do
|
8
|
+
Android.delete_all
|
9
|
+
Person.delete_all
|
10
|
+
Component.delete_all
|
11
|
+
|
12
|
+
@luke = Person.create(:name => LUKE)
|
13
|
+
@r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
|
14
|
+
@c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
|
15
|
+
|
16
|
+
@r2d2.components.create(:name => 'Rotors')
|
17
|
+
|
18
|
+
@r2d2.memories.create(:name => 'A pretty sunset')
|
19
|
+
@c3p0.sticker = Sticker.create(:name => 'OMG, PONIES!')
|
20
|
+
@tatooine = Place.create(:name => "Tatooine")
|
21
|
+
@r2d2.places << @tatooine
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'non-is_paranoid models' do
|
25
|
+
it 'should not be paranoid' do
|
26
|
+
Person.is_paranoid?.should eql(false)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should destroy as normal" do
|
30
|
+
lambda{
|
31
|
+
@luke.destroy
|
32
|
+
}.should change(Person, :count).by(-1)
|
33
|
+
|
34
|
+
lambda{
|
35
|
+
Person.count_with_destroyed
|
36
|
+
}.should raise_error(NoMethodError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'destroying' do
|
41
|
+
it "should soft-delete a record" do
|
42
|
+
lambda{
|
43
|
+
Android.destroy(@r2d2.id)
|
44
|
+
}.should change(Android, :count).from(2).to(1)
|
45
|
+
Android.count_with_destroyed.should == 2
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should not hit update/save related callbacks" do
|
49
|
+
lambda{
|
50
|
+
Android.first.update_attribute(:name, 'Robocop')
|
51
|
+
}.should raise_error
|
52
|
+
|
53
|
+
lambda{
|
54
|
+
Android.first.destroy
|
55
|
+
}.should_not raise_error
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should soft-delete matching items on Model.destroy_all" do
|
59
|
+
lambda{
|
60
|
+
Android.destroy_all("owner_id = #{@luke.id}")
|
61
|
+
}.should change(Android, :count).from(2).to(0)
|
62
|
+
Android.count_with_destroyed.should == 2
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'related models' do
|
66
|
+
it "should no longer show up in the relationship to the owner" do
|
67
|
+
@luke.androids.size.should == 2
|
68
|
+
@r2d2.destroy
|
69
|
+
@luke.androids.size.should == 1
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should soft-delete on dependent destroys" do
|
73
|
+
lambda{
|
74
|
+
@luke.destroy
|
75
|
+
}.should change(Android, :count).from(2).to(0)
|
76
|
+
Android.count_with_destroyed.should == 2
|
77
|
+
end
|
78
|
+
|
79
|
+
it "shouldn't have problems with has_many :through relationships that are paranoid" do
|
80
|
+
# TODO: this spec can be cleaner and more specific, replace it later
|
81
|
+
# Dings use a boolean non-standard is_paranoid field
|
82
|
+
# Scratch uses the defaults. Testing both ensures compatibility
|
83
|
+
[[:dings, Ding], [:scratches, Scratch]].each do |method, klass|
|
84
|
+
@r2d2.dings.should == []
|
85
|
+
|
86
|
+
dent = Dent.create(:description => 'really terrible', :android_id => @r2d2.id)
|
87
|
+
item = klass.create(:description => 'quite nasty', :dent_id => dent.id)
|
88
|
+
@r2d2.reload
|
89
|
+
@r2d2.send(method).should == [item]
|
90
|
+
|
91
|
+
dent.destroy
|
92
|
+
@r2d2.reload
|
93
|
+
@r2d2.send(method).should == []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "shouldn't have problems with has_many :through relationships that are not paranoid" do
|
98
|
+
@r2d2.dings.should == []
|
99
|
+
|
100
|
+
dent = Dent.create(:description => 'really terrible', :android_id => @r2d2.id)
|
101
|
+
hole = Hole.new(:description => 'What a big hole', :dent_id => dent.id)
|
102
|
+
|
103
|
+
@r2d2.reload
|
104
|
+
@r2d2.holes.should == [hole]
|
105
|
+
|
106
|
+
hole.destroy
|
107
|
+
@r2d2.reload
|
108
|
+
@r2d2.holes.should == []
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should not choke has_and_belongs_to_many relationships" do
|
112
|
+
@r2d2.places.should include(@tatooine)
|
113
|
+
@tatooine.destroy
|
114
|
+
@r2d2.reload
|
115
|
+
@r2d2.places.should_not include(@tatooine)
|
116
|
+
Place.all_with_destroyed.should include(@tatooine)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'finding destroyed models' do
|
122
|
+
it "should be able to find destroyed items via #find_with_destroyed" do
|
123
|
+
@r2d2.destroy
|
124
|
+
Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
|
125
|
+
Android.first_with_destroyed(:conditions => {:name => 'R2D2'}).should_not be_blank
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should be able to find only destroyed items via #find_destroyed_only" do
|
129
|
+
@r2d2.destroy
|
130
|
+
Android.all_destroyed_only.size.should == 1
|
131
|
+
Android.first_destroyed_only.should == @r2d2
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should not show destroyed models via :include" do
|
135
|
+
Person.first(:conditions => {:name => LUKE}, :include => :androids).androids.size.should == 2
|
136
|
+
@r2d2.destroy
|
137
|
+
person = Person.first(:conditions => {:name => LUKE}, :include => :androids)
|
138
|
+
# ensure that we're using the preload and not loading it via a find
|
139
|
+
Android.should_not_receive(:find)
|
140
|
+
person.androids.size.should == 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe 'calculations' do
|
145
|
+
it "should have a proper count inclusively and exclusively of destroyed items" do
|
146
|
+
@r2d2.destroy
|
147
|
+
@c3p0.destroy
|
148
|
+
Android.count.should == 0
|
149
|
+
Android.count_with_destroyed.should == 2
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should respond to various calculations" do
|
153
|
+
@r2d2.destroy
|
154
|
+
Android.sum('id').should == @c3p0.id
|
155
|
+
Android.sum_with_destroyed('id').should == @r2d2.id + @c3p0.id
|
156
|
+
Android.average_with_destroyed('id').should == (@r2d2.id + @c3p0.id) / 2.0
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'deletion' do
|
161
|
+
it "should actually remove records on #delete_all" do
|
162
|
+
lambda{
|
163
|
+
Android.delete_all
|
164
|
+
}.should change(Android, :count_with_destroyed).from(2).to(0)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should actually remove records on #delete" do
|
168
|
+
lambda{
|
169
|
+
Android.first.delete
|
170
|
+
}.should change(Android, :count_with_destroyed).from(2).to(1)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe 'restore' do
|
175
|
+
it "should allow restoring soft-deleted items" do
|
176
|
+
@r2d2.destroy
|
177
|
+
lambda{
|
178
|
+
@r2d2.restore
|
179
|
+
}.should change(Android, :count).from(1).to(2)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should not hit update/save related callbacks" do
|
183
|
+
@r2d2.destroy
|
184
|
+
|
185
|
+
lambda{
|
186
|
+
@r2d2.update_attribute(:name, 'Robocop')
|
187
|
+
}.should raise_error
|
188
|
+
|
189
|
+
lambda{
|
190
|
+
@r2d2.restore
|
191
|
+
}.should_not raise_error
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should restore dependent models when being restored by default" do
|
195
|
+
@r2d2.destroy
|
196
|
+
lambda{
|
197
|
+
@r2d2.restore
|
198
|
+
}.should change(Component, :count).from(0).to(1)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should provide the option to not restore dependent models" do
|
202
|
+
@r2d2.destroy
|
203
|
+
lambda{
|
204
|
+
@r2d2.restore(:include_destroyed_dependents => false)
|
205
|
+
}.should_not change(Component, :count)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should restore parent and child models specified via :include" do
|
209
|
+
sub_component = SubComponent.create(:name => 'part', :component_id => @r2d2.components.first.id)
|
210
|
+
@r2d2.destroy
|
211
|
+
SubComponent.first(:conditions => {:id => sub_component.id}).should be_nil
|
212
|
+
@r2d2.components.first.restore(:include => [:android, :sub_components])
|
213
|
+
SubComponent.first(:conditions => {:id => sub_component.id}).should_not be_nil
|
214
|
+
Android.find(@r2d2.id).should_not be_nil
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe 'validations' do
|
219
|
+
it "should not ignore destroyed items in validation checks unless scoped" do
|
220
|
+
# Androids are not validates_uniqueness_of scoped
|
221
|
+
@r2d2.destroy
|
222
|
+
lambda{
|
223
|
+
Android.create!(:name => 'R2D2')
|
224
|
+
}.should raise_error(ActiveRecord::RecordInvalid)
|
225
|
+
|
226
|
+
lambda{
|
227
|
+
# creating shouldn't raise an error
|
228
|
+
another_r2d2 = AndroidWithScopedUniqueness.create!(:name => 'R2D2')
|
229
|
+
# neither should destroying the second incarnation since the
|
230
|
+
# validates_uniqueness_of is only applied on create
|
231
|
+
another_r2d2.destroy
|
232
|
+
}.should_not raise_error
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '(parent)_with_destroyed' do
|
237
|
+
it "should be able to access destroyed parents" do
|
238
|
+
# Memory is has_many with a non-default primary key
|
239
|
+
# Sticker is a has_one with a default primary key
|
240
|
+
[Memory, Sticker].each do |klass|
|
241
|
+
instance = klass.last
|
242
|
+
parent = instance.android
|
243
|
+
instance.android.destroy
|
244
|
+
|
245
|
+
# reload so the model doesn't remember the parent
|
246
|
+
instance.reload
|
247
|
+
instance.android.should == nil
|
248
|
+
instance.android_with_destroyed.should == parent
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
it "should be able to access destroyed children" do
|
253
|
+
comps = @r2d2.components
|
254
|
+
comps.to_s # I have no idea why this makes it pass, but hey, here it is
|
255
|
+
@r2d2.components.first.destroy
|
256
|
+
@r2d2.components_with_destroyed.should == comps
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should return nil if no destroyed parent exists" do
|
260
|
+
sticker = Sticker.new(:name => 'Rainbows')
|
261
|
+
# because the default relationship works this way, i.e.
|
262
|
+
sticker.android.should == nil
|
263
|
+
sticker.android_with_destroyed.should == nil
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should not break method_missing's defined before the is_paranoid call" do
|
267
|
+
# we've defined a method_missing on Sticker
|
268
|
+
# that changes the sticker name.
|
269
|
+
sticker = Sticker.new(:name => "Ponies!")
|
270
|
+
lambda{
|
271
|
+
sticker.some_crazy_method_that_we_certainly_do_not_respond_to
|
272
|
+
}.should change(sticker, :name).to(Sticker::MM_NAME)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe 'alternate fields and field values' do
|
277
|
+
it "should properly function for boolean values" do
|
278
|
+
# ninjas are invisible by default. not being ninjas, we can only
|
279
|
+
# find those that are visible
|
280
|
+
ninja = Ninja.create(:name => 'Esteban', :visible => true)
|
281
|
+
ninja.vanish # aliased to destroy
|
282
|
+
Ninja.first.should be_blank
|
283
|
+
Ninja.find_with_destroyed(:first).should == ninja
|
284
|
+
Ninja.count.should == 0
|
285
|
+
|
286
|
+
# we're only interested in pirates who are alive by default
|
287
|
+
pirate = Pirate.create(:name => 'Reginald')
|
288
|
+
pirate.destroy
|
289
|
+
Pirate.first.should be_blank
|
290
|
+
Pirate.find_with_destroyed(:first).should == pirate
|
291
|
+
Pirate.count.should == 0
|
292
|
+
|
293
|
+
# we're only interested in pirates who are dead by default.
|
294
|
+
# zombie pirates ftw!
|
295
|
+
DeadPirate.first.id.should == pirate.id
|
296
|
+
lambda{
|
297
|
+
DeadPirate.first.destroy
|
298
|
+
}.should change(Pirate, :count).from(0).to(1)
|
299
|
+
DeadPirate.count.should == 0
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
describe 'after_destroy and before_destroy callbacks' do
|
304
|
+
it "should rollback if before_destroy fails" do
|
305
|
+
edward = UndestroyablePirate.create(:name => 'Edward')
|
306
|
+
lambda{
|
307
|
+
edward.destroy
|
308
|
+
}.should_not change(UndestroyablePirate, :count)
|
309
|
+
end
|
310
|
+
|
311
|
+
it "should rollback if after_destroy raises an error" do
|
312
|
+
raul = RandomPirate.create(:name => 'Raul')
|
313
|
+
lambda{
|
314
|
+
begin
|
315
|
+
raul.destroy
|
316
|
+
rescue => ex
|
317
|
+
ex.message.should == 'after_destroy works'
|
318
|
+
end
|
319
|
+
}.should_not change(RandomPirate, :count)
|
320
|
+
end
|
321
|
+
|
322
|
+
it "should handle callbacks normally assuming no failures are encountered" do
|
323
|
+
component = Component.first
|
324
|
+
lambda{
|
325
|
+
component.destroy
|
326
|
+
}.should change(component, :name).to(Component::NEW_NAME)
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
describe "alternate primary key" do
|
332
|
+
it "should destroy without problem" do
|
333
|
+
uuid = Uuid.create(:name => "foo")
|
334
|
+
uuid.destroy.should be_true
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
class Person < ActiveRecord::Base #:nodoc:
|
2
|
+
validates_uniqueness_of :name
|
3
|
+
has_many :androids, :foreign_key => :owner_id, :dependent => :destroy
|
4
|
+
end
|
5
|
+
|
6
|
+
class Android < ActiveRecord::Base #:nodoc:
|
7
|
+
is_paranoid
|
8
|
+
validates_uniqueness_of :name
|
9
|
+
has_many :components, :dependent => :destroy
|
10
|
+
has_one :sticker
|
11
|
+
has_many :memories, :foreign_key => 'parent_id'
|
12
|
+
has_many :dents
|
13
|
+
has_many :dings, :through => :dents
|
14
|
+
has_many :scratches, :through => :dents
|
15
|
+
has_many :holes, :through => :dents
|
16
|
+
has_and_belongs_to_many :places
|
17
|
+
|
18
|
+
# this code is to ensure that our destroy and restore methods
|
19
|
+
# work without triggering before/after_update callbacks
|
20
|
+
before_update :raise_hell
|
21
|
+
def raise_hell
|
22
|
+
raise "hell"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Dent < ActiveRecord::Base #:nodoc:
|
27
|
+
is_paranoid
|
28
|
+
belongs_to :android
|
29
|
+
has_many :dings
|
30
|
+
has_many :scratches
|
31
|
+
has_many :holes
|
32
|
+
end
|
33
|
+
|
34
|
+
class Ding < ActiveRecord::Base #:nodoc:
|
35
|
+
is_paranoid :field => [:not_deleted, true, false]
|
36
|
+
belongs_to :dent
|
37
|
+
end
|
38
|
+
|
39
|
+
class Scratch < ActiveRecord::Base #:nodoc:
|
40
|
+
is_paranoid
|
41
|
+
belongs_to :dent
|
42
|
+
end
|
43
|
+
|
44
|
+
class Hole < ActiveRecord::Base #:nodoc:
|
45
|
+
belongs_to :dent
|
46
|
+
end
|
47
|
+
|
48
|
+
class Component < ActiveRecord::Base #:nodoc:
|
49
|
+
is_paranoid
|
50
|
+
belongs_to :android, :dependent => :destroy
|
51
|
+
has_many :sub_components, :dependent => :destroy
|
52
|
+
NEW_NAME = 'Something Else!'
|
53
|
+
|
54
|
+
after_destroy :change_name
|
55
|
+
def change_name
|
56
|
+
self.update_attribute(:name, NEW_NAME)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class SubComponent < ActiveRecord::Base #:nodoc:
|
61
|
+
is_paranoid
|
62
|
+
belongs_to :component, :dependent => :destroy
|
63
|
+
end
|
64
|
+
|
65
|
+
class Memory < ActiveRecord::Base #:nodoc:
|
66
|
+
is_paranoid
|
67
|
+
belongs_to :android, :class_name => "Android", :foreign_key => "parent_id"
|
68
|
+
end
|
69
|
+
|
70
|
+
class Sticker < ActiveRecord::Base #:nodoc:
|
71
|
+
MM_NAME = "You've got method_missing"
|
72
|
+
|
73
|
+
# this simply serves to ensure that we don't break method_missing
|
74
|
+
# if it is implemented on a class and called before is_paranoid
|
75
|
+
def method_missing name, *args, &block
|
76
|
+
self.name = MM_NAME
|
77
|
+
end
|
78
|
+
|
79
|
+
is_paranoid
|
80
|
+
belongs_to :android
|
81
|
+
end
|
82
|
+
|
83
|
+
class AndroidWithScopedUniqueness < ActiveRecord::Base #:nodoc:
|
84
|
+
set_table_name :androids
|
85
|
+
validates_uniqueness_of :name, :scope => :deleted_at
|
86
|
+
is_paranoid
|
87
|
+
end
|
88
|
+
|
89
|
+
class Place < ActiveRecord::Base #:nodoc:
|
90
|
+
is_paranoid
|
91
|
+
has_and_belongs_to_many :androids
|
92
|
+
end
|
93
|
+
|
94
|
+
class AndroidsPlaces < ActiveRecord::Base #:nodoc:
|
95
|
+
end
|
96
|
+
|
97
|
+
class Ninja < ActiveRecord::Base #:nodoc:
|
98
|
+
validates_uniqueness_of :name, :scope => :visible
|
99
|
+
is_paranoid :field => [:visible, false, true]
|
100
|
+
|
101
|
+
alias_method :vanish, :destroy
|
102
|
+
end
|
103
|
+
|
104
|
+
class Pirate < ActiveRecord::Base #:nodoc:
|
105
|
+
is_paranoid :field => [:alive, false, true]
|
106
|
+
end
|
107
|
+
|
108
|
+
class DeadPirate < ActiveRecord::Base #:nodoc:
|
109
|
+
set_table_name :pirates
|
110
|
+
is_paranoid :field => [:alive, true, false]
|
111
|
+
end
|
112
|
+
|
113
|
+
class RandomPirate < ActiveRecord::Base #:nodoc:
|
114
|
+
set_table_name :pirates
|
115
|
+
|
116
|
+
def after_destroy
|
117
|
+
raise 'after_destroy works'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class UndestroyablePirate < ActiveRecord::Base #:nodoc:
|
122
|
+
set_table_name :pirates
|
123
|
+
is_paranoid :field => [:alive, false, true]
|
124
|
+
|
125
|
+
def before_destroy
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Uuid < ActiveRecord::Base #:nodoc:
|
131
|
+
set_primary_key "uuid"
|
132
|
+
|
133
|
+
def before_create
|
134
|
+
self.uuid = "295b3430-85b8-012c-cfe4-002332cf7d5e"
|
135
|
+
end
|
136
|
+
|
137
|
+
is_paranoid
|
138
|
+
end
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 20090317164830) do
|
2
|
+
create_table "androids", :force => true do |t|
|
3
|
+
t.string "name"
|
4
|
+
t.integer "owner_id"
|
5
|
+
t.datetime "deleted_at"
|
6
|
+
t.datetime "created_at"
|
7
|
+
t.datetime "updated_at"
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table "dents", :force => true do |t|
|
11
|
+
t.integer "android_id"
|
12
|
+
t.string "description"
|
13
|
+
t.datetime "deleted_at"
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table "dings", :force => true do |t|
|
17
|
+
t.integer "dent_id"
|
18
|
+
t.string "description"
|
19
|
+
t.boolean "not_deleted"
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "scratches", :force => true do |t|
|
23
|
+
t.integer "dent_id"
|
24
|
+
t.string "description"
|
25
|
+
t.datetime "deleted_at"
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table "holes", :force => true do |t|
|
29
|
+
t.integer "dent_id"
|
30
|
+
t.string "description"
|
31
|
+
end
|
32
|
+
|
33
|
+
create_table "androids_places", :force => true, :id => false do |t|
|
34
|
+
t.integer "android_id"
|
35
|
+
t.integer "place_id"
|
36
|
+
end
|
37
|
+
|
38
|
+
create_table "places", :force => true do |t|
|
39
|
+
t.string "name"
|
40
|
+
t.datetime "deleted_at"
|
41
|
+
end
|
42
|
+
|
43
|
+
create_table "people", :force => true do |t|
|
44
|
+
t.string "name"
|
45
|
+
t.datetime "created_at"
|
46
|
+
t.datetime "updated_at"
|
47
|
+
end
|
48
|
+
|
49
|
+
create_table "components", :force => true do |t|
|
50
|
+
t.string "name"
|
51
|
+
t.integer "android_id"
|
52
|
+
t.datetime "deleted_at"
|
53
|
+
t.datetime "created_at"
|
54
|
+
t.datetime "updated_at"
|
55
|
+
end
|
56
|
+
|
57
|
+
create_table "sub_components", :force => true do |t|
|
58
|
+
t.string "name"
|
59
|
+
t.integer "component_id"
|
60
|
+
t.datetime "deleted_at"
|
61
|
+
end
|
62
|
+
|
63
|
+
create_table "memories", :force => true do |t|
|
64
|
+
t.string "name"
|
65
|
+
t.integer "parent_id"
|
66
|
+
t.datetime "deleted_at"
|
67
|
+
end
|
68
|
+
|
69
|
+
create_table "stickers", :force => true do |t|
|
70
|
+
t.string "name"
|
71
|
+
t.integer "android_id"
|
72
|
+
t.datetime "deleted_at"
|
73
|
+
end
|
74
|
+
|
75
|
+
create_table "ninjas", :force => true do |t|
|
76
|
+
t.string "name"
|
77
|
+
t.boolean "visible", :default => false
|
78
|
+
end
|
79
|
+
|
80
|
+
create_table "pirates", :force => true do |t|
|
81
|
+
t.string "name"
|
82
|
+
t.boolean "alive", :default => true
|
83
|
+
end
|
84
|
+
|
85
|
+
create_table "uuids", :id => false, :force => true do |t|
|
86
|
+
t.string "uuid", :limit => 36, :primary => true
|
87
|
+
t.string "name"
|
88
|
+
t.datetime "deleted_at"
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "#{File.dirname(__FILE__)}/../lib/is_paranoid"
|
3
|
+
require 'activerecord'
|
4
|
+
require 'yaml'
|
5
|
+
require 'spec'
|
6
|
+
|
7
|
+
def connect(environment)
|
8
|
+
conf = YAML::load(File.open(File.dirname(__FILE__) + '/database.yml'))
|
9
|
+
ActiveRecord::Base.establish_connection(conf[environment])
|
10
|
+
end
|
11
|
+
|
12
|
+
# Open ActiveRecord connection
|
13
|
+
connect('test')
|
14
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jomz-is_paranoid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 53
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 9
|
9
|
+
- 7
|
10
|
+
version: 0.9.7
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jeffrey Chupp
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2009-11-23 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: ""
|
23
|
+
email: jeff@semanticart.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.textile
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- CHANGELOG
|
33
|
+
- MIT-LICENSE
|
34
|
+
- README.textile
|
35
|
+
- Rakefile
|
36
|
+
- VERSION.yml
|
37
|
+
- init.rb
|
38
|
+
- is_paranoid.gemspec
|
39
|
+
- lib/is_paranoid.rb
|
40
|
+
- spec/database.yml
|
41
|
+
- spec/is_paranoid_spec.rb
|
42
|
+
- spec/models.rb
|
43
|
+
- spec/schema.rb
|
44
|
+
- spec/spec.opts
|
45
|
+
- spec/spec_helper.rb
|
46
|
+
has_rdoc: true
|
47
|
+
homepage: http://github.com/jchupp/is_paranoid/
|
48
|
+
licenses: []
|
49
|
+
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options:
|
52
|
+
- --charset=UTF-8
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.3.7
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
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.
|
80
|
+
test_files:
|
81
|
+
- spec/is_paranoid_spec.rb
|
82
|
+
- spec/models.rb
|
83
|
+
- spec/schema.rb
|
84
|
+
- spec/spec_helper.rb
|