woods-is_paranoid 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,76 @@
1
+ h1. is_paranoid ( same as it ever was )
2
+
3
+ h3. and you may ask yourself, well, how did I get here?
4
+
5
+ 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*.
6
+
7
+ h3. and you may ask yourself, how do I work this?
8
+
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
+
11
+ You need ActiveRecord 2.3 and you need to properly install this gem. Then you need a model with a deleted_at timestamp column on its database table. If that column is null, the item isn't deleted. If it has a timestamp, it should count as deleted.
12
+
13
+ So let's assume we have a model Automobile that has a deleted_at column on the automobiles table.
14
+
15
+ If you're working with Rails, in your environment.rb, add the following to your initializer block.
16
+
17
+ <pre>
18
+ Rails::Initializer.run do |config|
19
+ # ...
20
+ config.gem "jchupp-is_paranoid", :lib => 'is_paranoid', :version => ">= 0.0.1"
21
+ end
22
+ </pre>
23
+
24
+ Then in your ActiveRecord model
25
+
26
+ <pre>
27
+ class Automobile < ActiveRecord::Base
28
+ is_paranoid :with_calculations => true
29
+ end
30
+ </pre>
31
+
32
+ Now our automobiles are now soft-deleteable. In fact, that's more complicated than necessary since you don't have to use :with_calculations => true unless you want to get special calculation methods extended off of ActiveRecord::Calculations like count_with_destroyed and sum_with_destroyed.
33
+
34
+ <pre>
35
+ that_large_automobile = Automobile.create()
36
+ Automobile.count # => 1
37
+
38
+ that_large_automobile.destroy
39
+ Automobile.count # => 0
40
+ Automobile.count_with_destroyed # => 1
41
+
42
+ # where is that large automobile?
43
+ that_large_automobile = Automobile.find_with_destroyed(:all).first
44
+ that_large_automobile.restore
45
+ Automobile.count # => 1
46
+ </pre>
47
+
48
+ One thing to note, destroying is always undo-able, but deleting is not.
49
+
50
+ <pre>
51
+ Automobile.destroy_all
52
+ Automobile.count # => 0
53
+ Automobile.count_with_destroyed # => 1
54
+
55
+ Automobile.delete_all
56
+ Automobile.count_with_destroyed # => 0
57
+ # And you may say to yourself, "My god! What have I done?"
58
+ </pre>
59
+
60
+ h3. Note:
61
+
62
+ 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.
63
+
64
+ h3. and you may ask yourself, where does that highway go to?
65
+
66
+ 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.
67
+
68
+ Currently on the todo list:
69
+ * deal with validates_uniqueness_of issue
70
+ * add options for merging additional default_scope options (i.e. order, etc.)
71
+
72
+ h3. Thanks
73
+
74
+ 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.
75
+
76
+ [defscope]http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 2
@@ -0,0 +1,93 @@
1
+ require 'activerecord'
2
+
3
+ module IsParanoid
4
+ def self.included(base) # :nodoc:
5
+ base.extend SafetyNet
6
+ end
7
+
8
+ module SafetyNet
9
+ # Call this in your model to enable all the safety-net goodness
10
+ #
11
+ # Example:
12
+ #
13
+ # class Android < ActiveRecord::Base
14
+ # is_paranoid
15
+ # end
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
+ class_eval do
22
+ # This is the real magic. All calls made to this model will append
23
+ # the conditions deleted_at => nil. Exceptions require using
24
+ # exclusive_scope (see self.delete_all, self.count_with_destroyed,
25
+ # and self.find_with_destroyed )
26
+ default_scope :conditions => {:deleted_at => nil}
27
+
28
+ # Actually delete the model, bypassing the safety net. Because
29
+ # this method is called internally by Model.delete(id) and on the
30
+ # delete method in each instance, we don't need to specify those
31
+ # methods separately
32
+ def self.delete_all conditions = nil
33
+ self.with_exclusive_scope do
34
+ super conditions
35
+ end
36
+ end
37
+
38
+ # Return instances of all models matching the query regardless
39
+ # of whether or not they have been soft-deleted.
40
+ def self.find_with_destroyed *args
41
+ self.with_exclusive_scope { find(*args) }
42
+ end
43
+
44
+ # Returns all destroyed views
45
+ def self.find_only_destroyed *args
46
+ self.with_exclusive_scope { with_only_destroyed_scope { find(*args) } }
47
+ end
48
+
49
+ # Mark the model deleted_at as now.
50
+ def destroy_without_callbacks
51
+ self.update_attribute(:deleted_at, Time.now.utc)
52
+ end
53
+
54
+ # Override the default destroy to allow us to flag deleted_at.
55
+ # This preserves the before_destroy and after_destroy callbacks.
56
+ # Because this is also called internally by Model.destroy_all and
57
+ # the Model.destroy(id), we don't need to specify those methods
58
+ # separately.
59
+ def destroy
60
+ return false if callback(:before_destroy) == false
61
+ result = destroy_without_callbacks
62
+ callback(:after_destroy)
63
+ result
64
+ end
65
+
66
+ # Set deleted_at flag on a model to nil, effectively undoing the
67
+ # soft-deletion.
68
+ def restore
69
+ self.update_attribute(:deleted_at, nil)
70
+ end
71
+
72
+ protected
73
+
74
+ def self.with_only_destroyed_scope(&block)
75
+ with_scope({:find => { :conditions => ["deleted_at IS NOT NULL"] }}, &block)
76
+ end
77
+ end
78
+
79
+ if opts[:with_calculations]
80
+ self.extend(Module.new{
81
+ [:average, :calculate, :construct_count_options_from_args,
82
+ :count, :maximum, :minimum, :sum].each do |method| # EXAMPLE OUTPUT:
83
+ define_method "#{method}_with_destroyed" do |*args| # def count_with_destroyed(*args)
84
+ self.with_exclusive_scope{ self.send(method, *args) } # self.with_exclusive_scope{ self.send(:count, *args) }
85
+ end # end
86
+ end
87
+ })
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ ActiveRecord::Base.send(:include, IsParanoid)
data/spec/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ :adapter: sqlite3
3
+ :dbfile: is_paranoid.db
@@ -0,0 +1,105 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class Person < ActiveRecord::Base
4
+ has_many :androids, :foreign_key => :owner_id, :dependent => :destroy
5
+ end
6
+
7
+ class Android < ActiveRecord::Base
8
+ validates_uniqueness_of :name
9
+ is_paranoid :with_calculations => true
10
+ end
11
+
12
+ class NoCalculation < ActiveRecord::Base
13
+ is_paranoid
14
+ end
15
+
16
+ describe Android do
17
+ before(:each) do
18
+ Android.delete_all
19
+ Person.delete_all
20
+
21
+ @luke = Person.create(:name => 'Luke Skywalker')
22
+ @r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
23
+ @c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
24
+ end
25
+
26
+ it "should delete normally" do
27
+ Android.count_with_destroyed.should == 2
28
+ Android.delete_all
29
+ Android.count_with_destroyed.should == 0
30
+ end
31
+
32
+ it "should handle Model.destroy_all properly" do
33
+ lambda{
34
+ Android.destroy_all("owner_id = #{@luke.id}")
35
+ }.should change(Android, :count).from(2).to(0)
36
+ Android.count_with_destroyed.should == 2
37
+ end
38
+
39
+ it "should handle Model.destroy(id) properly" do
40
+ lambda{
41
+ Android.destroy(@r2d2.id)
42
+ }.should change(Android, :count).from(2).to(1)
43
+
44
+ Android.count_with_destroyed.should == 2
45
+ end
46
+
47
+ it "should be not show up in the relationship to the owner once deleted" do
48
+ @luke.androids.size.should == 2
49
+ @r2d2.destroy
50
+ @luke.androids.size.should == 1
51
+ Android.count.should == 1
52
+ Android.first(:conditions => {:name => 'R2D2'}).should be_blank
53
+ end
54
+
55
+ it "should be able to find deleted items via find_with_destroyed" do
56
+ @r2d2.destroy
57
+ Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
58
+ Android.find_with_destroyed(:first, :conditions => {:name => 'R2D2'}).should_not be_blank
59
+ end
60
+
61
+ it "should have a proper count inclusively and exclusively of deleted items" do
62
+ @r2d2.destroy
63
+ @c3p0.destroy
64
+ Android.count.should == 0
65
+ Android.count_with_destroyed.should == 2
66
+ end
67
+
68
+ it "should mark deleted on dependent destroys" do
69
+ lambda{
70
+ @luke.destroy
71
+ }.should change(Android, :count).from(2).to(0)
72
+ Android.count_with_destroyed.should == 2
73
+ end
74
+
75
+ it "should allow restoring" do
76
+ @r2d2.destroy
77
+ lambda{
78
+ @r2d2.restore
79
+ }.should change(Android, :count).from(1).to(2)
80
+ end
81
+
82
+ it "should find only destroyed videos" do
83
+ @r2d2.destroy
84
+ Android.find_only_destroyed(:all).should == [@r2d2]
85
+ end
86
+
87
+ it "should respond to various calculations if we specify that we want them" do
88
+ NoCalculation.respond_to?(:sum_with_destroyed).should == false
89
+ Android.respond_to?(:sum_with_destroyed).should == true
90
+
91
+ @r2d2.destroy
92
+ Android.sum('id').should == @c3p0.id
93
+ Android.sum_with_destroyed('id').should == @r2d2.id + @c3p0.id
94
+ end
95
+
96
+ # Note: this isn't necessarily ideal, this just serves to demostrate
97
+ # how it currently works
98
+ it "should not ignore deleted items in validation checks" do
99
+ @r2d2.destroy
100
+ lambda{
101
+ Android.create!(:name => 'R2D2')
102
+ }.should raise_error(ActiveRecord::RecordInvalid)
103
+ end
104
+
105
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,15 @@
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 "people", :force => true do |t|
11
+ t.string "name"
12
+ t.datetime "created_at"
13
+ t.datetime "updated_at"
14
+ end
15
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -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,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: woods-is_paranoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Jeffrey Chupp
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ""
17
+ email: jeff@semanticart.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.textile
24
+ files:
25
+ - README.textile
26
+ - VERSION.yml
27
+ - lib/is_paranoid.rb
28
+ - spec/database.yml
29
+ - spec/is_paranoid_spec.rb
30
+ - spec/schema.rb
31
+ - spec/spec.opts
32
+ - spec/spec_helper.rb
33
+ has_rdoc: true
34
+ homepage: http://github.com/jchupp/is_paranoid/
35
+ post_install_message:
36
+ rdoc_options:
37
+ - --inline-source
38
+ - --charset=UTF-8
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ 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.
60
+ test_files: []
61
+