rails3_acts_as_paranoid 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +171 -45
- data/lib/acts_as_paranoid/associations.rb +31 -0
- data/lib/acts_as_paranoid/core.rb +157 -0
- data/lib/acts_as_paranoid/relation.rb +37 -0
- data/lib/acts_as_paranoid/validations.rb +41 -0
- data/lib/rails3_acts_as_paranoid.rb +22 -243
- metadata +11 -8
- data/lib/validations/uniqueness_without_deleted.rb +0 -39
data/README.markdown
CHANGED
@@ -14,103 +14,227 @@ While porting it to Rails 3, I decided to apply the ideas behind those plugins t
|
|
14
14
|
|
15
15
|
You can enable ActsAsParanoid like this:
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
```ruby
|
18
|
+
class Paranoiac < ActiveRecord::Base
|
19
|
+
acts_as_paranoid
|
20
|
+
end
|
21
|
+
```
|
20
22
|
|
21
23
|
### Options
|
22
24
|
|
23
25
|
You can also specify the name of the column to store it's *deletion* and the type of data it holds:
|
24
26
|
|
25
|
-
-
|
26
|
-
-
|
27
|
+
- `:column => 'deleted_at'`
|
28
|
+
- `:column_type => 'time'`
|
27
29
|
|
28
|
-
The values shown are the defaults. While *column* can be anything (as long as it exists in your database), *type* is restricted to
|
30
|
+
The values shown are the defaults. While *column* can be anything (as long as it exists in your database), *type* is restricted to:
|
29
31
|
|
30
|
-
|
32
|
+
- `boolean`
|
33
|
+
- `time` or
|
34
|
+
- `string`
|
35
|
+
|
36
|
+
If your column type is a `string`, you can also specify which value to use when marking an object as deleted by passing `:deleted_value` (default is "deleted").
|
31
37
|
|
32
38
|
### Filtering
|
33
39
|
|
34
40
|
If a record is deleted by ActsAsParanoid, it won't be retrieved when accessing the database. So, `Paranoiac.all` will **not** include the deleted_records. if you want to access them, you have 2 choices:
|
35
41
|
|
36
|
-
|
37
|
-
|
42
|
+
```ruby
|
43
|
+
Paranoiac.only_deleted # retrieves the deleted records
|
44
|
+
Paranoiac.with_deleted # retrieves all records, deleted or not
|
45
|
+
```
|
46
|
+
|
47
|
+
When using the default `column_type` of `'time'`, the following extra scopes are provided:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
time = Time.now
|
51
|
+
|
52
|
+
Paranoiac.deleted_after_time(time)
|
53
|
+
Paranoiac.deleted_before_time(time)
|
54
|
+
|
55
|
+
# Or roll it all up and get a nice window:
|
56
|
+
Paranoiac.deleted_inside_time_window(time, 2.minutes)
|
57
|
+
```
|
38
58
|
|
39
59
|
### Real deletion
|
40
60
|
|
41
61
|
In order to really delete a record, just use:
|
42
62
|
|
43
|
-
|
44
|
-
|
63
|
+
```ruby
|
64
|
+
paranoiac.destroy!
|
65
|
+
Paranoiac.delete_all!(conditions)
|
66
|
+
```
|
45
67
|
|
46
|
-
You can also
|
68
|
+
You can also permanently delete a record by calling `destroy` or `delete_all` on it **twice**. If a record was already deleted (hidden by ActsAsParanoid) and you delete it again, it will be removed from the database. Take this example:
|
47
69
|
|
48
|
-
|
49
|
-
|
70
|
+
```ruby
|
71
|
+
p = Paranoiac.first
|
72
|
+
p.destroy # does NOT delete the first record, just hides it
|
73
|
+
Paranoiac.only_deleted.where(:id => p.id).destroy # deletes the first record from the database
|
74
|
+
```
|
50
75
|
|
51
76
|
### Recovery
|
52
77
|
|
53
78
|
Recovery is easy. Just invoke `recover` on it, like this:
|
54
79
|
|
55
|
-
|
80
|
+
```ruby
|
81
|
+
Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover
|
82
|
+
```
|
56
83
|
|
57
84
|
All associations marked as `:dependent => :destroy` are also recursively recovered. If you would like to disable this behavior, you can call `recover` with the `recursive` option:
|
58
85
|
|
59
|
-
|
86
|
+
```ruby
|
87
|
+
Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recursive => false)
|
88
|
+
```
|
60
89
|
|
61
|
-
If you would like to change
|
90
|
+
If you would like to change this default behavior for one model, you can use the `recover_dependent_associations` option
|
62
91
|
|
63
|
-
|
64
|
-
|
65
|
-
|
92
|
+
```ruby
|
93
|
+
class Paranoiac < ActiveRecord::Base
|
94
|
+
acts_as_paranoid :recover_dependent_associations => false
|
95
|
+
end
|
96
|
+
```
|
66
97
|
|
67
|
-
By default
|
98
|
+
By default, dependent records will be recovered if they were deleted within 2 minutes of the object upon which they depend. This restores the objects to the state before the recursive deletion without restoring other objects that were deleted earlier. The behavior is only available when both parent and dependant are using timestamp fields to mark deletion, which is the default behavior. This window can be changed with the `dependent_recovery_window` option:
|
68
99
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
100
|
+
```ruby
|
101
|
+
class Paranoiac < ActiveRecord::Base
|
102
|
+
acts_as_paranoid
|
103
|
+
has_many :paranoids, :dependent => :destroy
|
104
|
+
end
|
73
105
|
|
74
|
-
|
75
|
-
|
106
|
+
class Paranoid < ActiveRecord::Base
|
107
|
+
belongs_to :paranoic
|
76
108
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
109
|
+
# Paranoid objects will be recovered alongside Paranoic objects
|
110
|
+
# if they were deleted within 10 minutes of the Paranoic object
|
111
|
+
acts_as_paranoid :dependent_recovery_window => 10.minutes
|
112
|
+
end
|
113
|
+
```
|
81
114
|
|
82
115
|
or in the recover statement
|
83
116
|
|
84
|
-
|
117
|
+
```ruby
|
118
|
+
Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recovery_window => 30.seconds)
|
119
|
+
```
|
85
120
|
|
86
121
|
### Validation
|
87
122
|
ActiveRecord's built-in uniqueness validation does not account for records deleted by ActsAsParanoid. If you want to check for uniqueness among non-deleted records only, use the macro `validates_as_paranoid` in your model. Then, instead of using `validates_uniqueness_of`, use `validates_uniqueness_of_without_deleted`. This will keep deleted records from counting against the uniqueness check.
|
88
123
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
124
|
+
```ruby
|
125
|
+
class Paranoiac < ActiveRecord::Base
|
126
|
+
acts_as_paranoid
|
127
|
+
validates_as_paranoid
|
128
|
+
validates_uniqueness_of_without_deleted :name
|
129
|
+
end
|
94
130
|
|
95
|
-
|
96
|
-
|
131
|
+
p1 = Paranoiac.create(:name => 'foo')
|
132
|
+
p1.destroy
|
97
133
|
|
134
|
+
p2 = Paranoiac.new(:name => 'foo')
|
135
|
+
p2.valid? #=> true
|
136
|
+
p2.save
|
137
|
+
|
138
|
+
p1.recover #=> fails validation!
|
139
|
+
```
|
98
140
|
|
99
141
|
### Status
|
100
|
-
|
142
|
+
You can check the status of your paranoid objects with the `deleted?` helper
|
101
143
|
|
102
|
-
|
103
|
-
|
144
|
+
```ruby
|
145
|
+
Paranoiac.create(:name => 'foo').destroy
|
146
|
+
Paranoiac.with_deleted.first.deleted? #=> true
|
147
|
+
```
|
148
|
+
|
149
|
+
### Scopes
|
150
|
+
|
151
|
+
As you've probably guessed, `with_deleted` and `only_deleted` are scopes. You can, however, chain them freely with other scopes you might have. This
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
Paranoiac.pretty.with_deleted
|
155
|
+
```
|
156
|
+
|
157
|
+
is exactly the same as
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
Paranoiac.with_deleted.pretty
|
161
|
+
```
|
162
|
+
|
163
|
+
You can work freely with scopes and it will just work:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class Paranoiac < ActiveRecord::Base
|
167
|
+
acts_as_paranoid
|
168
|
+
scope :pretty, where(:pretty => true)
|
169
|
+
end
|
170
|
+
|
171
|
+
Paranoiac.create(:pretty => true)
|
172
|
+
|
173
|
+
Paranoiac.pretty.count #=> 1
|
174
|
+
Paranoiac.only_deleted.count #=> 0
|
175
|
+
Paranoiac.pretty.only_deleted.count #=> 0
|
176
|
+
|
177
|
+
Paranoiac.first.destroy
|
178
|
+
|
179
|
+
Paranoiac.pretty.count #=> 0
|
180
|
+
Paranoiac.only_deleted.count #=> 1
|
181
|
+
Paranoiac.pretty.only_deleted.count #=> 1
|
182
|
+
```
|
183
|
+
|
184
|
+
### Associations
|
185
|
+
|
186
|
+
Associations are also supported. From the simplest behaviors you'd expect to more nifty things like the ones mentioned previously or the usage of the `:with_deleted` option with `belongs_to`
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class ParanoiacParent < ActiveRecord::Base
|
190
|
+
has_many :children, :class_name => "ParanoiacChild"
|
191
|
+
end
|
192
|
+
|
193
|
+
class ParanoiacChild < ActiveRecord::Base
|
194
|
+
belongs_to :parent, :class_name => "ParanoiacParent"
|
195
|
+
belongs_to :parent_with_deleted, :class_name => "ParanoiacParent", :with_deleted => true
|
196
|
+
end
|
197
|
+
|
198
|
+
parent = ParanoiacParent.first
|
199
|
+
child = parent.children.create
|
200
|
+
parent.destroy
|
201
|
+
|
202
|
+
child.parent #=> nil
|
203
|
+
child.parent_with_deleted #=> ParanoiacParent (it works!)
|
204
|
+
```
|
104
205
|
|
105
206
|
## Caveats
|
106
207
|
|
107
208
|
Watch out for these caveats:
|
108
209
|
|
109
|
-
- You cannot use
|
110
|
-
- You cannot use scopes named `
|
210
|
+
- You cannot use scopes named `with_deleted` and `only_deleted`
|
211
|
+
- You cannot use scopes named `deleted_inside_time_window`, `deleted_before_time`, `deleted_after_time` **if** your paranoid column's type is `time`
|
111
212
|
- `unscoped` will return all records, deleted or not
|
112
213
|
|
113
|
-
|
214
|
+
# Support
|
215
|
+
|
216
|
+
This gem supports the most recent versions of Rails and Ruby.
|
217
|
+
|
218
|
+
## Rails
|
219
|
+
|
220
|
+
For Rails 3.2 check the README at the [rails3.2](https://github.com/goncalossilva/rails3_acts_as_paranoid/tree/rails3.2) branch and add this to your Gemfile:
|
221
|
+
|
222
|
+
gem "rails3_acts_as_paranoid", "~>0.2.0"
|
223
|
+
|
224
|
+
For Rails 3.1 check the README at the [rails3.1](https://github.com/goncalossilva/rails3_acts_as_paranoid/tree/rails3.1) branch and add this to your Gemfile:
|
225
|
+
|
226
|
+
gem "rails3_acts_as_paranoid", "~>0.1.4"
|
227
|
+
|
228
|
+
For Rails 3.0 check the README at the [rails3.0](https://github.com/goncalossilva/rails3_acts_as_paranoid/tree/rails3.0) branch and add this to your Gemfile:
|
229
|
+
|
230
|
+
gem "rails3_acts_as_paranoid", "~>0.0.9"
|
231
|
+
|
232
|
+
|
233
|
+
## Ruby
|
234
|
+
|
235
|
+
This gem is tested on Ruby 1.9, JRuby and Rubinius (both in 1.9 mode). It *might* work fine in 1.8, but it's not officially supported.
|
236
|
+
|
237
|
+
# Acknowledgements
|
114
238
|
|
115
239
|
* To [cheerfulstoic](https://github.com/cheerfulstoic) for adding recursive recovery
|
116
240
|
* To [Jonathan Vaught](https://github.com/gravelpup) for adding paranoid validations
|
@@ -118,5 +242,7 @@ Watch out for these caveats:
|
|
118
242
|
* To [flah00](https://github.com/flah00) for adding support for STI-based associations (with :dependent)
|
119
243
|
* To [vikramdhillon](https://github.com/vikramdhillon) for the idea and
|
120
244
|
initial implementation of support for string column type
|
245
|
+
* To [Craig Walker](https://github.com/softcraft-development) for Rails 3.1 support and fixing various pending issues
|
246
|
+
* To [Charles G.](https://github.com/chuckg) for Rails 3.2 support and for making a desperately needed global code refactoring
|
121
247
|
|
122
248
|
Copyright © 2010 Gonçalo Silva, released under the MIT license
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActsAsParanoid
|
2
|
+
module Associations
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.class_eval do
|
6
|
+
class << self
|
7
|
+
alias_method_chain :belongs_to, :deleted
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def belongs_to_with_deleted(target, options = {})
|
14
|
+
with_deleted = options.delete(:with_deleted)
|
15
|
+
result = belongs_to_without_deleted(target, options)
|
16
|
+
|
17
|
+
if with_deleted
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__
|
19
|
+
def #{target}_with_unscoped(*args)
|
20
|
+
return #{target}_without_unscoped(*args) unless association(:#{target}).klass.paranoid?
|
21
|
+
association(:#{target}).klass.with_deleted.scoping { #{target}_without_unscoped(*args) }
|
22
|
+
end
|
23
|
+
alias_method_chain :#{target}, :unscoped
|
24
|
+
RUBY
|
25
|
+
end
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ActsAsParanoid
|
2
|
+
module Core
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def self.extended(base)
|
9
|
+
base.define_callbacks :recover
|
10
|
+
end
|
11
|
+
|
12
|
+
def before_recover(method)
|
13
|
+
set_callback :recover, :before, method
|
14
|
+
end
|
15
|
+
|
16
|
+
def after_recover(method)
|
17
|
+
set_callback :recover, :after, method
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_deleted
|
21
|
+
without_paranoid_default_scope
|
22
|
+
end
|
23
|
+
|
24
|
+
def only_deleted
|
25
|
+
without_paranoid_default_scope.where("#{paranoid_column_reference} IS NOT ?", nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_all!(conditions = nil)
|
29
|
+
without_paranoid_default_scope.delete_all!(conditions)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_all(conditions = nil)
|
33
|
+
update_all ["#{paranoid_configuration[:column]} = ?", delete_now_value], conditions
|
34
|
+
end
|
35
|
+
|
36
|
+
def paranoid_default_scope_sql
|
37
|
+
self.scoped.table[paranoid_column].eq(nil).to_sql
|
38
|
+
end
|
39
|
+
|
40
|
+
def paranoid_column
|
41
|
+
paranoid_configuration[:column].to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
def paranoid_column_type
|
45
|
+
paranoid_configuration[:column_type].to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
def dependent_associations
|
49
|
+
self.reflect_on_all_associations.select {|a| [:destroy, :delete_all].include?(a.options[:dependent]) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete_now_value
|
53
|
+
case paranoid_configuration[:column_type]
|
54
|
+
when "time" then Time.now
|
55
|
+
when "boolean" then true
|
56
|
+
when "string" then paranoid_configuration[:deleted_value]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def without_paranoid_default_scope
|
63
|
+
scope = self.scoped.with_default_scope
|
64
|
+
scope.where_values.delete(paranoid_default_scope_sql)
|
65
|
+
|
66
|
+
scope
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def paranoid_value
|
71
|
+
self.send(self.class.paranoid_column)
|
72
|
+
end
|
73
|
+
|
74
|
+
def destroy!
|
75
|
+
with_transaction_returning_status do
|
76
|
+
run_callbacks :destroy do
|
77
|
+
destroy_dependent_associations!
|
78
|
+
self.class.delete_all!(self.class.primary_key.to_sym => self.id)
|
79
|
+
self.paranoid_value = self.class.delete_now_value
|
80
|
+
freeze
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def destroy
|
86
|
+
if paranoid_value.nil?
|
87
|
+
with_transaction_returning_status do
|
88
|
+
run_callbacks :destroy do
|
89
|
+
self.class.delete_all(self.class.primary_key.to_sym => self.id)
|
90
|
+
self.paranoid_value = self.class.delete_now_value
|
91
|
+
self
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
destroy!
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def recover(options={})
|
100
|
+
options = {
|
101
|
+
:recursive => self.class.paranoid_configuration[:recover_dependent_associations],
|
102
|
+
:recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
|
103
|
+
}.merge(options)
|
104
|
+
|
105
|
+
self.class.transaction do
|
106
|
+
run_callbacks :recover do
|
107
|
+
recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
|
108
|
+
|
109
|
+
self.paranoid_value = nil
|
110
|
+
self.save
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def recover_dependent_associations(window, options)
|
116
|
+
self.class.dependent_associations.each do |reflection|
|
117
|
+
next unless reflection.klass.paranoid?
|
118
|
+
|
119
|
+
scope = reflection.klass.only_deleted
|
120
|
+
|
121
|
+
# Merge in the association's scope
|
122
|
+
scope = scope.merge(association(reflection.name).association_scope)
|
123
|
+
|
124
|
+
# We can only recover by window if both parent and dependant have a
|
125
|
+
# paranoid column type of :time.
|
126
|
+
if self.class.paranoid_column_type == :time && reflection.klass.paranoid_column_type == :time
|
127
|
+
scope = scope.merge(reflection.klass.deleted_inside_time_window(paranoid_value, window))
|
128
|
+
end
|
129
|
+
|
130
|
+
scope.each do |object|
|
131
|
+
object.recover(options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def destroy_dependent_associations!
|
137
|
+
self.class.dependent_associations.each do |association|
|
138
|
+
if association.collection? && self.send(association.name).paranoid?
|
139
|
+
association.klass.with_deleted.instance_eval("find_all_by_#{association.foreign_key}(#{self.id.to_json})").each do |object|
|
140
|
+
object.destroy!
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def deleted?
|
147
|
+
!paranoid_value.nil?
|
148
|
+
end
|
149
|
+
alias_method :destroyed?, :deleted?
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def paranoid_value=(value)
|
154
|
+
self.send("#{self.class.paranoid_column}=", value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActsAsParanoid
|
2
|
+
module Relation
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
|
6
|
+
def paranoid?
|
7
|
+
klass.try(:paranoid?) ? true : false
|
8
|
+
end
|
9
|
+
|
10
|
+
def paranoid_deletion_attributes
|
11
|
+
{ klass.paranoid_column => klass.delete_now_value }
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :orig_delete_all, :delete_all
|
15
|
+
def delete_all!(conditions = nil)
|
16
|
+
if conditions
|
17
|
+
where(conditions).delete_all!
|
18
|
+
else
|
19
|
+
orig_delete_all
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_all(conditions = nil)
|
24
|
+
if paranoid?
|
25
|
+
update_all(paranoid_deletion_attributes, conditions)
|
26
|
+
else
|
27
|
+
delete_all!(conditions)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy!(id_or_array)
|
32
|
+
where(primary_key => id_or_array).orig_delete_all
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
3
|
+
module ActsAsParanoid
|
4
|
+
module Validations
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
class UniquenessWithoutDeletedValidator < ActiveRecord::Validations::UniquenessValidator
|
10
|
+
def validate_each(record, attribute, value)
|
11
|
+
finder_class = find_finder_class_for(record)
|
12
|
+
table = finder_class.arel_table
|
13
|
+
|
14
|
+
coder = record.class.serialized_attributes[attribute.to_s]
|
15
|
+
|
16
|
+
if value && coder
|
17
|
+
value = coder.dump value
|
18
|
+
end
|
19
|
+
|
20
|
+
relation = build_relation(finder_class, table, attribute, value)
|
21
|
+
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
|
22
|
+
|
23
|
+
Array.wrap(options[:scope]).each do |scope_item|
|
24
|
+
scope_value = record.send(scope_item)
|
25
|
+
relation = relation.and(table[scope_item].eq(scope_value))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Re-add ActsAsParanoid default scope conditions manually.
|
29
|
+
if finder_class.unscoped.where(finder_class.paranoid_default_scope_sql).where(relation).exists?
|
30
|
+
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def validates_uniqueness_of_without_deleted(*attr_names)
|
37
|
+
validates_with UniquenessWithoutDeletedValidator, _merge_attributes(attr_names)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,66 +1,20 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
require '
|
1
|
+
require 'active_record/base'
|
2
|
+
require 'active_record/relation'
|
3
|
+
require 'active_record/callbacks'
|
4
|
+
require 'acts_as_paranoid/core'
|
5
|
+
require 'acts_as_paranoid/associations'
|
6
|
+
require 'acts_as_paranoid/validations'
|
7
|
+
require 'acts_as_paranoid/relation'
|
3
8
|
|
4
9
|
|
5
|
-
module ActiveRecord
|
6
|
-
class Relation
|
7
|
-
def paranoid?
|
8
|
-
klass.try(:paranoid?) ? true : false
|
9
|
-
end
|
10
|
-
|
11
|
-
def paranoid_deletion_attributes
|
12
|
-
{ klass.paranoid_column => klass.delete_now_value }
|
13
|
-
end
|
14
|
-
|
15
|
-
alias_method :destroy!, :destroy
|
16
|
-
def destroy(id)
|
17
|
-
if paranoid?
|
18
|
-
update_all(paranoid_deletion_attributes, {:id => id})
|
19
|
-
else
|
20
|
-
destroy!(id)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
alias_method :really_delete_all!, :delete_all
|
25
|
-
|
26
|
-
def delete_all!(conditions = nil)
|
27
|
-
if conditions
|
28
|
-
# This idea comes out of Rails 3.1 ActiveRecord::Record.delete_all
|
29
|
-
where(conditions).delete_all!
|
30
|
-
else
|
31
|
-
really_delete_all!
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def delete_all(conditions = nil)
|
36
|
-
if paranoid?
|
37
|
-
update_all(paranoid_deletion_attributes, conditions)
|
38
|
-
else
|
39
|
-
delete_all!(conditions)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def arel=(a)
|
44
|
-
@arel = a
|
45
|
-
end
|
46
|
-
|
47
|
-
def with_deleted
|
48
|
-
wd = self.clone
|
49
|
-
wd.default_scoped = false
|
50
|
-
wd.arel = self.build_arel
|
51
|
-
wd
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
10
|
module ActsAsParanoid
|
57
11
|
|
58
12
|
def paranoid?
|
59
|
-
self.included_modules.include?(
|
13
|
+
self.included_modules.include?(ActsAsParanoid::Core)
|
60
14
|
end
|
61
15
|
|
62
16
|
def validates_as_paranoid
|
63
|
-
|
17
|
+
include ActsAsParanoid::Validations
|
64
18
|
end
|
65
19
|
|
66
20
|
def acts_as_paranoid(options = {})
|
@@ -78,203 +32,28 @@ module ActsAsParanoid
|
|
78
32
|
|
79
33
|
return if paranoid?
|
80
34
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
scope :paranoid_deleted_around_time, lambda {|value, window|
|
85
|
-
if self.class.respond_to?(:paranoid?) && self.class.paranoid?
|
86
|
-
if self.class.paranoid_column_type == 'time' && ![true, false].include?(value)
|
87
|
-
self.where("#{self.class.paranoid_column} > ? AND #{self.class.paranoid_column} < ?", (value - window), (value + window))
|
88
|
-
else
|
89
|
-
self.only_deleted
|
90
|
-
end
|
91
|
-
end if paranoid_configuration[:column_type] == 'time'
|
92
|
-
}
|
93
|
-
|
94
|
-
include InstanceMethods
|
95
|
-
extend ClassMethods
|
96
|
-
end
|
97
|
-
|
98
|
-
module ClassMethods
|
99
|
-
def self.extended(base)
|
100
|
-
base.define_callbacks :recover
|
101
|
-
end
|
102
|
-
|
103
|
-
def before_recover(method)
|
104
|
-
set_callback :recover, :before, method
|
105
|
-
end
|
106
|
-
|
107
|
-
def after_recover(method)
|
108
|
-
set_callback :recover, :after, method
|
109
|
-
end
|
110
|
-
|
111
|
-
def with_deleted
|
112
|
-
self.unscoped
|
113
|
-
end
|
114
|
-
|
115
|
-
def only_deleted
|
116
|
-
self.unscoped.where("#{paranoid_column_reference} IS NOT ?", nil)
|
117
|
-
end
|
118
|
-
|
119
|
-
def deletion_conditions(id_or_array)
|
120
|
-
["id in (?)", [id_or_array].flatten]
|
121
|
-
end
|
122
|
-
|
123
|
-
def delete!(id_or_array)
|
124
|
-
delete_all!(deletion_conditions(id_or_array))
|
125
|
-
end
|
126
|
-
|
127
|
-
def delete(id_or_array)
|
128
|
-
delete_all(deletion_conditions(id_or_array))
|
129
|
-
end
|
130
|
-
|
131
|
-
def delete_all!(conditions = nil)
|
132
|
-
self.unscoped.delete_all!(conditions)
|
133
|
-
end
|
134
|
-
|
135
|
-
def delete_all(conditions = nil)
|
136
|
-
update_all ["#{paranoid_configuration[:column]} = ?", delete_now_value], conditions
|
137
|
-
end
|
138
|
-
|
139
|
-
def paranoid_column
|
140
|
-
paranoid_configuration[:column].to_sym
|
141
|
-
end
|
142
|
-
|
143
|
-
def paranoid_column_type
|
144
|
-
paranoid_configuration[:column_type].to_sym
|
145
|
-
end
|
146
|
-
|
147
|
-
def dependent_associations
|
148
|
-
self.reflect_on_all_associations.select {|a| [:destroy, :delete_all].include?(a.options[:dependent]) }
|
149
|
-
end
|
150
|
-
|
151
|
-
def delete_now_value
|
152
|
-
case paranoid_configuration[:column_type]
|
153
|
-
when "time" then Time.now
|
154
|
-
when "boolean" then true
|
155
|
-
when "string" then paranoid_configuration[:deleted_value]
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
module InstanceMethods
|
161
|
-
|
162
|
-
def paranoid_value
|
163
|
-
self.send(self.class.paranoid_column)
|
164
|
-
end
|
35
|
+
include ActsAsParanoid::Core
|
36
|
+
include ActsAsParanoid::Associations
|
165
37
|
|
166
|
-
|
167
|
-
|
168
|
-
run_callbacks :destroy do
|
169
|
-
act_on_dependent_destroy_associations
|
170
|
-
self.class.delete_all!(self.class.primary_key.to_sym => self.id)
|
171
|
-
self.paranoid_value = self.class.delete_now_value
|
172
|
-
freeze
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def destroy
|
178
|
-
if paranoid_value.nil?
|
179
|
-
with_transaction_returning_status do
|
180
|
-
run_callbacks :destroy do
|
181
|
-
self.class.delete_all(self.class.primary_key.to_sym => self.id)
|
182
|
-
self.paranoid_value = self.class.delete_now_value
|
183
|
-
self
|
184
|
-
end
|
185
|
-
end
|
186
|
-
else
|
187
|
-
destroy!
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def delete!
|
192
|
-
with_transaction_returning_status do
|
193
|
-
act_on_dependent_destroy_associations
|
194
|
-
self.class.delete_all!(self.class.primary_key.to_sym => self.id)
|
195
|
-
self.paranoid_value = self.class.delete_now_value
|
196
|
-
freeze
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def delete
|
201
|
-
if paranoid_value.nil?
|
202
|
-
with_transaction_returning_status do
|
203
|
-
self.class.delete_all(self.class.primary_key.to_sym => self.id)
|
204
|
-
self.paranoid_value = self.class.delete_now_value
|
205
|
-
self
|
206
|
-
end
|
207
|
-
else
|
208
|
-
delete!
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def recover(options={})
|
213
|
-
options = {
|
214
|
-
:recursive => self.class.paranoid_configuration[:recover_dependent_associations],
|
215
|
-
:recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
|
216
|
-
}.merge(options)
|
217
|
-
|
218
|
-
self.class.transaction do
|
219
|
-
run_callbacks :recover do
|
220
|
-
recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
|
221
|
-
|
222
|
-
self.paranoid_value = nil
|
223
|
-
self.save
|
224
|
-
end
|
225
|
-
end
|
226
|
-
end
|
38
|
+
# Magic!
|
39
|
+
default_scope { where(paranoid_default_scope_sql) }
|
227
40
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
self.send(association.name).paranoid_deleted_around_time(paranoid_value, window).each do |object|
|
233
|
-
object.recover(options) if object.respond_to?(:recover)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
elsif association.macro == :has_one && association.klass.paranoid?
|
237
|
-
association.klass.unscoped do
|
238
|
-
object = association.klass.paranoid_deleted_around_time(paranoid_value, window).send('find_by_'+association.foreign_key, self.id)
|
239
|
-
object.recover(options) if object && object.respond_to?(:recover)
|
240
|
-
end
|
241
|
-
elsif association.klass.paranoid?
|
242
|
-
association.klass.unscoped do
|
243
|
-
id = self.send(association.foreign_key)
|
244
|
-
object = association.klass.paranoid_deleted_around_time(paranoid_value, window).find_by_id(id)
|
245
|
-
object.recover(options) if object && object.respond_to?(:recover)
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
def act_on_dependent_destroy_associations
|
252
|
-
self.class.dependent_associations.each do |association|
|
253
|
-
if association.collection? && self.send(association.name).paranoid?
|
254
|
-
association.klass.with_deleted.instance_eval("find_all_by_#{association.foreign_key}(#{self.id.to_json})").each do |object|
|
255
|
-
object.destroy!
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
259
|
-
end
|
41
|
+
if paranoid_configuration[:column_type] == 'time'
|
42
|
+
scope :deleted_inside_time_window, lambda {|time, window|
|
43
|
+
deleted_after_time((time - window)).deleted_before_time((time + window))
|
44
|
+
}
|
260
45
|
|
261
|
-
|
262
|
-
|
263
|
-
end
|
264
|
-
alias_method :destroyed?, :deleted?
|
265
|
-
|
266
|
-
private
|
267
|
-
def paranoid_value=(value)
|
268
|
-
self.send("#{self.class.paranoid_column}=", value)
|
46
|
+
scope :deleted_after_time, lambda { |time| where("#{paranoid_column} > ?", time) }
|
47
|
+
scope :deleted_before_time, lambda { |time| where("#{paranoid_column} < ?", time) }
|
269
48
|
end
|
270
|
-
|
271
49
|
end
|
272
|
-
|
273
50
|
end
|
274
51
|
|
275
|
-
|
276
52
|
# Extend ActiveRecord's functionality
|
277
53
|
ActiveRecord::Base.send :extend, ActsAsParanoid
|
278
54
|
|
55
|
+
# Override ActiveRecord::Relation's behavior
|
56
|
+
ActiveRecord::Relation.send :include, ActsAsParanoid::Relation
|
57
|
+
|
279
58
|
# Push the recover callback onto the activerecord callback list
|
280
59
|
ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails3_acts_as_paranoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-03-
|
12
|
+
date: 2012-03-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement: &
|
16
|
+
requirement: &70201325143880 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '3.2'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70201325143880
|
25
25
|
description: Active Record (~>3.2) plugin which allows you to hide and restore records
|
26
26
|
without actually deleting them. Check its GitHub page for more in-depth information.
|
27
27
|
email:
|
@@ -30,11 +30,14 @@ executables: []
|
|
30
30
|
extensions: []
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
|
+
- lib/acts_as_paranoid/associations.rb
|
34
|
+
- lib/acts_as_paranoid/core.rb
|
35
|
+
- lib/acts_as_paranoid/relation.rb
|
36
|
+
- lib/acts_as_paranoid/validations.rb
|
33
37
|
- lib/rails3_acts_as_paranoid.rb
|
34
|
-
- lib/validations/uniqueness_without_deleted.rb
|
35
38
|
- LICENSE
|
36
39
|
- README.markdown
|
37
|
-
homepage: https://github.com/
|
40
|
+
homepage: https://github.com/goncalossilva/rails3_acts_as_paranoid
|
38
41
|
licenses: []
|
39
42
|
post_install_message:
|
40
43
|
rdoc_options: []
|
@@ -48,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
51
|
version: '0'
|
49
52
|
segments:
|
50
53
|
- 0
|
51
|
-
hash: -
|
54
|
+
hash: -2548769094078379763
|
52
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
56
|
none: false
|
54
57
|
requirements:
|
@@ -57,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
60
|
version: 1.3.6
|
58
61
|
requirements: []
|
59
62
|
rubyforge_project: rails3_acts_as_paranoid
|
60
|
-
rubygems_version: 1.8.
|
63
|
+
rubygems_version: 1.8.17
|
61
64
|
signing_key:
|
62
65
|
specification_version: 3
|
63
66
|
summary: Active Record (~>3.2) plugin which allows you to hide and restore records
|
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'active_support/core_ext/array/wrap'
|
2
|
-
|
3
|
-
module ParanoidValidations
|
4
|
-
class UniquenessWithoutDeletedValidator < ActiveRecord::Validations::UniquenessValidator
|
5
|
-
def validate_each(record, attribute, value)
|
6
|
-
finder_class = find_finder_class_for(record)
|
7
|
-
|
8
|
-
if value && record.class.serialized_attributes.key?(attribute.to_s)
|
9
|
-
value = YAML.dump value
|
10
|
-
end
|
11
|
-
|
12
|
-
table = Arel::Table.new(record.class.table_name)
|
13
|
-
sql, params = build_relation(finder_class, table, attribute, value)
|
14
|
-
|
15
|
-
# This is the only changed line from the base class version - it does finder_class.unscoped
|
16
|
-
relation = finder_class.where(sql, *params)
|
17
|
-
|
18
|
-
Array.wrap(options[:scope]).each do |scope_item|
|
19
|
-
scope_value = record.send(scope_item)
|
20
|
-
relation = relation.where(scope_item => scope_value)
|
21
|
-
end
|
22
|
-
|
23
|
-
if record.persisted?
|
24
|
-
# TODO : This should be in Arel
|
25
|
-
relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
|
26
|
-
end
|
27
|
-
|
28
|
-
if relation.exists?
|
29
|
-
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
module ClassMethods
|
35
|
-
def validates_uniqueness_of_without_deleted(*attr_names)
|
36
|
-
validates_with UniquenessWithoutDeletedValidator, _merge_attributes(attr_names)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|