cmoran92-paranoia 2.0.2a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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