woods-is_paranoid 0.0.2

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 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
+