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.
- data/CHANGELOG +85 -0
- data/MIT-LICENSE +20 -0
- data/README +5 -0
- data/RUNNING_UNIT_TESTS +10 -0
- data/lib/paranoid_fu.rb +6 -0
- data/lib/paranoid_fu/association_preload.rb +12 -0
- data/lib/paranoid_fu/associations.rb +30 -0
- data/lib/paranoid_fu/belongs_to_polymorphic_association.rb +25 -0
- data/lib/paranoid_fu/paranoid.rb +134 -0
- data/lib/paranoid_fu/reflection_conditions.rb +24 -0
- data/test/database.yml +18 -0
- data/test/fixtures/categories.yml +19 -0
- data/test/fixtures/categories_widgets.yml +12 -0
- data/test/fixtures/orders.yml +8 -0
- data/test/fixtures/taggings.yml +9 -0
- data/test/fixtures/tags.yml +6 -0
- data/test/fixtures/widgets.yml +8 -0
- data/test/paranoid_test.rb +323 -0
- data/test/schema.rb +35 -0
- data/test/test_helper.rb +47 -0
- metadata +74 -0
data/CHANGELOG
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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
data/RUNNING_UNIT_TESTS
ADDED
@@ -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!!
|
data/lib/paranoid_fu.rb
ADDED
@@ -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
|
data/test/database.yml
ADDED
@@ -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,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
|
data/test/schema.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|