paranoid_fu 0.4.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.
@@ -0,0 +1,85 @@
1
+ * (5 Nov 2009)
2
+ Rewrite to use named_scopes
3
+ Remove with_deleted option, now it's the default behavior
4
+ Add without_deleted option to get the previous behavior
5
+ Fix belongs_to polymorphic associations
6
+
7
+ * (16 Apr 2009)
8
+
9
+ Allow :with_deleted and :only_deleted options to work with count and calculate.
10
+ Fixes compatibility with will_paginate. [James Le Cuirot]
11
+
12
+ * (4 Oct 2007)
13
+
14
+ Update for Edge rails: remove support for legacy #count args
15
+
16
+ * (2 Feb 2007)
17
+
18
+ Add support for custom primary keys [Jeff Dean]
19
+
20
+ * (2 July 2006)
21
+
22
+ Add paranoid delete_all implementation [Marshall Roch]
23
+
24
+ * (23 May 2006)
25
+
26
+ Allow setting of future dates for content expiration.
27
+
28
+ * (15 May 2006)
29
+
30
+ Added support for dynamic finders
31
+
32
+ * (28 Mar 2006)
33
+
34
+ Updated for Rails 1.1. I love removing code.
35
+
36
+ Refactored #find method
37
+ Nested Scopes
38
+
39
+ *0.3.1* (20 Dec 2005)
40
+
41
+ * took out deleted association code for 'chainsaw butchery of base classes' [sorry Erik Terpstra]
42
+ * verified tests pass on Rails 1.0
43
+
44
+ *0.3* (27 Nov 2005)
45
+
46
+ * Deleted models will find deleted associations by default now [Erik Terpstra]
47
+ * Added :group as valid option for find [Michael Dabney]
48
+ * Changed the module namespace to Caboose::Acts::Paranoid
49
+
50
+ *0.2.0* (6 Nov 2005)
51
+
52
+ * Upgrade to Rails 1.0 RC4. ActiveRecord::Base#constrain has been replaced with scope_with.
53
+
54
+ *0.1.7* (22 Oct 2005)
55
+
56
+ * Added :with_deleted as a valid option of ActiveRecord::Base#find
57
+
58
+ *0.1.6* (25 Sep 2005)
59
+
60
+ * Fixed bug where nested constrains would get clobbered after multiple queries
61
+
62
+ *0.1.5* (22 Sep 2005)
63
+
64
+ * Fixed bug where acts_as_paranoid would clobber other constrains
65
+ * Simplified acts_as_paranoid mixin including.
66
+
67
+ *0.1.4* (18 Sep 2005)
68
+
69
+ * First RubyForge release
70
+
71
+ *0.1.3* (18 Sep 2005)
72
+
73
+ * ignore multiple calls to acts_as_paranoid on the same model
74
+
75
+ *0.1.2* (18 Sep 2005)
76
+
77
+ * fixed a bug that kept you from selecting the first deleted record
78
+
79
+ *0.1.1* (18 Sep 2005)
80
+
81
+ * Fixed bug that kept you from selecting deleted records by ID
82
+
83
+ *0.1* (17 Sep 2005)
84
+
85
+ * Initial gem
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005 Rick Olson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,5 @@
1
+ = paranoid_fu
2
+
3
+ Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp. ActiveRecord is required.
4
+
5
+ http://github.com/scambra/paranoid_fu
@@ -0,0 +1,10 @@
1
+ 1. Pick Rails version. Either dump this plugin in a Rails app and run it from there, or specify it as an ENV var:
2
+
3
+ RAILS=2.2.2 rake
4
+ RAILS=2.2.2 ruby test/paranoid_test.rb
5
+
6
+ 2. Setup your database. By default sqlite3 is used, and no further setup is necessary. You can pick any of the listed databases in test/database.yml. Be sure to create the database first.
7
+
8
+ DB=mysql rake
9
+
10
+ 3. Profit!!
@@ -0,0 +1,6 @@
1
+ module ParanoidFu; end
2
+ ActiveRecord::Associations::BelongsToPolymorphicAssociation.send :include, ParanoidFu::BelongsToPolymorphicAssociation
3
+ ActiveRecord::Reflection::MacroReflection.send :include, ParanoidFu::ReflectionConditions
4
+ ActiveRecord::Base.send :extend, ParanoidFu::Associations
5
+ ActiveRecord::Base.send :extend, ParanoidFu::AssociationPreload
6
+ ActiveRecord::Base.send :include, ParanoidFu::Paranoid
@@ -0,0 +1,12 @@
1
+ module ParanoidFu
2
+ module AssociationPreload
3
+ def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
4
+ reflection = reflect_on_association(reflection_name)
5
+ # only it's needed to reject deleted records for polymorphic belongs_to, because in other case deleted records won't be loaded
6
+ if reflection.options[:without_deleted] && reflection.options[:polymorphic]
7
+ associated_records.delete_if {|item| item.class.paranoid? && item.deleted?}
8
+ end
9
+ super
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ module ParanoidFu #:nodoc:
2
+ module Associations
3
+ # === Options
4
+ # [:without_deleted]
5
+ # Don't load associated object if it's deleted.
6
+ def belongs_to(association_id, options = {})
7
+ without_deleted = options.delete :without_deleted
8
+ returning super(association_id, options) do
9
+ restore_without_deleted(association_id, without_deleted)
10
+ end
11
+ end
12
+
13
+ def has_many(association_id, options = {}, &extension)
14
+ without_deleted = options.delete :without_deleted
15
+ returning super(association_id, options, &extension) do
16
+ restore_without_deleted(association_id, without_deleted)
17
+ end
18
+ end
19
+
20
+ protected
21
+ def restore_without_deleted(association_id, value)
22
+ if value
23
+ reflection = reflect_on_association(association_id)
24
+ reflection.options[:without_deleted] = value
25
+ # has_many :through doesn't use sanitized_conditions, so we set the conditions in options hash
26
+ reflection.options[:conditions] = reflection.sanitized_conditions unless reflection.options[:polymorphic]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module ParanoidFu
2
+ module BelongsToPolymorphicAssociation
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :find_target, :paranoid_fu
6
+ end
7
+ end
8
+
9
+ def find_target_with_paranoid_fu
10
+ old_conditions = @reflection.options[:conditions]
11
+ if @reflection.options[:without_deleted]
12
+ # conditions method is not called if conditions isn't set in options hash
13
+ @reflection.options[:conditions] = association_class.merge_conditions(@reflection.options[:conditions], association_class.without_deleted_conditions(association_class.table_name))
14
+ end
15
+ find_target_without_paranoid_fu
16
+ ensure
17
+ # restore conditions in options hash
18
+ @reflection.options[:conditions] = old_conditions
19
+ end
20
+
21
+ def conditions
22
+ @conditions ||= interpolate_sql(association_class.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,134 @@
1
+ module ParanoidFu #:nodoc:
2
+ # Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.
3
+ # This assumes the table has a deleted_at date/time field. Most normal model operations will work, but there will be some oddities.
4
+ #
5
+ # class Widget < ActiveRecord::Base
6
+ # paranoid_fu
7
+ # end
8
+ #
9
+ # Widget.find(:all)
10
+ # # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
11
+ #
12
+ # Widget.find(:first, :conditions => ['title = ?', 'test'], :order => 'title')
13
+ # # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test' ORDER BY title LIMIT 1
14
+ #
15
+ # Widget.find_with_deleted(:all)
16
+ # # SELECT * FROM widgets
17
+ #
18
+ # Widget.find_only_deleted(:all)
19
+ # # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
20
+ #
21
+ # Widget.find_with_deleted(1).deleted?
22
+ # # Returns true if the record was previously destroyed, false if not
23
+ #
24
+ # Widget.count
25
+ # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL
26
+ #
27
+ # Widget.count ['title = ?', 'test']
28
+ # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test'
29
+ #
30
+ # Widget.count_with_deleted
31
+ # # SELECT COUNT(*) FROM widgets
32
+ #
33
+ # Widget.count_only_deleted
34
+ # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NOT NULL
35
+ #
36
+ # Widget.delete_all
37
+ # # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36'
38
+ #
39
+ # Widget.delete_all!
40
+ # # DELETE FROM widgets
41
+ #
42
+ # @widget.destroy
43
+ # # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36' WHERE id = 1
44
+ #
45
+ # @widget.destroy!
46
+ # # DELETE FROM widgets WHERE id = 1
47
+ #
48
+ module Paranoid
49
+ def self.included(base) # :nodoc:
50
+ base.extend ClassMethods
51
+ end
52
+
53
+ module ClassMethods
54
+ def paranoid_fu(options = {})
55
+ unless paranoid? # don't let AR call this twice
56
+ cattr_accessor :deleted_attribute
57
+ self.deleted_attribute = options[:with] || :deleted_at
58
+ alias_method :destroy_without_callbacks!, :destroy_without_callbacks
59
+ class << self
60
+ alias_method :delete_all!, :delete_all
61
+ end
62
+ named_scope :without_deleted, lambda{ {:conditions => without_deleted_conditions} }
63
+ named_scope :only_deleted, lambda{ {:conditions => only_deleted_conditions} }
64
+ end
65
+ include InstanceMethods
66
+ end
67
+
68
+ def paranoid?
69
+ self.included_modules.include?(InstanceMethods)
70
+ end
71
+ end
72
+
73
+ module InstanceMethods #:nodoc:
74
+ def self.included(base) # :nodoc:
75
+ base.extend ClassMethods
76
+ end
77
+
78
+ module ClassMethods
79
+ def delete_all(conditions = nil)
80
+ self.update_all ["#{self.deleted_attribute} = ?", current_time], conditions
81
+ end
82
+
83
+ def without_deleted_conditions(table_name = table_name)
84
+ ["#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > ?", current_time]
85
+ end
86
+
87
+ def only_deleted_conditions(table_name = table_name)
88
+ ["#{table_name}.#{deleted_attribute} IS NOT NULL AND #{table_name}.#{deleted_attribute} <= ?", current_time]
89
+ end
90
+
91
+ protected
92
+ def current_time
93
+ default_timezone == :utc ? Time.now.utc : Time.now
94
+ end
95
+ end
96
+
97
+ def destroy_without_callbacks
98
+ unless new_record?
99
+ self.class.update_all self.class.send(:sanitize_sql, ["#{self.class.deleted_attribute} = ?", (self.deleted_at = self.class.send(:current_time))]), ["#{self.class.primary_key} = ?", id]
100
+ end
101
+ freeze
102
+ end
103
+
104
+ def destroy_with_callbacks!
105
+ return false if callback(:before_destroy) == false
106
+ result = destroy_without_callbacks!
107
+ callback(:after_destroy)
108
+ result
109
+ end
110
+
111
+ def destroy!
112
+ transaction { destroy_with_callbacks! }
113
+ end
114
+
115
+ def deleted?
116
+ !!read_attribute(:deleted_at)
117
+ end
118
+
119
+ def recover!
120
+ self.deleted_at = nil
121
+ save!
122
+ end
123
+
124
+ def recover_with_associations!(*associations)
125
+ self.recover!
126
+ associations.to_a.each do |assoc|
127
+ self.send(assoc).all.each do |a|
128
+ a.recover! if a.class.paranoid?
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,24 @@
1
+ module ParanoidFu
2
+ module ReflectionConditions
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :sanitized_conditions, :paranoid_fu
6
+ end
7
+ end
8
+
9
+ # Returns the SQL string that corresponds to the <tt>:conditions</tt>
10
+ # option of the macro, if given, or +nil+ otherwise.
11
+ def sanitized_conditions_with_paranoid_fu
12
+ sanitized_conditions_without_paranoid_fu
13
+ if !self.options[:polymorphic] && self.options.delete(:without_deleted)
14
+ klass = if self.through_reflection
15
+ self.through_reflection.klass
16
+ else
17
+ self.klass
18
+ end
19
+ @sanitized_conditions = klass.merge_conditions(@sanitized_conditions, klass.without_deleted_conditions(klass.table_name)) if klass
20
+ end
21
+ @sanitized_conditions
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: paranoid_fu_plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: paranoid_fu_plugin.sqlite3.db
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: paranoid_fu_plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: paranoid_fu_plugin_test
@@ -0,0 +1,19 @@
1
+ category_1:
2
+ id: 1
3
+ widget_id: 1
4
+ title: 'category 1'
5
+ category_2:
6
+ id: 2
7
+ widget_id: 1
8
+ title: 'category 2'
9
+ deleted_at: '2005-01-01 00:00:00'
10
+ category_3:
11
+ id: 3
12
+ widget_id: 2
13
+ title: 'category 3'
14
+ deleted_at: '2005-01-01 00:00:00'
15
+ category_4:
16
+ id: 4
17
+ widget_id: 2
18
+ title: 'category 4'
19
+ deleted_at: '2005-01-01 00:00:00'
@@ -0,0 +1,12 @@
1
+ cw_1:
2
+ category_id: 1
3
+ widget_id: 1
4
+ cw_2:
5
+ category_id: 2
6
+ widget_id: 1
7
+ cw_3:
8
+ category_id: 3
9
+ widget_id: 2
10
+ cw_4:
11
+ category_id: 4
12
+ widget_id: 2
@@ -0,0 +1,8 @@
1
+ order_1:
2
+ id: 1
3
+ item_id: 2
4
+ item_type: Widget
5
+ order_2:
6
+ id: 2
7
+ item_id: 1
8
+ item_type: Category
@@ -0,0 +1,9 @@
1
+ tagging_1:
2
+ id: 1
3
+ tag_id: 1
4
+ widget_id: 1
5
+ deleted_at: '2005-01-01 00:00:00'
6
+ tagging_2:
7
+ id: 2
8
+ tag_id: 2
9
+ widget_id: 1
@@ -0,0 +1,6 @@
1
+ tag_1:
2
+ id: 1
3
+ name: 'tag 1'
4
+ tag_2:
5
+ id: 2
6
+ name: 'tag 1'
@@ -0,0 +1,8 @@
1
+ widget_1:
2
+ id: 1
3
+ title: 'widget 1'
4
+ widget_2:
5
+ id: 2
6
+ title: 'deleted widget 2'
7
+ deleted_at: '2005-01-01 00:00:00'
8
+ category_id: 3
@@ -0,0 +1,323 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Tagging < ActiveRecord::Base
4
+ belongs_to :tag
5
+ belongs_to :widget
6
+ paranoid_fu
7
+ end
8
+
9
+ class Widget < ActiveRecord::Base
10
+ paranoid_fu
11
+ has_many :categories, :dependent => :destroy
12
+ has_and_belongs_to_many :habtm_categories, :class_name => 'Category'
13
+ has_one :category
14
+ belongs_to :parent_category, :class_name => 'Category'
15
+ has_many :taggings
16
+ has_many :tags, :through => :taggings, :without_deleted => true
17
+ has_many :any_tags, :through => :taggings, :class_name => 'Tag', :source => :tag
18
+ end
19
+
20
+ class Category < ActiveRecord::Base
21
+ paranoid_fu
22
+ belongs_to :widget, :without_deleted => true
23
+ belongs_to :any_widget, :class_name => 'Widget', :foreign_key => 'widget_id'
24
+
25
+ def self.search(name, options = {})
26
+ without_deleted.all options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
27
+ end
28
+
29
+ def self.search_with_deleted(name, options = {})
30
+ all options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
31
+ end
32
+ end
33
+
34
+ class Tag < ActiveRecord::Base
35
+ has_many :taggings
36
+ has_many :widgets, :through => :taggings
37
+ end
38
+
39
+ class Order < ActiveRecord::Base
40
+ belongs_to :item, :polymorphic => true, :without_deleted => true
41
+ belongs_to :any_item, :polymorphic => true, :foreign_key => 'item_id', :foreign_type => 'item_type'
42
+ end
43
+
44
+ class NonParanoidAndroid < ActiveRecord::Base
45
+ end
46
+
47
+ class ParanoidTest < ActiveSupport::TestCase
48
+ fixtures :widgets, :categories, :categories_widgets, :tags, :taggings, :orders
49
+
50
+ def test_without_deleted_scope
51
+ assert_equal [1, 2], Widget.all.ids
52
+ assert_equal [1], Widget.without_deleted.all.ids
53
+ end
54
+
55
+ def test_only_deleted_scope
56
+ assert_equal [2], Widget.only_deleted.all.ids
57
+ assert_equal [1, 2], Widget.all.ids
58
+ end
59
+
60
+ def test_should_exists_with_deleted
61
+ assert Widget.exists?(2)
62
+ assert !Widget.without_deleted.exists?(2)
63
+ end
64
+
65
+ def test_should_exists_only_deleted
66
+ assert Widget.only_deleted.exists?(2)
67
+ assert !Widget.only_deleted.exists?(1)
68
+ end
69
+
70
+ def test_should_count_with_deleted
71
+ assert_equal 1, Widget.without_deleted.count
72
+ assert_equal 2, Widget.count
73
+ assert_equal 1, Widget.only_deleted.count
74
+ assert_equal 2, Widget.calculate(:count, :all)
75
+ assert_equal 1, Widget.without_deleted.calculate(:count, :all)
76
+ end
77
+
78
+ def test_should_set_deleted_at
79
+ assert_equal 1, Widget.without_deleted.count
80
+ assert_equal 1, Category.without_deleted.count
81
+ widgets(:widget_1).destroy
82
+ assert_equal 0, Widget.without_deleted.count
83
+ assert_equal 0, Category.without_deleted.count
84
+ assert_equal 2, Widget.count
85
+ assert_equal 4, Category.count
86
+ end
87
+
88
+ def test_should_destroy
89
+ assert_equal 1, Widget.without_deleted.count
90
+ assert_equal 1, Category.without_deleted.count
91
+ widgets(:widget_1).destroy!
92
+ assert_equal 0, Widget.without_deleted.count
93
+ assert_equal 0, Category.without_deleted.count
94
+ assert_equal 1, Widget.only_deleted.count
95
+ assert_equal 1, Widget.count
96
+ # Category doesn't get destroyed because the dependent before_destroy callback uses #destroy
97
+ assert_equal 4, Category.count
98
+ end
99
+
100
+ def test_should_set_deleted_at_when_delete_all
101
+ assert_equal 1, Widget.without_deleted.count
102
+ assert_equal 2, Widget.count
103
+ assert_equal 1, Category.without_deleted.count
104
+ Widget.delete_all
105
+ assert_equal 0, Widget.without_deleted.count
106
+ # delete_all doesn't call #destroy, so the dependent callback never fires
107
+ assert_equal 1, Category.without_deleted.count
108
+ assert_equal 2, Widget.count
109
+ end
110
+
111
+ def test_should_set_deleted_at_when_delete_all_with_conditions
112
+ assert_equal 1, Widget.without_deleted.count
113
+ assert_equal 2, Widget.count
114
+ Widget.delete_all("id < 3")
115
+ assert_equal 0, Widget.without_deleted.count
116
+ assert_equal 2, Widget.count
117
+ end
118
+
119
+ def test_should_delete_all
120
+ assert_equal 1, Category.without_deleted.count
121
+ assert_equal 4, Category.count
122
+ Category.delete_all!
123
+ assert_equal 0, Category.without_deleted.count
124
+ assert_equal 0, Category.count
125
+ end
126
+
127
+ def test_should_delete_all_with_conditions
128
+ assert_equal 1, Category.without_deleted.count
129
+ assert_equal 4, Category.count
130
+ Category.delete_all!("id < 3")
131
+ assert_equal 0, Category.without_deleted.count
132
+ assert_equal 2, Category.count
133
+ end
134
+
135
+ def test_should_not_count_deleted
136
+ assert_equal 1, Widget.without_deleted.count
137
+ assert_equal 1, Widget.without_deleted.count(:all, :conditions => ['title=?', 'widget 1'])
138
+ assert_equal 2, Widget.count
139
+ assert_equal 1, Widget.only_deleted.count
140
+ end
141
+
142
+ def test_should_find_deleted_has_many_associations
143
+ assert_equal 2, widgets(:widget_1).categories.size
144
+ assert_equal [categories(:category_1), categories(:category_2)], widgets(:widget_1).categories
145
+ end
146
+
147
+ def test_should_not_find_deleted_has_many_associations
148
+ assert_equal 1, widgets(:widget_1).categories.without_deleted.size
149
+ assert_equal [categories(:category_1)], widgets(:widget_1).categories.without_deleted
150
+ end
151
+
152
+ def test_should_find_deleted_habtm_associations
153
+ assert_equal 2, widgets(:widget_1).habtm_categories.size
154
+ assert_equal [categories(:category_1), categories(:category_2)], widgets(:widget_1).habtm_categories
155
+ end
156
+
157
+ def test_should_not_find_deleted_habtm_associations
158
+ assert_equal 1, widgets(:widget_1).habtm_categories.without_deleted.size
159
+ assert_equal [categories(:category_1)], widgets(:widget_1).habtm_categories.without_deleted
160
+ end
161
+
162
+ def test_should_not_find_deleted_has_many_through_associations_without_deleted
163
+ assert_equal 1, widgets(:widget_1).tags.size
164
+ assert_equal [tags(:tag_2)], widgets(:widget_1).tags
165
+ end
166
+
167
+ def test_should_find_has_many_through_associations
168
+ assert_equal 2, widgets(:widget_1).any_tags.size
169
+ assert_equal Tag.find(:all), widgets(:widget_1).any_tags
170
+ end
171
+
172
+ def test_should_not_find_deleted_belongs_to_associations_without_deleted
173
+ assert_nil Category.find(3).widget
174
+ end
175
+
176
+ def test_should_find_belongs_to_assocation
177
+ assert_equal Widget.find(2), Category.find(3).any_widget
178
+ end
179
+
180
+ def test_should_not_find_deleted_belongs_to_associations_polymorphic_without_deleted
181
+ assert_nil orders(:order_1).item
182
+ assert_equal categories(:category_1), orders(:order_2).item
183
+ end
184
+
185
+ def test_should_find_deleted_belongs_to_associations_polymorphic
186
+ assert_equal widgets(:widget_2), orders(:order_1).any_item
187
+ end
188
+
189
+ def test_should_find_first_with_deleted
190
+ assert_equal widgets(:widget_1), Widget.without_deleted.first
191
+ assert_equal 2, Widget.first(:order => 'id desc').id
192
+ end
193
+
194
+ def test_should_find_single_id
195
+ assert Widget.without_deleted.find(1)
196
+ assert Widget.find(2)
197
+ assert_raises(ActiveRecord::RecordNotFound) { Widget.without_deleted.find(2) }
198
+ end
199
+
200
+ def test_should_find_multiple_ids
201
+ assert_equal [1,2], Widget.find(1,2).sort_by { |w| w.id }.ids
202
+ assert_equal [1,2], Widget.find([1,2]).sort_by { |w| w.id }.ids
203
+ assert_raises(ActiveRecord::RecordNotFound) { Widget.without_deleted.find(1,2) }
204
+ end
205
+
206
+ def test_should_ignore_multiple_includes
207
+ Widget.class_eval { paranoid_fu }
208
+ assert Widget.find(1)
209
+ end
210
+
211
+ def test_should_not_override_scopes_when_counting
212
+ assert_equal 1, Widget.send(:with_scope, :find => { :conditions => "title = 'widget 1'" }) { Widget.without_deleted.count }
213
+ assert_equal 0, Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.without_deleted.count }
214
+ assert_equal 1, Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.count }
215
+ end
216
+
217
+ def test_should_not_override_scopes_when_finding
218
+ assert_equal [1], Widget.send(:with_scope, :find => { :conditions => "title = 'widget 1'" }) { Widget.without_deleted.find(:all) }.ids
219
+ assert_equal [], Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.without_deleted.find(:all) }.ids
220
+ assert_equal [2], Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.find(:all) }.ids
221
+ end
222
+
223
+ def test_should_allow_multiple_scoped_calls_when_finding
224
+ Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) do
225
+ assert_equal [2], Widget.find(:all).ids
226
+ assert_equal [2], Widget.find(:all).ids, "clobbers the constrain on the unmodified find"
227
+ assert_equal [], Widget.without_deleted.find(:all).ids
228
+ assert_equal [], Widget.without_deleted.find(:all).ids, 'clobbers the constrain on a paranoid find'
229
+ end
230
+ end
231
+
232
+ def test_should_allow_multiple_scoped_calls_when_counting
233
+ Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) do
234
+ assert_equal 1, Widget.calculate(:count, :all)
235
+ assert_equal 1, Widget.calculate(:count, :all), "clobbers the constrain on the unmodified find"
236
+ assert_equal 0, Widget.without_deleted.count
237
+ assert_equal 0, Widget.without_deleted.count, 'clobbers the constrain on a paranoid find'
238
+ end
239
+ end
240
+
241
+ def test_should_give_paranoid_status
242
+ assert Widget.paranoid?
243
+ assert !NonParanoidAndroid.paranoid?
244
+ end
245
+
246
+ def test_should_give_record_status
247
+ assert_equal false, Widget.find(1).deleted?
248
+ Widget.find(1).destroy
249
+ assert Widget.find(1).deleted?
250
+ end
251
+
252
+ def test_should_find_deleted_has_many_assocations_on_deleted_records_by_default
253
+ w = Widget.find 2
254
+ assert_equal 2, w.categories.find(:all).length
255
+ assert_equal 2, w.categories.find(:all).size
256
+ end
257
+
258
+ def test_should_find_deleted_habtm_assocations_on_deleted_records_by_default
259
+ w = Widget.find 2
260
+ assert_equal 2, w.habtm_categories.find(:all).length
261
+ assert_equal 2, w.habtm_categories.find(:all).size
262
+ end
263
+
264
+ def test_dynamic_finders
265
+ assert Widget.without_deleted.find_by_id(1)
266
+ assert_nil Widget.without_deleted.find_by_id(2)
267
+ end
268
+
269
+ def test_custom_finder_methods
270
+ w = Widget.all.inject({}) { |all, w| all.merge(w.id => w) }
271
+ assert_equal [1], Category.search('c').ids
272
+ assert_equal [1,2,3,4], Category.search_with_deleted('c', :order => 'id').ids
273
+ assert_equal [1], widgets(:widget_1).categories.search('c').collect(&:id)
274
+ assert_equal [1,2], widgets(:widget_1).categories.search_with_deleted('c').ids
275
+ assert_equal [], w[2].categories.search('c').ids
276
+ assert_equal [3,4], w[2].categories.search_with_deleted('c').ids
277
+ end
278
+
279
+ def test_should_recover_record
280
+ Widget.find(1).destroy
281
+ assert_equal true, Widget.find(1).deleted?
282
+
283
+ Widget.find(1).recover!
284
+ assert_equal false, Widget.find(1).deleted?
285
+ end
286
+
287
+ def test_should_recover_record_and_has_many_associations
288
+ Widget.find(1).destroy
289
+ assert_equal true, Widget.find(1).deleted?
290
+ assert_equal true, Category.find(1).deleted?
291
+
292
+ Widget.find(1).recover_with_associations!(:categories)
293
+ assert_equal false, Widget.find(1).deleted?
294
+ assert_equal false, Category.find(1).deleted?
295
+ end
296
+
297
+ def test_find_including_associations
298
+ w = Widget.find(1, :include => :categories)
299
+ assert_equal widgets(:widget_1), w
300
+ assert_equal 2, w.instance_variable_get(:@categories).size
301
+
302
+ c = Category.find(:first, :include => :widget, :conditions => {'widgets.title' => 'widget 1'})
303
+ assert_equal categories(:category_1), c
304
+ assert_equal widgets(:widget_1), c.instance_variable_get(:@widget)
305
+ c = Category.find(3, :include => :widget)
306
+ assert_equal categories(:category_3), c
307
+ assert_nil c.instance_variable_get(:@widget)
308
+ assert_raises(ActiveRecord::RecordNotFound) { Category.find(3, :include => :widget, :conditions => {'widgets.title' => 'deleted widget 2'}) }
309
+
310
+ o = Order.find(:first, :include => :item)
311
+ assert_equal orders(:order_1), o
312
+ assert_nil o.instance_variable_get(:@item)
313
+ o = Order.find(:first, :include => :any_item)
314
+ assert_equal orders(:order_1), o
315
+ assert_equal widgets(:widget_2), o.instance_variable_get(:@any_item)
316
+ end
317
+ end
318
+
319
+ class Array
320
+ def ids
321
+ collect &:id
322
+ end
323
+ end
@@ -0,0 +1,35 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+
3
+ create_table :widgets, :force => true do |t|
4
+ t.column :title, :string, :limit => 50
5
+ t.column :category_id, :integer
6
+ t.column :deleted_at, :timestamp
7
+ end
8
+
9
+ create_table :categories, :force => true do |t|
10
+ t.column :widget_id, :integer
11
+ t.column :title, :string, :limit => 50
12
+ t.column :deleted_at, :timestamp
13
+ end
14
+
15
+ create_table :categories_widgets, :force => true, :id => false do |t|
16
+ t.column :category_id, :integer
17
+ t.column :widget_id, :integer
18
+ end
19
+
20
+ create_table :tags, :force => true do |t|
21
+ t.column :name, :string, :limit => 50
22
+ end
23
+
24
+ create_table :taggings, :force => true do |t|
25
+ t.column :tag_id, :integer
26
+ t.column :widget_id, :integer
27
+ t.column :deleted_at, :timestamp
28
+ end
29
+
30
+ create_table :orders, :force => true do |t|
31
+ t.column :item_id, :integer
32
+ t.column :item_type, :string
33
+ end
34
+
35
+ end
@@ -0,0 +1,47 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'rubygems'
5
+ if ENV['RAILS'].nil?
6
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
7
+ else
8
+ # specific rails version targeted
9
+ # load activerecord and plugin manually
10
+ gem 'activerecord', "=#{ENV['RAILS']}"
11
+ require 'active_record'
12
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
13
+ Dir["#{$LOAD_PATH.last}/**/*.rb"].each do |path|
14
+ require path[$LOAD_PATH.last.size + 1..-1]
15
+ end
16
+ require File.join(File.dirname(__FILE__), '..', 'init.rb')
17
+ end
18
+ require 'active_record/fixtures'
19
+
20
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
21
+ # do this so fixtures will load
22
+ ActiveRecord::Base.configurations.update config
23
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
24
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
25
+
26
+ load(File.dirname(__FILE__) + "/schema.rb")
27
+
28
+ class ActiveSupport::TestCase #:nodoc:
29
+ include ActiveRecord::TestFixtures
30
+ self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
31
+
32
+ def create_fixtures(*table_names)
33
+ if block_given?
34
+ Fixtures.create_fixtures(self.class.fixture_path, table_names) { yield }
35
+ else
36
+ Fixtures.create_fixtures(self.class.fixture_path, table_names)
37
+ end
38
+ end
39
+
40
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
41
+ self.use_transactional_fixtures = true
42
+
43
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
44
+ self.use_instantiated_fixtures = false
45
+
46
+ # Add more helper methods to be used by all tests here...
47
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paranoid_fu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Sergio Cambra
8
+ autorequire: paranoid_fu
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-05 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: sergio@entrecables.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/paranoid_fu/association_preload.rb
26
+ - lib/paranoid_fu/associations.rb
27
+ - lib/paranoid_fu/belongs_to_polymorphic_association.rb
28
+ - lib/paranoid_fu/paranoid.rb
29
+ - lib/paranoid_fu/reflection_conditions.rb
30
+ - lib/paranoid_fu.rb
31
+ - test/database.yml
32
+ - test/fixtures/categories.yml
33
+ - test/fixtures/categories_widgets.yml
34
+ - test/fixtures/taggings.yml
35
+ - test/fixtures/tags.yml
36
+ - test/fixtures/widgets.yml
37
+ - test/fixtures/orders.yml
38
+ - test/paranoid_test.rb
39
+ - test/schema.rb
40
+ - test/test_helper.rb
41
+ - README
42
+ - MIT-LICENSE
43
+ - CHANGELOG
44
+ - RUNNING_UNIT_TESTS
45
+ has_rdoc: true
46
+ homepage: http://github.com/scambra/paranoid_fu
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.5
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: paranoid_fu keeps models from actually being deleted by setting a deleted_at field. It adds without_deleted and only_deleted named_scopes
73
+ test_files:
74
+ - test/paranoid_test.rb