├── .gitignore ├── lib ├── paranoia │ ├── version.rb │ └── rspec.rb └── paranoia.rb ├── Rakefile ├── Gemfile ├── .travis.yml ├── LICENSE ├── CONTRIBUTING.md ├── 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.2.0.alpha" 3 | end 4 | -------------------------------------------------------------------------------- /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 | 5 | platforms :jruby do 6 | gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'rails-5' 7 | end 8 | 9 | platforms :rbx do 10 | gem 'rubysl', '~> 2.0' 11 | gem 'rubysl-test-unit' 12 | gem 'rubinius-developer_tools' 13 | end 14 | 15 | rails = ENV['RAILS'] || '~> 4.2.0' 16 | 17 | gem 'rails', rails 18 | 19 | # Specify your gem's dependencies in paranoia.gemspec 20 | gemspec 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.0.0 6 | - 2.1.10 7 | - 2.2.5 8 | - 2.3.1 9 | - jruby-9.1.0.0 10 | 11 | env: 12 | matrix: 13 | - RAILS='~> 4.0.13' 14 | - RAILS='~> 4.1.15' 15 | - RAILS='~> 4.2.6' 16 | - RAILS='~> 5.0.0.rc1' 17 | 18 | matrix: 19 | exclude: 20 | - env: RAILS='~> 5.0.0.rc1' 21 | rvm: 2.0.0 22 | - env: RAILS='~> 5.0.0.rc1' 23 | rvm: 2.1.10 24 | allow_failures: 25 | - env: RAILS='~> 5.0.0.rc1' 26 | rvm: jruby-9.1.0.0 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Paranoia is an open source project and we encourage contributions. 2 | 3 | ## Filing an issue 4 | 5 | When filing an issue on the Paranoia project, please provide these details: 6 | 7 | * A comprehensive list of steps to reproduce the issue. 8 | * What you're *expecting* to happen compared with what's *actually* happening. 9 | * Your application's complete `Gemfile.lock`, and `Gemfile.lock` as text in a [Gist](https://gist.github.com) (*not as an image*) 10 | * Any relevant stack traces ("Full trace" preferred) 11 | 12 | In 99% of cases, this information is enough to determine the cause and solution 13 | to the problem that is being described. 14 | 15 | Please remember to format code using triple backticks (\`) so that it is neatly 16 | formatted when the issue is posted. 17 | 18 | ## Pull requests 19 | 20 | We gladly accept pull requests to add documentation, fix bugs and, in some circumstances, 21 | add new features to Paranoia. 22 | 23 | Here's a quick guide: 24 | 25 | 1. Fork the repo. 26 | 27 | 2. Run the tests. We only take pull requests with passing tests, and it's great 28 | to know that you have a clean slate. 29 | 30 | 3. Create new branch then make changes and add tests for your changes. Only 31 | refactoring and documentation changes require no new tests. If you are adding 32 | functionality or fixing a bug, we need tests! 33 | 34 | 4. Push to your fork and submit a pull request. 35 | -------------------------------------------------------------------------------- /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, 4, and 5, using much, much, much less code." 12 | s.description = <<-DSC 13 | Paranoia is a re-implementation of acts_as_paranoid for Rails 3, 4, and 5, 14 | using much, much, much less code. You would use either plugin / gem if you 15 | wished that when you called destroy on an Active Record object that it 16 | didn't actually destroy it, but just "hid" the record. Paranoia does this 17 | by setting a deleted_at field to the current time when you destroy a record, 18 | and hides it by scoping all queries on your model to only include records 19 | which do not have a deleted_at field. 20 | DSC 21 | 22 | s.required_rubygems_version = ">= 1.3.6" 23 | 24 | s.required_ruby_version = '>= 2.0' 25 | 26 | s.add_dependency 'activerecord', '>= 4.0', '< 5.1' 27 | 28 | s.add_development_dependency "bundler", ">= 1.0.0" 29 | s.add_development_dependency "rake" 30 | 31 | s.files = `git ls-files`.split("\n") 32 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 33 | s.require_path = 'lib' 34 | end 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # paranoia Changelog 2 | 3 | ## 2.2.0 (unreleased) 4 | 5 | * Ruby 2.0 or greater is required 6 | * Rails 5.0.0.beta1.1 support [@pigeonworks](https://github.com/pigeonworks) [@halostatue](https://github.com/halostatue) and [@gagalago](https://github.com/gagalago) 7 | * Previously `#really_destroyed?` may have been defined on non-paranoid models, it is now only available on paranoid models, use regular `#destroyed?` instead. 8 | 9 | ## 2.1.5 (2016-01-06) 10 | 11 | * Ruby 2.3 support 12 | 13 | ## 2.1.4 14 | 15 | ## 2.1.3 16 | 17 | ## 2.1.2 18 | 19 | ## 2.1.1 20 | 21 | ## 2.1.0 (2015-01-23) 22 | 23 | ### Major changes 24 | 25 | * `#destroyed?` is no longer overridden. Use `#paranoia_destroyed?` for the existing behaviour. [Washington Luiz](https://github.com/huoxito) 26 | * `#persisted?` is no longer overridden. 27 | * ActiveRecord 4.0 no longer has `#destroy!` as an alias for `#really_destroy`. 28 | * `#destroy` will now raise an exception if called on a readonly record. 29 | * `#destroy` on a hard deleted record is now a successful noop. 30 | * `#destroy` on a new record will set deleted_at (previously this raised an error) 31 | * `#destroy` and `#delete` always return self when successful. 32 | 33 | ### Bug Fixes 34 | 35 | * Calling `#destroy` twice will not hard-delete records. Use `#really_destroy` if this is desired. 36 | * Fix errors on non-paranoid has_one dependent associations 37 | 38 | ## 2.0.5 (2015-01-22) 39 | 40 | ### Bug fixes 41 | 42 | * 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) 43 | * 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) 44 | * Fix rspec 2 compatibility [#197](https://github.com/radar/paranoia/pull/197) [Emil Sågfors](https://github.com/lime) 45 | * Fix some deprecation warnings on rails 4.2 [Sergey Alekseev](https://github.com/sergey-alekseev) 46 | 47 | ## 2.0.4 (2014-12-02) 48 | 49 | ### Features 50 | * Add paranoia_scope as named version of default_scope [#184](https://github.com/radar/paranoia/pull/184) [Jozsef Nyitrai](https://github.com/nyjt) 51 | 52 | 53 | ### Bug Fixes 54 | * Fix initialization problems when missing table or no database connection [#186](https://github.com/radar/paranoia/issues/186) 55 | * 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) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paranoia 2 | 3 | Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/ActsAsParanoid/acts_as_paranoid) for Rails 3/4/5, using much, much, much less code. 4 | 5 | When your app is using Paranoia, calling `destroy` on an ActiveRecord object doesn't actually destroy the database record, but just *hides* it. 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 | ## Getting Started Video 12 | Setup and basic usage of the paranoia gem 13 | [GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia) 14 | 15 | ## Installation & Usage 16 | 17 | For Rails 3, please use version 1 of Paranoia: 18 | 19 | ``` ruby 20 | gem "paranoia", "~> 1.0" 21 | ``` 22 | 23 | For Rails 4 or 5, please use version 2 of Paranoia: 24 | 25 | ``` ruby 26 | gem "paranoia", "~> 2.0" 27 | ``` 28 | 29 | Of course you can install this from GitHub as well: 30 | 31 | ``` ruby 32 | gem "paranoia", :github => "rubysherpas/paranoia", :branch => "rails3" 33 | # or 34 | gem "paranoia", :github => "rubysherpas/paranoia", :branch => "rails4" 35 | ``` 36 | 37 | Then run: 38 | 39 | ``` shell 40 | bundle install 41 | ``` 42 | 43 | Updating is as simple as `bundle update paranoia`. 44 | 45 | #### Run your migrations for the desired models 46 | 47 | Run: 48 | 49 | ``` shell 50 | rails generate migration AddDeletedAtToClients deleted_at:datetime:index 51 | ``` 52 | 53 | and now you have a migration 54 | 55 | ``` ruby 56 | class AddDeletedAtToClients < ActiveRecord::Migration 57 | def change 58 | add_column :clients, :deleted_at, :datetime 59 | add_index :clients, :deleted_at 60 | end 61 | end 62 | ``` 63 | 64 | ### Usage 65 | 66 | #### In your model: 67 | 68 | ``` ruby 69 | class Client < ActiveRecord::Base 70 | acts_as_paranoid 71 | 72 | # ... 73 | end 74 | ``` 75 | 76 | Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: 77 | 78 | 79 | ``` ruby 80 | >> client.deleted_at 81 | # => nil 82 | >> client.destroy 83 | # => client 84 | >> client.deleted_at 85 | # => [current timestamp] 86 | ``` 87 | 88 | If you really want it gone *gone*, call `really_destroy!`: 89 | 90 | ``` ruby 91 | >> client.deleted_at 92 | # => nil 93 | >> client.really_destroy! 94 | # => client 95 | ``` 96 | 97 | If you want to use a column other than `deleted_at`, you can pass it as an option: 98 | 99 | ``` ruby 100 | class Client < ActiveRecord::Base 101 | acts_as_paranoid column: :destroyed_at 102 | 103 | ... 104 | end 105 | ``` 106 | 107 | 108 | If you want to skip adding the default scope: 109 | 110 | ``` ruby 111 | class Client < ActiveRecord::Base 112 | acts_as_paranoid without_default_scope: true 113 | 114 | ... 115 | end 116 | ``` 117 | 118 | If you want to access soft-deleted associations, override the getter method: 119 | 120 | ``` ruby 121 | def product 122 | Product.unscoped { super } 123 | end 124 | ``` 125 | 126 | If you want to include associated soft-deleted objects, you can (un)scope the association: 127 | 128 | ``` ruby 129 | class Person < ActiveRecord::Base 130 | belongs_to :group, -> { with_deleted } 131 | end 132 | 133 | Person.includes(:group).all 134 | ``` 135 | 136 | If you want to find all records, even those which are deleted: 137 | 138 | ``` ruby 139 | Client.with_deleted 140 | ``` 141 | 142 | If you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope): 143 | 144 | ``` ruby 145 | Client.without_deleted 146 | ``` 147 | 148 | If you want to find only the deleted records: 149 | 150 | ``` ruby 151 | Client.only_deleted 152 | ``` 153 | 154 | If you want to check if a record is soft-deleted: 155 | 156 | ``` ruby 157 | client.paranoia_destroyed? 158 | # or 159 | client.deleted? 160 | ``` 161 | 162 | If you want to restore a record: 163 | 164 | ``` ruby 165 | Client.restore(id) 166 | # or 167 | client.restore 168 | ``` 169 | 170 | If you want to restore a whole bunch of records: 171 | 172 | ``` ruby 173 | Client.restore([id1, id2, ..., idN]) 174 | ``` 175 | 176 | If you want to restore a record and their dependently destroyed associated records: 177 | 178 | ``` ruby 179 | Client.restore(id, :recursive => true) 180 | # or 181 | client.restore(:recursive => true) 182 | ``` 183 | 184 | For more information, please look at the tests. 185 | 186 | #### About indexes: 187 | 188 | Beware that you should adapt all your indexes for them to work as fast as previously. 189 | For example, 190 | 191 | ``` ruby 192 | add_index :clients, :group_id 193 | add_index :clients, [:group_id, :other_id] 194 | ``` 195 | 196 | should be replaced with 197 | 198 | ``` ruby 199 | add_index :clients, :group_id, where: "deleted_at IS NULL" 200 | add_index :clients, [:group_id, :other_id], where: "deleted_at IS NULL" 201 | ``` 202 | 203 | Of course, this is not necessary for the indexes you always use in association with `with_deleted` or `only_deleted`. 204 | 205 | ##### Unique Indexes 206 | 207 | Because NULL != NULL in standard SQL, we can not simply create a unique index 208 | on the deleted_at column and expect it to enforce that there only be one record 209 | with a certain combination of values. 210 | 211 | If your database supports them, good alternatives include partial indexes 212 | (above) and indexes on computed columns. E.g. 213 | 214 | ``` ruby 215 | add_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true 216 | ``` 217 | 218 | If not, an alternative is to create a separate column which is maintained 219 | alongside deleted_at for the sake of enforcing uniqueness. To that end, 220 | paranoia makes use of two method to make its destroy and restore actions: 221 | paranoia_restore_attributes and paranoia_destroy_attributes. 222 | 223 | ``` ruby 224 | add_column :clients, :active, :boolean 225 | add_index :clients, [:group_id, :active], unique: true 226 | 227 | class Client < ActiveRecord::Base 228 | # optionally have paranoia make use of your unique column, so that 229 | # your lookups will benefit from the unique index 230 | acts_as_paranoid column: :active, sentinel_value: true 231 | 232 | def paranoia_restore_attributes 233 | { 234 | deleted_at: nil, 235 | active: true 236 | } 237 | end 238 | 239 | def paranoia_destroy_attributes 240 | { 241 | deleted_at: current_time_from_proper_timezone, 242 | active: nil 243 | } 244 | end 245 | end 246 | ``` 247 | 248 | ## Acts As Paranoid Migration 249 | 250 | You can replace the older `acts_as_paranoid` methods as follows: 251 | 252 | | Old Syntax | New Syntax | 253 | |:-------------------------- |:------------------------------ | 254 | |`find_with_deleted(:all)` | `Client.with_deleted` | 255 | |`find_with_deleted(:first)` | `Client.with_deleted.first` | 256 | |`find_with_deleted(id)` | `Client.with_deleted.find(id)` | 257 | 258 | 259 | The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's 260 | `restore` method does not do this. 261 | 262 | ## Callbacks 263 | 264 | Paranoia provides several callbacks. It triggers `destroy` callback when the record is marked as deleted and `real_destroy` when the record is completely removed from database. It also calls `restore` callback when the record is restored via paranoia 265 | 266 | For example if you want to index your records in some search engine you can go like this: 267 | 268 | ```ruby 269 | class Product < ActiveRecord::Base 270 | acts_as_paranoid 271 | 272 | after_destroy :update_document_in_search_engine 273 | after_restore :update_document_in_search_engine 274 | after_real_destroy :remove_document_from_search_engine 275 | end 276 | ``` 277 | 278 | You can use these events just like regular Rails callbacks with before, after and around hooks. 279 | 280 | ## License 281 | 282 | This gem is released under the MIT license. 283 | -------------------------------------------------------------------------------- /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 | return unscope where: paranoia_column 26 | end 27 | all.tap { |x| x.default_scoped = false } 28 | end 29 | 30 | def only_deleted 31 | if paranoia_sentinel_value.nil? 32 | return with_deleted.where.not(paranoia_column => paranoia_sentinel_value) 33 | end 34 | # if paranoia_sentinel_value is not null, then it is possible that 35 | # some deleted rows will hold a null value in the paranoia column 36 | # these will not match != sentinel value because "NULL != value" is 37 | # NULL under the sql standard 38 | quoted_paranoia_column = connection.quote_column_name(paranoia_column) 39 | with_deleted.where("#{quoted_paranoia_column} IS NULL OR #{quoted_paranoia_column} != ?", paranoia_sentinel_value) 40 | end 41 | alias_method :deleted, :only_deleted 42 | 43 | def restore(id_or_ids, opts = {}) 44 | ids = Array(id_or_ids).flatten 45 | any_object_instead_of_id = ids.any? { |id| ActiveRecord::Base === id } 46 | if any_object_instead_of_id 47 | ids.map! { |id| ActiveRecord::Base === id ? id.id : id } 48 | ActiveSupport::Deprecation.warn("You are passing an instance of ActiveRecord::Base to `restore`. " \ 49 | "Please pass the id of the object by calling `.id`") 50 | end 51 | ids.map { |id| only_deleted.find(id).restore!(opts) } 52 | end 53 | end 54 | 55 | module Callbacks 56 | def self.extended(klazz) 57 | [:restore, :real_destroy].each do |callback_name| 58 | klazz.define_callbacks callback_name 59 | 60 | klazz.define_singleton_method("before_#{callback_name}") do |*args, &block| 61 | set_callback(callback_name, :before, *args, &block) 62 | end 63 | 64 | klazz.define_singleton_method("around_#{callback_name}") do |*args, &block| 65 | set_callback(callback_name, :around, *args, &block) 66 | end 67 | 68 | klazz.define_singleton_method("after_#{callback_name}") do |*args, &block| 69 | set_callback(callback_name, :after, *args, &block) 70 | end 71 | end 72 | end 73 | end 74 | 75 | def destroy 76 | transaction do 77 | run_callbacks(:destroy) do 78 | result = delete 79 | next result unless result && ActiveRecord::VERSION::STRING >= '4.2' 80 | each_counter_cached_associations do |association| 81 | foreign_key = association.reflection.foreign_key.to_sym 82 | next if destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key 83 | next unless send(association.reflection.name) 84 | association.decrement_counters 85 | end 86 | result 87 | end 88 | end 89 | end 90 | 91 | def delete 92 | raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? 93 | if persisted? 94 | # if a transaction exists, add the record so that after_commit 95 | # callbacks can be run 96 | add_to_transaction 97 | update_columns(paranoia_destroy_attributes) 98 | elsif !frozen? 99 | assign_attributes(paranoia_destroy_attributes) 100 | end 101 | self 102 | end 103 | 104 | def restore!(opts = {}) 105 | self.class.transaction do 106 | run_callbacks(:restore) do 107 | # Fixes a bug where the build would error because attributes were frozen. 108 | # This only happened on Rails versions earlier than 4.1. 109 | noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") 110 | if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen 111 | write_attribute paranoia_column, paranoia_sentinel_value 112 | update_columns(paranoia_restore_attributes) 113 | touch 114 | end 115 | restore_associated_records if opts[:recursive] 116 | end 117 | end 118 | 119 | self 120 | end 121 | alias :restore :restore! 122 | 123 | def paranoia_destroyed? 124 | send(paranoia_column) != paranoia_sentinel_value 125 | end 126 | alias :deleted? :paranoia_destroyed? 127 | 128 | def really_destroy! 129 | transaction do 130 | run_callbacks(:real_destroy) do 131 | dependent_reflections = self.class.reflections.select do |name, reflection| 132 | reflection.options[:dependent] == :destroy 133 | end 134 | if dependent_reflections.any? 135 | dependent_reflections.each do |name, reflection| 136 | association_data = self.send(name) 137 | # has_one association can return nil 138 | # .paranoid? will work for both instances and classes 139 | next unless association_data && association_data.paranoid? 140 | if reflection.collection? 141 | next association_data.with_deleted.each(&:really_destroy!) 142 | end 143 | association_data.really_destroy! 144 | end 145 | end 146 | write_attribute(paranoia_column, current_time_from_proper_timezone) 147 | destroy_without_paranoia 148 | end 149 | end 150 | end 151 | 152 | private 153 | 154 | def paranoia_restore_attributes 155 | { 156 | paranoia_column => paranoia_sentinel_value 157 | } 158 | end 159 | 160 | def paranoia_destroy_attributes 161 | { 162 | paranoia_column => current_time_from_proper_timezone 163 | } 164 | end 165 | 166 | # restore associated records that have been soft deleted when 167 | # we called #destroy 168 | def restore_associated_records 169 | destroyed_associations = self.class.reflect_on_all_associations.select do |association| 170 | association.options[:dependent] == :destroy 171 | end 172 | 173 | destroyed_associations.each do |association| 174 | association_data = send(association.name) 175 | 176 | unless association_data.nil? 177 | if association_data.paranoid? 178 | if association.collection? 179 | association_data.only_deleted.each { |record| record.restore(:recursive => true) } 180 | else 181 | association_data.restore(:recursive => true) 182 | end 183 | end 184 | end 185 | 186 | if association_data.nil? && association.macro.to_s == "has_one" 187 | association_class_name = association.klass.name 188 | association_foreign_key = association.foreign_key 189 | 190 | if association.type 191 | association_polymorphic_type = association.type 192 | association_find_conditions = { association_polymorphic_type => self.class.name.to_s, association_foreign_key => self.id } 193 | else 194 | association_find_conditions = { association_foreign_key => self.id } 195 | end 196 | 197 | association_class = association_class_name.constantize 198 | if association_class.paranoid? 199 | association_class.only_deleted.where(association_find_conditions).first.try!(:restore, recursive: true) 200 | end 201 | end 202 | end 203 | 204 | clear_association_cache if destroyed_associations.present? 205 | end 206 | end 207 | 208 | class ActiveRecord::Base 209 | def self.acts_as_paranoid(options={}) 210 | alias_method :really_destroyed?, :destroyed? 211 | alias_method :really_delete, :delete 212 | alias_method :destroy_without_paranoia, :destroy 213 | 214 | include Paranoia 215 | class_attribute :paranoia_column, :paranoia_sentinel_value 216 | 217 | self.paranoia_column = (options[:column] || :deleted_at).to_s 218 | self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } 219 | def self.paranoia_scope 220 | where(paranoia_column => paranoia_sentinel_value) 221 | end 222 | class << self; alias_method :without_deleted, :paranoia_scope end 223 | 224 | unless options[:without_default_scope] 225 | default_scope { paranoia_scope } 226 | end 227 | 228 | before_restore { 229 | self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers) 230 | } 231 | after_restore { 232 | self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) 233 | } 234 | end 235 | 236 | # Please do not use this method in production. 237 | # Pretty please. 238 | def self.I_AM_THE_DESTROYER! 239 | # TODO: actually implement spelling error fixes 240 | puts %Q{ 241 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 242 | Ryan: "What should this method do?" 243 | Sharon: "It should fix all the spelling errors on the page!" 244 | } 245 | end 246 | 247 | def self.paranoid? ; false ; end 248 | def paranoid? ; self.class.paranoid? ; end 249 | 250 | private 251 | 252 | def paranoia_column 253 | self.class.paranoia_column 254 | end 255 | 256 | def paranoia_sentinel_value 257 | self.class.paranoia_sentinel_value 258 | end 259 | end 260 | 261 | require 'paranoia/rspec' if defined? RSpec 262 | 263 | module ActiveRecord 264 | module Validations 265 | module UniquenessParanoiaValidator 266 | def build_relation(klass, table, attribute, value) 267 | relation = super(klass, table, attribute, value) 268 | return relation unless klass.respond_to?(:paranoia_column) 269 | arel_paranoia_scope = klass.arel_table[klass.paranoia_column].eq(klass.paranoia_sentinel_value) 270 | if ActiveRecord::VERSION::STRING >= "5.0" 271 | relation.where(arel_paranoia_scope) 272 | else 273 | relation.and(arel_paranoia_scope) 274 | end 275 | end 276 | end 277 | 278 | class UniquenessValidator < ActiveModel::EachValidator 279 | prepend UniquenessParanoiaValidator 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /test/paranoia_test.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'active_record' 3 | require 'minitest/autorun' 4 | require 'paranoia' 5 | 6 | test_framework = defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase 7 | 8 | def connect! 9 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:' 10 | end 11 | 12 | def setup! 13 | connect! 14 | { 15 | 'parent_model_with_counter_cache_columns' => 'related_models_count INTEGER DEFAULT 0', 16 | 'parent_models' => 'deleted_at DATETIME', 17 | 'paranoid_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 18 | 'paranoid_model_with_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 19 | 'paranoid_model_with_build_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32)', 20 | 'paranoid_model_with_anthor_class_name_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 21 | 'paranoid_model_with_foreign_key_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER', 22 | 'paranoid_model_with_timestamps' => 'parent_model_id INTEGER, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME', 23 | 'not_paranoid_model_with_belongs' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 24 | 'paranoid_model_with_has_one_and_builds' => 'parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER', 25 | 'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)', 26 | 'plain_models' => 'deleted_at DATETIME', 27 | 'callback_models' => 'deleted_at DATETIME', 28 | 'fail_callback_models' => 'deleted_at DATETIME', 29 | 'related_models' => 'parent_model_id INTEGER, parent_model_with_counter_cache_column_id INTEGER, deleted_at DATETIME', 30 | 'asplode_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 31 | 'employers' => 'name VARCHAR(32), deleted_at DATETIME', 32 | 'employees' => 'deleted_at DATETIME', 33 | 'jobs' => 'employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME', 34 | 'custom_column_models' => 'destroyed_at DATETIME', 35 | 'custom_sentinel_models' => 'deleted_at DATETIME NOT NULL', 36 | 'non_paranoid_models' => 'parent_model_id INTEGER', 37 | 'polymorphic_models' => 'parent_id INTEGER, parent_type STRING, deleted_at DATETIME', 38 | 'namespaced_paranoid_has_ones' => 'deleted_at DATETIME, paranoid_belongs_tos_id INTEGER', 39 | 'namespaced_paranoid_belongs_tos' => 'deleted_at DATETIME, paranoid_has_one_id INTEGER', 40 | 'unparanoid_unique_models' => 'name VARCHAR(32), paranoid_with_unparanoids_id INTEGER', 41 | 'active_column_models' => 'deleted_at DATETIME, active BOOLEAN', 42 | 'active_column_model_with_uniqueness_validations' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 43 | 'without_default_scope_models' => 'deleted_at DATETIME' 44 | }.each do |table_name, columns_as_sql_string| 45 | ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})" 46 | end 47 | end 48 | 49 | class WithDifferentConnection < ActiveRecord::Base 50 | establish_connection adapter: 'sqlite3', database: ':memory:' 51 | connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 52 | acts_as_paranoid 53 | end 54 | 55 | setup! 56 | 57 | class ParanoiaTest < test_framework 58 | def setup 59 | connection = ActiveRecord::Base.connection 60 | cleaner = ->(source) { 61 | ActiveRecord::Base.connection.execute "DELETE FROM #{source}" 62 | } 63 | 64 | if ActiveRecord::VERSION::MAJOR < 5 65 | connection.tables.each(&cleaner) 66 | else 67 | connection.data_sources.each(&cleaner) 68 | end 69 | end 70 | 71 | def test_plain_model_class_is_not_paranoid 72 | assert_equal false, PlainModel.paranoid? 73 | end 74 | 75 | def test_paranoid_model_class_is_paranoid 76 | assert_equal true, ParanoidModel.paranoid? 77 | end 78 | 79 | def test_plain_models_are_not_paranoid 80 | assert_equal false, PlainModel.new.paranoid? 81 | end 82 | 83 | def test_paranoid_models_are_paranoid 84 | assert_equal true, ParanoidModel.new.paranoid? 85 | end 86 | 87 | def test_paranoid_models_to_param 88 | model = ParanoidModel.new 89 | model.save 90 | to_param = model.to_param 91 | 92 | model.destroy 93 | 94 | assert model.to_param 95 | assert_equal to_param, model.to_param 96 | end 97 | 98 | def test_destroy_behavior_for_plain_models 99 | model = PlainModel.new 100 | assert_equal 0, model.class.count 101 | model.save! 102 | assert_equal 1, model.class.count 103 | model.destroy 104 | 105 | assert_equal true, model.deleted_at.nil? 106 | 107 | assert_equal 0, model.class.count 108 | assert_equal 0, model.class.unscoped.count 109 | end 110 | 111 | # Anti-regression test for #81, which would've introduced a bug to break this test. 112 | def test_destroy_behavior_for_plain_models_callbacks 113 | model = CallbackModel.new 114 | model.save 115 | model.remove_called_variables # clear called callback flags 116 | model.destroy 117 | 118 | assert_equal nil, model.instance_variable_get(:@update_callback_called) 119 | assert_equal nil, model.instance_variable_get(:@save_callback_called) 120 | assert_equal nil, model.instance_variable_get(:@validate_called) 121 | 122 | assert model.instance_variable_get(:@destroy_callback_called) 123 | assert model.instance_variable_get(:@after_destroy_callback_called) 124 | assert model.instance_variable_get(:@after_commit_callback_called) 125 | end 126 | 127 | 128 | def test_delete_behavior_for_plain_models_callbacks 129 | model = CallbackModel.new 130 | model.save 131 | model.remove_called_variables # clear called callback flags 132 | model.delete 133 | 134 | assert_equal nil, model.instance_variable_get(:@update_callback_called) 135 | assert_equal nil, model.instance_variable_get(:@save_callback_called) 136 | assert_equal nil, model.instance_variable_get(:@validate_called) 137 | assert_equal nil, model.instance_variable_get(:@destroy_callback_called) 138 | assert_equal nil, model.instance_variable_get(:@after_destroy_callback_called) 139 | assert_equal nil, model.instance_variable_get(:@after_commit_callback_called) 140 | end 141 | 142 | def test_delete_in_transaction_behavior_for_plain_models_callbacks 143 | model = CallbackModel.new 144 | model.save 145 | model.remove_called_variables # clear called callback flags 146 | CallbackModel.transaction do 147 | model.delete 148 | end 149 | 150 | assert_equal nil, model.instance_variable_get(:@update_callback_called) 151 | assert_equal nil, model.instance_variable_get(:@save_callback_called) 152 | assert_equal nil, model.instance_variable_get(:@validate_called) 153 | assert_equal nil, model.instance_variable_get(:@destroy_callback_called) 154 | assert_equal nil, model.instance_variable_get(:@after_destroy_callback_called) 155 | assert model.instance_variable_get(:@after_commit_callback_called) 156 | end 157 | 158 | def test_destroy_behavior_for_paranoid_models 159 | model = ParanoidModel.new 160 | assert_equal 0, model.class.count 161 | model.save! 162 | assert_equal 1, model.class.count 163 | model.destroy 164 | 165 | assert_equal false, model.deleted_at.nil? 166 | 167 | assert_equal 0, model.class.count 168 | assert_equal 1, model.class.unscoped.count 169 | end 170 | 171 | def test_update_columns_on_paranoia_destroyed 172 | record = ParentModel.create 173 | record.destroy 174 | 175 | assert record.update_columns deleted_at: Time.now 176 | end 177 | 178 | def test_scoping_behavior_for_paranoid_models 179 | parent1 = ParentModel.create 180 | parent2 = ParentModel.create 181 | p1 = ParanoidModel.create(:parent_model => parent1) 182 | p2 = ParanoidModel.create(:parent_model => parent2) 183 | p1.destroy 184 | p2.destroy 185 | assert_equal 0, parent1.paranoid_models.count 186 | assert_equal 1, parent1.paranoid_models.only_deleted.count 187 | assert_equal 1, parent1.paranoid_models.deleted.count 188 | assert_equal 0, parent1.paranoid_models.without_deleted.count 189 | p3 = ParanoidModel.create(:parent_model => parent1) 190 | assert_equal 2, parent1.paranoid_models.with_deleted.count 191 | assert_equal 1, parent1.paranoid_models.without_deleted.count 192 | assert_equal [p1,p3], parent1.paranoid_models.with_deleted 193 | end 194 | 195 | def test_destroy_behavior_for_custom_column_models 196 | model = CustomColumnModel.new 197 | assert_equal 0, model.class.count 198 | model.save! 199 | assert_nil model.destroyed_at 200 | assert_equal 1, model.class.count 201 | model.destroy 202 | 203 | assert_equal false, model.destroyed_at.nil? 204 | assert model.paranoia_destroyed? 205 | 206 | assert_equal 0, model.class.count 207 | assert_equal 1, model.class.unscoped.count 208 | assert_equal 1, model.class.only_deleted.count 209 | assert_equal 1, model.class.deleted.count 210 | end 211 | 212 | def test_default_sentinel_value 213 | assert_equal nil, ParanoidModel.paranoia_sentinel_value 214 | end 215 | 216 | def test_without_default_scope_option 217 | model = WithoutDefaultScopeModel.create 218 | model.destroy 219 | assert_equal 1, model.class.count 220 | assert_equal 1, model.class.only_deleted.count 221 | assert_equal 0, model.class.where(deleted_at: nil).count 222 | end 223 | 224 | def test_active_column_model 225 | model = ActiveColumnModel.new 226 | assert_equal 0, model.class.count 227 | model.save! 228 | assert_nil model.deleted_at 229 | assert_equal true, model.active 230 | assert_equal 1, model.class.count 231 | model.destroy 232 | 233 | assert_equal false, model.deleted_at.nil? 234 | assert_nil model.active 235 | assert model.paranoia_destroyed? 236 | 237 | assert_equal 0, model.class.count 238 | assert_equal 1, model.class.unscoped.count 239 | assert_equal 1, model.class.only_deleted.count 240 | assert_equal 1, model.class.deleted.count 241 | end 242 | 243 | def test_active_column_model_with_uniqueness_validation_only_checks_non_deleted_records 244 | a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") 245 | a.destroy 246 | b = ActiveColumnModelWithUniquenessValidation.new(name: "A") 247 | assert b.valid? 248 | end 249 | 250 | def test_active_column_model_with_uniqueness_validation_still_works_on_non_deleted_records 251 | a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") 252 | b = ActiveColumnModelWithUniquenessValidation.new(name: "A") 253 | refute b.valid? 254 | end 255 | 256 | def test_sentinel_value_for_custom_sentinel_models 257 | model = CustomSentinelModel.new 258 | assert_equal 0, model.class.count 259 | model.save! 260 | assert_equal DateTime.new(0), model.deleted_at 261 | assert_equal 1, model.class.count 262 | model.destroy 263 | 264 | assert DateTime.new(0) != model.deleted_at 265 | assert model.paranoia_destroyed? 266 | 267 | assert_equal 0, model.class.count 268 | assert_equal 1, model.class.unscoped.count 269 | assert_equal 1, model.class.only_deleted.count 270 | assert_equal 1, model.class.deleted.count 271 | 272 | model.restore 273 | assert_equal DateTime.new(0), model.deleted_at 274 | assert !model.destroyed? 275 | 276 | assert_equal 1, model.class.count 277 | assert_equal 1, model.class.unscoped.count 278 | assert_equal 0, model.class.only_deleted.count 279 | assert_equal 0, model.class.deleted.count 280 | end 281 | 282 | def test_destroy_behavior_for_featureful_paranoid_models 283 | model = get_featureful_model 284 | assert_equal 0, model.class.count 285 | model.save! 286 | assert_equal 1, model.class.count 287 | model.destroy 288 | 289 | assert_equal false, model.deleted_at.nil? 290 | 291 | assert_equal 0, model.class.count 292 | assert_equal 1, model.class.unscoped.count 293 | end 294 | 295 | def test_destroy_behavior_for_has_one_with_build_and_validation_error 296 | model = ParanoidModelWithHasOneAndBuild.create 297 | model.destroy 298 | end 299 | 300 | # Regression test for #24 301 | def test_chaining_for_paranoid_models 302 | scope = FeaturefulModel.where(:name => "foo").only_deleted 303 | assert_equal({'name' => "foo"}, scope.where_values_hash) 304 | end 305 | 306 | def test_only_destroyed_scope_for_paranoid_models 307 | model = ParanoidModel.new 308 | model.save 309 | model.destroy 310 | model2 = ParanoidModel.new 311 | model2.save 312 | 313 | assert_equal model, ParanoidModel.only_deleted.last 314 | assert_equal false, ParanoidModel.only_deleted.include?(model2) 315 | end 316 | 317 | def test_default_scope_for_has_many_relationships 318 | parent = ParentModel.create 319 | assert_equal 0, parent.related_models.count 320 | 321 | child = parent.related_models.create 322 | assert_equal 1, parent.related_models.count 323 | 324 | child.destroy 325 | assert_equal false, child.deleted_at.nil? 326 | 327 | assert_equal 0, parent.related_models.count 328 | assert_equal 1, parent.related_models.unscoped.count 329 | end 330 | 331 | def test_default_scope_for_has_many_through_relationships 332 | employer = Employer.create 333 | employee = Employee.create 334 | assert_equal 0, employer.jobs.count 335 | assert_equal 0, employer.employees.count 336 | assert_equal 0, employee.jobs.count 337 | assert_equal 0, employee.employers.count 338 | 339 | job = Job.create :employer => employer, :employee => employee 340 | assert_equal 1, employer.jobs.count 341 | assert_equal 1, employer.employees.count 342 | assert_equal 1, employee.jobs.count 343 | assert_equal 1, employee.employers.count 344 | 345 | employee2 = Employee.create 346 | job2 = Job.create :employer => employer, :employee => employee2 347 | employee2.destroy 348 | assert_equal 2, employer.jobs.count 349 | assert_equal 1, employer.employees.count 350 | 351 | job.destroy 352 | assert_equal 1, employer.jobs.count 353 | assert_equal 0, employer.employees.count 354 | assert_equal 0, employee.jobs.count 355 | assert_equal 0, employee.employers.count 356 | end 357 | 358 | def test_delete_behavior_for_callbacks 359 | model = CallbackModel.new 360 | model.save 361 | model.delete 362 | assert_equal nil, model.instance_variable_get(:@destroy_callback_called) 363 | end 364 | 365 | def test_destroy_behavior_for_callbacks 366 | model = CallbackModel.new 367 | model.save 368 | model.destroy 369 | assert model.instance_variable_get(:@destroy_callback_called) 370 | end 371 | 372 | def test_destroy_on_readonly_record 373 | # Just to demonstrate the AR behaviour 374 | model = NonParanoidModel.create! 375 | model.readonly! 376 | assert_raises ActiveRecord::ReadOnlyRecord do 377 | model.destroy 378 | end 379 | 380 | # Mirrors behaviour above 381 | model = ParanoidModel.create! 382 | model.readonly! 383 | assert_raises ActiveRecord::ReadOnlyRecord do 384 | model.destroy 385 | end 386 | end 387 | 388 | def test_destroy_on_really_destroyed_record 389 | model = ParanoidModel.create! 390 | model.really_destroy! 391 | assert model.really_destroyed? 392 | assert model.paranoia_destroyed? 393 | model.destroy 394 | assert model.really_destroyed? 395 | assert model.paranoia_destroyed? 396 | end 397 | 398 | def test_destroy_on_unsaved_record 399 | # Just to demonstrate the AR behaviour 400 | model = NonParanoidModel.new 401 | model.destroy! 402 | assert model.destroyed? 403 | model.destroy! 404 | assert model.destroyed? 405 | 406 | # Mirrors behaviour above 407 | model = ParanoidModel.new 408 | model.destroy! 409 | assert model.paranoia_destroyed? 410 | model.destroy! 411 | assert model.paranoia_destroyed? 412 | end 413 | 414 | def test_restore 415 | model = ParanoidModel.new 416 | model.save 417 | id = model.id 418 | model.destroy 419 | 420 | assert model.paranoia_destroyed? 421 | 422 | model = ParanoidModel.only_deleted.find(id) 423 | model.restore! 424 | model.reload 425 | 426 | assert_equal false, model.paranoia_destroyed? 427 | end 428 | 429 | def test_restore_on_object_return_self 430 | model = ParanoidModel.create 431 | model.destroy 432 | 433 | assert_equal model.class, model.restore.class 434 | end 435 | 436 | # Regression test for #92 437 | def test_destroy_twice 438 | model = ParanoidModel.new 439 | model.save 440 | model.destroy 441 | model.destroy 442 | 443 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 444 | end 445 | 446 | # Regression test for #92 447 | def test_destroy_bang_twice 448 | model = ParanoidModel.new 449 | model.save! 450 | model.destroy! 451 | model.destroy! 452 | 453 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 454 | end 455 | 456 | def test_destroy_return_value_on_success 457 | model = ParanoidModel.create 458 | return_value = model.destroy 459 | 460 | assert_equal(return_value, model) 461 | end 462 | 463 | def test_destroy_return_value_on_failure 464 | model = FailCallbackModel.create 465 | return_value = model.destroy 466 | 467 | assert_equal(return_value, false) 468 | end 469 | 470 | def test_restore_behavior_for_callbacks 471 | model = CallbackModel.new 472 | model.save 473 | id = model.id 474 | model.destroy 475 | 476 | assert model.paranoia_destroyed? 477 | 478 | model = CallbackModel.only_deleted.find(id) 479 | model.restore! 480 | model.reload 481 | 482 | assert model.instance_variable_get(:@restore_callback_called) 483 | end 484 | 485 | def test_really_destroy 486 | model = ParanoidModel.new 487 | model.save 488 | model.really_destroy! 489 | refute ParanoidModel.unscoped.exists?(model.id) 490 | end 491 | 492 | def test_real_destroy_dependent_destroy 493 | parent = ParentModel.create 494 | child1 = parent.very_related_models.create 495 | child2 = parent.non_paranoid_models.create 496 | child3 = parent.create_non_paranoid_model 497 | 498 | parent.really_destroy! 499 | 500 | refute RelatedModel.unscoped.exists?(child1.id) 501 | refute NonParanoidModel.unscoped.exists?(child2.id) 502 | refute NonParanoidModel.unscoped.exists?(child3.id) 503 | end 504 | 505 | def test_real_destroy_dependent_destroy_after_normal_destroy 506 | parent = ParentModel.create 507 | child = parent.very_related_models.create 508 | parent.destroy 509 | parent.really_destroy! 510 | refute RelatedModel.unscoped.exists?(child.id) 511 | end 512 | 513 | def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children 514 | parent_1 = ParentModel.create 515 | child_1 = parent_1.very_related_models.create 516 | 517 | parent_2 = ParentModel.create 518 | child_2 = parent_2.very_related_models.create 519 | parent_1.destroy 520 | parent_1.really_destroy! 521 | assert RelatedModel.unscoped.exists?(child_2.id) 522 | end 523 | 524 | def test_really_destroy_behavior_for_callbacks 525 | model = CallbackModel.new 526 | model.save 527 | model.really_destroy! 528 | 529 | assert model.instance_variable_get(:@real_destroy_callback_called) 530 | end 531 | 532 | def test_really_delete 533 | model = ParanoidModel.new 534 | model.save 535 | model.really_delete 536 | 537 | refute ParanoidModel.unscoped.exists?(model.id) 538 | end 539 | 540 | def test_multiple_restore 541 | a = ParanoidModel.new 542 | a.save 543 | a_id = a.id 544 | a.destroy 545 | 546 | b = ParanoidModel.new 547 | b.save 548 | b_id = b.id 549 | b.destroy 550 | 551 | c = ParanoidModel.new 552 | c.save 553 | c_id = c.id 554 | c.destroy 555 | 556 | ParanoidModel.restore([a_id, c_id]) 557 | 558 | a.reload 559 | b.reload 560 | c.reload 561 | 562 | refute a.paranoia_destroyed? 563 | assert b.paranoia_destroyed? 564 | refute c.paranoia_destroyed? 565 | end 566 | 567 | def test_restore_with_associations 568 | parent = ParentModel.create 569 | first_child = parent.very_related_models.create 570 | second_child = parent.non_paranoid_models.create 571 | 572 | parent.destroy 573 | assert_equal false, parent.deleted_at.nil? 574 | assert_equal false, first_child.reload.deleted_at.nil? 575 | assert_equal true, second_child.destroyed? 576 | 577 | parent.restore! 578 | assert_equal true, parent.deleted_at.nil? 579 | assert_equal false, first_child.reload.deleted_at.nil? 580 | assert_equal true, second_child.destroyed? 581 | 582 | parent.destroy 583 | parent.restore(:recursive => true) 584 | assert_equal true, parent.deleted_at.nil? 585 | assert_equal true, first_child.reload.deleted_at.nil? 586 | assert_equal true, second_child.destroyed? 587 | 588 | parent.destroy 589 | ParentModel.restore(parent.id, :recursive => true) 590 | assert_equal true, parent.reload.deleted_at.nil? 591 | assert_equal true, first_child.reload.deleted_at.nil? 592 | assert_equal true, second_child.destroyed? 593 | end 594 | 595 | # regression tests for #118 596 | def test_restore_with_has_one_association 597 | # setup and destroy test objects 598 | hasOne = ParanoidModelWithHasOne.create 599 | belongsTo = ParanoidModelWithBelong.create 600 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 601 | foreignKey = ParanoidModelWithForeignKeyBelong.create 602 | notParanoidModel = NotParanoidModelWithBelong.create 603 | 604 | hasOne.paranoid_model_with_belong = belongsTo 605 | hasOne.class_name_belong = anthorClassName 606 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 607 | hasOne.not_paranoid_model_with_belong = notParanoidModel 608 | hasOne.save! 609 | 610 | hasOne.destroy 611 | assert_equal false, hasOne.deleted_at.nil? 612 | assert_equal false, belongsTo.deleted_at.nil? 613 | 614 | # Does it restore has_one associations? 615 | hasOne.restore(:recursive => true) 616 | hasOne.save! 617 | 618 | assert_equal true, hasOne.reload.deleted_at.nil? 619 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 620 | assert_equal true, notParanoidModel.destroyed? 621 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 622 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 623 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 624 | end 625 | 626 | def test_new_restore_with_has_one_association 627 | # setup and destroy test objects 628 | hasOne = ParanoidModelWithHasOne.create 629 | belongsTo = ParanoidModelWithBelong.create 630 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 631 | foreignKey = ParanoidModelWithForeignKeyBelong.create 632 | notParanoidModel = NotParanoidModelWithBelong.create 633 | 634 | hasOne.paranoid_model_with_belong = belongsTo 635 | hasOne.class_name_belong = anthorClassName 636 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 637 | hasOne.not_paranoid_model_with_belong = notParanoidModel 638 | hasOne.save! 639 | 640 | hasOne.destroy 641 | assert_equal false, hasOne.deleted_at.nil? 642 | assert_equal false, belongsTo.deleted_at.nil? 643 | 644 | # Does it restore has_one associations? 645 | newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id) 646 | newHasOne.restore(:recursive => true) 647 | newHasOne.save! 648 | 649 | assert_equal true, hasOne.reload.deleted_at.nil? 650 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 651 | assert_equal true, notParanoidModel.destroyed? 652 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 653 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 654 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 655 | end 656 | 657 | def test_model_restore_with_has_one_association 658 | # setup and destroy test objects 659 | hasOne = ParanoidModelWithHasOne.create 660 | belongsTo = ParanoidModelWithBelong.create 661 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 662 | foreignKey = ParanoidModelWithForeignKeyBelong.create 663 | notParanoidModel = NotParanoidModelWithBelong.create 664 | 665 | hasOne.paranoid_model_with_belong = belongsTo 666 | hasOne.class_name_belong = anthorClassName 667 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 668 | hasOne.not_paranoid_model_with_belong = notParanoidModel 669 | hasOne.save! 670 | 671 | hasOne.destroy 672 | assert_equal false, hasOne.deleted_at.nil? 673 | assert_equal false, belongsTo.deleted_at.nil? 674 | 675 | # Does it restore has_one associations? 676 | ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true) 677 | hasOne.save! 678 | 679 | assert_equal true, hasOne.reload.deleted_at.nil? 680 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 681 | assert_equal true, notParanoidModel.destroyed? 682 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 683 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 684 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 685 | end 686 | 687 | def test_restore_with_nil_has_one_association 688 | # setup and destroy test object 689 | hasOne = ParanoidModelWithHasOne.create 690 | hasOne.destroy 691 | assert_equal false, hasOne.reload.deleted_at.nil? 692 | 693 | # Does it raise NoMethodException on restore of nil 694 | hasOne.restore(:recursive => true) 695 | 696 | assert hasOne.reload.deleted_at.nil? 697 | end 698 | 699 | def test_restore_with_module_scoped_has_one_association 700 | # setup and destroy test object 701 | hasOne = Namespaced::ParanoidHasOne.create 702 | hasOne.destroy 703 | assert_equal false, hasOne.reload.deleted_at.nil? 704 | 705 | # Does it raise "uninitialized constant ParanoidBelongsTo" 706 | # on restore of ParanoidHasOne? 707 | hasOne.restore(:recursive => true) 708 | 709 | assert hasOne.reload.deleted_at.nil? 710 | end 711 | 712 | # covers #185 713 | def test_restoring_recursive_has_one_restores_correct_object 714 | hasOnes = 2.times.map { ParanoidModelWithHasOne.create } 715 | belongsTos = 2.times.map { ParanoidModelWithBelong.create } 716 | 717 | hasOnes[0].update paranoid_model_with_belong: belongsTos[0] 718 | hasOnes[1].update paranoid_model_with_belong: belongsTos[1] 719 | 720 | hasOnes.each(&:destroy) 721 | 722 | ParanoidModelWithHasOne.restore(hasOnes[1].id, :recursive => true) 723 | hasOnes.each(&:reload) 724 | belongsTos.each(&:reload) 725 | 726 | # without #185, belongsTos[0] will be restored instead of belongsTos[1] 727 | refute_nil hasOnes[0].deleted_at 728 | refute_nil belongsTos[0].deleted_at 729 | assert_nil hasOnes[1].deleted_at 730 | assert_nil belongsTos[1].deleted_at 731 | end 732 | 733 | # covers #131 734 | def test_has_one_really_destroy_with_nil 735 | model = ParanoidModelWithHasOne.create 736 | model.really_destroy! 737 | 738 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 739 | end 740 | 741 | def test_has_one_really_destroy_with_record 742 | model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong } 743 | model.really_destroy! 744 | 745 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 746 | end 747 | 748 | def test_observers_notified 749 | a = ParanoidModelWithObservers.create 750 | a.destroy 751 | a.restore! 752 | 753 | assert a.observers_notified.select {|args| args == [:before_restore, a]} 754 | assert a.observers_notified.select {|args| args == [:after_restore, a]} 755 | end 756 | 757 | def test_observers_not_notified_if_not_supported 758 | a = ParanoidModelWithObservers.create 759 | a.destroy 760 | a.restore! 761 | # essentially, we're just ensuring that this doesn't crash 762 | end 763 | 764 | def test_validates_uniqueness_only_checks_non_deleted_records 765 | a = Employer.create!(name: "A") 766 | a.destroy 767 | b = Employer.new(name: "A") 768 | assert b.valid? 769 | end 770 | 771 | def test_validates_uniqueness_still_works_on_non_deleted_records 772 | a = Employer.create!(name: "A") 773 | b = Employer.new(name: "A") 774 | refute b.valid? 775 | end 776 | 777 | def test_updated_at_modification_on_restore 778 | parent1 = ParentModel.create 779 | pt1 = ParanoidModelWithTimestamp.create(:parent_model => parent1) 780 | ParanoidModelWithTimestamp.record_timestamps = false 781 | pt1.update_columns(created_at: 20.years.ago, updated_at: 10.years.ago, deleted_at: 10.years.ago) 782 | ParanoidModelWithTimestamp.record_timestamps = true 783 | assert pt1.updated_at < 10.minutes.ago 784 | refute pt1.deleted_at.nil? 785 | pt1.restore! 786 | assert pt1.deleted_at.nil? 787 | assert pt1.updated_at > 10.minutes.ago 788 | end 789 | 790 | def test_i_am_the_destroyer 791 | expected = %Q{ 792 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 793 | Ryan: "What should this method do?" 794 | Sharon: "It should fix all the spelling errors on the page!" 795 | } 796 | assert_output expected do 797 | ParanoidModel.I_AM_THE_DESTROYER! 798 | end 799 | end 800 | 801 | def test_destroy_fails_if_callback_raises_exception 802 | parent = AsplodeModel.create 803 | 804 | assert_raises(StandardError) { parent.destroy } 805 | 806 | #transaction should be rolled back, so parent NOT deleted 807 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 808 | end 809 | 810 | def test_destroy_fails_if_association_callback_raises_exception 811 | parent = ParentModel.create 812 | children = [] 813 | 3.times { children << parent.asplode_models.create } 814 | 815 | assert_raises(StandardError) { parent.destroy } 816 | 817 | #transaction should be rolled back, so parent and children NOT deleted 818 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 819 | refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception' 820 | end 821 | 822 | def test_restore_model_with_different_connection 823 | ActiveRecord::Base.remove_connection # Disconnect the main connection 824 | a = WithDifferentConnection.create 825 | a.destroy! 826 | a.restore! 827 | # This test passes if no exception is raised 828 | ensure 829 | setup! # Reconnect the main connection 830 | end 831 | 832 | def test_restore_clear_association_cache_if_associations_present 833 | parent = ParentModel.create 834 | 3.times { parent.very_related_models.create } 835 | 836 | parent.destroy 837 | 838 | assert_equal 0, parent.very_related_models.count 839 | assert_equal 0, parent.very_related_models.size 840 | 841 | parent.restore(recursive: true) 842 | 843 | assert_equal 3, parent.very_related_models.count 844 | assert_equal 3, parent.very_related_models.size 845 | end 846 | 847 | def test_model_without_db_connection 848 | ActiveRecord::Base.remove_connection 849 | 850 | NoConnectionModel.class_eval{ acts_as_paranoid } 851 | ensure 852 | setup! 853 | end 854 | 855 | def test_restore_recursive_on_polymorphic_has_one_association 856 | parent = ParentModel.create 857 | polymorphic = PolymorphicModel.create(parent: parent) 858 | 859 | parent.destroy 860 | 861 | assert_equal 0, polymorphic.class.count 862 | 863 | parent.restore(recursive: true) 864 | 865 | assert_equal 1, polymorphic.class.count 866 | end 867 | 868 | # Ensure that we're checking parent_type when restoring 869 | def test_missing_restore_recursive_on_polymorphic_has_one_association 870 | parent = ParentModel.create 871 | polymorphic = PolymorphicModel.create(parent_id: parent.id, parent_type: 'ParanoidModel') 872 | 873 | parent.destroy 874 | polymorphic.destroy 875 | 876 | assert_equal 0, polymorphic.class.count 877 | 878 | parent.restore(recursive: true) 879 | 880 | assert_equal 0, polymorphic.class.count 881 | end 882 | 883 | def test_counter_cache_column_update_on_destroy#_and_restore_and_really_destroy 884 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 885 | related_model = parent_model_with_counter_cache_column.related_models.create 886 | 887 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 888 | related_model.destroy 889 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 890 | end 891 | 892 | def test_callbacks_for_counter_cache_column_update_on_destroy 893 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 894 | related_model = parent_model_with_counter_cache_column.related_models.create 895 | 896 | assert_equal nil, related_model.instance_variable_get(:@after_destroy_callback_called) 897 | assert_equal nil, related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 898 | 899 | related_model.destroy 900 | 901 | assert related_model.instance_variable_get(:@after_destroy_callback_called) 902 | # assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 903 | end 904 | 905 | def test_uniqueness_for_unparanoid_associated 906 | parent_model = ParanoidWithUnparanoids.create 907 | related = parent_model.unparanoid_unique_models.create 908 | # will raise exception if model is not checked for paranoia 909 | related.valid? 910 | end 911 | 912 | # TODO: find a fix for Rails 4.1 913 | if ActiveRecord::VERSION::STRING !~ /\A4\.1/ 914 | def test_counter_cache_column_update_on_really_destroy 915 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 916 | related_model = parent_model_with_counter_cache_column.related_models.create 917 | 918 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 919 | related_model.really_destroy! 920 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 921 | end 922 | end 923 | 924 | # TODO: find a fix for Rails 4.0 and 4.1 925 | if ActiveRecord::VERSION::STRING >= '4.2' 926 | def test_callbacks_for_counter_cache_column_update_on_really_destroy! 927 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 928 | related_model = parent_model_with_counter_cache_column.related_models.create 929 | 930 | assert_equal nil, related_model.instance_variable_get(:@after_destroy_callback_called) 931 | assert_equal nil, related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 932 | 933 | related_model.really_destroy! 934 | 935 | assert related_model.instance_variable_get(:@after_destroy_callback_called) 936 | assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 937 | end 938 | end 939 | 940 | private 941 | def get_featureful_model 942 | FeaturefulModel.new(:name => "not empty") 943 | end 944 | end 945 | 946 | # Helper classes 947 | 948 | class ParanoidModel < ActiveRecord::Base 949 | belongs_to :parent_model 950 | acts_as_paranoid 951 | end 952 | 953 | class ParanoidWithUnparanoids < ActiveRecord::Base 954 | self.table_name = 'plain_models' 955 | has_many :unparanoid_unique_models 956 | end 957 | 958 | class UnparanoidUniqueModel < ActiveRecord::Base 959 | belongs_to :paranoid_with_unparanoids 960 | validates :name, :uniqueness => true 961 | end 962 | 963 | class FailCallbackModel < ActiveRecord::Base 964 | belongs_to :parent_model 965 | acts_as_paranoid 966 | 967 | before_destroy { |_| 968 | if ActiveRecord::VERSION::MAJOR < 5 969 | false 970 | else 971 | throw :abort 972 | end 973 | } 974 | end 975 | 976 | class FeaturefulModel < ActiveRecord::Base 977 | acts_as_paranoid 978 | validates :name, :presence => true, :uniqueness => true 979 | end 980 | 981 | class NonParanoidChildModel < ActiveRecord::Base 982 | validates :name, :presence => true, :uniqueness => true 983 | end 984 | 985 | class PlainModel < ActiveRecord::Base 986 | end 987 | 988 | class CallbackModel < ActiveRecord::Base 989 | acts_as_paranoid 990 | before_destroy { |model| model.instance_variable_set :@destroy_callback_called, true } 991 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 992 | before_update { |model| model.instance_variable_set :@update_callback_called, true } 993 | before_save { |model| model.instance_variable_set :@save_callback_called, true} 994 | before_real_destroy { |model| model.instance_variable_set :@real_destroy_callback_called, true } 995 | 996 | after_destroy { |model| model.instance_variable_set :@after_destroy_callback_called, true } 997 | after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true } 998 | 999 | validate { |model| model.instance_variable_set :@validate_called, true } 1000 | 1001 | def remove_called_variables 1002 | instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} 1003 | end 1004 | end 1005 | 1006 | class ParentModel < ActiveRecord::Base 1007 | acts_as_paranoid 1008 | has_many :paranoid_models 1009 | has_many :related_models 1010 | has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy 1011 | has_many :non_paranoid_models, dependent: :destroy 1012 | has_one :non_paranoid_model, dependent: :destroy 1013 | has_many :asplode_models, dependent: :destroy 1014 | has_one :polymorphic_model, as: :parent, dependent: :destroy 1015 | end 1016 | 1017 | class ParentModelWithCounterCacheColumn < ActiveRecord::Base 1018 | has_many :related_models 1019 | end 1020 | 1021 | class RelatedModel < ActiveRecord::Base 1022 | acts_as_paranoid 1023 | belongs_to :parent_model 1024 | belongs_to :parent_model_with_counter_cache_column, counter_cache: true 1025 | 1026 | after_destroy do |model| 1027 | if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 1028 | model.instance_variable_set :@after_destroy_callback_called, true 1029 | end 1030 | end 1031 | after_commit :set_after_commit_on_destroy_callback_called, on: :destroy 1032 | 1033 | def set_after_commit_on_destroy_callback_called 1034 | if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 1035 | self.instance_variable_set :@after_commit_on_destroy_callback_called, true 1036 | end 1037 | end 1038 | end 1039 | 1040 | class Employer < ActiveRecord::Base 1041 | acts_as_paranoid 1042 | validates_uniqueness_of :name 1043 | has_many :jobs 1044 | has_many :employees, :through => :jobs 1045 | end 1046 | 1047 | class Employee < ActiveRecord::Base 1048 | acts_as_paranoid 1049 | has_many :jobs 1050 | has_many :employers, :through => :jobs 1051 | end 1052 | 1053 | class Job < ActiveRecord::Base 1054 | acts_as_paranoid 1055 | belongs_to :employer 1056 | belongs_to :employee 1057 | end 1058 | 1059 | class CustomColumnModel < ActiveRecord::Base 1060 | acts_as_paranoid column: :destroyed_at 1061 | end 1062 | 1063 | class CustomSentinelModel < ActiveRecord::Base 1064 | acts_as_paranoid sentinel_value: DateTime.new(0) 1065 | end 1066 | 1067 | class WithoutDefaultScopeModel < ActiveRecord::Base 1068 | acts_as_paranoid without_default_scope: true 1069 | end 1070 | 1071 | class ActiveColumnModel < ActiveRecord::Base 1072 | acts_as_paranoid column: :active, sentinel_value: true 1073 | 1074 | def paranoia_restore_attributes 1075 | { 1076 | deleted_at: nil, 1077 | active: true 1078 | } 1079 | end 1080 | 1081 | def paranoia_destroy_attributes 1082 | { 1083 | deleted_at: current_time_from_proper_timezone, 1084 | active: nil 1085 | } 1086 | end 1087 | end 1088 | 1089 | class ActiveColumnModelWithUniquenessValidation < ActiveRecord::Base 1090 | validates :name, :uniqueness => true 1091 | acts_as_paranoid column: :active, sentinel_value: true 1092 | 1093 | def paranoia_restore_attributes 1094 | { 1095 | deleted_at: nil, 1096 | active: true 1097 | } 1098 | end 1099 | 1100 | def paranoia_destroy_attributes 1101 | { 1102 | deleted_at: current_time_from_proper_timezone, 1103 | active: nil 1104 | } 1105 | end 1106 | end 1107 | 1108 | class NonParanoidModel < ActiveRecord::Base 1109 | end 1110 | 1111 | class ParanoidModelWithObservers < ParanoidModel 1112 | def observers_notified 1113 | @observers_notified ||= [] 1114 | end 1115 | 1116 | def self.notify_observer(*args) 1117 | observers_notified << args 1118 | end 1119 | end 1120 | 1121 | class ParanoidModelWithoutObservers < ParanoidModel 1122 | self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers) 1123 | end 1124 | 1125 | # refer back to regression test for #118 1126 | class ParanoidModelWithHasOne < ParanoidModel 1127 | has_one :paranoid_model_with_belong, :dependent => :destroy 1128 | has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong" 1129 | has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id" 1130 | has_one :not_paranoid_model_with_belong, :dependent => :destroy 1131 | end 1132 | 1133 | class ParanoidModelWithHasOneAndBuild < ActiveRecord::Base 1134 | has_one :paranoid_model_with_build_belong, :dependent => :destroy 1135 | validates :color, :presence => true 1136 | after_validation :build_paranoid_model_with_build_belong, on: :create 1137 | 1138 | private 1139 | def build_paranoid_model_with_build_belong 1140 | super.tap { |child| child.name = "foo" } 1141 | end 1142 | end 1143 | 1144 | class ParanoidModelWithBuildBelong < ActiveRecord::Base 1145 | acts_as_paranoid 1146 | validates :name, :presence => true 1147 | belongs_to :paranoid_model_with_has_one_and_build 1148 | end 1149 | 1150 | class ParanoidModelWithBelong < ActiveRecord::Base 1151 | acts_as_paranoid 1152 | belongs_to :paranoid_model_with_has_one 1153 | end 1154 | 1155 | class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base 1156 | acts_as_paranoid 1157 | belongs_to :paranoid_model_with_has_one 1158 | end 1159 | 1160 | class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base 1161 | acts_as_paranoid 1162 | belongs_to :paranoid_model_with_has_one 1163 | end 1164 | 1165 | class ParanoidModelWithTimestamp < ActiveRecord::Base 1166 | belongs_to :parent_model 1167 | acts_as_paranoid 1168 | end 1169 | 1170 | class NotParanoidModelWithBelong < ActiveRecord::Base 1171 | belongs_to :paranoid_model_with_has_one 1172 | end 1173 | 1174 | class FlaggedModel < PlainModel 1175 | acts_as_paranoid :flag_column => :is_deleted 1176 | end 1177 | 1178 | class FlaggedModelWithCustomIndex < PlainModel 1179 | acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted 1180 | end 1181 | 1182 | class AsplodeModel < ActiveRecord::Base 1183 | acts_as_paranoid 1184 | before_destroy do |r| 1185 | raise StandardError, 'ASPLODE!' 1186 | end 1187 | end 1188 | 1189 | class NoConnectionModel < ActiveRecord::Base 1190 | end 1191 | 1192 | class PolymorphicModel < ActiveRecord::Base 1193 | acts_as_paranoid 1194 | belongs_to :parent, polymorphic: true 1195 | end 1196 | 1197 | module Namespaced 1198 | def self.table_name_prefix 1199 | "namespaced_" 1200 | end 1201 | 1202 | class ParanoidHasOne < ActiveRecord::Base 1203 | acts_as_paranoid 1204 | has_one :paranoid_belongs_to, dependent: :destroy 1205 | end 1206 | 1207 | class ParanoidBelongsTo < ActiveRecord::Base 1208 | acts_as_paranoid 1209 | belongs_to :paranoid_has_one 1210 | end 1211 | end 1212 | --------------------------------------------------------------------------------