paranoia 2.0.2 → 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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,
|