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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0fe3ef85f4b72d9f860b21f59bcf8f0416b6462f
4
- data.tar.gz: a60d2f35334726d1d2af0fc45c03fe0a8fbe8653
3
+ metadata.gz: 691a38d6362b87d499cca9ca12d0988c45acbdcd
4
+ data.tar.gz: 8bbbab8f2754a65876552d494c93373186f89a04
5
5
  SHA512:
6
- metadata.gz: d9c966fc4c04b1ce8bc50bdaf988f7d16290cece9913dcd49cfe738d8751f692450f4daf57e7fe0289c6575dad031317756462326232efb5418a03d427560642
7
- data.tar.gz: 76b091060495418ac3ccf2163a226b8ce43f6ec2e078e75a55e16c566bd968715a9a2694f3b4358e51e308a2be17f674ab9f50fc205c2e75e7e2d04b23ddef59
6
+ metadata.gz: 5bb99eaac69f702473907da7b3988ed9487b4bd8904817dcf6e670498692343414c7db9f4fb987157953fb8cefcccd91a9ddae1d13186ddcb0d4f366b2448818
7
+ data.tar.gz: d363de9761f82f26fcc927892b6cbe4f9ee60f9a41ad1ae878f7586d73f65a6c69263ffa1028e22b660aa9063823b33d867b4a090e4308267cb00bb80864e073
data/.travis.yml CHANGED
@@ -4,8 +4,7 @@ rvm:
4
4
  - 2.0.0
5
5
  - 2.1.0
6
6
  - jruby-19mode
7
- - rbx
8
7
 
9
8
  env:
10
- - RAILS='~> 4.0.2'
11
- - RAILS='~> 4.1.0.beta1'
9
+ - RAILS='~> 4.0.8'
10
+ - RAILS='~> 4.1.4'
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ platforms :rbx do
9
9
  gem 'rubinius-developer_tools'
10
10
  end
11
11
 
12
- rails = ENV['RAILS'] || '~> 4.0.2'
12
+ rails = ENV['RAILS'] || '~> 4.1.4'
13
13
 
14
14
  gem 'rails', rails
15
15
 
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 "hid" 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.
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 'paranoia', '~> 1.0'
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 'paranoia', '~> 2.0'
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 'paranoia', :github => 'radar/paranoia', :branch => 'master'
27
+ ``` ruby
28
+ gem "paranoia", :github => "radar/paranoia", :branch => "master"
27
29
  # or
28
- gem 'paranoia', :github => 'radar/paranoia', :branch => 'rails4'
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 => nil
74
- >> client.destroy => client
75
- >> client.deleted_at => [current timestamp]
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 => nil
82
- >> client.real_destroy! => client
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 => nil)
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
- if id.is_a?(Array)
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
- run_callbacks(:destroy) { touch_paranoia_column(true) }
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
- ActiveRecord::Base.transaction do
84
+ self.class.transaction do
71
85
  run_callbacks(:restore) do
72
- update_column paranoia_column, nil
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
- !!send(paranoia_column)
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
- if with_transaction
91
- with_transaction_returning_status { touch(paranoia_column) }
92
- else
93
- touch(paranoia_column)
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
- association = send(association.name)
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.paranoid?
108
- association.only_deleted.each { |record| record.restore(:recursive => true) }
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
- alias :really_destroy! :destroy
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
- default_scope { where(paranoia_column => nil) }
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
@@ -1,3 +1,3 @@
1
1
  module Paranoia
2
- VERSION = "2.0.2"
2
+ VERSION = "2.0.3"
3
3
  end
@@ -9,18 +9,35 @@ else
9
9
  end
10
10
  require File.expand_path(File.dirname(__FILE__) + "/../lib/paranoia")
11
11
 
12
- ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:'
13
- ActiveRecord::Base.connection.execute 'CREATE TABLE parent_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
14
- ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)'
15
- ActiveRecord::Base.connection.execute 'CREATE TABLE featureful_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME, name VARCHAR(32))'
16
- ActiveRecord::Base.connection.execute 'CREATE TABLE plain_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
17
- ActiveRecord::Base.connection.execute 'CREATE TABLE callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
18
- ActiveRecord::Base.connection.execute 'CREATE TABLE related_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER NOT NULL, deleted_at DATETIME)'
19
- ActiveRecord::Base.connection.execute 'CREATE TABLE employers (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
20
- ActiveRecord::Base.connection.execute 'CREATE TABLE employees (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
21
- 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)'
22
- ActiveRecord::Base.connection.execute 'CREATE TABLE custom_column_models (id INTEGER NOT NULL PRIMARY KEY, destroyed_at DATETIME)'
23
- ActiveRecord::Base.connection.execute 'CREATE TABLE non_paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER)'
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
- assert_equal nil, model.instance_variable_get(:@after_commit_callback_called)
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 test_real_destroy
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.2
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-01-16 00:00:00.000000000 Z
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.0
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,