paranoia-2.4.1/0000755000004100000410000000000013312077301013335 5ustar www-datawww-dataparanoia-2.4.1/.travis.yml0000644000004100000410000000074613312077301015455 0ustar www-datawww-datasudo: false language: ruby before_install: gem update --system cache: bundler rvm: - 2.2 - 2.3.5 - 2.4.3 - 2.5.0 - jruby-9.1.6.0 env: matrix: - RAILS='~> 4.2.0' - RAILS='~> 5.0.0' - RAILS='~> 5.1.0' - RAILS='~> 5.2.0' matrix: allow_failures: - env: RAILS='~> 4.2.0' rvm: jruby-9.1.6.0 - env: RAILS='~> 5.0.0' rvm: jruby-9.1.6.0 - env: RAILS='~> 5.1.0' rvm: jruby-9.1.6.0 - env: RAILS='~> 5.2.0' rvm: jruby-9.1.6.0 paranoia-2.4.1/test/0000755000004100000410000000000013312077301014314 5ustar www-datawww-dataparanoia-2.4.1/test/paranoia_test.rb0000644000004100000410000012773013312077301017504 0ustar www-datawww-datarequire 'bundler/setup' require 'active_record' require 'minitest/autorun' require 'paranoia' test_framework = defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=) ActiveRecord::Base.raise_in_transactional_callbacks = true end def connect! ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:' end def setup! connect! { 'parent_model_with_counter_cache_columns' => 'related_models_count INTEGER DEFAULT 0', 'parent_models' => 'deleted_at DATETIME', 'paranoid_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 'paranoid_model_with_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_build_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32)', 'paranoid_model_with_anthor_class_name_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_foreign_key_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER', 'paranoid_model_with_timestamps' => 'parent_model_id INTEGER, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME', 'not_paranoid_model_with_belongs' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 'not_paranoid_model_with_belongs_and_assocation_not_soft_destroyed_validator' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_has_one_and_builds' => 'parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER', 'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)', 'plain_models' => 'deleted_at DATETIME', 'callback_models' => 'deleted_at DATETIME', 'fail_callback_models' => 'deleted_at DATETIME', 'related_models' => 'parent_model_id INTEGER, parent_model_with_counter_cache_column_id INTEGER, deleted_at DATETIME', 'asplode_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 'employers' => 'name VARCHAR(32), deleted_at DATETIME', 'employees' => 'deleted_at DATETIME', 'jobs' => 'employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME', 'custom_column_models' => 'destroyed_at DATETIME', 'custom_sentinel_models' => 'deleted_at DATETIME NOT NULL', 'non_paranoid_models' => 'parent_model_id INTEGER', 'polymorphic_models' => 'parent_id INTEGER, parent_type STRING, deleted_at DATETIME', 'namespaced_paranoid_has_ones' => 'deleted_at DATETIME, paranoid_belongs_tos_id INTEGER', 'namespaced_paranoid_belongs_tos' => 'deleted_at DATETIME, paranoid_has_one_id INTEGER', 'unparanoid_unique_models' => 'name VARCHAR(32), paranoid_with_unparanoids_id INTEGER', 'active_column_models' => 'deleted_at DATETIME, active BOOLEAN', 'active_column_model_with_uniqueness_validations' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 'paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN, active_column_model_with_has_many_relationship_id INTEGER', 'active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 'without_default_scope_models' => 'deleted_at DATETIME' }.each do |table_name, columns_as_sql_string| ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})" end end class WithDifferentConnection < ActiveRecord::Base establish_connection adapter: 'sqlite3', database: ':memory:' connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' acts_as_paranoid end setup! class ParanoiaTest < test_framework def setup connection = ActiveRecord::Base.connection cleaner = ->(source) { ActiveRecord::Base.connection.execute "DELETE FROM #{source}" } if ActiveRecord::VERSION::MAJOR < 5 connection.tables.each(&cleaner) else connection.data_sources.each(&cleaner) end end def test_plain_model_class_is_not_paranoid assert_equal false, PlainModel.paranoid? end def test_paranoid_model_class_is_paranoid assert_equal true, ParanoidModel.paranoid? end def test_plain_models_are_not_paranoid assert_equal false, PlainModel.new.paranoid? end def test_paranoid_models_are_paranoid assert_equal true, ParanoidModel.new.paranoid? end def test_paranoid_models_to_param model = ParanoidModel.new model.save to_param = model.to_param model.destroy assert model.to_param assert_equal to_param, model.to_param end def test_destroy_behavior_for_plain_models model = PlainModel.new assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal true, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 0, model.class.unscoped.count end # Anti-regression test for #81, which would've introduced a bug to break this test. def test_destroy_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags model.destroy assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert model.instance_variable_get(:@destroy_callback_called) assert model.instance_variable_get(:@after_destroy_callback_called) assert model.instance_variable_get(:@after_commit_callback_called) end def test_delete_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags model.delete assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert_nil model.instance_variable_get(:@destroy_callback_called) assert_nil model.instance_variable_get(:@after_destroy_callback_called) assert_nil model.instance_variable_get(:@after_commit_callback_called) end def test_delete_in_transaction_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags CallbackModel.transaction do model.delete end assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert_nil model.instance_variable_get(:@destroy_callback_called) assert_nil model.instance_variable_get(:@after_destroy_callback_called) assert model.instance_variable_get(:@after_commit_callback_called) end def test_destroy_behavior_for_paranoid_models model = ParanoidModel.new assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count end def test_update_columns_on_paranoia_destroyed record = ParentModel.create record.destroy assert record.update_columns deleted_at: Time.now end def test_scoping_behavior_for_paranoid_models parent1 = ParentModel.create parent2 = ParentModel.create p1 = ParanoidModel.create(:parent_model => parent1) p2 = ParanoidModel.create(:parent_model => parent2) p1.destroy p2.destroy assert_equal 0, parent1.paranoid_models.count assert_equal 1, parent1.paranoid_models.only_deleted.count assert_equal 2, ParanoidModel.only_deleted.joins(:parent_model).count assert_equal 1, parent1.paranoid_models.deleted.count assert_equal 0, parent1.paranoid_models.without_deleted.count p3 = ParanoidModel.create(:parent_model => parent1) assert_equal 2, parent1.paranoid_models.with_deleted.count assert_equal 1, parent1.paranoid_models.without_deleted.count assert_equal [p1,p3], parent1.paranoid_models.with_deleted end def test_only_deleted_with_joins c1 = ActiveColumnModelWithHasManyRelationship.create(name: 'Jacky') c2 = ActiveColumnModelWithHasManyRelationship.create(name: 'Thomas') p1 = ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship.create(name: 'Hello', active_column_model_with_has_many_relationship: c1) c1.destroy assert_equal 1, ActiveColumnModelWithHasManyRelationship.count assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.count assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.joins(:paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships).count end def test_destroy_behavior_for_custom_column_models model = CustomColumnModel.new assert_equal 0, model.class.count model.save! assert_nil model.destroyed_at assert_equal 1, model.class.count model.destroy assert_equal false, model.destroyed_at.nil? assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count end def test_default_sentinel_value assert_nil ParanoidModel.paranoia_sentinel_value end def test_without_default_scope_option model = WithoutDefaultScopeModel.create model.destroy assert_equal 1, model.class.count assert_equal 1, model.class.only_deleted.count assert_equal 0, model.class.where(deleted_at: nil).count end def test_active_column_model model = ActiveColumnModel.new assert_equal 0, model.class.count model.save! assert_nil model.deleted_at assert_equal true, model.active assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_nil model.active assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count end def test_active_column_model_with_uniqueness_validation_only_checks_non_deleted_records a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") a.destroy b = ActiveColumnModelWithUniquenessValidation.new(name: "A") assert b.valid? end def test_active_column_model_with_uniqueness_validation_still_works_on_non_deleted_records a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") b = ActiveColumnModelWithUniquenessValidation.new(name: "A") refute b.valid? end def test_sentinel_value_for_custom_sentinel_models model = CustomSentinelModel.new assert_equal 0, model.class.count model.save! assert_equal DateTime.new(0), model.deleted_at assert_equal 1, model.class.count model.destroy assert DateTime.new(0) != model.deleted_at assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count model.restore assert_equal DateTime.new(0), model.deleted_at assert !model.destroyed? assert_equal 1, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 0, model.class.only_deleted.count assert_equal 0, model.class.deleted.count end def test_destroy_behavior_for_featureful_paranoid_models model = get_featureful_model assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count end def test_destroy_behavior_for_has_one_with_build_and_validation_error model = ParanoidModelWithHasOneAndBuild.create model.destroy end # Regression test for #24 def test_chaining_for_paranoid_models scope = FeaturefulModel.where(:name => "foo").only_deleted assert_equal({'name' => "foo"}, scope.where_values_hash) end def test_only_destroyed_scope_for_paranoid_models model = ParanoidModel.new model.save model.destroy model2 = ParanoidModel.new model2.save assert_equal model, ParanoidModel.only_deleted.last assert_equal false, ParanoidModel.only_deleted.include?(model2) end def test_default_scope_for_has_many_relationships parent = ParentModel.create assert_equal 0, parent.related_models.count child = parent.related_models.create assert_equal 1, parent.related_models.count child.destroy assert_equal false, child.deleted_at.nil? assert_equal 0, parent.related_models.count assert_equal 1, parent.related_models.unscoped.count end def test_default_scope_for_has_many_through_relationships employer = Employer.create employee = Employee.create assert_equal 0, employer.jobs.count assert_equal 0, employer.employees.count assert_equal 0, employee.jobs.count assert_equal 0, employee.employers.count job = Job.create :employer => employer, :employee => employee assert_equal 1, employer.jobs.count assert_equal 1, employer.employees.count assert_equal 1, employee.jobs.count assert_equal 1, employee.employers.count employee2 = Employee.create job2 = Job.create :employer => employer, :employee => employee2 employee2.destroy assert_equal 2, employer.jobs.count assert_equal 1, employer.employees.count job.destroy assert_equal 1, employer.jobs.count assert_equal 0, employer.employees.count assert_equal 0, employee.jobs.count assert_equal 0, employee.employers.count end def test_delete_behavior_for_callbacks model = CallbackModel.new model.save model.delete assert_nil model.instance_variable_get(:@destroy_callback_called) end def test_destroy_behavior_for_callbacks model = CallbackModel.new model.save model.destroy assert model.instance_variable_get(:@destroy_callback_called) end def test_destroy_on_readonly_record # Just to demonstrate the AR behaviour model = NonParanoidModel.create! model.readonly! assert_raises ActiveRecord::ReadOnlyRecord do model.destroy end # Mirrors behaviour above model = ParanoidModel.create! model.readonly! assert_raises ActiveRecord::ReadOnlyRecord do model.destroy end end def test_destroy_on_really_destroyed_record model = ParanoidModel.create! model.really_destroy! assert model.really_destroyed? assert model.paranoia_destroyed? model.destroy assert model.really_destroyed? assert model.paranoia_destroyed? end def test_destroy_on_unsaved_record # Just to demonstrate the AR behaviour model = NonParanoidModel.new model.destroy! assert model.destroyed? model.destroy! assert model.destroyed? # Mirrors behaviour above model = ParanoidModel.new model.destroy! assert model.paranoia_destroyed? model.destroy! assert model.paranoia_destroyed? end def test_restore model = ParanoidModel.new model.save id = model.id model.destroy assert model.paranoia_destroyed? model = ParanoidModel.only_deleted.find(id) model.restore! model.reload assert_equal false, model.paranoia_destroyed? end def test_restore_on_object_return_self model = ParanoidModel.create model.destroy assert_equal model.class, model.restore.class end # Regression test for #92 def test_destroy_twice model = ParanoidModel.new model.save model.destroy model.destroy assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count end # Regression test for #92 def test_destroy_bang_twice model = ParanoidModel.new model.save! model.destroy! model.destroy! assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count end def test_destroy_return_value_on_success model = ParanoidModel.create return_value = model.destroy assert_equal(return_value, model) end def test_destroy_return_value_on_failure model = FailCallbackModel.create return_value = model.destroy assert_equal(return_value, false) end def test_restore_behavior_for_callbacks model = CallbackModel.new model.save id = model.id model.destroy assert model.paranoia_destroyed? model = CallbackModel.only_deleted.find(id) model.restore! model.reload assert model.instance_variable_get(:@restore_callback_called) end def test_really_destroy model = ParanoidModel.new model.save model.really_destroy! refute ParanoidModel.unscoped.exists?(model.id) end def test_real_destroy_dependent_destroy parent = ParentModel.create child1 = parent.very_related_models.create child2 = parent.non_paranoid_models.create child3 = parent.create_non_paranoid_model parent.really_destroy! refute RelatedModel.unscoped.exists?(child1.id) refute NonParanoidModel.unscoped.exists?(child2.id) refute NonParanoidModel.unscoped.exists?(child3.id) end def test_real_destroy_dependent_destroy_after_normal_destroy parent = ParentModel.create child = parent.very_related_models.create parent.destroy parent.really_destroy! refute RelatedModel.unscoped.exists?(child.id) end def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children parent_1 = ParentModel.create child_1 = parent_1.very_related_models.create parent_2 = ParentModel.create child_2 = parent_2.very_related_models.create parent_1.destroy parent_1.really_destroy! assert RelatedModel.unscoped.exists?(child_2.id) end def test_really_destroy_behavior_for_callbacks model = CallbackModel.new model.save model.really_destroy! assert model.instance_variable_get(:@real_destroy_callback_called) end def test_really_destroy_behavior_for_active_column_model model = ActiveColumnModel.new model.save model.really_destroy! refute ParanoidModel.unscoped.exists?(model.id) end def test_really_delete model = ParanoidModel.new model.save model.really_delete refute ParanoidModel.unscoped.exists?(model.id) end def test_multiple_restore a = ParanoidModel.new a.save a_id = a.id a.destroy b = ParanoidModel.new b.save b_id = b.id b.destroy c = ParanoidModel.new c.save c_id = c.id c.destroy ParanoidModel.restore([a_id, c_id]) a.reload b.reload c.reload refute a.paranoia_destroyed? assert b.paranoia_destroyed? refute c.paranoia_destroyed? end def test_restore_with_associations_using_recovery_window parent = ParentModel.create first_child = parent.very_related_models.create second_child = parent.very_related_models.create parent.destroy second_child.update(deleted_at: parent.deleted_at + 11.minutes) parent.restore!(:recursive => true) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.reload.deleted_at.nil? parent.destroy second_child.update(deleted_at: parent.deleted_at + 11.minutes) parent.restore(:recursive => true, :recovery_window => 10.minutes) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal false, second_child.reload.deleted_at.nil? second_child.restore parent.destroy first_child.update(deleted_at: parent.deleted_at - 11.minutes) second_child.update(deleted_at: parent.deleted_at - 9.minutes) ParentModel.restore(parent.id, :recursive => true, :recovery_window => 10.minutes) assert_equal true, parent.reload.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.reload.deleted_at.nil? end def test_restore_with_associations parent = ParentModel.create first_child = parent.very_related_models.create second_child = parent.non_paranoid_models.create parent.destroy assert_equal false, parent.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.restore! assert_equal true, parent.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.destroy parent.restore(:recursive => true) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.destroy ParentModel.restore(parent.id, :recursive => true) assert_equal true, parent.reload.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? end # regression tests for #118 def test_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? hasOne.restore(:recursive => true) hasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_new_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id) newHasOne.restore(:recursive => true) newHasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_model_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true) hasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_restore_with_nil_has_one_association # setup and destroy test object hasOne = ParanoidModelWithHasOne.create hasOne.destroy assert_equal false, hasOne.reload.deleted_at.nil? # Does it raise NoMethodException on restore of nil hasOne.restore(:recursive => true) assert hasOne.reload.deleted_at.nil? end def test_restore_with_module_scoped_has_one_association # setup and destroy test object hasOne = Namespaced::ParanoidHasOne.create hasOne.destroy assert_equal false, hasOne.reload.deleted_at.nil? # Does it raise "uninitialized constant ParanoidBelongsTo" # on restore of ParanoidHasOne? hasOne.restore(:recursive => true) assert hasOne.reload.deleted_at.nil? end # covers #185 def test_restoring_recursive_has_one_restores_correct_object hasOnes = 2.times.map { ParanoidModelWithHasOne.create } belongsTos = 2.times.map { ParanoidModelWithBelong.create } hasOnes[0].update paranoid_model_with_belong: belongsTos[0] hasOnes[1].update paranoid_model_with_belong: belongsTos[1] hasOnes.each(&:destroy) ParanoidModelWithHasOne.restore(hasOnes[1].id, :recursive => true) hasOnes.each(&:reload) belongsTos.each(&:reload) # without #185, belongsTos[0] will be restored instead of belongsTos[1] refute_nil hasOnes[0].deleted_at refute_nil belongsTos[0].deleted_at assert_nil hasOnes[1].deleted_at assert_nil belongsTos[1].deleted_at end # covers #131 def test_has_one_really_destroy_with_nil model = ParanoidModelWithHasOne.create model.really_destroy! refute ParanoidModelWithBelong.unscoped.exists?(model.id) end def test_has_one_really_destroy_with_record model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong } model.really_destroy! refute ParanoidModelWithBelong.unscoped.exists?(model.id) end def test_observers_notified a = ParanoidModelWithObservers.create a.destroy a.restore! assert a.observers_notified.select {|args| args == [:before_restore, a]} assert a.observers_notified.select {|args| args == [:after_restore, a]} end def test_observers_not_notified_if_not_supported a = ParanoidModelWithObservers.create a.destroy a.restore! # essentially, we're just ensuring that this doesn't crash end def test_validates_uniqueness_only_checks_non_deleted_records a = Employer.create!(name: "A") a.destroy b = Employer.new(name: "A") assert b.valid? end def test_validates_uniqueness_still_works_on_non_deleted_records a = Employer.create!(name: "A") b = Employer.new(name: "A") refute b.valid? end def test_updated_at_modification_on_destroy paranoid_model = ParanoidModelWithTimestamp.create(:parent_model => ParentModel.create, :updated_at => 1.day.ago) assert paranoid_model.updated_at < 10.minutes.ago paranoid_model.destroy assert paranoid_model.updated_at > 10.minutes.ago end def test_updated_at_modification_on_restore parent1 = ParentModel.create pt1 = ParanoidModelWithTimestamp.create(:parent_model => parent1) ParanoidModelWithTimestamp.record_timestamps = false pt1.update_columns(created_at: 20.years.ago, updated_at: 10.years.ago, deleted_at: 10.years.ago) ParanoidModelWithTimestamp.record_timestamps = true assert pt1.updated_at < 10.minutes.ago refute pt1.deleted_at.nil? pt1.restore! assert pt1.deleted_at.nil? assert pt1.updated_at > 10.minutes.ago end def test_i_am_the_destroyer expected = %Q{ Sharon: "There should be a method called I_AM_THE_DESTROYER!" Ryan: "What should this method do?" Sharon: "It should fix all the spelling errors on the page!" } assert_output expected do ParanoidModel.I_AM_THE_DESTROYER! end end def test_destroy_fails_if_callback_raises_exception parent = AsplodeModel.create assert_raises(StandardError) { parent.destroy } #transaction should be rolled back, so parent NOT deleted refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' end def test_destroy_fails_if_association_callback_raises_exception parent = ParentModel.create children = [] 3.times { children << parent.asplode_models.create } assert_raises(StandardError) { parent.destroy } #transaction should be rolled back, so parent and children NOT deleted refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception' end def test_restore_model_with_different_connection ActiveRecord::Base.remove_connection # Disconnect the main connection a = WithDifferentConnection.create a.destroy! a.restore! # This test passes if no exception is raised ensure setup! # Reconnect the main connection end def test_restore_clear_association_cache_if_associations_present parent = ParentModel.create 3.times { parent.very_related_models.create } parent.destroy assert_equal 0, parent.very_related_models.count assert_equal 0, parent.very_related_models.size parent.restore(recursive: true) assert_equal 3, parent.very_related_models.count assert_equal 3, parent.very_related_models.size end def test_model_without_db_connection ActiveRecord::Base.remove_connection NoConnectionModel.class_eval{ acts_as_paranoid } ensure setup! end def test_restore_recursive_on_polymorphic_has_one_association parent = ParentModel.create polymorphic = PolymorphicModel.create(parent: parent) parent.destroy assert_equal 0, polymorphic.class.count parent.restore(recursive: true) assert_equal 1, polymorphic.class.count end # Ensure that we're checking parent_type when restoring def test_missing_restore_recursive_on_polymorphic_has_one_association parent = ParentModel.create polymorphic = PolymorphicModel.create(parent_id: parent.id, parent_type: 'ParanoidModel') parent.destroy polymorphic.destroy assert_equal 0, polymorphic.class.count parent.restore(recursive: true) assert_equal 0, polymorphic.class.count end def test_counter_cache_column_update_on_destroy#_and_restore_and_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_callbacks_for_counter_cache_column_update_on_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) related_model.destroy assert related_model.instance_variable_get(:@after_destroy_callback_called) # assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) end def test_uniqueness_for_unparanoid_associated parent_model = ParanoidWithUnparanoids.create related = parent_model.unparanoid_unique_models.create # will raise exception if model is not checked for paranoia related.valid? end def test_assocation_not_soft_destroyed_validator notParanoidModel = NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator.create parentModel = ParentModel.create assert notParanoidModel.valid? notParanoidModel.parent_model = parentModel assert notParanoidModel.valid? parentModel.destroy assert !notParanoidModel.valid? assert notParanoidModel.errors.full_messages.include? "Parent model has been soft-deleted" end # TODO: find a fix for Rails 4.1 if ActiveRecord::VERSION::STRING !~ /\A4\.1/ def test_counter_cache_column_update_on_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count related_model.really_destroy! assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end end # TODO: find a fix for Rails 4.0 and 4.1 if ActiveRecord::VERSION::STRING >= '4.2' def test_callbacks_for_counter_cache_column_update_on_really_destroy! parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) related_model.really_destroy! assert related_model.instance_variable_get(:@after_destroy_callback_called) assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) end def test_counter_cache_column_on_double_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_double_restore parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.restore related_model.restore assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_destroy_and_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.really_destroy! assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_restore parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count related_model.restore assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count end end private def get_featureful_model FeaturefulModel.new(:name => "not empty") end end # Helper classes class ParanoidModel < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid end class ParanoidWithUnparanoids < ActiveRecord::Base self.table_name = 'plain_models' has_many :unparanoid_unique_models end class UnparanoidUniqueModel < ActiveRecord::Base belongs_to :paranoid_with_unparanoids validates :name, :uniqueness => true end class FailCallbackModel < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid before_destroy { |_| if ActiveRecord::VERSION::MAJOR < 5 false else throw :abort end } end class FeaturefulModel < ActiveRecord::Base acts_as_paranoid validates :name, :presence => true, :uniqueness => true end class NonParanoidChildModel < ActiveRecord::Base validates :name, :presence => true, :uniqueness => true end class PlainModel < ActiveRecord::Base end class CallbackModel < ActiveRecord::Base acts_as_paranoid before_destroy { |model| model.instance_variable_set :@destroy_callback_called, true } before_restore { |model| model.instance_variable_set :@restore_callback_called, true } before_update { |model| model.instance_variable_set :@update_callback_called, true } before_save { |model| model.instance_variable_set :@save_callback_called, true} before_real_destroy { |model| model.instance_variable_set :@real_destroy_callback_called, true } after_destroy { |model| model.instance_variable_set :@after_destroy_callback_called, true } after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true } validate { |model| model.instance_variable_set :@validate_called, true } def remove_called_variables instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} end end class ParentModel < ActiveRecord::Base acts_as_paranoid has_many :paranoid_models has_many :related_models has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy has_many :non_paranoid_models, dependent: :destroy has_one :non_paranoid_model, dependent: :destroy has_many :asplode_models, dependent: :destroy has_one :polymorphic_model, as: :parent, dependent: :destroy end class ParentModelWithCounterCacheColumn < ActiveRecord::Base has_many :related_models end class RelatedModel < ActiveRecord::Base acts_as_paranoid belongs_to :parent_model belongs_to :parent_model_with_counter_cache_column, counter_cache: true after_destroy do |model| if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 model.instance_variable_set :@after_destroy_callback_called, true end end after_commit :set_after_commit_on_destroy_callback_called, on: :destroy def set_after_commit_on_destroy_callback_called if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 self.instance_variable_set :@after_commit_on_destroy_callback_called, true end end end class Employer < ActiveRecord::Base acts_as_paranoid validates_uniqueness_of :name has_many :jobs has_many :employees, :through => :jobs end class Employee < ActiveRecord::Base acts_as_paranoid has_many :jobs has_many :employers, :through => :jobs end class Job < ActiveRecord::Base acts_as_paranoid belongs_to :employer belongs_to :employee end class CustomColumnModel < ActiveRecord::Base acts_as_paranoid column: :destroyed_at end class CustomSentinelModel < ActiveRecord::Base acts_as_paranoid sentinel_value: DateTime.new(0) end class WithoutDefaultScopeModel < ActiveRecord::Base acts_as_paranoid without_default_scope: true end class ActiveColumnModel < ActiveRecord::Base acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ActiveColumnModelWithUniquenessValidation < ActiveRecord::Base validates :name, :uniqueness => true acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ActiveColumnModelWithHasManyRelationship < ActiveRecord::Base has_many :paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship < ActiveRecord::Base belongs_to :active_column_model_with_has_many_relationship acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class NonParanoidModel < ActiveRecord::Base end class ParanoidModelWithObservers < ParanoidModel def observers_notified @observers_notified ||= [] end def self.notify_observer(*args) observers_notified << args end end class ParanoidModelWithoutObservers < ParanoidModel self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers) end # refer back to regression test for #118 class ParanoidModelWithHasOne < ParanoidModel has_one :paranoid_model_with_belong, :dependent => :destroy has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong" has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id" has_one :not_paranoid_model_with_belong, :dependent => :destroy end class ParanoidModelWithHasOneAndBuild < ActiveRecord::Base has_one :paranoid_model_with_build_belong, :dependent => :destroy validates :color, :presence => true after_validation :build_paranoid_model_with_build_belong, on: :create private def build_paranoid_model_with_build_belong super.tap { |child| child.name = "foo" } end end class ParanoidModelWithBuildBelong < ActiveRecord::Base acts_as_paranoid validates :name, :presence => true belongs_to :paranoid_model_with_has_one_and_build end class ParanoidModelWithBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithTimestamp < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid end class NotParanoidModelWithBelong < ActiveRecord::Base belongs_to :paranoid_model_with_has_one end class NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator < NotParanoidModelWithBelong belongs_to :parent_model validates :parent_model, association_not_soft_destroyed: true end class FlaggedModel < PlainModel acts_as_paranoid :flag_column => :is_deleted end class FlaggedModelWithCustomIndex < PlainModel acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted end class AsplodeModel < ActiveRecord::Base acts_as_paranoid before_destroy do |r| raise StandardError, 'ASPLODE!' end end class NoConnectionModel < ActiveRecord::Base end class PolymorphicModel < ActiveRecord::Base acts_as_paranoid belongs_to :parent, polymorphic: true end module Namespaced def self.table_name_prefix "namespaced_" end class ParanoidHasOne < ActiveRecord::Base acts_as_paranoid has_one :paranoid_belongs_to, dependent: :destroy end class ParanoidBelongsTo < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_has_one end end paranoia-2.4.1/README.md0000644000004100000410000002377713312077301014634 0ustar www-datawww-data**Notice:** `paranoia` has some surprising behaviour (like overriding ActiveRecord's `delete` and `destroy`) and is not recommended for new projects. See [`discard`'s README](https://github.com/jhawthorn/discard#why-not-paranoia-or-acts_as_paranoid) for more details. Paranoia will continue to accept bug fixes and support new versions of Rails but isn't accepting new features. # Paranoia 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. 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. If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: :destroy` records, so please aim this method away from face when using. If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called. ***See [Destroying through association callbacks](#destroying-through-association-callbacks) for clarifying examples.*** ## Getting Started Video Setup and basic usage of the paranoia gem [GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia) ## Installation & Usage For Rails 3, please use version 1 of Paranoia: ``` ruby gem "paranoia", "~> 1.0" ``` For Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5): ``` ruby gem "paranoia", "~> 2.2" ``` Of course you can install this from GitHub as well from one of these examples: ``` ruby gem "paranoia", github: "rubysherpas/paranoia", branch: "rails3" gem "paranoia", github: "rubysherpas/paranoia", branch: "rails4" gem "paranoia", github: "rubysherpas/paranoia", branch: "rails5" ``` Then run: ``` shell bundle install ``` Updating is as simple as `bundle update paranoia`. #### Run your migrations for the desired models Run: ``` shell bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index ``` and now you have a migration ``` ruby class AddDeletedAtToClients < ActiveRecord::Migration def change add_column :clients, :deleted_at, :datetime add_index :clients, :deleted_at end end ``` ### Usage #### In your model: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid # ... end ``` Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: ``` ruby >> client.deleted_at # => nil >> client.destroy # => client >> client.deleted_at # => [current timestamp] ``` If you really want it gone *gone*, call `really_destroy!`: ``` ruby >> client.deleted_at # => nil >> client.really_destroy! # => client ``` If you want to use a column other than `deleted_at`, you can pass it as an option: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid column: :destroyed_at ... end ``` If you want to skip adding the default scope: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid without_default_scope: true ... end ``` If you want to access soft-deleted associations, override the getter method: ``` ruby def product Product.unscoped { super } end ``` If you want to include associated soft-deleted objects, you can (un)scope the association: ``` ruby class Person < ActiveRecord::Base belongs_to :group, -> { with_deleted } end Person.includes(:group).all ``` If you want to find all records, even those which are deleted: ``` ruby Client.with_deleted ``` If you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope): ``` ruby Client.without_deleted ``` If you want to find only the deleted records: ``` ruby Client.only_deleted ``` If you want to check if a record is soft-deleted: ``` ruby client.paranoia_destroyed? # or client.deleted? ``` If you want to restore a record: ``` ruby Client.restore(id) # or client.restore ``` If you want to restore a whole bunch of records: ``` ruby Client.restore([id1, id2, ..., idN]) ``` If you want to restore a record and their dependently destroyed associated records: ``` ruby Client.restore(id, :recursive => true) # or client.restore(:recursive => true) ``` If you want to restore a record and only those dependently destroyed associated records that were deleted within 2 minutes of the object upon which they depend: ``` ruby Client.restore(id, :recursive => true. :recovery_window => 2.minutes) # or client.restore(:recursive => true, :recovery_window => 2.minutes) ``` Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model. A Rails validator is provided should you require this functionality: ``` ruby validates :some_assocation, association_not_soft_destroyed: true ``` This validator makes sure that `some_assocation` is not soft destroyed. If the object is soft destroyed the main object is rendered invalid and an validation error is added. For more information, please look at the tests. #### About indexes: Beware that you should adapt all your indexes for them to work as fast as previously. For example, ``` ruby add_index :clients, :group_id add_index :clients, [:group_id, :other_id] ``` should be replaced with ``` ruby add_index :clients, :group_id, where: "deleted_at IS NULL" add_index :clients, [:group_id, :other_id], where: "deleted_at IS NULL" ``` Of course, this is not necessary for the indexes you always use in association with `with_deleted` or `only_deleted`. ##### Unique Indexes Because NULL != NULL in standard SQL, we can not simply create a unique index on the deleted_at column and expect it to enforce that there only be one record with a certain combination of values. If your database supports them, good alternatives include partial indexes (above) and indexes on computed columns. E.g. ``` ruby add_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true ``` If not, an alternative is to create a separate column which is maintained alongside deleted_at for the sake of enforcing uniqueness. To that end, paranoia makes use of two method to make its destroy and restore actions: paranoia_restore_attributes and paranoia_destroy_attributes. ``` ruby add_column :clients, :active, :boolean add_index :clients, [:group_id, :active], unique: true class Client < ActiveRecord::Base # optionally have paranoia make use of your unique column, so that # your lookups will benefit from the unique index acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end ``` ##### Destroying through association callbacks When dealing with `dependent: :destroy` associations and `acts_as_paranoid`, it's important to remember that whatever method is called on the parent model will be called on the child model. For example, given both models of an association have `acts_as_paranoid` defined: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid has_many :emails, dependent: :destroy end class Email < ActiveRecord::Base acts_as_paranoid belongs_to :client end ``` When we call `destroy` on the parent `client`, it will call `destroy` on all of its associated children `emails`: ``` ruby >> client.emails.count # => 5 >> client.destroy # => client >> client.deleted_at # => [current timestamp] >> Email.where(client_id: client.id).count # => 0 >> Email.with_deleted.where(client_id: client.id).count # => 5 ``` Similarly, when we call `really_destroy!` on the parent `client`, then each child `email` will also have `really_destroy!` called: ``` ruby >> client.emails.count # => 5 >> client.id # => 12345 >> client.really_destroy! # => client >> Client.find 12345 # => ActiveRecord::RecordNotFound >> Email.with_deleted.where(client_id: client.id).count # => 0 ``` However, if the child model `Email` does not have `acts_as_paranoid` set, then calling `destroy` on the parent `client` will also call `destroy` on each child `email`, thereby actually destroying them: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid has_many :emails, dependent: :destroy end class Email < ActiveRecord::Base belongs_to :client end >> client.emails.count # => 5 >> client.destroy # => client >> Email.where(client_id: client.id).count # => 0 >> Email.with_deleted.where(client_id: client.id).count # => NoMethodError: undefined method `with_deleted' for # ``` ## Acts As Paranoid Migration You can replace the older `acts_as_paranoid` methods as follows: | Old Syntax | New Syntax | |:-------------------------- |:------------------------------ | |`find_with_deleted(:all)` | `Client.with_deleted` | |`find_with_deleted(:first)` | `Client.with_deleted.first` | |`find_with_deleted(id)` | `Client.with_deleted.find(id)` | The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's `restore` method does not do this. ## Callbacks 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 For example if you want to index your records in some search engine you can go like this: ```ruby class Product < ActiveRecord::Base acts_as_paranoid after_destroy :update_document_in_search_engine after_restore :update_document_in_search_engine after_real_destroy :remove_document_from_search_engine end ``` You can use these events just like regular Rails callbacks with before, after and around hooks. ## License This gem is released under the MIT license. paranoia-2.4.1/CHANGELOG.md0000644000004100000410000001211213312077301015143 0ustar www-datawww-data# paranoia Changelog ## 2.4.1 * [#435](https://github.com/rubysherpas/paranoia/pull/435) Monkeypatch activerecord relations to work with rails 6.2.0 [Bartosz Bonisławski (@bbonislawski)](https://github.com/bbonislawski) ## 2.4.0 * [#423](https://github.com/rubysherpas/paranoia/pull/423) Add `paranoia_destroy` and `paranoia_delete` aliases [John Hawthorn (@jhawthorn)](https://github.com/jhawthorn) * [#408](https://github.com/rubysherpas/paranoia/pull/408) Fix instance variable `@_disable_counter_cache` not initialized warning. [Akira Matsuda (@amatsuda)](https://github.com/amatsuda) * [#412](https://github.com/rubysherpas/paranoia/pull/412) Fix `really_destroy!` behavior with `sentinel_value` [Steve Rice (@steverice)](https://github.com/steverice) ## 2.3.1 * [#397](https://github.com/rubysherpas/paranoia/pull/397) Bump active record max version to support 5.1 final ## 2.3.0 (2017-04-14) * [#393](https://github.com/rubysherpas/paranoia/pull/393) Drop support for Rails 4.1 and begin supporting Rails 5.1. [Miklós Fazekas (@mfazekas)](https://github.com/mfazekas) * [#391](https://github.com/rubysherpas/paranoia/pull/391) Use Contributor Covenant Version 1.4 [Ben A. Morgan (@BenMorganIO)](https://github.com/BenMorganIO) * [#390](https://github.com/rubysherpas/paranoia/pull/390) Fix counter cache with double destroy, really_destroy, and restore [Chris Oliver (@excid3)](https://github.com/excid3) * [#389](https://github.com/rubysherpas/paranoia/pull/389) Added association not soft destroyed validator _Fixes [#380](https://github.com/rubysherpas/paranoia/issues/380)_ [Edward Poot (@edwardmp)](https://github.com/edwardmp) * [#383](https://github.com/rubysherpas/paranoia/pull/383) Add recovery window feature _Fixes [#359](https://github.com/rubysherpas/paranoia/issues/359)_ [Andrzej Piątyszek (@konto-andrzeja)](https://github.com/konto-andrzeja) ## 2.2.1 (2017-02-15) * [#371](https://github.com/rubysherpas/paranoia/pull/371) Use ActiveSupport.on_load to correctly re-open ActiveRecord::Base _Fixes [#335](https://github.com/rubysherpas/paranoia/issues/335) and [#381](https://github.com/rubysherpas/paranoia/issues/381)._ [Iaan Krynauw (@iaankrynauw)](https://github.com/iaankrynauw) * [#377](https://github.com/rubysherpas/paranoia/pull/377) Touch record on paranoia-destroy. _Fixes [#296](https://github.com/rubysherpas/paranoia/issues/296)._ [René (@rbr)](https://github.com/rbr) * [#379](https://github.com/rubysherpas/paranoia/pull/379) Fixes a problem of ambiguous table names when using only_deleted method. _Fixes [#26](https://github.com/rubysherpas/paranoia/issues/26) and [#27](https://github.com/rubysherpas/paranoia/pull/27)._ [Thomas Romera (@Erowlin)](https://github.com/Erowlin) ## 2.2.0 (2016-10-21) * Ruby 2.0 or greater is required * Rails 5.0.0.beta1.1 support [@pigeonworks](https://github.com/pigeonworks) [@halostatue](https://github.com/halostatue) and [@gagalago](https://github.com/gagalago) * Previously `#really_destroyed?` may have been defined on non-paranoid models, it is now only available on paranoid models, use regular `#destroyed?` instead. ## 2.1.5 (2016-01-06) * Ruby 2.3 support ## 2.1.4 ## 2.1.3 ## 2.1.2 ## 2.1.1 ## 2.1.0 (2015-01-23) ### Major changes * `#destroyed?` is no longer overridden. Use `#paranoia_destroyed?` for the existing behaviour. [Washington Luiz](https://github.com/huoxito) * `#persisted?` is no longer overridden. * ActiveRecord 4.0 no longer has `#destroy!` as an alias for `#really_destroy!`. * `#destroy` will now raise an exception if called on a readonly record. * `#destroy` on a hard deleted record is now a successful noop. * `#destroy` on a new record will set deleted_at (previously this raised an error) * `#destroy` and `#delete` always return self when successful. ### Bug Fixes * Calling `#destroy` twice will not hard-delete records. Use `#really_destroy!` if this is desired. * Fix errors on non-paranoid has_one dependent associations ## 2.0.5 (2015-01-22) ### Bug fixes * 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) * 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) * Fix rspec 2 compatibility [#197](https://github.com/radar/paranoia/pull/197) [Emil Sågfors](https://github.com/lime) * Fix some deprecation warnings on rails 4.2 [Sergey Alekseev](https://github.com/sergey-alekseev) ## 2.0.4 (2014-12-02) ### Features * Add paranoia_scope as named version of default_scope [#184](https://github.com/radar/paranoia/pull/184) [Jozsef Nyitrai](https://github.com/nyjt) ### Bug Fixes * Fix initialization problems when missing table or no database connection [#186](https://github.com/radar/paranoia/issues/186) * 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) paranoia-2.4.1/.gitignore0000644000004100000410000000005413312077301015324 0ustar www-datawww-datapkg/* *.gem .bundle tmp .rvmrc Gemfile.lock paranoia-2.4.1/CODE_OF_CONDUCT.md0000644000004100000410000000623013312077301016135 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ben@benmorgan.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ paranoia-2.4.1/LICENSE0000644000004100000410000000162513312077301014346 0ustar www-datawww-dataPermission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. paranoia-2.4.1/Rakefile0000644000004100000410000000024113312077301014777 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks task :test do Dir['test/*_test.rb'].each do |testfile| load testfile end end task :default => :test paranoia-2.4.1/lib/0000755000004100000410000000000013312077301014103 5ustar www-datawww-dataparanoia-2.4.1/lib/paranoia.rb0000644000004100000410000002521713312077301016231 0ustar www-datawww-datarequire 'active_record' unless defined? ActiveRecord if [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR] == [5, 2] require 'paranoia/active_record_5_2' end module Paranoia @@default_sentinel_value = nil # Change default_sentinel_value in a rails initializer def self.default_sentinel_value=(val) @@default_sentinel_value = val end def self.default_sentinel_value @@default_sentinel_value end def self.included(klazz) klazz.extend Query end module Query def paranoid? ; true ; end def with_deleted if ActiveRecord::VERSION::STRING >= "4.1" return unscope where: paranoia_column end all.tap { |x| x.default_scoped = false } end def only_deleted if paranoia_sentinel_value.nil? return with_deleted.where.not(paranoia_column => paranoia_sentinel_value) end # if paranoia_sentinel_value is not null, then it is possible that # some deleted rows will hold a null value in the paranoia column # these will not match != sentinel value because "NULL != value" is # NULL under the sql standard # Scoping with the table_name is mandatory to avoid ambiguous errors when joining tables. scoped_quoted_paranoia_column = "#{self.table_name}.#{connection.quote_column_name(paranoia_column)}" with_deleted.where("#{scoped_quoted_paranoia_column} IS NULL OR #{scoped_quoted_paranoia_column} != ?", paranoia_sentinel_value) end alias_method :deleted, :only_deleted def restore(id_or_ids, opts = {}) ids = Array(id_or_ids).flatten any_object_instead_of_id = ids.any? { |id| ActiveRecord::Base === id } if any_object_instead_of_id ids.map! { |id| ActiveRecord::Base === id ? id.id : id } ActiveSupport::Deprecation.warn("You are passing an instance of ActiveRecord::Base to `restore`. " \ "Please pass the id of the object by calling `.id`") end ids.map { |id| only_deleted.find(id).restore!(opts) } end end def paranoia_destroy transaction do run_callbacks(:destroy) do @_disable_counter_cache = deleted? result = paranoia_delete next result unless result && ActiveRecord::VERSION::STRING >= '4.2' each_counter_cached_associations do |association| foreign_key = association.reflection.foreign_key.to_sym next if destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key next unless send(association.reflection.name) association.decrement_counters end @_disable_counter_cache = false result end end end alias_method :destroy, :paranoia_destroy def paranoia_destroy! paranoia_destroy || raise(ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record", self)) end def paranoia_delete raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? if persisted? # if a transaction exists, add the record so that after_commit # callbacks can be run add_to_transaction update_columns(paranoia_destroy_attributes) elsif !frozen? assign_attributes(paranoia_destroy_attributes) end self end alias_method :delete, :paranoia_delete def restore!(opts = {}) self.class.transaction do run_callbacks(:restore) do recovery_window_range = get_recovery_window_range(opts) # Fixes a bug where the build would error because attributes were frozen. # This only happened on Rails versions earlier than 4.1. noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") if within_recovery_window?(recovery_window_range) && ((noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen) @_disable_counter_cache = !paranoia_destroyed? write_attribute paranoia_column, paranoia_sentinel_value update_columns(paranoia_restore_attributes) each_counter_cached_associations do |association| if send(association.reflection.name) association.increment_counters end end @_disable_counter_cache = false end restore_associated_records(recovery_window_range) if opts[:recursive] end end self end alias :restore :restore! def get_recovery_window_range(opts) return opts[:recovery_window_range] if opts[:recovery_window_range] return unless opts[:recovery_window] (deleted_at - opts[:recovery_window]..deleted_at + opts[:recovery_window]) end def within_recovery_window?(recovery_window_range) return true unless recovery_window_range recovery_window_range.cover?(deleted_at) end def paranoia_destroyed? send(paranoia_column) != paranoia_sentinel_value end alias :deleted? :paranoia_destroyed? def really_destroy! transaction do run_callbacks(:real_destroy) do @_disable_counter_cache = paranoia_destroyed? dependent_reflections = self.class.reflections.select do |name, reflection| reflection.options[:dependent] == :destroy end if dependent_reflections.any? dependent_reflections.each do |name, reflection| association_data = self.send(name) # has_one association can return nil # .paranoid? will work for both instances and classes next unless association_data && association_data.paranoid? if reflection.collection? next association_data.with_deleted.each(&:really_destroy!) end association_data.really_destroy! end end update_columns(paranoia_destroy_attributes) destroy_without_paranoia end end end private def each_counter_cached_associations !(defined?(@_disable_counter_cache) && @_disable_counter_cache) ? super : [] end def paranoia_restore_attributes { paranoia_column => paranoia_sentinel_value }.merge(timestamp_attributes_with_current_time) end def paranoia_destroy_attributes { paranoia_column => current_time_from_proper_timezone }.merge(timestamp_attributes_with_current_time) end def timestamp_attributes_with_current_time timestamp_attributes_for_update_in_model.each_with_object({}) { |attr,hash| hash[attr] = current_time_from_proper_timezone } end # restore associated records that have been soft deleted when # we called #destroy def restore_associated_records(recovery_window_range = nil) destroyed_associations = self.class.reflect_on_all_associations.select do |association| association.options[:dependent] == :destroy end destroyed_associations.each do |association| association_data = send(association.name) unless association_data.nil? if association_data.paranoid? if association.collection? association_data.only_deleted.each do |record| record.restore(:recursive => true, :recovery_window_range => recovery_window_range) end else association_data.restore(:recursive => true, :recovery_window_range => recovery_window_range) end end end if association_data.nil? && association.macro.to_s == "has_one" association_class_name = association.klass.name association_foreign_key = association.foreign_key if association.type association_polymorphic_type = association.type association_find_conditions = { association_polymorphic_type => self.class.name.to_s, association_foreign_key => self.id } else association_find_conditions = { association_foreign_key => self.id } end association_class = association_class_name.constantize if association_class.paranoid? association_class.only_deleted.where(association_find_conditions).first .try!(:restore, recursive: true, :recovery_window_range => recovery_window_range) end end end clear_association_cache if destroyed_associations.present? end end ActiveSupport.on_load(:active_record) do class ActiveRecord::Base def self.acts_as_paranoid(options={}) define_model_callbacks :restore, :real_destroy alias_method :really_destroyed?, :destroyed? alias_method :really_delete, :delete alias_method :destroy_without_paranoia, :destroy include Paranoia class_attribute :paranoia_column, :paranoia_sentinel_value self.paranoia_column = (options[:column] || :deleted_at).to_s self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } def self.paranoia_scope where(paranoia_column => paranoia_sentinel_value) end class << self; alias_method :without_deleted, :paranoia_scope end unless options[:without_default_scope] default_scope { paranoia_scope } end before_restore { self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers) } after_restore { self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) } end # Please do not use this method in production. # Pretty please. def self.I_AM_THE_DESTROYER! # TODO: actually implement spelling error fixes puts %Q{ Sharon: "There should be a method called I_AM_THE_DESTROYER!" Ryan: "What should this method do?" Sharon: "It should fix all the spelling errors on the page!" } end def self.paranoid? ; false ; end def paranoid? ; self.class.paranoid? ; end private def paranoia_column self.class.paranoia_column end def paranoia_sentinel_value self.class.paranoia_sentinel_value end end end require 'paranoia/rspec' if defined? RSpec module ActiveRecord module Validations module UniquenessParanoiaValidator def build_relation(klass, *args) relation = super return relation unless klass.respond_to?(:paranoia_column) arel_paranoia_scope = klass.arel_table[klass.paranoia_column].eq(klass.paranoia_sentinel_value) if ActiveRecord::VERSION::STRING >= "5.0" relation.where(arel_paranoia_scope) else relation.and(arel_paranoia_scope) end end end class UniquenessValidator < ActiveModel::EachValidator prepend UniquenessParanoiaValidator end class AssociationNotSoftDestroyedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) # if association is soft destroyed, add an error if value.present? && value.paranoia_destroyed? record.errors[attribute] << 'has been soft-deleted' end end end end end paranoia-2.4.1/lib/paranoia/0000755000004100000410000000000013312077301015675 5ustar www-datawww-dataparanoia-2.4.1/lib/paranoia/version.rb0000644000004100000410000000005713312077301017711 0ustar www-datawww-datamodule Paranoia VERSION = '2.4.1'.freeze end paranoia-2.4.1/lib/paranoia/active_record_5_2.rb0000644000004100000410000000231513312077301021501 0ustar www-datawww-datamodule HandleParanoiaDestroyedInBelongsToAssociation def handle_dependency return unless load_target case options[:dependent] when :destroy target.destroy if target.respond_to?(:paranoia_destroyed?) raise ActiveRecord::Rollback unless target.paranoia_destroyed? else raise ActiveRecord::Rollback unless target.destroyed? end else target.send(options[:dependent]) end end end module HandleParanoiaDestroyedInHasOneAssociation def delete(method = options[:dependent]) if load_target case method when :delete target.delete when :destroy target.destroyed_by_association = reflection target.destroy if target.respond_to?(:paranoia_destroyed?) throw(:abort) unless target.paranoia_destroyed? else throw(:abort) unless target.destroyed? end when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end end ActiveRecord::Associations::BelongsToAssociation.prepend HandleParanoiaDestroyedInBelongsToAssociation ActiveRecord::Associations::HasOneAssociation.prepend HandleParanoiaDestroyedInHasOneAssociation paranoia-2.4.1/lib/paranoia/rspec.rb0000644000004100000410000000135213312077301017337 0ustar www-datawww-datarequire 'rspec/expectations' # Validate the subject's class did call "acts_as_paranoid" RSpec::Matchers.define :act_as_paranoid do match { |subject| subject.class.ancestors.include?(Paranoia) } failure_message_proc = lambda do "expected #{subject.class} to use `acts_as_paranoid`" end failure_message_when_negated_proc = lambda do "expected #{subject.class} not to use `acts_as_paranoid`" end if respond_to?(:failure_message_when_negated) failure_message(&failure_message_proc) failure_message_when_negated(&failure_message_when_negated_proc) else # RSpec 2 compatibility: failure_message_for_should(&failure_message_proc) failure_message_for_should_not(&failure_message_when_negated_proc) end end paranoia-2.4.1/CONTRIBUTING.md0000644000004100000410000000236613312077301015575 0ustar www-datawww-dataParanoia is an open source project and we encourage contributions. ## Filing an issue When filing an issue on the Paranoia project, please provide these details: * A comprehensive list of steps to reproduce the issue. * What you're *expecting* to happen compared with what's *actually* happening. * Your application's complete `Gemfile.lock`, and `Gemfile.lock` as text in a [Gist](https://gist.github.com) (*not as an image*) * Any relevant stack traces ("Full trace" preferred) In 99% of cases, this information is enough to determine the cause and solution to the problem that is being described. Please remember to format code using triple backticks (\`) so that it is neatly formatted when the issue is posted. ## Pull requests We gladly accept pull requests to add documentation, fix bugs and, in some circumstances, add new features to Paranoia. Here's a quick guide: 1. Fork the repo. 2. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate. 3. Create new branch then make changes and add tests for your changes. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need tests! 4. Push to your fork and submit a pull request. paranoia-2.4.1/Gemfile0000644000004100000410000000054413312077301014633 0ustar www-datawww-datasource 'https://rubygems.org' gem 'sqlite3', platforms: [:ruby] platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' end platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubysl-test-unit' gem 'rubinius-developer_tools' end rails = ENV['RAILS'] || '~> 5.2.0' gem 'rails', rails # Specify your gem's dependencies in paranoia.gemspec gemspec paranoia-2.4.1/paranoia.gemspec0000644000004100000410000000271313312077301016477 0ustar www-datawww-data# -*- encoding: utf-8 -*- require File.expand_path("../lib/paranoia/version", __FILE__) Gem::Specification.new do |s| s.name = "paranoia" s.version = Paranoia::VERSION s.platform = Gem::Platform::RUBY s.authors = %w(radarlistener@gmail.com) s.email = %w(ben@benmorgan.io john.hawthorn@gmail.com) s.homepage = "https://github.com/rubysherpas/paranoia" s.license = 'MIT' s.summary = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, 4, and 5, using much, much, much less code." s.description = <<-DSC Paranoia is a re-implementation of acts_as_paranoid for Rails 3, 4, and 5, 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. DSC s.required_rubygems_version = ">= 1.3.6" s.required_ruby_version = '>= 2.0' s.add_dependency 'activerecord', '>= 4.0', '< 5.3' s.add_development_dependency "bundler", ">= 1.0.0" s.add_development_dependency "rake" s.files = `git ls-files`.split("\n") s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact s.require_path = 'lib' end