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