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 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 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.
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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 3
3
+ :patch: 0
4
+ :major: 0
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
- def is_paranoid
17
- class_eval do
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 => {:deleted_at => nil}
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 do
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(:deleted_at, Time.now.utc)
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 destroy Model.destroy, we don't need to specify those methods
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(:deleted_at, nil)
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.find_with_destroyed(:first, :conditions => {:name => 'R2D2'}).should_not be_blank
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.2
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-20 00:00:00 -07:00
12
+ date: 2009-03-28 00:00:00 -07:00
13
13
  default_executable:
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: activerecord
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
- - Rakefile
37
- - MIT-LICENSE
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