paranoid_fu 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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