mislav-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/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.markdown ADDED
@@ -0,0 +1,64 @@
1
+ Are you paranoid?
2
+ =================
3
+
4
+ Destroying records is a one-way ticket--you are permanently sending data
5
+ down the drain. *Unless*, of course, you are using this plugin.
6
+
7
+ Simply declare models paranoid:
8
+
9
+ class User < ActiveRecord::Base
10
+ is_paranoid
11
+ end
12
+
13
+ You will need to add the "deleted_at" datetime column on each model table
14
+ you declare paranoid. This is how the plugin tracks destroyed state.
15
+
16
+
17
+ Destroying
18
+ ----------
19
+
20
+ Calling `destroy` should work as you expect, only it doesn't actually delete the record:
21
+
22
+ User.count #=> 1
23
+
24
+ User.first.destroy
25
+
26
+ User.count #=> 0
27
+
28
+ # user is still there, only hidden:
29
+ User.count_with_destroyed #=> 1
30
+
31
+ What `destroy` does is that it sets the "deleted\_at" column to the current time.
32
+ Records that have a value for "deleted\_at" are considered deleted and are filtered
33
+ out from all requests using `default_scope` ActiveRecord feature:
34
+
35
+ default_scope :conditions => {:deleted_at => nil}
36
+
37
+ Restoring
38
+ ---------
39
+
40
+ No sense in keeping the data if we can't restore it, right?
41
+
42
+ user = User.find_with_destroyed(:first)
43
+
44
+ user.restore
45
+
46
+ User.count #=> 1
47
+
48
+ Restoring resets the "deleted_at" value back to `nil`.
49
+
50
+ Extra methods
51
+ -------------
52
+
53
+ Extra class methods provided by this plugin are:
54
+
55
+ 1. `Model.count_with_destroyed(*args)`
56
+ 2. `Model.find_with_destroyed(*args)`
57
+ 2. `Model.find_only_destroyed(*args)`
58
+
59
+
60
+ Pitfalls
61
+ --------
62
+
63
+ * `validates_uniqueness_of` does not ignore items marked with a "deleted_at" flag
64
+ * various eager-loading and associations-related issues (see ["Killing is_paranoid"](http://blog.semanticart.com/killing_is_paranoid/))
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ Spec::Rake::SpecTask.new do |t|
4
+ t.ruby_opts = ['-rubygems']
5
+ t.libs = ['lib', 'spec']
6
+ t.spec_opts = ['--color']
7
+ t.spec_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :gem do
11
+ system %(rm -f *.gem; gem build is_paranoid.gemspec)
12
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'is_paranoid'
@@ -0,0 +1,136 @@
1
+ require 'active_record'
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
+ def is_paranoid
17
+ class_eval do
18
+ # This is the real magic. All calls made to this model will
19
+ # append the conditions deleted_at => nil. Exceptions require
20
+ # using with_destroyed_scope (see self.delete_all,
21
+ # self.count_with_destroyed, and self.find_with_destroyed )
22
+ default_scope :conditions => {:deleted_at => nil}
23
+
24
+ # Actually delete the model, bypassing the safety net. Because
25
+ # this method is called internally by Model.delete(id) and on the
26
+ # delete method in each instance, we don't need to specify those
27
+ # methods separately
28
+ def self.delete_all conditions = nil
29
+ self.with_destroyed_scope { super conditions }
30
+ end
31
+
32
+ # Return a count that includes the soft-deleted models.
33
+ def self.count_with_destroyed *args
34
+ self.with_destroyed_scope { count(*args) }
35
+ end
36
+
37
+ # Perform a count only on destroyed instances.
38
+ def self.count_only_destroyed *args
39
+ self.with_only_destroyed_scope { count(*args) }
40
+ end
41
+
42
+ # Return instances of all models matching the query regardless
43
+ # of whether or not they have been soft-deleted.
44
+ def self.find_with_destroyed *args
45
+ self.with_destroyed_scope { find(*args) }
46
+ end
47
+
48
+ # Perform a find only on destroyed instances.
49
+ def self.find_only_destroyed *args
50
+ self.with_only_destroyed_scope { find(*args) }
51
+ end
52
+
53
+ # Returns true if the requested record exists, even if it has
54
+ # been soft-deleted.
55
+ def self.exists_with_destroyed? *args
56
+ self.with_destroyed_scope { exists?(*args) }
57
+ end
58
+
59
+ # Returns true if the requested record has been soft-deleted.
60
+ def self.exists_only_destroyed? *args
61
+ self.with_only_destroyed_scope { exists?(*args) }
62
+ end
63
+
64
+ # Override the default destroy to allow us to flag deleted_at.
65
+ # This preserves the before_destroy and after_destroy callbacks.
66
+ # Because this is also called internally by Model.destroy_all and
67
+ # the Model.destroy(id), we don't need to specify those methods
68
+ # separately.
69
+ def destroy
70
+ return false if callback(:before_destroy) == false
71
+ result = destroy_without_callbacks
72
+ callback(:after_destroy)
73
+ result
74
+ end
75
+
76
+ # Set deleted_at flag on a model to nil, effectively undoing the
77
+ # soft-deletion.
78
+ def restore
79
+ self.deleted_at_will_change!
80
+ self.deleted_at = nil
81
+ update_without_callbacks
82
+ end
83
+
84
+ # Has this model been soft-deleted?
85
+ def destroyed?
86
+ super || !deleted_at.nil?
87
+ end
88
+
89
+ protected
90
+
91
+ # Mark the model deleted_at as now.
92
+ def destroy_without_callbacks
93
+ self.deleted_at = current_time_from_proper_timezone
94
+ update_without_callbacks
95
+ end
96
+
97
+ def self.with_only_destroyed_scope(&block)
98
+ with_destroyed_scope do
99
+ table = connection.quote_table_name(table_name)
100
+ attr = connection.quote_column_name(:deleted_at)
101
+ with_scope(:find => { :conditions => "#{table}.#{attr} IS NOT NULL" }, &block)
102
+ end
103
+ end
104
+
105
+ def self.with_destroyed_scope
106
+ find = current_scoped_methods[:find]
107
+
108
+ if find[:conditions]
109
+ original = find[:conditions].dup
110
+
111
+ begin
112
+ case find[:conditions]
113
+ when Hash:
114
+ if find[:conditions][:deleted_at].nil?
115
+ find[:conditions].delete(:deleted_at)
116
+ end
117
+ when String:
118
+ conditions = sanitize_conditions(:deleted_at => nil)
119
+ find[:conditions].gsub!(conditions, '1=1')
120
+ end
121
+
122
+ result = yield
123
+ ensure
124
+ find[:conditions] = original
125
+ return result if result
126
+ end
127
+ else
128
+ yield
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ ActiveRecord::Base.send(:include, IsParanoid)
@@ -0,0 +1,104 @@
1
+ require '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
10
+ named_scope :ordered, :order => 'name DESC'
11
+ named_scope :r2d2, :conditions => { :name => 'R2D2' }
12
+ named_scope :c3p0, :conditions => { :name => 'C3P0' }
13
+ end
14
+
15
+ describe Android do
16
+ before(:each) do
17
+ Android.connection.execute 'DELETE FROM androids'
18
+ Person.connection.execute 'DELETE FROM people'
19
+
20
+ @luke = Person.create(:name => 'Luke Skywalker')
21
+ @r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
22
+ @c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
23
+ end
24
+
25
+ it "should delete normally" do
26
+ Android.count_with_destroyed.should == 2
27
+ Android.delete_all
28
+ Android.count_with_destroyed.should == 0
29
+ end
30
+
31
+ it "should handle Model.destroy_all properly" do
32
+ lambda{
33
+ Android.destroy_all("owner_id = #{@luke.id}")
34
+ }.should change(Android, :count).from(2).to(0)
35
+ Android.count_with_destroyed.should == 2
36
+ end
37
+
38
+ it "should handle Model.destroy(id) properly" do
39
+ lambda{
40
+ Android.destroy(@r2d2.id)
41
+ }.should change(Android, :count).by(-1)
42
+
43
+ Android.count_with_destroyed.should == 2
44
+ end
45
+
46
+ it "should be not show up in the relationship to the owner once deleted" do
47
+ @luke.androids.size.should == 2
48
+ @r2d2.destroy
49
+ @luke.androids.size.should == 1
50
+ Android.count.should == 1
51
+ Android.first(:conditions => {:name => 'R2D2'}).should be_blank
52
+ end
53
+
54
+ it "should be able to find deleted items via find_with_destroyed" do
55
+ @r2d2.destroy
56
+ Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
57
+ Android.find_with_destroyed(:first, :conditions => {:name => 'R2D2'}).should_not be_blank
58
+ end
59
+
60
+ it "should have a proper count inclusively and exclusively of deleted items" do
61
+ @r2d2.destroy
62
+ @c3p0.destroy
63
+ Android.count.should == 0
64
+ Android.count_with_destroyed.should == 2
65
+ end
66
+
67
+ it "should mark deleted on dependent destroys" do
68
+ lambda{
69
+ @luke.destroy
70
+ }.should change(Android, :count).by(-2)
71
+ Android.count_with_destroyed.should == 2
72
+ end
73
+
74
+ it "should allow restoring" do
75
+ @r2d2.destroy
76
+ lambda{
77
+ @r2d2.restore
78
+ }.should change(Android, :count).by(1)
79
+ end
80
+
81
+ # Note: this isn't necessarily ideal, this just serves to demostrate
82
+ # how it currently works
83
+ it "should not ignore deleted items in validation checks" do
84
+ @r2d2.destroy
85
+ lambda{
86
+ Android.create!(:name => 'R2D2')
87
+ }.should raise_error(ActiveRecord::RecordInvalid)
88
+ end
89
+
90
+ it "should find only destroyed videos" do
91
+ @r2d2.destroy
92
+ Android.find_only_destroyed(:all).should == [@r2d2]
93
+ end
94
+
95
+ it "should honor named scopes" do
96
+ @r2d2.destroy
97
+ @c3p0.destroy
98
+ Android.r2d2.find_only_destroyed(:all).should == [@r2d2]
99
+ Android.c3p0.ordered.find_only_destroyed(:all).should == [@c3p0]
100
+ Android.ordered.find_only_destroyed(:all).should == [@r2d2,@c3p0]
101
+ Android.r2d2.c3p0.find_only_destroyed(:all).should == []
102
+ Android.find_only_destroyed(:all).should == [@r2d2,@c3p0]
103
+ end
104
+ end
@@ -0,0 +1,28 @@
1
+ require 'yaml'
2
+ require 'active_record'
3
+ require 'is_paranoid'
4
+ require 'stringio'
5
+
6
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
7
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
8
+
9
+ old_stdout = $stdout
10
+ $stdout = StringIO.new
11
+
12
+ begin
13
+ ActiveRecord::Schema.define do
14
+ create_table :androids do |t|
15
+ t.string :name
16
+ t.integer :owner_id
17
+ t.datetime :deleted_at
18
+ t.timestamps
19
+ end
20
+
21
+ create_table :people do |t|
22
+ t.string :name
23
+ t.timestamps
24
+ end
25
+ end
26
+ ensure
27
+ $stdout = old_stdout
28
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mislav-is_paranoid
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Jeffrey Chupp
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2009-03-20 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 0
34
+ version: 2.3.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description:
38
+ email: jeff@semanticart.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - init.rb
47
+ - lib/is_paranoid.rb
48
+ - README.markdown
49
+ - Rakefile
50
+ - MIT-LICENSE
51
+ - spec/is_paranoid_spec.rb
52
+ - spec/spec_helper.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/jchupp/is_paranoid/
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.7
84
+ signing_key:
85
+ specification_version: 2
86
+ 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.
87
+ test_files: []
88
+