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 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,