Skip to content

Commit

Permalink
Merge upstream/rails4
Browse files Browse the repository at this point in the history
  • Loading branch information
wioux committed May 8, 2015
2 parents 54d1b2e + e1320fb commit d6a7681
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 57 deletions.
7 changes: 3 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
language: ruby
rvm:
- 1.9.3
- 2.0.0
- 2.1.0
- 2.2.0
- jruby-19mode

env:
- RAILS='~> 4.0.8'
- RAILS='~> 4.1.4'
- RAILS='~> 4.2.0'
- RAILS='~> 4.0.13'
- RAILS='~> 4.1.10'
- RAILS='~> 4.2.1'
37 changes: 15 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ gem "paranoia", "~> 2.0"
Of course you can install this from GitHub as well:

``` ruby
gem "paranoia", :github => "radar/paranoia", :branch => "master"
gem "paranoia", :github => "radar/paranoia", :branch => "rails3"
# or
gem "paranoia", :github => "radar/paranoia", :branch => "rails4"
```
Expand Down Expand Up @@ -124,6 +124,16 @@ def product
end
```

If you want to include associated soft-deleted objects, you can (un)scope the association:

``` ruby
class Person < ActiveRecord::Base
belongs_to :group, -> { with_deleted }
end

Person.includes(:group).all
```

If you want to find all records, even those which are deleted:

``` ruby
Expand All @@ -148,6 +158,8 @@ If you want to restore a record:

``` ruby
Client.restore(id)
# or
client.restore
```

If you want to restore a whole bunch of records:
Expand All @@ -160,6 +172,8 @@ If you want to restore a record and their dependently destroyed associated recor

``` ruby
Client.restore(id, :recursive => true)
# or
client.restore(:recursive => true)
```

If you want callbacks to trigger before a restore:
Expand All @@ -184,27 +198,6 @@ You can replace the older `acts_as_paranoid` methods as follows:
The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's
`restore` method does not do this.

## Support for Unique Keys with Null Values

Most databases ignore null columns when it comes to resolving unique index
constraints. This means unique constraints that involve nullable columns may be
problematic. Instead of using `NULL` to represent a not-deleted row, you can pick
a value that you want paranoia to mean not deleted. Note that you can/should
now apply a `NOT NULL` constraint to your `deleted_at` column.

Per model:

```ruby
# pick some value
acts_as_paranoid sentinel_value: DateTime.new(0)
```

or globally in a rails initializer, e.g. `config/initializer/paranoia.rb`

```ruby
Paranoia.default_sentinel_value = DateTime.new(0)
```

## License

This gem is released under the MIT license.
31 changes: 24 additions & 7 deletions lib/paranoia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,19 @@ def with_deleted
end

def only_deleted
with_deleted.where.not(table_name => { paranoia_column => paranoia_sentinel_value} )
with_deleted.where.not(paranoia_column => paranoia_sentinel_value)
end
alias :deleted :only_deleted

def restore(id, opts = {})
Array(id).flatten.map { |one_id| only_deleted.find(one_id).restore!(opts) }
def restore(id_or_ids, opts = {})
ids = Array(id_or_ids).flatten
any_object_instead_of_id = ids.any? { |id| ActiveRecord::Base === id }
if any_object_instead_of_id
ids.map! { |id| ActiveRecord::Base === id ? id.id : id }
ActiveSupport::Deprecation.warn("You are passing an instance of ActiveRecord::Base to `restore`. " \
"Please pass the id of the object by calling `.id`")
end
ids.map { |id| only_deleted.find(id).restore!(opts) }
end
end

Expand All @@ -64,7 +71,18 @@ def self.extended(klazz)
def destroy
transaction do
run_callbacks(:destroy) do
touch_paranoia_column unless destroyed?
result = touch_paranoia_column
if result && ActiveRecord::VERSION::STRING >= '4.2'
each_counter_cached_associations do |association|
foreign_key = association.reflection.foreign_key.to_sym
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
if send(association.reflection.name)
association.decrement_counters
end
end
end
end
result
end
end
end
Expand Down Expand Up @@ -104,8 +122,7 @@ def paranoia_destroyed?

# touch paranoia column.
# insert time to paranoia column.
# @param with_transaction [Boolean] exec with ActiveRecord Transactions.
def touch_paranoia_column(with_transaction=false)
def touch_paranoia_column
raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
if persisted?
touch(paranoia_column)
Expand Down Expand Up @@ -196,7 +213,7 @@ def really_destroy!
self.paranoia_dependent_recovery_window = options[:dependent_recovery_window] || Paranoia.default_dependent_recovery_window

def self.paranoia_scope
where(table_name => { paranoia_column => paranoia_sentinel_value })
where(paranoia_column => paranoia_sentinel_value)
end
default_scope { paranoia_scope }

Expand Down
2 changes: 1 addition & 1 deletion lib/paranoia/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Paranoia
VERSION = "2.1.0"
VERSION = "2.1.2"
end
117 changes: 94 additions & 23 deletions test/paranoia_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,32 @@ def connect!

def setup!
connect!
ActiveRecord::Base.connection.execute 'CREATE TABLE parent_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)'
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)'
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_model_with_build_belongs (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32))'
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)'
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)'
ActiveRecord::Base.connection.execute 'CREATE TABLE not_paranoid_model_with_belongs (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER)'
ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_model_with_has_one_and_builds (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER)'
ActiveRecord::Base.connection.execute 'CREATE TABLE featureful_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME, name VARCHAR(32))'
ActiveRecord::Base.connection.execute 'CREATE TABLE plain_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE fail_callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE related_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER NOT NULL, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE asplode_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE employers (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE employees (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)'
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)'
ActiveRecord::Base.connection.execute 'CREATE TABLE custom_column_models (id INTEGER NOT NULL PRIMARY KEY, destroyed_at DATETIME)'
ActiveRecord::Base.connection.execute 'CREATE TABLE custom_sentinel_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME NOT NULL)'
ActiveRecord::Base.connection.execute 'CREATE TABLE non_paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER)'
ActiveRecord::Base.connection.execute 'CREATE TABLE polymorphic_models (id INTEGER NOT NULL PRIMARY KEY, parent_id INTEGER, parent_type STRING, deleted_at DATETIME)'
{
'parent_model_with_counter_cache_columns' => 'related_models_count INTEGER DEFAULT 0',
'parent_models' => 'deleted_at DATETIME',
'paranoid_models' => 'parent_model_id INTEGER, deleted_at DATETIME',
'paranoid_model_with_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER',
'paranoid_model_with_build_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32)',
'paranoid_model_with_anthor_class_name_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER',
'paranoid_model_with_foreign_key_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER',
'not_paranoid_model_with_belongs' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER',
'paranoid_model_with_has_one_and_builds' => 'parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER',
'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)',
'plain_models' => 'deleted_at DATETIME',
'callback_models' => 'deleted_at DATETIME',
'fail_callback_models' => 'deleted_at DATETIME',
'related_models' => 'parent_model_id INTEGER, parent_model_with_counter_cache_column_id INTEGER, deleted_at DATETIME',
'asplode_models' => 'parent_model_id INTEGER, deleted_at DATETIME',
'employers' => 'deleted_at DATETIME',
'employees' => 'deleted_at DATETIME',
'jobs' => 'employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME',
'custom_column_models' => 'destroyed_at DATETIME',
'custom_sentinel_models' => 'deleted_at DATETIME NOT NULL',
'non_paranoid_models' => 'parent_model_id INTEGER',
'polymorphic_models' => 'parent_id INTEGER, parent_type STRING, deleted_at DATETIME'
}.each do |table_name, columns_as_sql_string|
ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})"
end
end

class WithDifferentConnection < ActiveRecord::Base
Expand Down Expand Up @@ -797,6 +802,56 @@ def test_missing_restore_recursive_on_polymorphic_has_one_association
assert_equal 0, polymorphic.class.count
end

def test_counter_cache_column_update_on_destroy#_and_restore_and_really_destroy
parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create
related_model = parent_model_with_counter_cache_column.related_models.create

assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count
related_model.destroy
assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count
end

def test_callbacks_for_counter_cache_column_update_on_destroy
parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create
related_model = parent_model_with_counter_cache_column.related_models.create

assert_equal nil, related_model.instance_variable_get(:@after_destroy_callback_called)
assert_equal nil, related_model.instance_variable_get(:@after_commit_on_destroy_callback_called)

related_model.destroy

assert related_model.instance_variable_get(:@after_destroy_callback_called)
# assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called)
end

# TODO: find a fix for Rails 4.1
if ActiveRecord::VERSION::STRING !~ /\A4\.1/
def test_counter_cache_column_update_on_really_destroy
parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create
related_model = parent_model_with_counter_cache_column.related_models.create

assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count
related_model.really_destroy!
assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count
end
end

# TODO: find a fix for Rails 4.0 and 4.1
if ActiveRecord::VERSION::STRING >= '4.2'
def test_callbacks_for_counter_cache_column_update_on_really_destroy!
parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create
related_model = parent_model_with_counter_cache_column.related_models.create

assert_equal nil, related_model.instance_variable_get(:@after_destroy_callback_called)
assert_equal nil, related_model.instance_variable_get(:@after_commit_on_destroy_callback_called)

related_model.really_destroy!

assert related_model.instance_variable_get(:@after_destroy_callback_called)
assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called)
end
end

private
def get_featureful_model
FeaturefulModel.new(:name => "not empty")
Expand Down Expand Up @@ -853,9 +908,27 @@ class ParentModel < ActiveRecord::Base
has_one :polymorphic_model, as: :parent, dependent: :destroy
end

class ParentModelWithCounterCacheColumn < ActiveRecord::Base
has_many :related_models
end

class RelatedModel < ActiveRecord::Base
acts_as_paranoid
belongs_to :parent_model
belongs_to :parent_model_with_counter_cache_column, counter_cache: true

after_destroy do |model|
if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0
model.instance_variable_set :@after_destroy_callback_called, true
end
end
after_commit :set_after_commit_on_destroy_callback_called, on: :destroy

def set_after_commit_on_destroy_callback_called
if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0
self.instance_variable_set :@after_commit_on_destroy_callback_called, true
end
end
end

class Employer < ActiveRecord::Base
Expand Down Expand Up @@ -953,8 +1026,6 @@ class FlaggedModelWithCustomIndex < PlainModel
acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted
end



class AsplodeModel < ActiveRecord::Base
acts_as_paranoid
before_destroy do |r|
Expand Down

0 comments on commit d6a7681

Please sign in to comment.