paranoia 2.0.2 → 2.0.3
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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -3
- data/Gemfile +1 -1
- data/README.md +69 -36
- data/lib/paranoia.rb +90 -21
- data/lib/paranoia/rspec.rb +13 -0
- data/lib/paranoia/version.rb +1 -1
- data/test/paranoia_test.rb +318 -14
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 691a38d6362b87d499cca9ca12d0988c45acbdcd
|
4
|
+
data.tar.gz: 8bbbab8f2754a65876552d494c93373186f89a04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5bb99eaac69f702473907da7b3988ed9487b4bd8904817dcf6e670498692343414c7db9f4fb987157953fb8cefcccd91a9ddae1d13186ddcb0d4f366b2448818
|
7
|
+
data.tar.gz: d363de9761f82f26fcc927892b6cbe4f9ee60f9a41ad1ae878f7586d73f65a6c69263ffa1028e22b660aa9063823b33d867b4a090e4308267cb00bb80864e073
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,36 +1,38 @@
|
|
1
1
|
# Paranoia
|
2
2
|
|
3
|
-
Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/technoweenie/acts_as_paranoid) for Rails 3, using much, much, much less code.
|
3
|
+
Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/technoweenie/acts_as_paranoid) for Rails 3 and Rails 4, using much, much, much less code.
|
4
4
|
|
5
|
-
You would use either plugin / gem if you wished that when you called `destroy` on an Active Record object that it didn't actually destroy it, but just
|
5
|
+
You would use either plugin / gem if you wished that when you called `destroy` on an Active Record object that it didn't actually destroy it, but just *hide* the record. Paranoia does this by setting a `deleted_at` field to the current time when you `destroy` a record, and hides it by scoping all queries on your model to only include records which do not have a `deleted_at` field.
|
6
6
|
|
7
|
-
If you wish to actually destroy an object you may call `really_destroy!`.
|
7
|
+
If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: destroy` records, so please aim this method away from face when using.**
|
8
|
+
|
9
|
+
If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called.
|
8
10
|
|
9
11
|
## Installation & Usage
|
10
12
|
|
11
13
|
For Rails 3, please use version 1 of Paranoia:
|
12
14
|
|
13
|
-
```ruby
|
14
|
-
gem
|
15
|
+
``` ruby
|
16
|
+
gem "paranoia", "~> 1.0"
|
15
17
|
```
|
16
18
|
|
17
19
|
For Rails 4, please use version 2 of Paranoia:
|
18
20
|
|
19
|
-
```ruby
|
20
|
-
gem
|
21
|
+
``` ruby
|
22
|
+
gem "paranoia", "~> 2.0"
|
21
23
|
```
|
22
24
|
|
23
25
|
Of course you can install this from GitHub as well:
|
24
26
|
|
25
|
-
```ruby
|
26
|
-
gem
|
27
|
+
``` ruby
|
28
|
+
gem "paranoia", :github => "radar/paranoia", :branch => "master"
|
27
29
|
# or
|
28
|
-
gem
|
30
|
+
gem "paranoia", :github => "radar/paranoia", :branch => "rails4"
|
29
31
|
```
|
30
32
|
|
31
33
|
Then run:
|
32
34
|
|
33
|
-
```shell
|
35
|
+
``` shell
|
34
36
|
bundle install
|
35
37
|
```
|
36
38
|
|
@@ -40,16 +42,17 @@ Updating is as simple as `bundle update paranoia`.
|
|
40
42
|
|
41
43
|
Run:
|
42
44
|
|
43
|
-
```shell
|
44
|
-
rails generate migration AddDeletedAtToClients deleted_at:datetime
|
45
|
+
``` shell
|
46
|
+
rails generate migration AddDeletedAtToClients deleted_at:datetime:index
|
45
47
|
```
|
46
48
|
|
47
49
|
and now you have a migration
|
48
50
|
|
49
|
-
```ruby
|
51
|
+
``` ruby
|
50
52
|
class AddDeletedAtToClients < ActiveRecord::Migration
|
51
53
|
def change
|
52
54
|
add_column :clients, :deleted_at, :datetime
|
55
|
+
add_index :clients, :deleted_at
|
53
56
|
end
|
54
57
|
end
|
55
58
|
```
|
@@ -58,33 +61,38 @@ end
|
|
58
61
|
|
59
62
|
#### In your model:
|
60
63
|
|
61
|
-
```ruby
|
64
|
+
``` ruby
|
62
65
|
class Client < ActiveRecord::Base
|
63
66
|
acts_as_paranoid
|
64
67
|
|
65
|
-
...
|
68
|
+
# ...
|
66
69
|
end
|
67
70
|
```
|
68
71
|
|
69
72
|
Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column:
|
70
73
|
|
71
74
|
|
72
|
-
```
|
73
|
-
>> client.deleted_at
|
74
|
-
|
75
|
-
>> client.
|
75
|
+
``` ruby
|
76
|
+
>> client.deleted_at
|
77
|
+
# => nil
|
78
|
+
>> client.destroy
|
79
|
+
# => client
|
80
|
+
>> client.deleted_at
|
81
|
+
# => [current timestamp]
|
76
82
|
```
|
77
83
|
|
78
|
-
If you really want it gone *gone*, call `really_destroy
|
84
|
+
If you really want it gone *gone*, call `really_destroy!`:
|
79
85
|
|
80
|
-
```
|
81
|
-
>> client.deleted_at
|
82
|
-
|
86
|
+
``` ruby
|
87
|
+
>> client.deleted_at
|
88
|
+
# => nil
|
89
|
+
>> client.really_destroy!
|
90
|
+
# => client
|
83
91
|
```
|
84
92
|
|
85
93
|
If you want a method to be called on destroy, simply provide a `before_destroy` callback:
|
86
94
|
|
87
|
-
```ruby
|
95
|
+
``` ruby
|
88
96
|
class Client < ActiveRecord::Base
|
89
97
|
acts_as_paranoid
|
90
98
|
|
@@ -94,13 +102,13 @@ class Client < ActiveRecord::Base
|
|
94
102
|
# do stuff
|
95
103
|
end
|
96
104
|
|
97
|
-
...
|
105
|
+
# ...
|
98
106
|
end
|
99
107
|
```
|
100
108
|
|
101
109
|
If you want to use a column other than `deleted_at`, you can pass it as an option:
|
102
110
|
|
103
|
-
```ruby
|
111
|
+
``` ruby
|
104
112
|
class Client < ActiveRecord::Base
|
105
113
|
acts_as_paranoid column: :destroyed_at
|
106
114
|
|
@@ -110,7 +118,7 @@ end
|
|
110
118
|
|
111
119
|
If you want to access soft-deleted associations, override the getter method:
|
112
120
|
|
113
|
-
```ruby
|
121
|
+
``` ruby
|
114
122
|
def product
|
115
123
|
Product.unscoped { super }
|
116
124
|
end
|
@@ -118,43 +126,43 @@ end
|
|
118
126
|
|
119
127
|
If you want to find all records, even those which are deleted:
|
120
128
|
|
121
|
-
```ruby
|
129
|
+
``` ruby
|
122
130
|
Client.with_deleted
|
123
131
|
```
|
124
132
|
|
125
133
|
If you want to find only the deleted records:
|
126
134
|
|
127
|
-
```ruby
|
135
|
+
``` ruby
|
128
136
|
Client.only_deleted
|
129
137
|
```
|
130
138
|
|
131
139
|
If you want to check if a record is soft-deleted:
|
132
140
|
|
133
|
-
```ruby
|
141
|
+
``` ruby
|
134
142
|
client.destroyed?
|
135
143
|
```
|
136
144
|
|
137
145
|
If you want to restore a record:
|
138
146
|
|
139
|
-
```ruby
|
147
|
+
``` ruby
|
140
148
|
Client.restore(id)
|
141
149
|
```
|
142
150
|
|
143
151
|
If you want to restore a whole bunch of records:
|
144
152
|
|
145
|
-
```ruby
|
153
|
+
``` ruby
|
146
154
|
Client.restore([id1, id2, ..., idN])
|
147
155
|
```
|
148
156
|
|
149
157
|
If you want to restore a record and their dependently destroyed associated records:
|
150
158
|
|
151
|
-
```ruby
|
159
|
+
``` ruby
|
152
160
|
Client.restore(id, :recursive => true)
|
153
161
|
```
|
154
162
|
|
155
163
|
If you want callbacks to trigger before a restore:
|
156
164
|
|
157
|
-
```ruby
|
165
|
+
``` ruby
|
158
166
|
before_restore :callback_name_goes_here
|
159
167
|
```
|
160
168
|
|
@@ -162,7 +170,7 @@ For more information, please look at the tests.
|
|
162
170
|
|
163
171
|
## Acts As Paranoid Migration
|
164
172
|
|
165
|
-
You can replace the older acts_as_paranoid methods as follows:
|
173
|
+
You can replace the older `acts_as_paranoid` methods as follows:
|
166
174
|
|
167
175
|
| Old Syntax | New Syntax |
|
168
176
|
|:-------------------------- |:------------------------------ |
|
@@ -170,6 +178,31 @@ You can replace the older acts_as_paranoid methods as follows:
|
|
170
178
|
|`find_with_deleted(:first)` | `Client.with_deleted.first` |
|
171
179
|
|`find_with_deleted(id)` | `Client.with_deleted.find(id)` |
|
172
180
|
|
181
|
+
|
182
|
+
The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's
|
183
|
+
`restore` method does not do this.
|
184
|
+
|
185
|
+
## Support for Unique Keys with Null Values
|
186
|
+
|
187
|
+
Most databases ignore null columns when it comes to resolving unique index
|
188
|
+
constraints. This means unique constraints that involve nullable columns may be
|
189
|
+
problematic. Instead of using `NULL` to represent a not-deleted row, you can pick
|
190
|
+
a value that you want paranoia to mean not deleted. Note that you can/should
|
191
|
+
now apply a `NOT NULL` constraint to your `deleted_at` column.
|
192
|
+
|
193
|
+
Per model:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# pick some value
|
197
|
+
acts_as_paranoid sentinel_value: DateTime.new(0)
|
198
|
+
```
|
199
|
+
|
200
|
+
or globally in a rails initializer, e.g. `config/initializer/paranoia.rb`
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
Paranoia.default_sentinel_value = DateTime.new(0)
|
204
|
+
```
|
205
|
+
|
173
206
|
## License
|
174
207
|
|
175
208
|
This gem is released under the MIT license.
|
data/lib/paranoia.rb
CHANGED
@@ -1,4 +1,17 @@
|
|
1
|
+
require 'active_record' unless defined? ActiveRecord
|
2
|
+
|
1
3
|
module Paranoia
|
4
|
+
@@default_sentinel_value = nil
|
5
|
+
|
6
|
+
# Change default_sentinel_value in a rails initilizer
|
7
|
+
def self.default_sentinel_value=(val)
|
8
|
+
@@default_sentinel_value = val
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.default_sentinel_value
|
12
|
+
@@default_sentinel_value
|
13
|
+
end
|
14
|
+
|
2
15
|
def self.included(klazz)
|
3
16
|
klazz.extend Query
|
4
17
|
klazz.extend Callbacks
|
@@ -16,16 +29,12 @@ module Paranoia
|
|
16
29
|
end
|
17
30
|
|
18
31
|
def only_deleted
|
19
|
-
with_deleted.where.not(paranoia_column =>
|
32
|
+
with_deleted.where.not(table_name => { paranoia_column => paranoia_sentinel_value} )
|
20
33
|
end
|
21
34
|
alias :deleted :only_deleted
|
22
35
|
|
23
36
|
def restore(id, opts = {})
|
24
|
-
|
25
|
-
id.map { |one_id| restore(one_id, opts) }
|
26
|
-
else
|
27
|
-
only_deleted.find(id).restore!(opts)
|
28
|
-
end
|
37
|
+
Array(id).flatten.map { |one_id| only_deleted.find(one_id).restore!(opts) }
|
29
38
|
end
|
30
39
|
end
|
31
40
|
|
@@ -48,7 +57,12 @@ module Paranoia
|
|
48
57
|
end
|
49
58
|
|
50
59
|
def destroy
|
51
|
-
|
60
|
+
callbacks_result = transaction do
|
61
|
+
run_callbacks(:destroy) do
|
62
|
+
touch_paranoia_column
|
63
|
+
end
|
64
|
+
end
|
65
|
+
callbacks_result ? self : false
|
52
66
|
end
|
53
67
|
|
54
68
|
# As of Rails 4.1.0 +destroy!+ will no longer remove the record from the db
|
@@ -67,17 +81,25 @@ module Paranoia
|
|
67
81
|
end
|
68
82
|
|
69
83
|
def restore!(opts = {})
|
70
|
-
|
84
|
+
self.class.transaction do
|
71
85
|
run_callbacks(:restore) do
|
72
|
-
|
86
|
+
# Fixes a bug where the build would error because attributes were frozen.
|
87
|
+
# This only happened on Rails versions earlier than 4.1.
|
88
|
+
noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1")
|
89
|
+
if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen
|
90
|
+
write_attribute paranoia_column, paranoia_sentinel_value
|
91
|
+
update_column paranoia_column, paranoia_sentinel_value
|
92
|
+
end
|
73
93
|
restore_associated_records if opts[:recursive]
|
74
94
|
end
|
75
95
|
end
|
96
|
+
|
97
|
+
self
|
76
98
|
end
|
77
99
|
alias :restore :restore!
|
78
100
|
|
79
101
|
def destroyed?
|
80
|
-
|
102
|
+
send(paranoia_column) != paranoia_sentinel_value
|
81
103
|
end
|
82
104
|
alias :deleted? :destroyed?
|
83
105
|
|
@@ -87,10 +109,15 @@ module Paranoia
|
|
87
109
|
# insert time to paranoia column.
|
88
110
|
# @param with_transaction [Boolean] exec with ActiveRecord Transactions.
|
89
111
|
def touch_paranoia_column(with_transaction=false)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
112
|
+
# This method is (potentially) called from really_destroy
|
113
|
+
# The object the method is being called on may be frozen
|
114
|
+
# Let's not touch it if it's frozen.
|
115
|
+
unless self.frozen?
|
116
|
+
if with_transaction
|
117
|
+
with_transaction_returning_status { touch(paranoia_column) }
|
118
|
+
else
|
119
|
+
touch(paranoia_column)
|
120
|
+
end
|
94
121
|
end
|
95
122
|
end
|
96
123
|
|
@@ -102,25 +129,61 @@ module Paranoia
|
|
102
129
|
end
|
103
130
|
|
104
131
|
destroyed_associations.each do |association|
|
105
|
-
|
132
|
+
association_data = send(association.name)
|
133
|
+
|
134
|
+
unless association_data.nil?
|
135
|
+
if association_data.paranoid?
|
136
|
+
if association.collection?
|
137
|
+
association_data.only_deleted.each { |record| record.restore(:recursive => true) }
|
138
|
+
else
|
139
|
+
association_data.restore(:recursive => true)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
106
143
|
|
107
|
-
if association.
|
108
|
-
association.
|
144
|
+
if association_data.nil? && association.macro.to_s == "has_one"
|
145
|
+
association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
|
146
|
+
association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
|
147
|
+
Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
|
109
148
|
end
|
110
149
|
end
|
150
|
+
|
151
|
+
clear_association_cache if destroyed_associations.present?
|
111
152
|
end
|
112
153
|
end
|
113
154
|
|
114
155
|
class ActiveRecord::Base
|
115
156
|
def self.acts_as_paranoid(options={})
|
116
|
-
|
157
|
+
raise "primary key required for "+self.name unless self.primary_key
|
117
158
|
alias :destroy! :destroy
|
118
159
|
alias :delete! :delete
|
160
|
+
def really_destroy!
|
161
|
+
dependent_reflections = self.class.reflections.select do |name, reflection|
|
162
|
+
reflection.options[:dependent] == :destroy
|
163
|
+
end
|
164
|
+
if dependent_reflections.any?
|
165
|
+
dependent_reflections.each do |name, _|
|
166
|
+
associated_records = self.send(name)
|
167
|
+
# has_one association can return nil
|
168
|
+
if associated_records && associated_records.respond_to?(:with_deleted)
|
169
|
+
# Paranoid models will have this method, non-paranoid models will not
|
170
|
+
associated_records.with_deleted.each(&:really_destroy!)
|
171
|
+
self.send(name).reload
|
172
|
+
elsif associated_records && !associated_records.respond_to?(:each) # single record
|
173
|
+
associated_records.really_destroy!
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
touch_paranoia_column if ActiveRecord::VERSION::STRING >= "4.1"
|
178
|
+
destroy!
|
179
|
+
end
|
180
|
+
|
119
181
|
include Paranoia
|
120
|
-
class_attribute :paranoia_column
|
182
|
+
class_attribute :paranoia_column, :paranoia_sentinel_value
|
121
183
|
|
122
|
-
self.paranoia_column = options[:column] || :deleted_at
|
123
|
-
|
184
|
+
self.paranoia_column = (options[:column] || :deleted_at).to_s
|
185
|
+
self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value }
|
186
|
+
default_scope { where(table_name => { paranoia_column => paranoia_sentinel_value }) }
|
124
187
|
|
125
188
|
before_restore {
|
126
189
|
self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers)
|
@@ -156,4 +219,10 @@ class ActiveRecord::Base
|
|
156
219
|
def paranoia_column
|
157
220
|
self.class.paranoia_column
|
158
221
|
end
|
222
|
+
|
223
|
+
def paranoia_sentinel_value
|
224
|
+
self.class.paranoia_sentinel_value
|
225
|
+
end
|
159
226
|
end
|
227
|
+
|
228
|
+
require 'paranoia/rspec' if defined? RSpec
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec/expectations'
|
2
|
+
|
3
|
+
# Validate the subject's class did call "acts_as_paranoid"
|
4
|
+
RSpec::Matchers.define :act_as_paranoid do
|
5
|
+
match { |subject| subject.class.ancestors.include?(Paranoia) }
|
6
|
+
|
7
|
+
failure_message { "expected #{subject.class} to use `acts_as_paranoid`" }
|
8
|
+
failure_message_when_negated { "expected #{subject.class} not to use `acts_as_paranoid`" }
|
9
|
+
|
10
|
+
# RSpec 2 compatibility:
|
11
|
+
alias_method :failure_message_for_should, :failure_message
|
12
|
+
alias_method :failure_message_for_should_not, :failure_message_when_negated
|
13
|
+
end
|
data/lib/paranoia/version.rb
CHANGED
data/test/paranoia_test.rb
CHANGED
@@ -9,18 +9,35 @@ else
|
|
9
9
|
end
|
10
10
|
require File.expand_path(File.dirname(__FILE__) + "/../lib/paranoia")
|
11
11
|
|
12
|
-
|
13
|
-
ActiveRecord::Base.
|
14
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
15
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
16
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
17
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
18
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
19
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
20
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
21
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
22
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
23
|
-
ActiveRecord::Base.connection.execute 'CREATE TABLE
|
12
|
+
def connect!
|
13
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:'
|
14
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE parent_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
15
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)'
|
16
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_model_with_belongs (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER)'
|
17
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_model_with_anthor_class_name_belongs (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER)'
|
18
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_model_with_foreign_key_belongs (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER)'
|
19
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE featureful_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME, name VARCHAR(32))'
|
20
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE plain_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
21
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
22
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE fail_callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
23
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE related_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER NOT NULL, deleted_at DATETIME)'
|
24
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE asplode_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)'
|
25
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE employers (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
26
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE employees (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
27
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE jobs (id INTEGER NOT NULL PRIMARY KEY, employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME)'
|
28
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE custom_column_models (id INTEGER NOT NULL PRIMARY KEY, destroyed_at DATETIME)'
|
29
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE custom_sentinel_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME NOT NULL)'
|
30
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE non_paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER)'
|
31
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE idless_models (deleted_at DATETIME)'
|
32
|
+
end
|
33
|
+
|
34
|
+
class WithDifferentConnection < ActiveRecord::Base
|
35
|
+
establish_connection adapter: 'sqlite3', database: ':memory:'
|
36
|
+
connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
|
37
|
+
acts_as_paranoid
|
38
|
+
end
|
39
|
+
|
40
|
+
connect!
|
24
41
|
|
25
42
|
class ParanoiaTest < test_framework
|
26
43
|
def setup
|
@@ -97,7 +114,7 @@ class ParanoiaTest < test_framework
|
|
97
114
|
assert_equal nil, model.instance_variable_get(:@validate_called)
|
98
115
|
assert_equal nil, model.instance_variable_get(:@destroy_callback_called)
|
99
116
|
assert_equal nil, model.instance_variable_get(:@after_destroy_callback_called)
|
100
|
-
|
117
|
+
assert model.instance_variable_get(:@after_commit_callback_called)
|
101
118
|
end
|
102
119
|
|
103
120
|
def test_destroy_behavior_for_paranoid_models
|
@@ -145,6 +162,36 @@ class ParanoiaTest < test_framework
|
|
145
162
|
assert_equal 1, model.class.deleted.count
|
146
163
|
end
|
147
164
|
|
165
|
+
def test_default_sentinel_value
|
166
|
+
assert_equal nil, ParanoidModel.paranoia_sentinel_value
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_sentinel_value_for_custom_sentinel_models
|
170
|
+
model = CustomSentinelModel.new
|
171
|
+
assert_equal 0, model.class.count
|
172
|
+
model.save!
|
173
|
+
assert_equal DateTime.new(0), model.deleted_at
|
174
|
+
assert_equal 1, model.class.count
|
175
|
+
model.destroy
|
176
|
+
|
177
|
+
assert DateTime.new(0) != model.deleted_at
|
178
|
+
assert model.destroyed?
|
179
|
+
|
180
|
+
assert_equal 0, model.class.count
|
181
|
+
assert_equal 1, model.class.unscoped.count
|
182
|
+
assert_equal 1, model.class.only_deleted.count
|
183
|
+
assert_equal 1, model.class.deleted.count
|
184
|
+
|
185
|
+
model.restore
|
186
|
+
assert_equal DateTime.new(0), model.deleted_at
|
187
|
+
assert !model.destroyed?
|
188
|
+
|
189
|
+
assert_equal 1, model.class.count
|
190
|
+
assert_equal 1, model.class.unscoped.count
|
191
|
+
assert_equal 0, model.class.only_deleted.count
|
192
|
+
assert_equal 0, model.class.deleted.count
|
193
|
+
end
|
194
|
+
|
148
195
|
def test_destroy_behavior_for_featureful_paranoid_models
|
149
196
|
model = get_featureful_model
|
150
197
|
assert_equal 0, model.class.count
|
@@ -246,6 +293,13 @@ class ParanoiaTest < test_framework
|
|
246
293
|
assert_equal false, model.destroyed?
|
247
294
|
end
|
248
295
|
|
296
|
+
def test_restore_on_object_return_self
|
297
|
+
model = ParanoidModel.create
|
298
|
+
model.destroy
|
299
|
+
|
300
|
+
assert_equal model.class, model.restore.class
|
301
|
+
end
|
302
|
+
|
249
303
|
# Regression test for #92
|
250
304
|
def test_destroy_twice
|
251
305
|
model = ParanoidModel.new
|
@@ -256,6 +310,20 @@ class ParanoiaTest < test_framework
|
|
256
310
|
assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count
|
257
311
|
end
|
258
312
|
|
313
|
+
def test_destroy_return_value_on_success
|
314
|
+
model = ParanoidModel.create
|
315
|
+
return_value = model.destroy
|
316
|
+
|
317
|
+
assert_equal(return_value, model)
|
318
|
+
end
|
319
|
+
|
320
|
+
def test_destroy_return_value_on_failure
|
321
|
+
model = FailCallbackModel.create
|
322
|
+
return_value = model.destroy
|
323
|
+
|
324
|
+
assert_equal(return_value, false)
|
325
|
+
end
|
326
|
+
|
259
327
|
def test_restore_behavior_for_callbacks
|
260
328
|
model = CallbackModel.new
|
261
329
|
model.save
|
@@ -271,13 +339,39 @@ class ParanoiaTest < test_framework
|
|
271
339
|
assert model.instance_variable_get(:@restore_callback_called)
|
272
340
|
end
|
273
341
|
|
274
|
-
def
|
342
|
+
def test_really_destroy
|
275
343
|
model = ParanoidModel.new
|
276
344
|
model.save
|
277
345
|
model.really_destroy!
|
278
346
|
refute ParanoidModel.unscoped.exists?(model.id)
|
279
347
|
end
|
280
348
|
|
349
|
+
def test_real_destroy_dependent_destroy
|
350
|
+
parent = ParentModel.create
|
351
|
+
child = parent.very_related_models.create
|
352
|
+
parent.really_destroy!
|
353
|
+
refute RelatedModel.unscoped.exists?(child.id)
|
354
|
+
end
|
355
|
+
|
356
|
+
def test_real_destroy_dependent_destroy_after_normal_destroy
|
357
|
+
parent = ParentModel.create
|
358
|
+
child = parent.very_related_models.create
|
359
|
+
parent.destroy
|
360
|
+
parent.really_destroy!
|
361
|
+
refute RelatedModel.unscoped.exists?(child.id)
|
362
|
+
end
|
363
|
+
|
364
|
+
def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children
|
365
|
+
parent_1 = ParentModel.create
|
366
|
+
child_1 = parent_1.very_related_models.create
|
367
|
+
|
368
|
+
parent_2 = ParentModel.create
|
369
|
+
child_2 = parent_2.very_related_models.create
|
370
|
+
parent_1.destroy
|
371
|
+
parent_1.really_destroy!
|
372
|
+
assert RelatedModel.unscoped.exists?(child_2.id)
|
373
|
+
end
|
374
|
+
|
281
375
|
if ActiveRecord::VERSION::STRING < "4.1"
|
282
376
|
def test_real_destroy
|
283
377
|
model = ParanoidModel.new
|
@@ -350,6 +444,113 @@ class ParanoiaTest < test_framework
|
|
350
444
|
assert_equal true, second_child.destroyed?
|
351
445
|
end
|
352
446
|
|
447
|
+
# regression tests for #118
|
448
|
+
def test_restore_with_has_one_association
|
449
|
+
# setup and destroy test objects
|
450
|
+
hasOne = ParanoidModelWithHasOne.create
|
451
|
+
belongsTo = ParanoidModelWithBelong.create
|
452
|
+
anthorClassName = ParanoidModelWithAnthorClassNameBelong.create
|
453
|
+
foreignKey = ParanoidModelWithForeignKeyBelong.create
|
454
|
+
hasOne.paranoid_model_with_belong = belongsTo
|
455
|
+
hasOne.class_name_belong = anthorClassName
|
456
|
+
hasOne.paranoid_model_with_foreign_key_belong = foreignKey
|
457
|
+
hasOne.save!
|
458
|
+
|
459
|
+
hasOne.destroy
|
460
|
+
assert_equal false, hasOne.deleted_at.nil?
|
461
|
+
assert_equal false, belongsTo.deleted_at.nil?
|
462
|
+
|
463
|
+
# Does it restore has_one associations?
|
464
|
+
hasOne.restore(:recursive => true)
|
465
|
+
hasOne.save!
|
466
|
+
|
467
|
+
assert_equal true, hasOne.reload.deleted_at.nil?
|
468
|
+
assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}"
|
469
|
+
assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record"
|
470
|
+
assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record"
|
471
|
+
assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record"
|
472
|
+
end
|
473
|
+
|
474
|
+
def test_new_restore_with_has_one_association
|
475
|
+
# setup and destroy test objects
|
476
|
+
hasOne = ParanoidModelWithHasOne.create
|
477
|
+
belongsTo = ParanoidModelWithBelong.create
|
478
|
+
anthorClassName = ParanoidModelWithAnthorClassNameBelong.create
|
479
|
+
foreignKey = ParanoidModelWithForeignKeyBelong.create
|
480
|
+
hasOne.paranoid_model_with_belong = belongsTo
|
481
|
+
hasOne.class_name_belong = anthorClassName
|
482
|
+
hasOne.paranoid_model_with_foreign_key_belong = foreignKey
|
483
|
+
hasOne.save!
|
484
|
+
|
485
|
+
hasOne.destroy
|
486
|
+
assert_equal false, hasOne.deleted_at.nil?
|
487
|
+
assert_equal false, belongsTo.deleted_at.nil?
|
488
|
+
|
489
|
+
# Does it restore has_one associations?
|
490
|
+
newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id)
|
491
|
+
newHasOne.restore(:recursive => true)
|
492
|
+
newHasOne.save!
|
493
|
+
|
494
|
+
assert_equal true, hasOne.reload.deleted_at.nil?
|
495
|
+
assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}"
|
496
|
+
assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record"
|
497
|
+
assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record"
|
498
|
+
assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record"
|
499
|
+
end
|
500
|
+
|
501
|
+
def test_model_restore_with_has_one_association
|
502
|
+
# setup and destroy test objects
|
503
|
+
hasOne = ParanoidModelWithHasOne.create
|
504
|
+
belongsTo = ParanoidModelWithBelong.create
|
505
|
+
anthorClassName = ParanoidModelWithAnthorClassNameBelong.create
|
506
|
+
foreignKey = ParanoidModelWithForeignKeyBelong.create
|
507
|
+
hasOne.paranoid_model_with_belong = belongsTo
|
508
|
+
hasOne.class_name_belong = anthorClassName
|
509
|
+
hasOne.paranoid_model_with_foreign_key_belong = foreignKey
|
510
|
+
hasOne.save!
|
511
|
+
|
512
|
+
hasOne.destroy
|
513
|
+
assert_equal false, hasOne.deleted_at.nil?
|
514
|
+
assert_equal false, belongsTo.deleted_at.nil?
|
515
|
+
|
516
|
+
# Does it restore has_one associations?
|
517
|
+
ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true)
|
518
|
+
hasOne.save!
|
519
|
+
|
520
|
+
assert_equal true, hasOne.reload.deleted_at.nil?
|
521
|
+
assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}"
|
522
|
+
assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record"
|
523
|
+
assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record"
|
524
|
+
assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record"
|
525
|
+
end
|
526
|
+
|
527
|
+
def test_restore_with_nil_has_one_association
|
528
|
+
# setup and destroy test object
|
529
|
+
hasOne = ParanoidModelWithHasOne.create
|
530
|
+
hasOne.destroy
|
531
|
+
assert_equal false, hasOne.reload.deleted_at.nil?
|
532
|
+
|
533
|
+
# Does it raise NoMethodException on restore of nil
|
534
|
+
hasOne.restore(:recursive => true)
|
535
|
+
|
536
|
+
assert hasOne.reload.deleted_at.nil?
|
537
|
+
end
|
538
|
+
|
539
|
+
# covers #131
|
540
|
+
def test_has_one_really_destroy_with_nil
|
541
|
+
model = ParanoidModelWithHasOne.create
|
542
|
+
model.really_destroy!
|
543
|
+
|
544
|
+
refute ParanoidModelWithBelong.unscoped.exists?(model.id)
|
545
|
+
end
|
546
|
+
|
547
|
+
def test_has_one_really_destroy_with_record
|
548
|
+
model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong }
|
549
|
+
model.really_destroy!
|
550
|
+
|
551
|
+
refute ParanoidModelWithBelong.unscoped.exists?(model.id)
|
552
|
+
end
|
553
|
+
|
353
554
|
def test_observers_notified
|
354
555
|
a = ParanoidModelWithObservers.create
|
355
556
|
a.destroy
|
@@ -375,6 +576,57 @@ class ParanoiaTest < test_framework
|
|
375
576
|
}, output
|
376
577
|
end
|
377
578
|
|
579
|
+
def test_destroy_fails_if_callback_raises_exception
|
580
|
+
parent = AsplodeModel.create
|
581
|
+
|
582
|
+
assert_raises(StandardError) { parent.destroy }
|
583
|
+
|
584
|
+
#transaction should be rolled back, so parent NOT deleted
|
585
|
+
refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception'
|
586
|
+
end
|
587
|
+
|
588
|
+
def test_destroy_fails_if_association_callback_raises_exception
|
589
|
+
parent = ParentModel.create
|
590
|
+
children = []
|
591
|
+
3.times { children << parent.asplode_models.create }
|
592
|
+
|
593
|
+
assert_raises(StandardError) { parent.destroy }
|
594
|
+
|
595
|
+
#transaction should be rolled back, so parent and children NOT deleted
|
596
|
+
refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception'
|
597
|
+
refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception'
|
598
|
+
end
|
599
|
+
|
600
|
+
def test_restore_model_with_different_connection
|
601
|
+
ActiveRecord::Base.remove_connection # Disconnect the main connection
|
602
|
+
a = WithDifferentConnection.create
|
603
|
+
a.destroy!
|
604
|
+
a.restore!
|
605
|
+
# This test passes if no exception is raised
|
606
|
+
connect! # Reconnect the main connection
|
607
|
+
end
|
608
|
+
|
609
|
+
def test_restore_clear_association_cache_if_associations_present
|
610
|
+
parent = ParentModel.create
|
611
|
+
3.times { parent.very_related_models.create }
|
612
|
+
|
613
|
+
parent.destroy
|
614
|
+
|
615
|
+
assert_equal 0, parent.very_related_models.count
|
616
|
+
assert_equal 0, parent.very_related_models.size
|
617
|
+
|
618
|
+
parent.restore(recursive: true)
|
619
|
+
|
620
|
+
assert_equal 3, parent.very_related_models.count
|
621
|
+
assert_equal 3, parent.very_related_models.size
|
622
|
+
end
|
623
|
+
|
624
|
+
def test_model_without_primary_key
|
625
|
+
assert_raises(RuntimeError) do
|
626
|
+
IdlessModel.class_eval{ acts_as_paranoid }
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
378
630
|
private
|
379
631
|
def get_featureful_model
|
380
632
|
FeaturefulModel.new(:name => "not empty")
|
@@ -388,6 +640,13 @@ class ParanoidModel < ActiveRecord::Base
|
|
388
640
|
acts_as_paranoid
|
389
641
|
end
|
390
642
|
|
643
|
+
class FailCallbackModel < ActiveRecord::Base
|
644
|
+
belongs_to :parent_model
|
645
|
+
acts_as_paranoid
|
646
|
+
|
647
|
+
before_destroy { |_| false }
|
648
|
+
end
|
649
|
+
|
391
650
|
class FeaturefulModel < ActiveRecord::Base
|
392
651
|
acts_as_paranoid
|
393
652
|
validates :name, :presence => true, :uniqueness => true
|
@@ -419,6 +678,7 @@ class ParentModel < ActiveRecord::Base
|
|
419
678
|
has_many :related_models
|
420
679
|
has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy
|
421
680
|
has_many :non_paranoid_models, dependent: :destroy
|
681
|
+
has_many :asplode_models, dependent: :destroy
|
422
682
|
end
|
423
683
|
|
424
684
|
class RelatedModel < ActiveRecord::Base
|
@@ -448,6 +708,10 @@ class CustomColumnModel < ActiveRecord::Base
|
|
448
708
|
acts_as_paranoid column: :destroyed_at
|
449
709
|
end
|
450
710
|
|
711
|
+
class CustomSentinelModel < ActiveRecord::Base
|
712
|
+
acts_as_paranoid sentinel_value: DateTime.new(0)
|
713
|
+
end
|
714
|
+
|
451
715
|
class NonParanoidModel < ActiveRecord::Base
|
452
716
|
end
|
453
717
|
|
@@ -464,3 +728,43 @@ end
|
|
464
728
|
class ParanoidModelWithoutObservers < ParanoidModel
|
465
729
|
self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers)
|
466
730
|
end
|
731
|
+
|
732
|
+
# refer back to regression test for #118
|
733
|
+
class ParanoidModelWithHasOne < ParanoidModel
|
734
|
+
has_one :paranoid_model_with_belong, :dependent => :destroy
|
735
|
+
has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong"
|
736
|
+
has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id"
|
737
|
+
end
|
738
|
+
|
739
|
+
class ParanoidModelWithBelong < ActiveRecord::Base
|
740
|
+
acts_as_paranoid
|
741
|
+
belongs_to :paranoid_model_with_has_one
|
742
|
+
end
|
743
|
+
|
744
|
+
class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base
|
745
|
+
acts_as_paranoid
|
746
|
+
belongs_to :paranoid_model_with_has_one
|
747
|
+
end
|
748
|
+
|
749
|
+
class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base
|
750
|
+
acts_as_paranoid
|
751
|
+
belongs_to :paranoid_model_with_has_one
|
752
|
+
end
|
753
|
+
|
754
|
+
class FlaggedModel < PlainModel
|
755
|
+
acts_as_paranoid :flag_column => :is_deleted
|
756
|
+
end
|
757
|
+
|
758
|
+
class FlaggedModelWithCustomIndex < PlainModel
|
759
|
+
acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted
|
760
|
+
end
|
761
|
+
|
762
|
+
class AsplodeModel < ActiveRecord::Base
|
763
|
+
acts_as_paranoid
|
764
|
+
before_destroy do |r|
|
765
|
+
raise StandardError, 'ASPLODE!'
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
class IdlessModel < ActiveRecord::Base
|
770
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paranoia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radarlistener@gmail.com
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- README.md
|
71
71
|
- Rakefile
|
72
72
|
- lib/paranoia.rb
|
73
|
+
- lib/paranoia/rspec.rb
|
73
74
|
- lib/paranoia/version.rb
|
74
75
|
- paranoia.gemspec
|
75
76
|
- test/paranoia_test.rb
|
@@ -92,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
93
|
version: 1.3.6
|
93
94
|
requirements: []
|
94
95
|
rubyforge_project: paranoia
|
95
|
-
rubygems_version: 2.2.
|
96
|
+
rubygems_version: 2.2.2
|
96
97
|
signing_key:
|
97
98
|
specification_version: 4
|
98
99
|
summary: Paranoia is a re-implementation of acts_as_paranoid for Rails 3, using much,
|