cmoran92-paranoia 2.0.2a

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ tmp
5
+ .rvmrc
6
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - jruby-19mode
7
+
8
+ env:
9
+ - RAILS='~> 4.0.8'
10
+ - RAILS='~> 4.1.4'
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sqlite3', :platforms => [:ruby]
4
+ gem 'activerecord-jdbcsqlite3-adapter', :platforms => [:jruby]
5
+
6
+ platforms :rbx do
7
+ gem 'rubysl', '~> 2.0'
8
+ gem 'rubysl-test-unit'
9
+ gem 'rubinius-developer_tools'
10
+ end
11
+
12
+ rails = ENV['RAILS'] || '~> 4.1.4'
13
+
14
+ gem 'rails', rails
15
+
16
+ # Specify your gem's dependencies in paranoia.gemspec
17
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Permission is hereby granted, without written agreement and without
2
+ license or royalty fees, to use, copy, modify, and distribute this
3
+ software and its documentation for any purpose, provided that the
4
+ above copyright notice and the following two paragraphs appear in
5
+ all copies of this software.
6
+
7
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
8
+ DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
9
+ ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
10
+ IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
11
+ DAMAGE.
12
+
13
+ THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14
+ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
15
+ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
16
+ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
17
+ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
data/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # Paranoia
2
+
3
+ Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/technoweenie/acts_as_paranoid) for Rails 3 and Rails 4, using much, much, much less code.
4
+
5
+ You would use either plugin / gem if you wished that when you called `destroy` on an Active Record object that it didn't actually destroy it, but just *hide* the record. Paranoia does this by setting a `deleted_at` field to the current time when you `destroy` a record, and hides it by scoping all queries on your model to only include records which do not have a `deleted_at` field.
6
+
7
+ If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: destroy` records, so please aim this method away from face when using.**
8
+
9
+ If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called.
10
+
11
+ ## Installation & Usage
12
+
13
+ For Rails 3, please use version 1 of Paranoia:
14
+
15
+ ``` ruby
16
+ gem "paranoia", "~> 1.0"
17
+ ```
18
+
19
+ For Rails 4, please use version 2 of Paranoia:
20
+
21
+ ``` ruby
22
+ gem "paranoia", "~> 2.0"
23
+ ```
24
+
25
+ Of course you can install this from GitHub as well:
26
+
27
+ ``` ruby
28
+ gem "paranoia", :github => "radar/paranoia", :branch => "master"
29
+ # or
30
+ gem "paranoia", :github => "radar/paranoia", :branch => "rails4"
31
+ ```
32
+
33
+ Then run:
34
+
35
+ ``` shell
36
+ bundle install
37
+ ```
38
+
39
+ Updating is as simple as `bundle update paranoia`.
40
+
41
+ #### Run your migrations for the desired models
42
+
43
+ Run:
44
+
45
+ ``` shell
46
+ rails generate migration AddDeletedAtToClients deleted_at:datetime:index
47
+ ```
48
+
49
+ and now you have a migration
50
+
51
+ ``` ruby
52
+ class AddDeletedAtToClients < ActiveRecord::Migration
53
+ def change
54
+ add_column :clients, :deleted_at, :datetime
55
+ add_index :clients, :deleted_at
56
+ end
57
+ end
58
+ ```
59
+
60
+ ### Usage
61
+
62
+ #### In your model:
63
+
64
+ ``` ruby
65
+ class Client < ActiveRecord::Base
66
+ acts_as_paranoid
67
+
68
+ # ...
69
+ end
70
+ ```
71
+
72
+ Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column:
73
+
74
+
75
+ ``` ruby
76
+ >> client.deleted_at
77
+ # => nil
78
+ >> client.destroy
79
+ # => client
80
+ >> client.deleted_at
81
+ # => [current timestamp]
82
+ ```
83
+
84
+ If you really want it gone *gone*, call `really_destroy!`:
85
+
86
+ ``` ruby
87
+ >> client.deleted_at
88
+ # => nil
89
+ >> client.really_destroy!
90
+ # => client
91
+ ```
92
+
93
+ If you want a method to be called on destroy, simply provide a `before_destroy` callback:
94
+
95
+ ``` ruby
96
+ class Client < ActiveRecord::Base
97
+ acts_as_paranoid
98
+
99
+ before_destroy :some_method
100
+
101
+ def some_method
102
+ # do stuff
103
+ end
104
+
105
+ # ...
106
+ end
107
+ ```
108
+
109
+ If you want to use a column other than `deleted_at`, you can pass it as an option:
110
+
111
+ ``` ruby
112
+ class Client < ActiveRecord::Base
113
+ acts_as_paranoid column: :destroyed_at
114
+
115
+ ...
116
+ end
117
+ ```
118
+
119
+ If you want to access soft-deleted associations, override the getter method:
120
+
121
+ ``` ruby
122
+ def product
123
+ Product.unscoped { super }
124
+ end
125
+ ```
126
+
127
+ If you want to find all records, even those which are deleted:
128
+
129
+ ``` ruby
130
+ Client.with_deleted
131
+ ```
132
+
133
+ If you want to find only the deleted records:
134
+
135
+ ``` ruby
136
+ Client.only_deleted
137
+ ```
138
+
139
+ If you want to check if a record is soft-deleted:
140
+
141
+ ``` ruby
142
+ client.destroyed?
143
+ ```
144
+
145
+ If you want to restore a record:
146
+
147
+ ``` ruby
148
+ Client.restore(id)
149
+ ```
150
+
151
+ If you want to restore a whole bunch of records:
152
+
153
+ ``` ruby
154
+ Client.restore([id1, id2, ..., idN])
155
+ ```
156
+
157
+ If you want to restore a record and their dependently destroyed associated records:
158
+
159
+ ``` ruby
160
+ Client.restore(id, :recursive => true)
161
+ ```
162
+
163
+ If you want callbacks to trigger before a restore:
164
+
165
+ ``` ruby
166
+ before_restore :callback_name_goes_here
167
+ ```
168
+
169
+ For more information, please look at the tests.
170
+
171
+ ## Acts As Paranoid Migration
172
+
173
+ You can replace the older `acts_as_paranoid` methods as follows:
174
+
175
+ | Old Syntax | New Syntax |
176
+ |:-------------------------- |:------------------------------ |
177
+ |`find_with_deleted(:all)` | `Client.with_deleted` |
178
+ |`find_with_deleted(:first)` | `Client.with_deleted.first` |
179
+ |`find_with_deleted(id)` | `Client.with_deleted.find(id)` |
180
+
181
+
182
+ The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's
183
+ `restore` method does not do this.
184
+
185
+ ## Support for Unique Keys with Null Values
186
+
187
+ Most databases ignore null columns when it comes to resolving unique index
188
+ constraints. This means unique constraints that involve nullable columns may be
189
+ problematic. Instead of using `NULL` to represent a not-deleted row, you can pick
190
+ a value that you want paranoia to mean not deleted. Note that you can/should
191
+ now apply a `NOT NULL` constraint to your `deleted_at` column.
192
+
193
+ Per model:
194
+
195
+ ```ruby
196
+ # pick some value
197
+ acts_as_paranoid sentinel_value: DateTime.new(0)
198
+ ```
199
+
200
+ or globally in a rails initializer, e.g. `config/initializer/paranoia.rb`
201
+
202
+ ```ruby
203
+ Paranoia.default_sentinel_value = DateTime.new(0)
204
+ ```
205
+
206
+ ## License
207
+
208
+ This gem is released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :test do
5
+ Dir['test/*_test.rb'].each do |testfile|
6
+ load testfile
7
+ end
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,13 @@
1
+ require 'rspec/expectations'
2
+
3
+ # Validate the subject's class did call "acts_as_paranoid"
4
+ RSpec::Matchers.define :act_as_paranoid do
5
+ match { |subject| subject.class.ancestors.include?(Paranoia) }
6
+
7
+ failure_message { "expected #{subject.class} to use `acts_as_paranoid`" }
8
+ failure_message_when_negated { "expected #{subject.class} not to use `acts_as_paranoid`" }
9
+
10
+ # RSpec 2 compatibility:
11
+ alias_method :failure_message_for_should, :failure_message
12
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
13
+ end
@@ -0,0 +1,3 @@
1
+ module Paranoia
2
+ VERSION = "2.0.2a"
3
+ end
data/lib/paranoia.rb ADDED
@@ -0,0 +1,227 @@
1
+ require 'active_record' unless defined? ActiveRecord
2
+
3
+ module Paranoia
4
+ @@default_sentinel_value = nil
5
+
6
+ # Change default_sentinel_value in a rails initilizer
7
+ def self.default_sentinel_value=(val)
8
+ @@default_sentinel_value = val
9
+ end
10
+
11
+ def self.default_sentinel_value
12
+ @@default_sentinel_value
13
+ end
14
+
15
+ def self.included(klazz)
16
+ klazz.extend Query
17
+ klazz.extend Callbacks
18
+ end
19
+
20
+ module Query
21
+ def paranoid? ; true ; end
22
+
23
+ def with_deleted
24
+ if ActiveRecord::VERSION::STRING >= "4.1"
25
+ unscope where: paranoia_column
26
+ else
27
+ all.tap { |x| x.default_scoped = false }
28
+ end
29
+ end
30
+
31
+ def only_deleted
32
+ with_deleted.where.not(table_name => { paranoia_column => paranoia_sentinel_value} )
33
+ end
34
+ alias :deleted :only_deleted
35
+
36
+ def restore(id, opts = {})
37
+ Array(id).flatten.map { |one_id| only_deleted.find(one_id).restore!(opts) }
38
+ end
39
+ end
40
+
41
+ module Callbacks
42
+ def self.extended(klazz)
43
+ klazz.define_callbacks :restore
44
+
45
+ klazz.define_singleton_method("before_restore") do |*args, &block|
46
+ set_callback(:restore, :before, *args, &block)
47
+ end
48
+
49
+ klazz.define_singleton_method("around_restore") do |*args, &block|
50
+ set_callback(:restore, :around, *args, &block)
51
+ end
52
+
53
+ klazz.define_singleton_method("after_restore") do |*args, &block|
54
+ set_callback(:restore, :after, *args, &block)
55
+ end
56
+ end
57
+ end
58
+
59
+ def destroy
60
+ callbacks_result = transaction do
61
+ run_callbacks(:destroy) do
62
+ touch_paranoia_column
63
+ end
64
+ end
65
+ callbacks_result ? self : false
66
+ end
67
+
68
+ # As of Rails 4.1.0 +destroy!+ will no longer remove the record from the db
69
+ # unless you touch the paranoia column before.
70
+ # We need to override it here otherwise children records might be removed
71
+ # when they shouldn't
72
+ if ActiveRecord::VERSION::STRING >= "4.1"
73
+ def destroy!
74
+ destroyed? ? super : destroy || raise(ActiveRecord::RecordNotDestroyed)
75
+ end
76
+ end
77
+
78
+ def delete
79
+ return if new_record?
80
+ touch_paranoia_column(false)
81
+ end
82
+
83
+ def restore!(opts = {})
84
+ self.class.transaction do
85
+ run_callbacks(:restore) do
86
+ # Fixes a bug where the build would error because attributes were frozen.
87
+ # This only happened on Rails versions earlier than 4.1.
88
+ noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1")
89
+ if (noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen
90
+ write_attribute paranoia_column, paranoia_sentinel_value
91
+ update_column paranoia_column, paranoia_sentinel_value
92
+ end
93
+ restore_associated_records if opts[:recursive]
94
+ end
95
+ end
96
+
97
+ self
98
+ end
99
+ alias :restore :restore!
100
+
101
+ def destroyed?
102
+ send(paranoia_column) != paranoia_sentinel_value
103
+ end
104
+ alias :deleted? :destroyed?
105
+
106
+ private
107
+
108
+ # touch paranoia column.
109
+ # insert time to paranoia column.
110
+ # @param with_transaction [Boolean] exec with ActiveRecord Transactions.
111
+ def touch_paranoia_column(with_transaction=false)
112
+ # This method is (potentially) called from really_destroy
113
+ # The object the method is being called on may be frozen
114
+ # Let's not touch it if it's frozen.
115
+ unless self.frozen?
116
+ if with_transaction
117
+ with_transaction_returning_status { touch(paranoia_column) }
118
+ else
119
+ touch(paranoia_column)
120
+ end
121
+ end
122
+ end
123
+
124
+ # restore associated records that have been soft deleted when
125
+ # we called #destroy
126
+ def restore_associated_records
127
+ destroyed_associations = self.class.reflect_on_all_associations.select do |association|
128
+ association.options[:dependent] == :destroy
129
+ end
130
+
131
+ destroyed_associations.each do |association|
132
+ association_data = send(association.name)
133
+
134
+ unless association_data.nil?
135
+ if association_data.paranoid?
136
+ if association.collection?
137
+ association_data.only_deleted.each { |record| record.restore(:recursive => true) }
138
+ else
139
+ association_data.restore(:recursive => true)
140
+ end
141
+ end
142
+ end
143
+
144
+ if association_data.nil? && association.macro.to_s == "has_one"
145
+ association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
146
+ association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
147
+ Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
148
+ end
149
+ end
150
+
151
+ clear_association_cache if destroyed_associations.present?
152
+ end
153
+ end
154
+
155
+ class ActiveRecord::Base
156
+ def self.acts_as_paranoid(options={})
157
+ alias :destroy! :destroy
158
+ alias :delete! :delete
159
+ def really_destroy!
160
+ dependent_reflections = self.class.reflections.select do |name, reflection|
161
+ reflection.options[:dependent] == :destroy
162
+ end
163
+ if dependent_reflections.any?
164
+ dependent_reflections.each do |name, _|
165
+ associated_records = self.send(name)
166
+ # has_one association can return nil
167
+ if associated_records && associated_records.respond_to?(:with_deleted)
168
+ # Paranoid models will have this method, non-paranoid models will not
169
+ associated_records.with_deleted.each(&:really_destroy!)
170
+ self.send(name).reload
171
+ elsif associated_records && !associated_records.respond_to?(:each) # single record
172
+ associated_records.really_destroy!
173
+ end
174
+ end
175
+ end
176
+ touch_paranoia_column if ActiveRecord::VERSION::STRING >= "4.1"
177
+ destroy!
178
+ end
179
+
180
+ include Paranoia
181
+ class_attribute :paranoia_column, :paranoia_sentinel_value
182
+
183
+ self.paranoia_column = (options[:column] || :deleted_at).to_s
184
+ self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value }
185
+ default_scope { where(table_name => { paranoia_column => paranoia_sentinel_value }) }
186
+
187
+ before_restore {
188
+ self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers)
189
+ }
190
+ after_restore {
191
+ self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers)
192
+ }
193
+ end
194
+
195
+ # Please do not use this method in production.
196
+ # Pretty please.
197
+ def self.I_AM_THE_DESTROYER!
198
+ # TODO: actually implement spelling error fixes
199
+ puts %Q{
200
+ Sharon: "There should be a method called I_AM_THE_DESTROYER!"
201
+ Ryan: "What should this method do?"
202
+ Sharon: "It should fix all the spelling errors on the page!"
203
+ }
204
+ end
205
+
206
+ def self.paranoid? ; false ; end
207
+ def paranoid? ; self.class.paranoid? ; end
208
+
209
+ # Override the persisted method to allow for the paranoia gem.
210
+ # If a paranoid record is selected, then we only want to check
211
+ # if it's a new record, not if it is "destroyed".
212
+ def persisted?
213
+ paranoid? ? !new_record? : super
214
+ end
215
+
216
+ private
217
+
218
+ def paranoia_column
219
+ self.class.paranoia_column
220
+ end
221
+
222
+ def paranoia_sentinel_value
223
+ self.class.paranoia_sentinel_value
224
+ end
225
+ end
226
+
227
+ require 'paranoia/rspec' if defined? RSpec
data/paranoia.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/paranoia/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "cmoran92-paranoia"
6
+ s.version = Paranoia::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["radarlistener@gmail.com"]
9
+ s.email = []
10
+ s.homepage = "http://rubygems.org/gems/paranoia"
11
+ s.summary = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, using much, much, much less code."
12
+ s.description = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, using much, much, much less code. You would use either plugin / gem if you wished that when you called destroy on an Active Record object that it didn't actually destroy it, but just \"hid\" the record. Paranoia does this by setting a deleted_at field to the current time when you destroy a record, and hides it by scoping all queries on your model to only include records which do not have a deleted_at field."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.rubyforge_project = "paranoia"
16
+
17
+ s.add_dependency "activerecord", "~> 4.0"
18
+
19
+ s.add_development_dependency "bundler", ">= 1.0.0"
20
+ s.add_development_dependency "rake"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
24
+ s.require_path = 'lib'
25
+ end