From fd7590438b3a0abb0cca8854f575de598d4699c3 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Thu, 28 Jul 2016 09:59:52 -0500 Subject: [PATCH 01/10] Update homepage in gemspec The homepage is supposed to be where you can find the code. It is displayed on rubygems.org page. Currently it will redirect you back to the same page. Should point at this github repo. --- paranoia.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paranoia.gemspec b/paranoia.gemspec index 9887d846..62ce3d50 100644 --- a/paranoia.gemspec +++ b/paranoia.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.authors = %w(radarlistener@gmail.com) s.email = %w(ben@benmorgan.io john.hawthorn@gmail.com) - s.homepage = "http://rubygems.org/gems/paranoia" + 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 From 44e018d9caf241191a10e6a3f763762784a734f1 Mon Sep 17 00:00:00 2001 From: Noah Matisoff Date: Sat, 20 Aug 2016 16:31:34 -0700 Subject: [PATCH 02/10] Update README to use proper version for Rails 5 --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a3162c3..6020b3ae 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,18 @@ For Rails 3, please use version 1 of Paranoia: gem "paranoia", "~> 1.0" ``` -For Rails 4 or 5, please use version 2 of Paranoia: +For Rails 4, please use version 2 of Paranoia: ``` ruby gem "paranoia", "~> 2.0" ``` +For Rails 5, please use version 2.2.0.pre (pre-release): + +``` ruby +gem "paranoia", "~> 2.2.0.pre" +``` + Of course you can install this from GitHub as well from one of these examples: ``` ruby From 3efb5583f061226801e88d0813af391233b80d56 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 20 Oct 2016 12:59:08 -0700 Subject: [PATCH 03/10] Version 2.2.0 --- README.md | 10 ++-------- lib/paranoia/version.rb | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6020b3ae..46dfd898 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,10 @@ For Rails 3, please use version 1 of Paranoia: gem "paranoia", "~> 1.0" ``` -For Rails 4, please use version 2 of Paranoia: +For Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5): ``` ruby -gem "paranoia", "~> 2.0" -``` - -For Rails 5, please use version 2.2.0.pre (pre-release): - -``` ruby -gem "paranoia", "~> 2.2.0.pre" +gem "paranoia", "~> 2.2" ``` Of course you can install this from GitHub as well from one of these examples: diff --git a/lib/paranoia/version.rb b/lib/paranoia/version.rb index a4ba7155..667a0a78 100644 --- a/lib/paranoia/version.rb +++ b/lib/paranoia/version.rb @@ -1,3 +1,3 @@ module Paranoia - VERSION = "2.2.0.pre" + VERSION = "2.2.0" end From 795ecf49b710b8e77b16d0246c364e7f52efabca Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 20 Oct 2016 13:10:05 -0700 Subject: [PATCH 04/10] Ignore failures from all jruby's on travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 429f6b76..911d9726 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,5 +18,9 @@ matrix: - env: RAILS='~> 5.0.0' rvm: 2.1.10 allow_failures: + - env: RAILS='~> 4.1.15' + rvm: jruby-9.1.0.0 + - env: RAILS='~> 4.2.6' + rvm: jruby-9.1.0.0 - env: RAILS='~> 5.0.0' rvm: jruby-9.1.0.0 From ed14d3d9cd9ff742a63db732168450e01a0a08bd Mon Sep 17 00:00:00 2001 From: "Mark J. Lehman" Date: Tue, 29 Nov 2016 12:07:30 -0800 Subject: [PATCH 05/10] Add explicit language about dependent: :destroy --- README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46dfd898..faeadf41 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ When your app is using Paranoia, calling `destroy` on an ActiveRecord object doe If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: :destroy` records, so please aim this method away from face when using. -If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called. +If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called. ***See [Destroying through association callbacks](#destroying-through-association-callbacks) for clarifying examples.*** ## Getting Started Video Setup and basic usage of the paranoia gem @@ -245,6 +245,77 @@ class Client < ActiveRecord::Base 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: From 78fa25a00f685d8f12d422bded1ceb93d9ba3433 Mon Sep 17 00:00:00 2001 From: Richard Lee Date: Mon, 5 Dec 2016 00:27:04 +0800 Subject: [PATCH 06/10] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e8ca18..865c4c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # paranoia Changelog -## 2.2.0 (unreleased) +## 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) From 9b798bf67a62997ef4717e9d11d5648607579241 Mon Sep 17 00:00:00 2001 From: "Ben A. Morgan" Date: Tue, 29 Nov 2016 15:56:32 -0500 Subject: [PATCH 07/10] update ruby and rails versions --- .travis.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 911d9726..2909e810 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,24 +3,24 @@ language: ruby cache: bundler rvm: - 2.1.10 - - 2.2.5 - - 2.3.1 - - jruby-9.1.0.0 + - 2.2.6 + - 2.3.3 + - jruby-9.1.6.0 env: matrix: - - RAILS='~> 4.1.15' - - RAILS='~> 4.2.6' - - RAILS='~> 5.0.0' + - RAILS='~> 4.1.16' + - RAILS='~> 4.2.7.1' + - RAILS='~> 5.0.0.1' matrix: exclude: - - env: RAILS='~> 5.0.0' + - env: RAILS='~> 5.0.0.1' rvm: 2.1.10 allow_failures: - - env: RAILS='~> 4.1.15' - rvm: jruby-9.1.0.0 - - env: RAILS='~> 4.2.6' - rvm: jruby-9.1.0.0 - - env: RAILS='~> 5.0.0' - rvm: jruby-9.1.0.0 + - env: RAILS='~> 4.1.16' + rvm: jruby-9.1.6.0 + - env: RAILS='~> 4.2.7.1' + rvm: jruby-9.1.6.0 + - env: RAILS='~> 5.0.0.1' + rvm: jruby-9.1.6.0 From 07ec7f6e68e61f0dfd9024ea01cdeae1a90533fa Mon Sep 17 00:00:00 2001 From: Iaan Krynauw Date: Mon, 12 Dec 2016 10:03:48 +0200 Subject: [PATCH 08/10] Use ActiveSupport.on_load to correctly re-open ActiveRecord::Base. https://github.com/rubysherpas/paranoia/issues/335 --- lib/paranoia.rb | 78 +++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/paranoia.rb b/lib/paranoia.rb index 489d2d62..0c0e3657 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -205,56 +205,58 @@ def restore_associated_records end end -class ActiveRecord::Base - def self.acts_as_paranoid(options={}) - 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 +ActiveSupport.on_load(:active_record) do + class ActiveRecord::Base + def self.acts_as_paranoid(options={}) + 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 + 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 + 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 + # 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 + end - def self.paranoid? ; false ; end - def paranoid? ; self.class.paranoid? ; end + def self.paranoid? ; false ; end + def paranoid? ; self.class.paranoid? ; end - private + private - def paranoia_column - self.class.paranoia_column - end + def paranoia_column + self.class.paranoia_column + end - def paranoia_sentinel_value - self.class.paranoia_sentinel_value + def paranoia_sentinel_value + self.class.paranoia_sentinel_value + end end end From 4128c201fbfbf08f52deedee15fb9469a7f8f9eb Mon Sep 17 00:00:00 2001 From: rbr Date: Sat, 28 Jan 2017 20:15:02 +0100 Subject: [PATCH 09/10] Touch record on paranoia-destroy. Fixes #296 Touch record on destroy by leveraging the paranoia_destroy_attributes. Applied the same to the restore-method as this eliminates the extra query. --- lib/paranoia.rb | 9 ++++++--- test/paranoia_test.rb | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/paranoia.rb b/lib/paranoia.rb index 0c0e3657..7a7a99e7 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -110,7 +110,6 @@ def restore!(opts = {}) if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen write_attribute paranoia_column, paranoia_sentinel_value update_columns(paranoia_restore_attributes) - touch end restore_associated_records if opts[:recursive] end @@ -154,13 +153,17 @@ def really_destroy! 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 diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index a1bceeee..3951aac5 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -774,6 +774,13 @@ def test_validates_uniqueness_still_works_on_non_deleted_records 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) From a756b208ea6df26eebeeea81b6b4da07141f0364 Mon Sep 17 00:00:00 2001 From: Thomas Romera Date: Tue, 19 Jul 2016 11:06:45 +0200 Subject: [PATCH 10/10] Fixes a problem of ambiguous table names when using only_deleted method and joining tables that have a scope on `with_deleted` Update homepage in gemspec The homepage is supposed to be where you can find the code. It is displayed on rubygems.org page. Currently it will redirect you back to the same page. Should point at this github repo. Update README to use proper version for Rails 5 Version 2.2.0 Ignore failures from all jruby's on travis Add explicit language about dependent: :destroy Update CHANGELOG.md update ruby and rails versions Use ActiveSupport.on_load to correctly re-open ActiveRecord::Base. https://github.com/rubysherpas/paranoia/issues/335 Touch record on paranoia-destroy. Fixes #296 Touch record on destroy by leveraging the paranoia_destroy_attributes. Applied the same to the restore-method as this eliminates the extra query. --- lib/paranoia.rb | 5 ++-- test/paranoia_test.rb | 57 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/paranoia.rb b/lib/paranoia.rb index 7a7a99e7..7db3b8e9 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -35,8 +35,9 @@ def only_deleted # 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 - quoted_paranoia_column = connection.quote_column_name(paranoia_column) - with_deleted.where("#{quoted_paranoia_column} IS NULL OR #{quoted_paranoia_column} != ?", paranoia_sentinel_value) + # 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 diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index 3951aac5..429a1177 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -40,6 +40,8 @@ def setup! '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})" @@ -182,8 +184,11 @@ def test_scoping_behavior_for_paranoid_models 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) @@ -192,6 +197,17 @@ def test_scoping_behavior_for_paranoid_models 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 @@ -1112,6 +1128,45 @@ def paranoia_destroy_attributes 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 @@ -1215,4 +1270,4 @@ class ParanoidBelongsTo < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_has_one end -end +end \ No newline at end of file