├── .gitignore ├── lib ├── paranoia │ ├── version.rb │ └── rspec.rb └── paranoia.rb ├── .travis.yml ├── Rakefile ├── Gemfile ├── LICENSE ├── paranoia.gemspec ├── CHANGELOG.md ├── README.md └── test └── paranoia_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | tmp 5 | .rvmrc 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /lib/paranoia/version.rb: -------------------------------------------------------------------------------- 1 | module Paranoia 2 | VERSION = "2.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.0 6 | - 2.2.0 7 | - jruby-19mode 8 | 9 | env: 10 | - RAILS='~> 4.0.8' 11 | - RAILS='~> 4.1.4' 12 | - RAILS='~> 4.2.0' 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | task :test do 5 | Dir['test/*_test.rb'].each do |testfile| 6 | load testfile 7 | end 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sqlite3', :platforms => [:ruby] 4 | gem 'activerecord-jdbcsqlite3-adapter', :platforms => [:jruby] 5 | 6 | platforms :rbx do 7 | gem 'rubysl', '~> 2.0' 8 | gem 'rubysl-test-unit' 9 | gem 'rubinius-developer_tools' 10 | end 11 | 12 | rails = ENV['RAILS'] || '~> 4.2.0' 13 | 14 | gem 'rails', rails 15 | 16 | # Specify your gem's dependencies in paranoia.gemspec 17 | gemspec 18 | -------------------------------------------------------------------------------- /lib/paranoia/rspec.rb: -------------------------------------------------------------------------------- 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_proc = lambda do 8 | "expected #{subject.class} to use `acts_as_paranoid`" 9 | end 10 | 11 | failure_message_when_negated_proc = lambda do 12 | "expected #{subject.class} not to use `acts_as_paranoid`" 13 | end 14 | 15 | if respond_to?(:failure_message_when_negated) 16 | failure_message(&failure_message_proc) 17 | failure_message_when_negated(&failure_message_when_negated_proc) 18 | else 19 | # RSpec 2 compatibility: 20 | failure_message_for_should(&failure_message_proc) 21 | failure_message_for_should_not(&failure_message_when_negated_proc) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, without written agreement and without 2 | license or royalty fees, to use, copy, modify, and distribute this 3 | software and its documentation for any purpose, provided that the 4 | above copyright notice and the following two paragraphs appear in 5 | all copies of this software. 6 | 7 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR 8 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 9 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 10 | IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 11 | DAMAGE. 12 | 13 | THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 14 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 16 | ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO 17 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 18 | -------------------------------------------------------------------------------- /paranoia.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path("../lib/paranoia/version", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "paranoia" 6 | s.version = Paranoia::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["radarlistener@gmail.com"] 9 | s.email = [] 10 | s.homepage = "http://rubygems.org/gems/paranoia" 11 | s.summary = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, using much, much, much less code." 12 | s.description = "Paranoia is a re-implementation of 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." 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | s.rubyforge_project = "paranoia" 16 | 17 | s.add_dependency "activerecord", "~> 4.0" 18 | 19 | s.add_development_dependency "bundler", ">= 1.0.0" 20 | s.add_development_dependency "rake" 21 | 22 | s.files = `git ls-files`.split("\n") 23 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 24 | s.require_path = 'lib' 25 | end 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # paranoia Changelog 2 | 3 | ## 2.1.0 (2015-01-23) 4 | 5 | ### Major changes 6 | 7 | * `#destroyed?` is no longer overridden. Use `#paranoia_destroyed?` for the existing behaviour. [Washington Luiz](https://github.com/huoxito) 8 | * `#persisted?` is no longer overridden. 9 | * ActiveRecord 4.0 no longer has `#destroy!` as an alias for `#really_destroy`. 10 | * `#destroy` will now raise an exception if called on a readonly record. 11 | * `#destroy` on a hard deleted record is now a successful noop. 12 | * `#destroy` on a new record will set deleted_at (previously this raised an error) 13 | * `#destroy` and `#delete` always return self when successful. 14 | 15 | ### Bug Fixes 16 | 17 | * Calling `#destroy` twice will not hard-delete records. Use `#really_destroy` if this is desired. 18 | * Fix errors on non-paranoid has_one dependent associations 19 | 20 | ## 2.0.5 (2015-01-22) 21 | 22 | ### Bug fixes 23 | 24 | * Fix restoring polymorphic has_one relationships [#189](https://github.com/radar/paranoia/pull/189) [#174](https://github.com/radar/paranoia/issues/174) [Patrick Koperwas](https://github.com/PatKoperwas) 25 | * Fix errors when restoring a model with a has_one against a non-paranoid model. [#168](https://github.com/radar/paranoia/pull/168) [Shreyas Agarwal](https://github.com/shreyas123) 26 | * Fix rspec 2 compatibility [#197](https://github.com/radar/paranoia/pull/197) [Emil Sågfors](https://github.com/lime) 27 | * Fix some deprecation warnings on rails 4.2 [Sergey Alekseev](https://github.com/sergey-alekseev) 28 | 29 | ## 2.0.4 (2014-12-02) 30 | 31 | ### Features 32 | * Add paranoia_scope as named version of default_scope [#184](https://github.com/radar/paranoia/pull/184) [Jozsef Nyitrai](https://github.com/nyjt) 33 | 34 | 35 | ### Bug Fixes 36 | * Fix initialization problems when missing table or no database connection [#186](https://github.com/radar/paranoia/issues/186) 37 | * Fix broken restore of has_one associations [#185](https://github.com/radar/paranoia/issues/185) [#171](https://github.com/radar/paranoia/pull/171) [Martin Sereinig](https://github.com/srecnig) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paranoia 2 | 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 | 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 | 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. 10 | 11 | ## Installation & Usage 12 | 13 | For Rails 3, please use version 1 of Paranoia: 14 | 15 | ``` ruby 16 | gem "paranoia", "~> 1.0" 17 | ``` 18 | 19 | For Rails 4, please use version 2 of Paranoia: 20 | 21 | ``` ruby 22 | gem "paranoia", "~> 2.0" 23 | ``` 24 | 25 | Of course you can install this from GitHub as well: 26 | 27 | ``` ruby 28 | gem "paranoia", :github => "radar/paranoia", :branch => "master" 29 | # or 30 | gem "paranoia", :github => "radar/paranoia", :branch => "rails4" 31 | ``` 32 | 33 | Then run: 34 | 35 | ``` shell 36 | bundle install 37 | ``` 38 | 39 | Updating is as simple as `bundle update paranoia`. 40 | 41 | #### Run your migrations for the desired models 42 | 43 | Run: 44 | 45 | ``` shell 46 | rails generate migration AddDeletedAtToClients deleted_at:datetime:index 47 | ``` 48 | 49 | and now you have a migration 50 | 51 | ``` ruby 52 | class AddDeletedAtToClients < ActiveRecord::Migration 53 | def change 54 | add_column :clients, :deleted_at, :datetime 55 | add_index :clients, :deleted_at 56 | end 57 | end 58 | ``` 59 | 60 | ### Usage 61 | 62 | #### In your model: 63 | 64 | ``` ruby 65 | class Client < ActiveRecord::Base 66 | acts_as_paranoid 67 | 68 | # ... 69 | end 70 | ``` 71 | 72 | Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: 73 | 74 | 75 | ``` ruby 76 | >> client.deleted_at 77 | # => nil 78 | >> client.destroy 79 | # => client 80 | >> client.deleted_at 81 | # => [current timestamp] 82 | ``` 83 | 84 | If you really want it gone *gone*, call `really_destroy!`: 85 | 86 | ``` ruby 87 | >> client.deleted_at 88 | # => nil 89 | >> client.really_destroy! 90 | # => client 91 | ``` 92 | 93 | If you want a method to be called on destroy, simply provide a `before_destroy` callback: 94 | 95 | ``` ruby 96 | class Client < ActiveRecord::Base 97 | acts_as_paranoid 98 | 99 | before_destroy :some_method 100 | 101 | def some_method 102 | # do stuff 103 | end 104 | 105 | # ... 106 | end 107 | ``` 108 | 109 | If you want to use a column other than `deleted_at`, you can pass it as an option: 110 | 111 | ``` ruby 112 | class Client < ActiveRecord::Base 113 | acts_as_paranoid column: :destroyed_at 114 | 115 | ... 116 | end 117 | ``` 118 | 119 | If you want to access soft-deleted associations, override the getter method: 120 | 121 | ``` ruby 122 | def product 123 | Product.unscoped { super } 124 | end 125 | ``` 126 | 127 | If you want to find all records, even those which are deleted: 128 | 129 | ``` ruby 130 | Client.with_deleted 131 | ``` 132 | 133 | If you want to find only the deleted records: 134 | 135 | ``` ruby 136 | Client.only_deleted 137 | ``` 138 | 139 | If you want to check if a record is soft-deleted: 140 | 141 | ``` ruby 142 | client.paranoia_destroyed? 143 | # or 144 | client.deleted? 145 | ``` 146 | 147 | If you want to restore a record: 148 | 149 | ``` ruby 150 | Client.restore(id) 151 | ``` 152 | 153 | If you want to restore a whole bunch of records: 154 | 155 | ``` ruby 156 | Client.restore([id1, id2, ..., idN]) 157 | ``` 158 | 159 | If you want to restore a record and their dependently destroyed associated records: 160 | 161 | ``` ruby 162 | Client.restore(id, :recursive => true) 163 | ``` 164 | 165 | If you want callbacks to trigger before a restore: 166 | 167 | ``` ruby 168 | before_restore :callback_name_goes_here 169 | ``` 170 | 171 | For more information, please look at the tests. 172 | 173 | ## Acts As Paranoid Migration 174 | 175 | You can replace the older `acts_as_paranoid` methods as follows: 176 | 177 | | Old Syntax | New Syntax | 178 | |:-------------------------- |:------------------------------ | 179 | |`find_with_deleted(:all)` | `Client.with_deleted` | 180 | |`find_with_deleted(:first)` | `Client.with_deleted.first` | 181 | |`find_with_deleted(id)` | `Client.with_deleted.find(id)` | 182 | 183 | 184 | The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's 185 | `restore` method does not do this. 186 | 187 | ## Support for Unique Keys with Null Values 188 | 189 | Most databases ignore null columns when it comes to resolving unique index 190 | constraints. This means unique constraints that involve nullable columns may be 191 | problematic. Instead of using `NULL` to represent a not-deleted row, you can pick 192 | a value that you want paranoia to mean not deleted. Note that you can/should 193 | now apply a `NOT NULL` constraint to your `deleted_at` column. 194 | 195 | Per model: 196 | 197 | ```ruby 198 | # pick some value 199 | acts_as_paranoid sentinel_value: DateTime.new(0) 200 | ``` 201 | 202 | or globally in a rails initializer, e.g. `config/initializer/paranoia.rb` 203 | 204 | ```ruby 205 | Paranoia.default_sentinel_value = DateTime.new(0) 206 | ``` 207 | 208 | ## License 209 | 210 | This gem is released under the MIT license. 211 | -------------------------------------------------------------------------------- /lib/paranoia.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' unless defined? ActiveRecord 2 | 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 | 15 | def self.included(klazz) 16 | klazz.extend Query 17 | klazz.extend Callbacks 18 | end 19 | 20 | module Query 21 | def paranoid? ; true ; end 22 | 23 | def with_deleted 24 | if ActiveRecord::VERSION::STRING >= "4.1" 25 | unscope where: paranoia_column 26 | else 27 | all.tap { |x| x.default_scoped = false } 28 | end 29 | end 30 | 31 | def only_deleted 32 | with_deleted.where.not(paranoia_column => paranoia_sentinel_value) 33 | end 34 | alias :deleted :only_deleted 35 | 36 | def restore(id, opts = {}) 37 | Array(id).flatten.map { |one_id| only_deleted.find(one_id).restore!(opts) } 38 | end 39 | end 40 | 41 | module Callbacks 42 | def self.extended(klazz) 43 | klazz.define_callbacks :restore 44 | 45 | klazz.define_singleton_method("before_restore") do |*args, &block| 46 | set_callback(:restore, :before, *args, &block) 47 | end 48 | 49 | klazz.define_singleton_method("around_restore") do |*args, &block| 50 | set_callback(:restore, :around, *args, &block) 51 | end 52 | 53 | klazz.define_singleton_method("after_restore") do |*args, &block| 54 | set_callback(:restore, :after, *args, &block) 55 | end 56 | end 57 | end 58 | 59 | def destroy 60 | transaction do 61 | run_callbacks(:destroy) do 62 | touch_paranoia_column 63 | end 64 | end 65 | end 66 | 67 | def delete 68 | touch_paranoia_column 69 | end 70 | 71 | def restore!(opts = {}) 72 | self.class.transaction do 73 | run_callbacks(:restore) do 74 | # Fixes a bug where the build would error because attributes were frozen. 75 | # This only happened on Rails versions earlier than 4.1. 76 | noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") 77 | if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen 78 | write_attribute paranoia_column, paranoia_sentinel_value 79 | update_column paranoia_column, paranoia_sentinel_value 80 | end 81 | restore_associated_records if opts[:recursive] 82 | end 83 | end 84 | 85 | self 86 | end 87 | alias :restore :restore! 88 | 89 | def paranoia_destroyed? 90 | send(paranoia_column) != paranoia_sentinel_value 91 | end 92 | alias :deleted? :paranoia_destroyed? 93 | 94 | private 95 | 96 | # touch paranoia column. 97 | # insert time to paranoia column. 98 | # @param with_transaction [Boolean] exec with ActiveRecord Transactions. 99 | def touch_paranoia_column(with_transaction=false) 100 | raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? 101 | if persisted? 102 | touch(paranoia_column) 103 | elsif !frozen? 104 | write_attribute(paranoia_column, current_time_from_proper_timezone) 105 | end 106 | self 107 | end 108 | 109 | # restore associated records that have been soft deleted when 110 | # we called #destroy 111 | def restore_associated_records 112 | destroyed_associations = self.class.reflect_on_all_associations.select do |association| 113 | association.options[:dependent] == :destroy 114 | end 115 | 116 | destroyed_associations.each do |association| 117 | association_data = send(association.name) 118 | 119 | unless association_data.nil? 120 | if association_data.paranoid? 121 | if association.collection? 122 | association_data.only_deleted.each { |record| record.restore(:recursive => true) } 123 | else 124 | association_data.restore(:recursive => true) 125 | end 126 | end 127 | end 128 | 129 | if association_data.nil? && association.macro.to_s == "has_one" 130 | association_class_name = association.class_name 131 | association_foreign_key = association.foreign_key 132 | 133 | if association.type 134 | association_polymorphic_type = association.type 135 | association_find_conditions = { association_polymorphic_type => self.class.name.to_s, association_foreign_key => self.id } 136 | else 137 | association_find_conditions = { association_foreign_key => self.id } 138 | end 139 | 140 | association_class = Object.const_get(association_class_name) 141 | if association_class.paranoid? 142 | association_class.only_deleted.where(association_find_conditions).first.try!(:restore, recursive: true) 143 | end 144 | end 145 | end 146 | 147 | clear_association_cache if destroyed_associations.present? 148 | end 149 | end 150 | 151 | class ActiveRecord::Base 152 | def self.acts_as_paranoid(options={}) 153 | alias :really_destroyed? :destroyed? 154 | alias :really_delete :delete 155 | 156 | alias :destroy_without_paranoia :destroy 157 | def really_destroy! 158 | dependent_reflections = self.class.reflections.select do |name, reflection| 159 | reflection.options[:dependent] == :destroy 160 | end 161 | if dependent_reflections.any? 162 | dependent_reflections.each do |name, reflection| 163 | association_data = self.send(name) 164 | # has_one association can return nil 165 | # .paranoid? will work for both instances and classes 166 | if association_data && association_data.paranoid? 167 | if reflection.collection? 168 | association_data.with_deleted.each(&:really_destroy!) 169 | else 170 | association_data.really_destroy! 171 | end 172 | end 173 | end 174 | end 175 | write_attribute(paranoia_column, current_time_from_proper_timezone) 176 | destroy_without_paranoia 177 | end 178 | 179 | include Paranoia 180 | class_attribute :paranoia_column, :paranoia_sentinel_value 181 | 182 | self.paranoia_column = (options[:column] || :deleted_at).to_s 183 | self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } 184 | def self.paranoia_scope 185 | where(paranoia_column => paranoia_sentinel_value) 186 | end 187 | default_scope { paranoia_scope } 188 | 189 | before_restore { 190 | self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers) 191 | } 192 | after_restore { 193 | self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) 194 | } 195 | end 196 | 197 | # Please do not use this method in production. 198 | # Pretty please. 199 | def self.I_AM_THE_DESTROYER! 200 | # TODO: actually implement spelling error fixes 201 | puts %Q{ 202 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 203 | Ryan: "What should this method do?" 204 | Sharon: "It should fix all the spelling errors on the page!" 205 | } 206 | end 207 | 208 | def self.paranoid? ; false ; end 209 | def paranoid? ; self.class.paranoid? ; end 210 | 211 | private 212 | 213 | def paranoia_column 214 | self.class.paranoia_column 215 | end 216 | 217 | def paranoia_sentinel_value 218 | self.class.paranoia_sentinel_value 219 | end 220 | end 221 | 222 | require 'paranoia/rspec' if defined? RSpec 223 | -------------------------------------------------------------------------------- /test/paranoia_test.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | ActiveRecord::Base.raise_in_transactional_callbacks = true if ActiveRecord::VERSION::STRING >= '4.2' 3 | 4 | require 'minitest/autorun' 5 | test_framework = defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase 6 | 7 | require File.expand_path(File.dirname(__FILE__) + "/../lib/paranoia") 8 | 9 | def connect! 10 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:' 11 | end 12 | 13 | def setup! 14 | connect! 15 | ActiveRecord::Base.connection.execute 'CREATE TABLE parent_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 16 | ActiveRecord::Base.connection.execute 'CREATE TABLE paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)' 17 | 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)' 18 | 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))' 19 | 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)' 20 | 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)' 21 | 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)' 22 | 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)' 23 | ActiveRecord::Base.connection.execute 'CREATE TABLE featureful_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME, name VARCHAR(32))' 24 | ActiveRecord::Base.connection.execute 'CREATE TABLE plain_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 25 | ActiveRecord::Base.connection.execute 'CREATE TABLE callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 26 | ActiveRecord::Base.connection.execute 'CREATE TABLE fail_callback_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 27 | ActiveRecord::Base.connection.execute 'CREATE TABLE related_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER NOT NULL, deleted_at DATETIME)' 28 | ActiveRecord::Base.connection.execute 'CREATE TABLE asplode_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER, deleted_at DATETIME)' 29 | ActiveRecord::Base.connection.execute 'CREATE TABLE employers (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 30 | ActiveRecord::Base.connection.execute 'CREATE TABLE employees (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 31 | 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)' 32 | ActiveRecord::Base.connection.execute 'CREATE TABLE custom_column_models (id INTEGER NOT NULL PRIMARY KEY, destroyed_at DATETIME)' 33 | ActiveRecord::Base.connection.execute 'CREATE TABLE custom_sentinel_models (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME NOT NULL)' 34 | ActiveRecord::Base.connection.execute 'CREATE TABLE non_paranoid_models (id INTEGER NOT NULL PRIMARY KEY, parent_model_id INTEGER)' 35 | ActiveRecord::Base.connection.execute 'CREATE TABLE polymorphic_models (id INTEGER NOT NULL PRIMARY KEY, parent_id INTEGER, parent_type STRING, deleted_at DATETIME)' 36 | end 37 | 38 | class WithDifferentConnection < ActiveRecord::Base 39 | establish_connection adapter: 'sqlite3', database: ':memory:' 40 | connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 41 | acts_as_paranoid 42 | end 43 | 44 | setup! 45 | 46 | class ParanoiaTest < test_framework 47 | def setup 48 | ActiveRecord::Base.connection.tables.each do |table| 49 | ActiveRecord::Base.connection.execute "DELETE FROM #{table}" 50 | end 51 | end 52 | 53 | def test_plain_model_class_is_not_paranoid 54 | assert_equal false, PlainModel.paranoid? 55 | end 56 | 57 | def test_paranoid_model_class_is_paranoid 58 | assert_equal true, ParanoidModel.paranoid? 59 | end 60 | 61 | def test_plain_models_are_not_paranoid 62 | assert_equal false, PlainModel.new.paranoid? 63 | end 64 | 65 | def test_paranoid_models_are_paranoid 66 | assert_equal true, ParanoidModel.new.paranoid? 67 | end 68 | 69 | def test_paranoid_models_to_param 70 | model = ParanoidModel.new 71 | model.save 72 | to_param = model.to_param 73 | 74 | model.destroy 75 | 76 | assert model.to_param 77 | assert_equal to_param, model.to_param 78 | end 79 | 80 | def test_destroy_behavior_for_plain_models 81 | model = PlainModel.new 82 | assert_equal 0, model.class.count 83 | model.save! 84 | assert_equal 1, model.class.count 85 | model.destroy 86 | 87 | assert_equal true, model.deleted_at.nil? 88 | 89 | assert_equal 0, model.class.count 90 | assert_equal 0, model.class.unscoped.count 91 | end 92 | 93 | # Anti-regression test for #81, which would've introduced a bug to break this test. 94 | def test_destroy_behavior_for_plain_models_callbacks 95 | model = CallbackModel.new 96 | model.save 97 | model.remove_called_variables # clear called callback flags 98 | model.destroy 99 | 100 | assert_equal nil, model.instance_variable_get(:@update_callback_called) 101 | assert_equal nil, model.instance_variable_get(:@save_callback_called) 102 | assert_equal nil, model.instance_variable_get(:@validate_called) 103 | 104 | assert model.instance_variable_get(:@destroy_callback_called) 105 | assert model.instance_variable_get(:@after_destroy_callback_called) 106 | assert model.instance_variable_get(:@after_commit_callback_called) 107 | end 108 | 109 | 110 | def test_delete_behavior_for_plain_models_callbacks 111 | model = CallbackModel.new 112 | model.save 113 | model.remove_called_variables # clear called callback flags 114 | model.delete 115 | 116 | assert_equal nil, model.instance_variable_get(:@update_callback_called) 117 | assert_equal nil, model.instance_variable_get(:@save_callback_called) 118 | assert_equal nil, model.instance_variable_get(:@validate_called) 119 | assert_equal nil, model.instance_variable_get(:@destroy_callback_called) 120 | assert_equal nil, model.instance_variable_get(:@after_destroy_callback_called) 121 | assert model.instance_variable_get(:@after_commit_callback_called) 122 | end 123 | 124 | def test_destroy_behavior_for_paranoid_models 125 | model = ParanoidModel.new 126 | assert_equal 0, model.class.count 127 | model.save! 128 | assert_equal 1, model.class.count 129 | model.destroy 130 | 131 | assert_equal false, model.deleted_at.nil? 132 | 133 | assert_equal 0, model.class.count 134 | assert_equal 1, model.class.unscoped.count 135 | end 136 | 137 | def test_update_columns_on_paranoia_destroyed 138 | record = ParentModel.create 139 | record.destroy 140 | 141 | assert record.update_columns deleted_at: Time.now 142 | end 143 | 144 | def test_scoping_behavior_for_paranoid_models 145 | parent1 = ParentModel.create 146 | parent2 = ParentModel.create 147 | p1 = ParanoidModel.create(:parent_model => parent1) 148 | p2 = ParanoidModel.create(:parent_model => parent2) 149 | p1.destroy 150 | p2.destroy 151 | assert_equal 0, parent1.paranoid_models.count 152 | assert_equal 1, parent1.paranoid_models.only_deleted.count 153 | assert_equal 1, parent1.paranoid_models.deleted.count 154 | p3 = ParanoidModel.create(:parent_model => parent1) 155 | assert_equal 2, parent1.paranoid_models.with_deleted.count 156 | assert_equal [p1,p3], parent1.paranoid_models.with_deleted 157 | end 158 | 159 | def test_destroy_behavior_for_custom_column_models 160 | model = CustomColumnModel.new 161 | assert_equal 0, model.class.count 162 | model.save! 163 | assert_nil model.destroyed_at 164 | assert_equal 1, model.class.count 165 | model.destroy 166 | 167 | assert_equal false, model.destroyed_at.nil? 168 | assert model.paranoia_destroyed? 169 | 170 | assert_equal 0, model.class.count 171 | assert_equal 1, model.class.unscoped.count 172 | assert_equal 1, model.class.only_deleted.count 173 | assert_equal 1, model.class.deleted.count 174 | end 175 | 176 | def test_default_sentinel_value 177 | assert_equal nil, ParanoidModel.paranoia_sentinel_value 178 | end 179 | 180 | def test_sentinel_value_for_custom_sentinel_models 181 | model = CustomSentinelModel.new 182 | assert_equal 0, model.class.count 183 | model.save! 184 | assert_equal DateTime.new(0), model.deleted_at 185 | assert_equal 1, model.class.count 186 | model.destroy 187 | 188 | assert DateTime.new(0) != model.deleted_at 189 | assert model.paranoia_destroyed? 190 | 191 | assert_equal 0, model.class.count 192 | assert_equal 1, model.class.unscoped.count 193 | assert_equal 1, model.class.only_deleted.count 194 | assert_equal 1, model.class.deleted.count 195 | 196 | model.restore 197 | assert_equal DateTime.new(0), model.deleted_at 198 | assert !model.destroyed? 199 | 200 | assert_equal 1, model.class.count 201 | assert_equal 1, model.class.unscoped.count 202 | assert_equal 0, model.class.only_deleted.count 203 | assert_equal 0, model.class.deleted.count 204 | end 205 | 206 | def test_destroy_behavior_for_featureful_paranoid_models 207 | model = get_featureful_model 208 | assert_equal 0, model.class.count 209 | model.save! 210 | assert_equal 1, model.class.count 211 | model.destroy 212 | 213 | assert_equal false, model.deleted_at.nil? 214 | 215 | assert_equal 0, model.class.count 216 | assert_equal 1, model.class.unscoped.count 217 | end 218 | 219 | def test_destroy_behavior_for_has_one_with_build_and_validation_error 220 | model = ParanoidModelWithHasOneAndBuild.create 221 | model.destroy 222 | end 223 | 224 | # Regression test for #24 225 | def test_chaining_for_paranoid_models 226 | scope = FeaturefulModel.where(:name => "foo").only_deleted 227 | assert_equal "foo", scope.where_values_hash['name'] 228 | assert_equal 2, scope.where_values.count 229 | end 230 | 231 | def test_only_destroyed_scope_for_paranoid_models 232 | model = ParanoidModel.new 233 | model.save 234 | model.destroy 235 | model2 = ParanoidModel.new 236 | model2.save 237 | 238 | assert_equal model, ParanoidModel.only_deleted.last 239 | assert_equal false, ParanoidModel.only_deleted.include?(model2) 240 | end 241 | 242 | def test_default_scope_for_has_many_relationships 243 | parent = ParentModel.create 244 | assert_equal 0, parent.related_models.count 245 | 246 | child = parent.related_models.create 247 | assert_equal 1, parent.related_models.count 248 | 249 | child.destroy 250 | assert_equal false, child.deleted_at.nil? 251 | 252 | assert_equal 0, parent.related_models.count 253 | assert_equal 1, parent.related_models.unscoped.count 254 | end 255 | 256 | def test_default_scope_for_has_many_through_relationships 257 | employer = Employer.create 258 | employee = Employee.create 259 | assert_equal 0, employer.jobs.count 260 | assert_equal 0, employer.employees.count 261 | assert_equal 0, employee.jobs.count 262 | assert_equal 0, employee.employers.count 263 | 264 | job = Job.create :employer => employer, :employee => employee 265 | assert_equal 1, employer.jobs.count 266 | assert_equal 1, employer.employees.count 267 | assert_equal 1, employee.jobs.count 268 | assert_equal 1, employee.employers.count 269 | 270 | employee2 = Employee.create 271 | job2 = Job.create :employer => employer, :employee => employee2 272 | employee2.destroy 273 | assert_equal 2, employer.jobs.count 274 | assert_equal 1, employer.employees.count 275 | 276 | job.destroy 277 | assert_equal 1, employer.jobs.count 278 | assert_equal 0, employer.employees.count 279 | assert_equal 0, employee.jobs.count 280 | assert_equal 0, employee.employers.count 281 | end 282 | 283 | def test_delete_behavior_for_callbacks 284 | model = CallbackModel.new 285 | model.save 286 | model.delete 287 | assert_equal nil, model.instance_variable_get(:@destroy_callback_called) 288 | end 289 | 290 | def test_destroy_behavior_for_callbacks 291 | model = CallbackModel.new 292 | model.save 293 | model.destroy 294 | assert model.instance_variable_get(:@destroy_callback_called) 295 | end 296 | 297 | def test_destroy_on_readonly_record 298 | # Just to demonstrate the AR behaviour 299 | model = NonParanoidModel.create! 300 | model.readonly! 301 | assert_raises ActiveRecord::ReadOnlyRecord do 302 | model.destroy 303 | end 304 | 305 | # Mirrors behaviour above 306 | model = ParanoidModel.create! 307 | model.readonly! 308 | assert_raises ActiveRecord::ReadOnlyRecord do 309 | model.destroy 310 | end 311 | end 312 | 313 | def test_destroy_on_really_destroyed_record 314 | model = ParanoidModel.create! 315 | model.really_destroy! 316 | assert model.really_destroyed? 317 | assert model.paranoia_destroyed? 318 | model.destroy 319 | assert model.really_destroyed? 320 | assert model.paranoia_destroyed? 321 | end 322 | 323 | def test_destroy_on_unsaved_record 324 | # Just to demonstrate the AR behaviour 325 | model = NonParanoidModel.new 326 | model.destroy! 327 | assert model.really_destroyed? 328 | model.destroy! 329 | assert model.really_destroyed? 330 | 331 | # Mirrors behaviour above 332 | model = ParanoidModel.new 333 | model.destroy! 334 | assert model.paranoia_destroyed? 335 | model.destroy! 336 | assert model.paranoia_destroyed? 337 | end 338 | 339 | def test_restore 340 | model = ParanoidModel.new 341 | model.save 342 | id = model.id 343 | model.destroy 344 | 345 | assert model.paranoia_destroyed? 346 | 347 | model = ParanoidModel.only_deleted.find(id) 348 | model.restore! 349 | model.reload 350 | 351 | assert_equal false, model.paranoia_destroyed? 352 | end 353 | 354 | def test_restore_on_object_return_self 355 | model = ParanoidModel.create 356 | model.destroy 357 | 358 | assert_equal model.class, model.restore.class 359 | end 360 | 361 | # Regression test for #92 362 | def test_destroy_twice 363 | model = ParanoidModel.new 364 | model.save 365 | model.destroy 366 | model.destroy 367 | 368 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 369 | end 370 | 371 | # Regression test for #92 372 | def test_destroy_bang_twice 373 | model = ParanoidModel.new 374 | model.save! 375 | model.destroy! 376 | model.destroy! 377 | 378 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 379 | end 380 | 381 | def test_destroy_return_value_on_success 382 | model = ParanoidModel.create 383 | return_value = model.destroy 384 | 385 | assert_equal(return_value, model) 386 | end 387 | 388 | def test_destroy_return_value_on_failure 389 | model = FailCallbackModel.create 390 | return_value = model.destroy 391 | 392 | assert_equal(return_value, false) 393 | end 394 | 395 | def test_restore_behavior_for_callbacks 396 | model = CallbackModel.new 397 | model.save 398 | id = model.id 399 | model.destroy 400 | 401 | assert model.paranoia_destroyed? 402 | 403 | model = CallbackModel.only_deleted.find(id) 404 | model.restore! 405 | model.reload 406 | 407 | assert model.instance_variable_get(:@restore_callback_called) 408 | end 409 | 410 | def test_really_destroy 411 | model = ParanoidModel.new 412 | model.save 413 | model.really_destroy! 414 | refute ParanoidModel.unscoped.exists?(model.id) 415 | end 416 | 417 | def test_real_destroy_dependent_destroy 418 | parent = ParentModel.create 419 | child1 = parent.very_related_models.create 420 | child2 = parent.non_paranoid_models.create 421 | child3 = parent.create_non_paranoid_model 422 | 423 | parent.really_destroy! 424 | 425 | refute RelatedModel.unscoped.exists?(child1.id) 426 | refute NonParanoidModel.unscoped.exists?(child2.id) 427 | refute NonParanoidModel.unscoped.exists?(child3.id) 428 | end 429 | 430 | def test_real_destroy_dependent_destroy_after_normal_destroy 431 | parent = ParentModel.create 432 | child = parent.very_related_models.create 433 | parent.destroy 434 | parent.really_destroy! 435 | refute RelatedModel.unscoped.exists?(child.id) 436 | end 437 | 438 | def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children 439 | parent_1 = ParentModel.create 440 | child_1 = parent_1.very_related_models.create 441 | 442 | parent_2 = ParentModel.create 443 | child_2 = parent_2.very_related_models.create 444 | parent_1.destroy 445 | parent_1.really_destroy! 446 | assert RelatedModel.unscoped.exists?(child_2.id) 447 | end 448 | 449 | def test_really_delete 450 | model = ParanoidModel.new 451 | model.save 452 | model.really_delete 453 | 454 | refute ParanoidModel.unscoped.exists?(model.id) 455 | end 456 | 457 | def test_multiple_restore 458 | a = ParanoidModel.new 459 | a.save 460 | a_id = a.id 461 | a.destroy 462 | 463 | b = ParanoidModel.new 464 | b.save 465 | b_id = b.id 466 | b.destroy 467 | 468 | c = ParanoidModel.new 469 | c.save 470 | c_id = c.id 471 | c.destroy 472 | 473 | ParanoidModel.restore([a_id, c_id]) 474 | 475 | a.reload 476 | b.reload 477 | c.reload 478 | 479 | refute a.paranoia_destroyed? 480 | assert b.paranoia_destroyed? 481 | refute c.paranoia_destroyed? 482 | end 483 | 484 | def test_restore_with_associations 485 | parent = ParentModel.create 486 | first_child = parent.very_related_models.create 487 | second_child = parent.non_paranoid_models.create 488 | 489 | parent.destroy 490 | assert_equal false, parent.deleted_at.nil? 491 | assert_equal false, first_child.reload.deleted_at.nil? 492 | assert_equal true, second_child.destroyed? 493 | 494 | parent.restore! 495 | assert_equal true, parent.deleted_at.nil? 496 | assert_equal false, first_child.reload.deleted_at.nil? 497 | assert_equal true, second_child.destroyed? 498 | 499 | parent.destroy 500 | parent.restore(:recursive => true) 501 | assert_equal true, parent.deleted_at.nil? 502 | assert_equal true, first_child.reload.deleted_at.nil? 503 | assert_equal true, second_child.destroyed? 504 | 505 | parent.destroy 506 | ParentModel.restore(parent.id, :recursive => true) 507 | assert_equal true, parent.reload.deleted_at.nil? 508 | assert_equal true, first_child.reload.deleted_at.nil? 509 | assert_equal true, second_child.destroyed? 510 | end 511 | 512 | # regression tests for #118 513 | def test_restore_with_has_one_association 514 | # setup and destroy test objects 515 | hasOne = ParanoidModelWithHasOne.create 516 | belongsTo = ParanoidModelWithBelong.create 517 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 518 | foreignKey = ParanoidModelWithForeignKeyBelong.create 519 | notParanoidModel = NotParanoidModelWithBelong.create 520 | 521 | hasOne.paranoid_model_with_belong = belongsTo 522 | hasOne.class_name_belong = anthorClassName 523 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 524 | hasOne.not_paranoid_model_with_belong = notParanoidModel 525 | hasOne.save! 526 | 527 | hasOne.destroy 528 | assert_equal false, hasOne.deleted_at.nil? 529 | assert_equal false, belongsTo.deleted_at.nil? 530 | 531 | # Does it restore has_one associations? 532 | hasOne.restore(:recursive => true) 533 | hasOne.save! 534 | 535 | assert_equal true, hasOne.reload.deleted_at.nil? 536 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 537 | assert_equal true, notParanoidModel.destroyed? 538 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 539 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 540 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 541 | end 542 | 543 | def test_new_restore_with_has_one_association 544 | # setup and destroy test objects 545 | hasOne = ParanoidModelWithHasOne.create 546 | belongsTo = ParanoidModelWithBelong.create 547 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 548 | foreignKey = ParanoidModelWithForeignKeyBelong.create 549 | notParanoidModel = NotParanoidModelWithBelong.create 550 | 551 | hasOne.paranoid_model_with_belong = belongsTo 552 | hasOne.class_name_belong = anthorClassName 553 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 554 | hasOne.not_paranoid_model_with_belong = notParanoidModel 555 | hasOne.save! 556 | 557 | hasOne.destroy 558 | assert_equal false, hasOne.deleted_at.nil? 559 | assert_equal false, belongsTo.deleted_at.nil? 560 | 561 | # Does it restore has_one associations? 562 | newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id) 563 | newHasOne.restore(:recursive => true) 564 | newHasOne.save! 565 | 566 | assert_equal true, hasOne.reload.deleted_at.nil? 567 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 568 | assert_equal true, notParanoidModel.destroyed? 569 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 570 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 571 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 572 | end 573 | 574 | def test_model_restore_with_has_one_association 575 | # setup and destroy test objects 576 | hasOne = ParanoidModelWithHasOne.create 577 | belongsTo = ParanoidModelWithBelong.create 578 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 579 | foreignKey = ParanoidModelWithForeignKeyBelong.create 580 | notParanoidModel = NotParanoidModelWithBelong.create 581 | 582 | hasOne.paranoid_model_with_belong = belongsTo 583 | hasOne.class_name_belong = anthorClassName 584 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 585 | hasOne.not_paranoid_model_with_belong = notParanoidModel 586 | hasOne.save! 587 | 588 | hasOne.destroy 589 | assert_equal false, hasOne.deleted_at.nil? 590 | assert_equal false, belongsTo.deleted_at.nil? 591 | 592 | # Does it restore has_one associations? 593 | ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true) 594 | hasOne.save! 595 | 596 | assert_equal true, hasOne.reload.deleted_at.nil? 597 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 598 | assert_equal true, notParanoidModel.destroyed? 599 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 600 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 601 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 602 | end 603 | 604 | def test_restore_with_nil_has_one_association 605 | # setup and destroy test object 606 | hasOne = ParanoidModelWithHasOne.create 607 | hasOne.destroy 608 | assert_equal false, hasOne.reload.deleted_at.nil? 609 | 610 | # Does it raise NoMethodException on restore of nil 611 | hasOne.restore(:recursive => true) 612 | 613 | assert hasOne.reload.deleted_at.nil? 614 | end 615 | 616 | # covers #185 617 | def test_restoring_recursive_has_one_restores_correct_object 618 | hasOnes = 2.times.map { ParanoidModelWithHasOne.create } 619 | belongsTos = 2.times.map { ParanoidModelWithBelong.create } 620 | 621 | hasOnes[0].update paranoid_model_with_belong: belongsTos[0] 622 | hasOnes[1].update paranoid_model_with_belong: belongsTos[1] 623 | 624 | hasOnes.each(&:destroy) 625 | 626 | ParanoidModelWithHasOne.restore(hasOnes[1].id, :recursive => true) 627 | hasOnes.each(&:reload) 628 | belongsTos.each(&:reload) 629 | 630 | # without #185, belongsTos[0] will be restored instead of belongsTos[1] 631 | refute_nil hasOnes[0].deleted_at 632 | refute_nil belongsTos[0].deleted_at 633 | assert_nil hasOnes[1].deleted_at 634 | assert_nil belongsTos[1].deleted_at 635 | end 636 | 637 | # covers #131 638 | def test_has_one_really_destroy_with_nil 639 | model = ParanoidModelWithHasOne.create 640 | model.really_destroy! 641 | 642 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 643 | end 644 | 645 | def test_has_one_really_destroy_with_record 646 | model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong } 647 | model.really_destroy! 648 | 649 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 650 | end 651 | 652 | def test_observers_notified 653 | a = ParanoidModelWithObservers.create 654 | a.destroy 655 | a.restore! 656 | 657 | assert a.observers_notified.select {|args| args == [:before_restore, a]} 658 | assert a.observers_notified.select {|args| args == [:after_restore, a]} 659 | end 660 | 661 | def test_observers_not_notified_if_not_supported 662 | a = ParanoidModelWithObservers.create 663 | a.destroy 664 | a.restore! 665 | # essentially, we're just ensuring that this doesn't crash 666 | end 667 | 668 | def test_i_am_the_destroyer 669 | expected = %Q{ 670 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 671 | Ryan: "What should this method do?" 672 | Sharon: "It should fix all the spelling errors on the page!" 673 | } 674 | assert_output expected do 675 | ParanoidModel.I_AM_THE_DESTROYER! 676 | end 677 | end 678 | 679 | def test_destroy_fails_if_callback_raises_exception 680 | parent = AsplodeModel.create 681 | 682 | assert_raises(StandardError) { parent.destroy } 683 | 684 | #transaction should be rolled back, so parent NOT deleted 685 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 686 | end 687 | 688 | def test_destroy_fails_if_association_callback_raises_exception 689 | parent = ParentModel.create 690 | children = [] 691 | 3.times { children << parent.asplode_models.create } 692 | 693 | assert_raises(StandardError) { parent.destroy } 694 | 695 | #transaction should be rolled back, so parent and children NOT deleted 696 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 697 | refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception' 698 | end 699 | 700 | def test_restore_model_with_different_connection 701 | ActiveRecord::Base.remove_connection # Disconnect the main connection 702 | a = WithDifferentConnection.create 703 | a.destroy! 704 | a.restore! 705 | # This test passes if no exception is raised 706 | ensure 707 | setup! # Reconnect the main connection 708 | end 709 | 710 | def test_restore_clear_association_cache_if_associations_present 711 | parent = ParentModel.create 712 | 3.times { parent.very_related_models.create } 713 | 714 | parent.destroy 715 | 716 | assert_equal 0, parent.very_related_models.count 717 | assert_equal 0, parent.very_related_models.size 718 | 719 | parent.restore(recursive: true) 720 | 721 | assert_equal 3, parent.very_related_models.count 722 | assert_equal 3, parent.very_related_models.size 723 | end 724 | 725 | def test_model_without_db_connection 726 | ActiveRecord::Base.remove_connection 727 | 728 | NoConnectionModel.class_eval{ acts_as_paranoid } 729 | ensure 730 | setup! 731 | end 732 | 733 | def test_restore_recursive_on_polymorphic_has_one_association 734 | parent = ParentModel.create 735 | polymorphic = PolymorphicModel.create(parent: parent) 736 | 737 | parent.destroy 738 | 739 | assert_equal 0, polymorphic.class.count 740 | 741 | parent.restore(recursive: true) 742 | 743 | assert_equal 1, polymorphic.class.count 744 | end 745 | 746 | # Ensure that we're checking parent_type when restoring 747 | def test_missing_restore_recursive_on_polymorphic_has_one_association 748 | parent = ParentModel.create 749 | polymorphic = PolymorphicModel.create(parent_id: parent.id, parent_type: 'ParanoidModel') 750 | 751 | parent.destroy 752 | polymorphic.destroy 753 | 754 | assert_equal 0, polymorphic.class.count 755 | 756 | parent.restore(recursive: true) 757 | 758 | assert_equal 0, polymorphic.class.count 759 | end 760 | 761 | private 762 | def get_featureful_model 763 | FeaturefulModel.new(:name => "not empty") 764 | end 765 | end 766 | 767 | # Helper classes 768 | 769 | class ParanoidModel < ActiveRecord::Base 770 | belongs_to :parent_model 771 | acts_as_paranoid 772 | end 773 | 774 | class FailCallbackModel < ActiveRecord::Base 775 | belongs_to :parent_model 776 | acts_as_paranoid 777 | 778 | before_destroy { |_| false } 779 | end 780 | 781 | class FeaturefulModel < ActiveRecord::Base 782 | acts_as_paranoid 783 | validates :name, :presence => true, :uniqueness => true 784 | end 785 | 786 | class PlainModel < ActiveRecord::Base 787 | end 788 | 789 | class CallbackModel < ActiveRecord::Base 790 | acts_as_paranoid 791 | before_destroy {|model| model.instance_variable_set :@destroy_callback_called, true } 792 | before_restore {|model| model.instance_variable_set :@restore_callback_called, true } 793 | before_update {|model| model.instance_variable_set :@update_callback_called, true } 794 | before_save {|model| model.instance_variable_set :@save_callback_called, true} 795 | 796 | after_destroy {|model| model.instance_variable_set :@after_destroy_callback_called, true } 797 | after_commit {|model| model.instance_variable_set :@after_commit_callback_called, true } 798 | 799 | validate {|model| model.instance_variable_set :@validate_called, true } 800 | 801 | def remove_called_variables 802 | instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} 803 | end 804 | end 805 | 806 | class ParentModel < ActiveRecord::Base 807 | acts_as_paranoid 808 | has_many :paranoid_models 809 | has_many :related_models 810 | has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy 811 | has_many :non_paranoid_models, dependent: :destroy 812 | has_one :non_paranoid_model, dependent: :destroy 813 | has_many :asplode_models, dependent: :destroy 814 | has_one :polymorphic_model, as: :parent, dependent: :destroy 815 | end 816 | 817 | class RelatedModel < ActiveRecord::Base 818 | acts_as_paranoid 819 | belongs_to :parent_model 820 | end 821 | 822 | class Employer < ActiveRecord::Base 823 | acts_as_paranoid 824 | has_many :jobs 825 | has_many :employees, :through => :jobs 826 | end 827 | 828 | class Employee < ActiveRecord::Base 829 | acts_as_paranoid 830 | has_many :jobs 831 | has_many :employers, :through => :jobs 832 | end 833 | 834 | class Job < ActiveRecord::Base 835 | acts_as_paranoid 836 | belongs_to :employer 837 | belongs_to :employee 838 | end 839 | 840 | class CustomColumnModel < ActiveRecord::Base 841 | acts_as_paranoid column: :destroyed_at 842 | end 843 | 844 | class CustomSentinelModel < ActiveRecord::Base 845 | acts_as_paranoid sentinel_value: DateTime.new(0) 846 | end 847 | 848 | class NonParanoidModel < ActiveRecord::Base 849 | end 850 | 851 | class ParanoidModelWithObservers < ParanoidModel 852 | def observers_notified 853 | @observers_notified ||= [] 854 | end 855 | 856 | def self.notify_observer(*args) 857 | observers_notified << args 858 | end 859 | end 860 | 861 | class ParanoidModelWithoutObservers < ParanoidModel 862 | self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers) 863 | end 864 | 865 | # refer back to regression test for #118 866 | class ParanoidModelWithHasOne < ParanoidModel 867 | has_one :paranoid_model_with_belong, :dependent => :destroy 868 | has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong" 869 | has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id" 870 | has_one :not_paranoid_model_with_belong, :dependent => :destroy 871 | end 872 | 873 | class ParanoidModelWithHasOneAndBuild < ActiveRecord::Base 874 | has_one :paranoid_model_with_build_belong, :dependent => :destroy 875 | validates :color, :presence => true 876 | after_validation :build_paranoid_model_with_build_belong, on: :create 877 | 878 | private 879 | def build_paranoid_model_with_build_belong 880 | super.tap { |child| child.name = "foo" } 881 | end 882 | end 883 | 884 | class ParanoidModelWithBuildBelong < ActiveRecord::Base 885 | acts_as_paranoid 886 | validates :name, :presence => true 887 | belongs_to :paranoid_model_with_has_one_and_build 888 | end 889 | 890 | class ParanoidModelWithBelong < ActiveRecord::Base 891 | acts_as_paranoid 892 | belongs_to :paranoid_model_with_has_one 893 | end 894 | 895 | class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base 896 | acts_as_paranoid 897 | belongs_to :paranoid_model_with_has_one 898 | end 899 | 900 | class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base 901 | acts_as_paranoid 902 | belongs_to :paranoid_model_with_has_one 903 | end 904 | 905 | class NotParanoidModelWithBelong < ActiveRecord::Base 906 | belongs_to :paranoid_model_with_has_one 907 | end 908 | 909 | class FlaggedModel < PlainModel 910 | acts_as_paranoid :flag_column => :is_deleted 911 | end 912 | 913 | class FlaggedModelWithCustomIndex < PlainModel 914 | acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted 915 | end 916 | 917 | 918 | 919 | class AsplodeModel < ActiveRecord::Base 920 | acts_as_paranoid 921 | before_destroy do |r| 922 | raise StandardError, 'ASPLODE!' 923 | end 924 | end 925 | 926 | class NoConnectionModel < ActiveRecord::Base 927 | end 928 | 929 | class PolymorphicModel < ActiveRecord::Base 930 | acts_as_paranoid 931 | belongs_to :parent, polymorphic: true 932 | end 933 | --------------------------------------------------------------------------------