Commit 46200fd5 by ray

Merge branch 'rails4' into setting_restore

parents db5b2d19 d6574439
...@@ -7,5 +7,5 @@ rvm: ...@@ -7,5 +7,5 @@ rvm:
- rbx - rbx
env: env:
- RAILS='~> 4.0.2' - RAILS='~> 4.0.4'
- RAILS='~> 4.1.0.beta1' - RAILS='~> 4.1.0'
...@@ -2,37 +2,37 @@ ...@@ -2,37 +2,37 @@
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. 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.
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. 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.
If you wish to actually destroy an object you may call `really_destroy!`. 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.**
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. 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.
## Installation & Usage ## Installation & Usage
For Rails 3, please use version 1 of Paranoia: For Rails 3, please use version 1 of Paranoia:
```ruby ``` ruby
gem 'paranoia', '~> 1.0' gem "paranoia", "~> 1.0"
``` ```
For Rails 4, please use version 2 of Paranoia: For Rails 4, please use version 2 of Paranoia:
```ruby ``` ruby
gem 'paranoia', '~> 2.0' gem "paranoia", "~> 2.0"
``` ```
Of course you can install this from GitHub as well: Of course you can install this from GitHub as well:
```ruby ``` ruby
gem 'paranoia', :github => 'radar/paranoia', :branch => 'master' gem "paranoia", :github => "radar/paranoia", :branch => "master"
# or # or
gem 'paranoia', :github => 'radar/paranoia', :branch => 'rails4' gem "paranoia", :github => "radar/paranoia", :branch => "rails4"
``` ```
Then run: Then run:
```shell ``` shell
bundle install bundle install
``` ```
...@@ -42,13 +42,13 @@ Updating is as simple as `bundle update paranoia`. ...@@ -42,13 +42,13 @@ Updating is as simple as `bundle update paranoia`.
Run: Run:
```shell ``` shell
rails generate migration AddDeletedAtToClients deleted_at:datetime:index rails generate migration AddDeletedAtToClients deleted_at:datetime:index
``` ```
and now you have a migration and now you have a migration
```ruby ``` ruby
class AddDeletedAtToClients < ActiveRecord::Migration class AddDeletedAtToClients < ActiveRecord::Migration
def change def change
add_column :clients, :deleted_at, :datetime add_column :clients, :deleted_at, :datetime
...@@ -61,33 +61,38 @@ end ...@@ -61,33 +61,38 @@ end
#### In your model: #### In your model:
```ruby ``` ruby
class Client < ActiveRecord::Base class Client < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
... # ...
end end
``` ```
Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column:
``` ``` ruby
>> client.deleted_at => nil >> client.deleted_at
>> client.destroy => client # => nil
>> client.deleted_at => [current timestamp] >> client.destroy
# => client
>> client.deleted_at
# => [current timestamp]
``` ```
If you really want it gone *gone*, call `really_destroy!` If you really want it gone *gone*, call `really_destroy!`:
``` ``` ruby
>> client.deleted_at => nil >> client.deleted_at
>> client.really_destroy! => client # => nil
>> client.really_destroy!
# => client
``` ```
If you want a method to be called on destroy, simply provide a `before_destroy` callback: If you want a method to be called on destroy, simply provide a `before_destroy` callback:
```ruby ``` ruby
class Client < ActiveRecord::Base class Client < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
...@@ -97,13 +102,13 @@ class Client < ActiveRecord::Base ...@@ -97,13 +102,13 @@ class Client < ActiveRecord::Base
# do stuff # do stuff
end end
... # ...
end end
``` ```
If you want to use a column other than `deleted_at`, you can pass it as an option: If you want to use a column other than `deleted_at`, you can pass it as an option:
```ruby ``` ruby
class Client < ActiveRecord::Base class Client < ActiveRecord::Base
acts_as_paranoid column: :destroyed_at acts_as_paranoid column: :destroyed_at
...@@ -113,7 +118,7 @@ end ...@@ -113,7 +118,7 @@ end
If you want to access soft-deleted associations, override the getter method: If you want to access soft-deleted associations, override the getter method:
```ruby ``` ruby
def product def product
Product.unscoped { super } Product.unscoped { super }
end end
...@@ -121,43 +126,43 @@ end ...@@ -121,43 +126,43 @@ end
If you want to find all records, even those which are deleted: If you want to find all records, even those which are deleted:
```ruby ``` ruby
Client.with_deleted Client.with_deleted
``` ```
If you want to find only the deleted records: If you want to find only the deleted records:
```ruby ``` ruby
Client.only_deleted Client.only_deleted
``` ```
If you want to check if a record is soft-deleted: If you want to check if a record is soft-deleted:
```ruby ``` ruby
client.destroyed? client.destroyed?
``` ```
If you want to restore a record: If you want to restore a record:
```ruby ``` ruby
Client.restore(id) Client.restore(id)
``` ```
If you want to restore a whole bunch of records: If you want to restore a whole bunch of records:
```ruby ``` ruby
Client.restore([id1, id2, ..., idN]) Client.restore([id1, id2, ..., idN])
``` ```
If you want to restore a record and their dependently destroyed associated records: If you want to restore a record and their dependently destroyed associated records:
```ruby ``` ruby
Client.restore(id, :recursive => true) Client.restore(id, :recursive => true)
``` ```
If you want callbacks to trigger before a restore: If you want callbacks to trigger before a restore:
```ruby ``` ruby
before_restore :callback_name_goes_here before_restore :callback_name_goes_here
``` ```
...@@ -165,7 +170,7 @@ For more information, please look at the tests. ...@@ -165,7 +170,7 @@ For more information, please look at the tests.
## Acts As Paranoid Migration ## Acts As Paranoid Migration
You can replace the older acts_as_paranoid methods as follows: You can replace the older `acts_as_paranoid` methods as follows:
| Old Syntax | New Syntax | | Old Syntax | New Syntax |
|:-------------------------- |:------------------------------ | |:-------------------------- |:------------------------------ |
......
require 'active_record' unless defined? ActiveRecord
module Paranoia module Paranoia
def self.included(klazz) def self.included(klazz)
klazz.extend Query klazz.extend Query
...@@ -88,10 +90,15 @@ module Paranoia ...@@ -88,10 +90,15 @@ module Paranoia
# insert time to paranoia column. # insert time to paranoia column.
# @param with_transaction [Boolean] exec with ActiveRecord Transactions. # @param with_transaction [Boolean] exec with ActiveRecord Transactions.
def touch_paranoia_column(with_transaction=false) def touch_paranoia_column(with_transaction=false)
if with_transaction # This method is (potentially) called from really_destroy
with_transaction_returning_status { touch(paranoia_column) } # The object the method is being called on may be frozen
else # Let's not touch it if it's frozen.
touch(paranoia_column) unless self.frozen?
if with_transaction
with_transaction_returning_status { touch(paranoia_column) }
else
touch(paranoia_column)
end
end end
end end
...@@ -124,9 +131,23 @@ end ...@@ -124,9 +131,23 @@ end
class ActiveRecord::Base class ActiveRecord::Base
def self.acts_as_paranoid(options={}) def self.acts_as_paranoid(options={})
alias :really_destroy! :destroy
alias :destroy! :destroy alias :destroy! :destroy
alias :delete! :delete alias :delete! :delete
def really_destroy!
dependent_reflections = self.reflections.select do |name, reflection|
reflection.options[:dependent] == :destroy
end
if dependent_reflections.any?
dependent_reflections.each do |name, _|
associated_records = self.send(name)
# Paranoid models will have this method, non-paranoid models will not
associated_records = associated_records.with_deleted if associated_records.respond_to?(:with_deleted)
associated_records.each(&:really_destroy!)
end
end
destroy!
end
include Paranoia include Paranoia
class_attribute :paranoia_column class_attribute :paranoia_column
...@@ -168,3 +189,5 @@ class ActiveRecord::Base ...@@ -168,3 +189,5 @@ class ActiveRecord::Base
self.class.paranoia_column self.class.paranoia_column
end end
end end
require 'paranoia/rspec' if defined? RSpec
require 'rspec/expectations'
# Validate the subject's class did call "acts_as_paranoid"
RSpec::Matchers.define :act_as_paranoid do
match { |subject| subject.class.ancestors.include?(Paranoia) }
failure_message_for_should { "#{subject.class} should use `acts_as_paranoid`" }
failure_message_for_should_not { "#{subject.class} should not use `acts_as_paranoid`" }
end
...@@ -287,13 +287,39 @@ class ParanoiaTest < test_framework ...@@ -287,13 +287,39 @@ class ParanoiaTest < test_framework
assert model.instance_variable_get(:@restore_callback_called) assert model.instance_variable_get(:@restore_callback_called)
end end
def test_real_destroy def test_really_destroy
model = ParanoidModel.new model = ParanoidModel.new
model.save model.save
model.really_destroy! model.really_destroy!
refute ParanoidModel.unscoped.exists?(model.id) refute ParanoidModel.unscoped.exists?(model.id)
end end
def test_real_destroy_dependent_destroy
parent = ParentModel.create
child = parent.very_related_models.create
parent.really_destroy!
refute RelatedModel.unscoped.exists?(child.id)
end
def test_real_destroy_dependent_destroy_after_normal_destroy
parent = ParentModel.create
child = parent.very_related_models.create
parent.destroy
parent.really_destroy!
refute RelatedModel.unscoped.exists?(child.id)
end
def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children
parent_1 = ParentModel.create
child_1 = parent_1.very_related_models.create
parent_2 = ParentModel.create
child_2 = parent_2.very_related_models.create
parent_1.destroy
parent_1.really_destroy!
assert RelatedModel.unscoped.exists?(child_2.id)
end
if ActiveRecord::VERSION::STRING < "4.1" if ActiveRecord::VERSION::STRING < "4.1"
def test_real_destroy def test_real_destroy
model = ParanoidModel.new model = ParanoidModel.new
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment