jchupp-is_paranoid 0.7.1 → 0.8.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/Rakefile ADDED
@@ -0,0 +1,22 @@
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
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
+ :patch: 0
2
3
  :major: 0
3
- :minor: 7
4
- :patch: 1
4
+ :minor: 8
data/lib/is_paranoid.rb CHANGED
@@ -3,128 +3,164 @@ require 'activerecord'
3
3
  module IsParanoid
4
4
  # Call this in your model to enable all the safety-net goodness
5
5
  #
6
- # Example:
6
+ # Example:
7
7
  #
8
- # class Android < ActiveRecord::Base
9
- # is_paranoid
10
- # end
8
+ # class Android < ActiveRecord::Base
9
+ # is_paranoid
10
+ # end
11
11
  #
12
+
12
13
  def is_paranoid opts = {}
13
14
  opts[:field] ||= [:deleted_at, Proc.new{Time.now.utc}, nil]
14
15
  class_inheritable_accessor :destroyed_field, :field_destroyed, :field_not_destroyed
15
16
  self.destroyed_field, self.field_destroyed, self.field_not_destroyed = opts[:field]
16
17
 
17
- include ClassAndInstanceMethods
18
- end
18
+ # This is the real magic. All calls made to this model will append
19
+ # the conditions deleted_at => nil (or whatever your destroyed_field
20
+ # and field_not_destroyed are). All exceptions require using
21
+ # exclusive_scope (see self.delete_all, self.count_with_destroyed,
22
+ # and self.find_with_destroyed defined in the module ClassMethods)
23
+ default_scope :conditions => {destroyed_field => field_not_destroyed}
19
24
 
20
- module ClassAndInstanceMethods
21
- def self.included(base)
22
- base.class_eval do
23
- # This is the real magic. All calls made to this model will append
24
- # the conditions deleted_at => nil (or whatever your destroyed_field
25
- # and field_not_destroyed are). All exceptions require using
26
- # exclusive_scope (see self.delete_all, self.count_with_destroyed,
27
- # and self.find_with_destroyed )
28
- default_scope :conditions => {destroyed_field => field_not_destroyed}
29
-
30
- # Actually delete the model, bypassing the safety net. Because
31
- # this method is called internally by Model.delete(id) and on the
32
- # delete method in each instance, we don't need to specify those
33
- # methods separately
34
- def self.delete_all conditions = nil
35
- self.with_exclusive_scope { super conditions }
36
- end
25
+ extend ClassMethods
26
+ include InstanceMethods
27
+ end
37
28
 
38
- # Mark the model deleted_at as now.
39
- def destroy_without_callbacks
40
- self.class.update_all(
41
- "#{destroyed_field} = #{self.class.connection.quote(( field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed))}",
42
- "id = #{self.id}"
43
- )
44
- end
29
+ module ClassMethods
30
+ # Actually delete the model, bypassing the safety net. Because
31
+ # this method is called internally by Model.delete(id) and on the
32
+ # delete method in each instance, we don't need to specify those
33
+ # methods separately
34
+ def delete_all conditions = nil
35
+ self.with_exclusive_scope { super conditions }
36
+ end
45
37
 
46
- # Override the default destroy to allow us to flag deleted_at.
47
- # This preserves the before_destroy and after_destroy callbacks.
48
- # Because this is also called internally by Model.destroy_all and
49
- # the Model.destroy(id), we don't need to specify those methods
50
- # separately.
51
- def destroy
52
- return false if callback(:before_destroy) == false
53
- result = destroy_without_callbacks
54
- callback(:after_destroy)
55
- result
56
- end
38
+ # Use update_all with an exclusive scope to restore undo the soft-delete.
39
+ # This bypasses update-related callbacks.
40
+ #
41
+ # By default, restores cascade through associations that are
42
+ # :dependent => :destroy and under is_paranoid. You can prevent restoration
43
+ # of associated models by passing :include_destroyed_dependents => false,
44
+ # for example:
45
+ # Android.restore(:include_destroyed_dependents => false)
46
+ def restore(id, options = {})
47
+ options.reverse_merge!({:include_destroyed_dependents => true})
48
+ with_exclusive_scope do
49
+ update_all(
50
+ "#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
51
+ "id = #{id}"
52
+ )
53
+ end
57
54
 
58
- # Use update_all with an exclusive scope to restore undo the soft-delete.
59
- # This bypasses update-related callbacks.
60
- #
61
- # By default, restores cascade through associations that are
62
- # :dependent => :destroy and under is_paranoid. You can prevent restoration
63
- # of associated models by passing :include_destroyed_dependents => false,
64
- # for example:
65
- # Android.restore(:include_destroyed_dependents => false)
66
- def self.restore(id, options = {})
67
- options.reverse_merge!({:include_destroyed_dependents => true})
68
- with_exclusive_scope do
69
- update_all(
70
- "#{destroyed_field} = #{connection.quote(field_not_destroyed)}",
71
- "id = #{id}"
72
- )
73
- end
74
- if options[:include_destroyed_dependents]
75
- self.reflect_on_all_associations.each do |association|
76
- if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
77
- association.klass.find_destroyed_only(:all,
78
- :conditions => ["#{association.primary_key_name} = ?", id]
79
- ).each do |model|
80
- model.restore
81
- end
82
- end
55
+ if options[:include_destroyed_dependents]
56
+ self.reflect_on_all_associations.each do |association|
57
+ if association.options[:dependent] == :destroy and association.klass.respond_to?(:restore)
58
+ association.klass.find_destroyed_only(:all,
59
+ :conditions => ["#{association.primary_key_name} = ?", id]
60
+ ).each do |model|
61
+ model.restore
83
62
  end
84
63
  end
85
64
  end
65
+ end
66
+ end
86
67
 
87
- # Set deleted_at flag on a model to field_not_destroyed, effectively
88
- # undoing the soft-deletion.
89
- def restore(options = {})
90
- self.class.restore(id, options)
91
- end
68
+ # find_with_destroyed and other blah_with_destroyed and
69
+ # blah_destroyed_only methods are defined here
70
+ def method_missing name, *args
71
+ if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
72
+ self.extend(Module.new{
73
+ if $2 == '_with_destroyed'
74
+ # Example:
75
+ # def count_with_destroyed(*args)
76
+ # self.with_exclusive_scope{ self.send(:count, *args) }
77
+ # end
78
+ define_method name do |*args|
79
+ self.with_exclusive_scope{ self.send($1, *args) }
80
+ end
81
+ else
92
82
 
93
- # find_with_destroyed and other blah_with_destroyed and
94
- # blah_destroyed_only methods are defined here
95
- def self.method_missing name, *args
96
- if name.to_s =~ /^(.*)(_destroyed_only|_with_destroyed)$/ and self.respond_to?($1)
97
- self.extend(Module.new{
98
- if $2 == '_with_destroyed' # Example:
99
- define_method name do |*args| # def count_with_destroyed(*args)
100
- self.with_exclusive_scope{ self.send($1, *args) } # self.with_exclusive_scope{ self.send(:count, *args) }
101
- end # end
102
- else
103
- # Example:
104
- # def count_destroyed_only(*args)
105
- # self.with_exclusive_scope do
106
- # with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
107
- # self.send(:count, *args)
108
- # end
109
- # end
110
- # end
111
- define_method name do |*args|
112
- self.with_exclusive_scope do
113
- with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
114
- self.send($1, *args)
115
- end
116
- end
83
+ # Example:
84
+ # def count_destroyed_only(*args)
85
+ # self.with_exclusive_scope do
86
+ # with_scope({:find => { :conditions => ["#{destroyed_field} IS NOT ?", nil] }}) do
87
+ # self.send(:count, *args)
88
+ # end
89
+ # end
90
+ # end
91
+ define_method name do |*args|
92
+ self.with_exclusive_scope do
93
+ with_scope({:find => { :conditions => ["#{self.table_name}.#{destroyed_field} IS NOT ?", field_not_destroyed] }}) do
94
+ self.send($1, *args)
117
95
  end
118
96
  end
119
- })
120
- self.send(name, *args)
121
- else
122
- super(name, *args)
97
+ end
98
+
123
99
  end
124
- end
100
+ })
101
+ self.send(name, *args)
102
+ else
103
+ super(name, *args)
125
104
  end
126
105
  end
127
106
  end
107
+
108
+ module InstanceMethods
109
+
110
+ def method_missing name, *args
111
+ # if we're trying for a _____with_destroyed method
112
+ # and we can respond to the _____ method
113
+ # and we have an association by the name of _____
114
+ if name.to_s =~ /^(.*)(_with_destroyed)$/ and
115
+ self.respond_to?($1) and
116
+ (assoc = self.class.reflect_on_all_associations.detect{|a| a.name.to_s == $1})
117
+
118
+ parent_klass = Object.module_eval("::#{assoc.class_name}", __FILE__, __LINE__)
119
+
120
+ self.class.send(
121
+ :include,
122
+ Module.new{ # Example:
123
+ define_method name do |*args| # def android_with_destroyed
124
+ parent_klass.find_with_destroyed( # Android.find_with_destroyed(
125
+ self.send(assoc.primary_key_name) # self.send(:android_id)
126
+ ) # )
127
+ end # end
128
+ }
129
+ )
130
+ self.send(name, *args)
131
+ else
132
+ super(name, *args)
133
+ end
134
+ end
135
+
136
+ # Mark the model deleted_at as now.
137
+ def destroy_without_callbacks
138
+ self.class.update_all(
139
+ "#{destroyed_field} = #{self.class.connection.quote(( field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed))}",
140
+ "id = #{self.id}"
141
+ )
142
+ end
143
+
144
+ # Override the default destroy to allow us to flag deleted_at.
145
+ # This preserves the before_destroy and after_destroy callbacks.
146
+ # Because this is also called internally by Model.destroy_all and
147
+ # the Model.destroy(id), we don't need to specify those methods
148
+ # separately.
149
+ def destroy
150
+ return false if callback(:before_destroy) == false
151
+ result = destroy_without_callbacks
152
+ callback(:after_destroy)
153
+ result
154
+ end
155
+
156
+ # Set deleted_at flag on a model to field_not_destroyed, effectively
157
+ # undoing the soft-deletion.
158
+ def restore(options = {})
159
+ self.class.restore(id, options)
160
+ end
161
+
162
+ end
163
+
128
164
  end
129
165
 
130
- ActiveRecord::Base.send(:extend, IsParanoid)
166
+ ActiveRecord::Base.send(:extend, IsParanoid)
@@ -10,7 +10,23 @@ describe IsParanoid do
10
10
  @luke = Person.create(:name => 'Luke Skywalker')
11
11
  @r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
12
12
  @c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
13
+
13
14
  @r2d2.components.create(:name => 'Rotors')
15
+
16
+ @r2d2.memories.create(:name => 'A pretty sunset')
17
+ @c3p0.sticker = Sticker.create(:name => 'OMG, PONIES!')
18
+ end
19
+
20
+ describe 'non-is_paranoid models' do
21
+ it "should destroy as normal" do
22
+ lambda{
23
+ @luke.destroy
24
+ }.should change(Person, :count).by(-1)
25
+
26
+ lambda{
27
+ Person.count_with_destroyed
28
+ }.should raise_error(NoMethodError)
29
+ end
14
30
  end
15
31
 
16
32
  describe 'destroying' do
@@ -51,7 +67,6 @@ describe IsParanoid do
51
67
  }.should change(Android, :count).from(2).to(0)
52
68
  Android.count_with_destroyed.should == 2
53
69
  end
54
-
55
70
  end
56
71
  end
57
72
 
@@ -152,6 +167,23 @@ describe IsParanoid do
152
167
  end
153
168
  end
154
169
 
170
+ describe 'accessing destroyed parent models' do
171
+ it "should be able to access destroyed parents via parent_with_destroyed" do
172
+ # Memory is has_many with a non-default primary key
173
+ # Sticker is a has_one with a default primary key
174
+ [Memory, Sticker].each do |klass|
175
+ instance = klass.last
176
+ parent = instance.android
177
+ instance.android.destroy
178
+
179
+ # reload so the model doesn't remember the parent
180
+ instance.reload
181
+ instance.android.should == nil
182
+ instance.android_with_destroyed.should == parent
183
+ end
184
+ end
185
+ end
186
+
155
187
  describe 'alternate fields and field values' do
156
188
  it "should properly function for boolean values" do
157
189
  # ninjas are invisible by default. not being ninjas, we can only
@@ -160,12 +192,14 @@ describe IsParanoid do
160
192
  ninja.vanish # aliased to destroy
161
193
  Ninja.first.should be_blank
162
194
  Ninja.find_with_destroyed(:first).should == ninja
195
+ Ninja.count.should == 0
163
196
 
164
197
  # we're only interested in pirates who are alive by default
165
198
  pirate = Pirate.create(:name => 'Reginald')
166
199
  pirate.destroy
167
200
  Pirate.first.should be_blank
168
201
  Pirate.find_with_destroyed(:first).should == pirate
202
+ Pirate.count.should == 0
169
203
 
170
204
  # we're only interested in pirates who are dead by default.
171
205
  # zombie pirates ftw!
@@ -173,6 +207,7 @@ describe IsParanoid do
173
207
  lambda{
174
208
  DeadPirate.first.destroy
175
209
  }.should change(Pirate, :count).from(0).to(1)
210
+ DeadPirate.count.should == 0
176
211
  end
177
212
  end
178
213
 
data/spec/models.rb CHANGED
@@ -6,6 +6,8 @@ end
6
6
  class Android < ActiveRecord::Base #:nodoc:
7
7
  validates_uniqueness_of :name
8
8
  has_many :components, :dependent => :destroy
9
+ has_one :sticker
10
+ has_many :memories, :foreign_key => 'parent_id'
9
11
 
10
12
  is_paranoid
11
13
 
@@ -20,13 +22,23 @@ end
20
22
  class Component < ActiveRecord::Base #:nodoc:
21
23
  is_paranoid
22
24
  NEW_NAME = 'Something Else!'
23
-
25
+
24
26
  after_destroy :change_name
25
27
  def change_name
26
28
  self.update_attribute(:name, NEW_NAME)
27
29
  end
28
30
  end
29
31
 
32
+ class Memory < ActiveRecord::Base #:nodoc:
33
+ is_paranoid
34
+ belongs_to :android, :class_name => "Android", :foreign_key => "parent_id"
35
+ end
36
+
37
+ class Sticker < ActiveRecord::Base #:nodoc
38
+ is_paranoid
39
+ belongs_to :android
40
+ end
41
+
30
42
  class AndroidWithScopedUniqueness < ActiveRecord::Base #:nodoc:
31
43
  set_table_name :androids
32
44
  validates_uniqueness_of :name, :scope => :deleted_at
data/spec/schema.rb CHANGED
@@ -21,6 +21,18 @@ ActiveRecord::Schema.define(:version => 20090317164830) do
21
21
  t.datetime "updated_at"
22
22
  end
23
23
 
24
+ create_table "memories", :force => true do |t|
25
+ t.string "name"
26
+ t.integer "parent_id"
27
+ t.datetime "deleted_at"
28
+ end
29
+
30
+ create_table "stickers", :force => true do |t|
31
+ t.string "name"
32
+ t.integer "android_id"
33
+ t.datetime "deleted_at"
34
+ end
35
+
24
36
  create_table "ninjas", :force => true do |t|
25
37
  t.string "name"
26
38
  t.boolean "visible", :default => false
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.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Chupp
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-02 00:00:00 -07:00
12
+ date: 2009-05-12 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -19,10 +19,11 @@ executables: []
19
19
 
20
20
  extensions: []
21
21
 
22
- extra_rdoc_files: []
23
-
22
+ extra_rdoc_files:
23
+ - README.textile
24
24
  files:
25
25
  - README.textile
26
+ - Rakefile
26
27
  - VERSION.yml
27
28
  - lib/is_paranoid.rb
28
29
  - spec/database.yml
@@ -35,7 +36,6 @@ has_rdoc: true
35
36
  homepage: http://github.com/jchupp/is_paranoid/
36
37
  post_install_message:
37
38
  rdoc_options:
38
- - --inline-source
39
39
  - --charset=UTF-8
40
40
  require_paths:
41
41
  - lib
@@ -58,5 +58,8 @@ rubygems_version: 1.2.0
58
58
  signing_key:
59
59
  specification_version: 3
60
60
  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.
61
- test_files: []
62
-
61
+ test_files:
62
+ - spec/is_paranoid_spec.rb
63
+ - spec/models.rb
64
+ - spec/schema.rb
65
+ - spec/spec_helper.rb