paranoid_create 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ paranoid.db
2
+ .rvmrc
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source('http://rubygems.org')
2
+
3
+ # Specify your gem's dependencies in paranoid.gemspec
4
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Xspond Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,109 @@
1
+ h1. paranoid
2
+
3
+ h3. advice and disclaimer
4
+
5
+ You should never expect _any_ library to work or behave exactly how you want it to: test, test, test and file an issue if you have any problems. Bonus points if you include sample failing code. Extra bonus points if you send a pull request that implements a feature/fixes a bug.
6
+
7
+ h3. How did I get here?
8
+
9
+ Sometimes you want to delete something in ActiveRecord, but you realize you might need it later (for an undo feature, or just as a safety net, etc.). There are a plethora of plugins that accomplish this, the most famous of which was the venerable acts_as_paranoid which is great but not really actively developed any more. What's more, acts_as_paranoid was written for an older version of ActiveRecord. Is_paranoid was written for ActiveRecord 2.3 and default_scope. This however became, as the author stated, a mess of hacks to catch all the edge cases. *Paranoid* is an attempt to utilize ActiveRecord::Relation and JoinDependency in ActiveRecord 3 to do all the heavy lifting without using default_scope and with_exclusive_scope.
10
+
11
+ h3. How does it work?
12
+
13
+ You should read the specs, or the RDOC, or even the source itself (which is very readable), but for the lazy, here's the hand-holding:
14
+
15
+ You need ActiveRecord 3 and you need to properly install this gem. Then you need a model with a field to serve as a flag column on its database table. For this example we'll use a timestamp named "deleted_at". If that column is null, the item isn't deleted. If it has a timestamp, it should count as deleted.
16
+
17
+ So let's assume we have a model Automobile that has a deleted_at column on the automobiles table.
18
+
19
+ If you're working with Rails, in your Gemfile, add the following (you may want to change the version number).
20
+
21
+ <pre>
22
+ gem "paranoid", :require => 'paranoid', :version => ">= 0.1.0"
23
+ </pre>
24
+
25
+ Then in your ActiveRecord model
26
+
27
+ <pre>
28
+ class Automobile < ActiveRecord::Base
29
+ paranoid
30
+ end
31
+ </pre>
32
+
33
+ Now our automobiles are soft-deleteable.
34
+
35
+ <pre>
36
+ that_large_automobile = Automobile.create()
37
+ Automobile.count # => 1
38
+
39
+ that_large_automobile.destroy
40
+ Automobile.count # => 0
41
+ Automobile.with_destroyed.count # => 1
42
+
43
+ # where is that large automobile?
44
+ that_large_automobile = Automobile.with_destroyed.first
45
+ that_large_automobile.restore
46
+ Automobile.count # => 1
47
+ </pre>
48
+
49
+ One thing to note, destroying is always undo-able, but deleting is not. This is a behavior difference between acts_as_paranoid and paranoid and the same as is_paranoid.
50
+
51
+ <pre>
52
+ Automobile.destroy_all
53
+ Automobile.count # => 0
54
+ Automobile.with_destroyed.count # => 1
55
+
56
+ Automobile.delete_all
57
+ Automobile.with_destroyed.count # => 0
58
+ # And you may say to yourself, "My god! What have I done?"
59
+ </pre>
60
+
61
+ You can also lookup only destroyed record with with_destroyed_only.
62
+
63
+ <pre>
64
+ auto1 = Automobile.create()
65
+ auto2 = Automobile.create()
66
+ auto2.destroy
67
+ Automobile.count # => 1
68
+ Automobile.with_destroyed.count # => 2
69
+ Automobile.with_destroyed_only.count # => 1
70
+ Automobile.with_destroyed_only.first # => auto2
71
+ </pre>
72
+
73
+ h3. Specifying alternate rules for what should be considered destroyed
74
+
75
+ "deleted_at" as a timestamp is what acts_as_paranoid uses to define what is and isn't destroyed (see above), but you can specify alternate options with paranoid. In the paranoid line of your model you can specify the field, the value the field should have if the entry should count as destroyed, and the value the field should have if the entry is not destroyed. Consider the following models:
76
+
77
+ <pre>
78
+ class Pirate < ActiveRecord::Base
79
+ paranoid :field => [:alive, false, true]
80
+ end
81
+
82
+ class DeadPirate < ActiveRecord::Base
83
+ set_table_name :pirates
84
+ paranoid :field => [:alive, true, false]
85
+ end
86
+ </pre>
87
+
88
+ These two models share the same table, but when we are finding Pirates, we're only interested in those that are alive. To break it down, we specify :alive as our field to check, false as what the model field should be marked at when destroyed and true to what the field should be if they're not destroyed. DeadPirates are specified as the opposite. Check out the specs if you're still confused.
89
+
90
+ h3. Note about validates_uniqueness_of:
91
+
92
+ validates_uniqueness_of does not, by default, ignore items marked with a deleted_at (or other field name) flag. This is a behavior difference between paranoid and acts_as_paranoid and the same as is_paranoid. You can overcome this by specifying the field name you are using to mark destroyed items as your scope. Example:
93
+
94
+ <pre>
95
+ class Android < ActiveRecord::Base
96
+ validates_uniqueness_of :name, :scope => :deleted_at
97
+ paranoid
98
+ end
99
+ </pre>
100
+
101
+ And now the validates_uniqueness_of will ignore items that are destroyed.
102
+
103
+ h3. and you may ask yourself, where does that highway go to?
104
+
105
+ If you find any bugs, have any ideas of features you think are missing, or find things you're like to see work differently, feel free to file an issue or send a pull request.
106
+
107
+ h3. Thanks
108
+
109
+ Thanks to Rick Olson for acts_as_paranoid and to Jeffrey Chupp for is_paranoid.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require('bundler')
2
+ require('rspec/core/rake_task')
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc('Run RSpec')
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.verbose = false
9
+ end
10
+
11
+ task(:default => :spec)
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'paranoid'
@@ -0,0 +1,65 @@
1
+ module Paranoid
2
+ module Base
3
+ # Call this in your model to enable paranoid.
4
+ #
5
+ # === Examples
6
+ #
7
+ # Post < ActiveRecord::Base
8
+ # paranoid
9
+ # end
10
+ #
11
+ # Item < ActiveRecord::Base
12
+ # paranoid :field => [:available, fales, true]
13
+ # end
14
+ #
15
+ # === Options
16
+ #
17
+ # [:field]
18
+ # The field used to recognize a record as destroyed.
19
+ # Default: :deleted_at
20
+ # IsParanoid Compatibility: Also accepts an Array of form
21
+ # [field_name, destroyed_value, not_destroyed_value]
22
+ # however :destroyed_value and :not_destroyed_value will
23
+ # be ignored
24
+ #
25
+ # [:destroyed_value]
26
+ # The value to set the paranoid field to on destroy.
27
+ # Can be either a static value or a Proc which will be
28
+ # evaluated when destroy is called.
29
+ # Default: Proc.new{Time.now.utc}
30
+ #
31
+ # [:not_destroyed_value]
32
+ # The value used to recognize a record as not destroyed.
33
+ # Default: nil
34
+ def paranoid(opts = {})
35
+ return if paranoid?
36
+ @paranoid = true
37
+
38
+ opts[:field] ||= [:deleted_at, Proc.new{Time.now.utc}, nil]
39
+ class_inheritable_accessor :destroyed_field, :field_destroyed, :field_not_destroyed
40
+ if opts[:field].is_a?(Array)
41
+ self.destroyed_field, self.field_destroyed, self.field_not_destroyed = opts[:field]
42
+ else
43
+ self.destroyed_field = opts.key?(:field) ? opts[:field] : :deleted_at
44
+ self.field_destroyed = opts.key?(:destroyed_value) ? opts[:destroyed_value] : Proc.new{Time.now.utc}
45
+ self.field_not_destroyed = opts.key?(:not_destroyed_value) ? opts[:not_destroyed_value] : nil
46
+ end
47
+
48
+ include Paranoid::ParanoidMethods
49
+
50
+ class_eval do
51
+ class << self
52
+ delegate :with_destroyed, :with_destroyed_only, :to => :scoped
53
+ end
54
+ end
55
+ end
56
+
57
+ # Returns true if the model is paranoid and paranoid is enabled
58
+ def paranoid?
59
+ @paranoid = (self != ActiveRecord::Base && self.superclass.paranoid?) unless defined?(@paranoid)
60
+ @paranoid
61
+ end
62
+ end
63
+ end
64
+
65
+ ActiveRecord::Base.class_eval { extend Paranoid::Base }
@@ -0,0 +1,24 @@
1
+ module Paranoid
2
+ module JoinAssociation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :association_join, :paranoid
7
+ end
8
+
9
+ # Overrides ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation#association_join
10
+ # adding paranoid conditions when necessary
11
+ def association_join_with_paranoid
12
+ return @join if @join
13
+ result = association_join_without_paranoid
14
+ if reflection.klass.paranoid?
15
+ aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
16
+ pb = ActiveRecord::PredicateBuilder.new(arel_engine)
17
+ result.concat(pb.build_from_hash(reflection.klass.paranoid_condition, aliased_table))
18
+ end
19
+ result
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.class_eval { include Paranoid::JoinAssociation }
@@ -0,0 +1,92 @@
1
+ module Paranoid
2
+ module ParanoidMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ extend ClassMethods
7
+ alias_method_chain :create_or_update, :paranoid
8
+ end
9
+
10
+ module ClassMethods
11
+ # Returns the condition used to scope the return to exclude
12
+ # soft deleted records
13
+ def paranoid_condition
14
+ {destroyed_field => field_not_destroyed}
15
+ end
16
+
17
+ # Returns the condition used to scope the return to contain
18
+ # only soft deleted records
19
+ def paranoid_only_condition
20
+ val = field_not_destroyed.respond_to?(:call) ? field_not_destroyed.call : field_not_destroyed
21
+ column_sql = self.sanitize_sql_for_assignment(destroyed_field)
22
+ case val
23
+ when nil then "#{column_sql} IS NOT NULL"
24
+ else ["#{column_sql} != ?", val]
25
+ end
26
+ end
27
+
28
+ # Temporarily disables paranoid on the model
29
+ def disable_paranoid
30
+ if block_given?
31
+ @paranoid = false
32
+ yield
33
+ else
34
+ raise 'Only block form is supported'
35
+ end
36
+ ensure
37
+ @paranoid = true
38
+ end
39
+ end
40
+
41
+ # Restores the record
42
+ def restore
43
+ set_destroyed(field_not_destroyed.respond_to?(:call) ? field_not_destroyed.call : field_not_destroyed)
44
+
45
+ self.class.reflect_on_all_associations.each do |association|
46
+ if association.options[:dependent] == :destroy && association.klass.paranoid?
47
+ restore_related(association.klass, association.primary_key_name, association.options[:primary_key] || 'id', association.options) if association.macro.to_s =~ /^has/
48
+ end
49
+ end
50
+
51
+ @destroyed = false
52
+ self
53
+ end
54
+
55
+ # Override the default destroy to allow us to soft delete records.
56
+ # This preserves the before_destroy and after_destroy callbacks.
57
+ # Because this is also called internally by Model.destroy_all and
58
+ # the Model.destroy(id), we don't need to specify those methods
59
+ # separately.
60
+ def destroy
61
+ _run_destroy_callbacks do
62
+ set_destroyed(field_destroyed.respond_to?(:call) ? field_destroyed.call : field_destroyed)
63
+ @destroyed = true
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ protected
70
+
71
+ # Overrides ActiveRecord::Base#create_or_update
72
+ # to disable paranoid during the create and update operations
73
+ def create_or_update_with_paranoid
74
+ self.class.disable_paranoid { create_or_update_without_paranoid }
75
+ end
76
+
77
+ # Set the value for the destroyed field.
78
+ def set_destroyed(val)
79
+ self[destroyed_field] = val
80
+ updates = Arel::Nodes::SqlLiteral.new(self.class.send(:sanitize_sql_for_assignment, {destroyed_field => val}))
81
+ self.class.unscoped.with_destroyed.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(updates)
82
+ @destroyed = true
83
+ end
84
+
85
+ # Restores related records
86
+ def restore_related(klass, key_name, id_name, options)
87
+ klass.unscoped.with_destroyed_only.where(klass.arel_table[key_name].eq(send(id_name))).each do |model|
88
+ model.restore
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,81 @@
1
+ module Paranoid
2
+ module Relation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :arel, :paranoid
7
+ alias_method_chain :delete_all, :paranoid
8
+ alias_method_chain :except, :paranoid
9
+ alias_method_chain :only, :paranoid
10
+ end
11
+
12
+ # Returns true if the relation should be scoped to
13
+ # exclude soft deleted records
14
+ def add_paranoid_condition?
15
+ @add_paranoid = true unless defined?(@add_paranoid)
16
+ @klass.paranoid? && @add_paranoid
17
+ end
18
+
19
+ # Overrides ActiveRecord::Relation#arel
20
+ def arel_with_paranoid
21
+ if add_paranoid_condition?
22
+ @arel ||= without_destroyed.arel_without_paranoid
23
+ else
24
+ arel_without_paranoid
25
+ end
26
+ end
27
+
28
+ # Overrides ActiveRecord::Relation#delete_all
29
+ # forcing delete_all to ignore deleted flag
30
+ def delete_all_with_paranoid(*args)
31
+ if add_paranoid_condition?
32
+ with_destroyed.delete_all_without_paranoid(*args)
33
+ else
34
+ delete_all_without_paranoid(*args)
35
+ end
36
+ end
37
+
38
+ # Overrides ActiveRecord::Relation#except
39
+ def except_with_paranoid(*args)
40
+ result = except_without_paranoid(*args)
41
+ result.instance_variable_set(:@add_paranoid, @add_paranoid) if defined?(@add_paranoid)
42
+ result
43
+ end
44
+
45
+ # Overrides ActiveRecord::Relation#only
46
+ def only_with_paranoid(*args)
47
+ result = only_without_paranoid(*args)
48
+ result.instance_variable_set(:@add_paranoid, @add_paranoid) if defined?(@add_paranoid)
49
+ result
50
+ end
51
+
52
+ # Returns a new relation scoped to include soft deleted records
53
+ def with_destroyed
54
+ clone.tap {|relation| relation.skip_paranoid_condition }
55
+ end
56
+
57
+ # Returns a new relation scoped to include only deleted records
58
+ def with_destroyed_only
59
+ where(@klass.paranoid_only_condition).tap {|relation| relation.skip_paranoid_condition }
60
+ end
61
+
62
+ # Can be used to force the exclusion of soft deleted records down
63
+ # the chain from a with_destroyed call. *WARNING*: with_destroyed
64
+ # will do nothing after this has been called! So
65
+ # Model.without_destroyed.with_destroyed.all will *NOT* return
66
+ # soft deleted records
67
+ def without_destroyed
68
+ where(@klass.paranoid_condition).tap {|relation| relation.skip_paranoid_condition }
69
+ end
70
+
71
+ protected
72
+
73
+ # Tell the relation to skip adding the paranoid conditions. DO NOT
74
+ # call directly. Call with_destroyed.
75
+ def skip_paranoid_condition
76
+ @add_paranoid = false
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveRecord::Relation.class_eval { include Paranoid::Relation }
@@ -0,0 +1,3 @@
1
+ module Paranoid
2
+ VERSION = '0.0.10'
3
+ end
data/lib/paranoid.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'active_record'
2
+ require 'paranoid/base'
3
+ require 'paranoid/paranoid_methods'
4
+ require 'paranoid/relation'
5
+ require 'paranoid/join_association'
data/paranoid.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # -*- encoding: utf-8 -*-
4
+ $:.push(File.expand_path('../lib', __FILE__))
5
+ require('paranoid/version')
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'paranoid_create'
9
+ s.version = Paranoid::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ['Philipp Ullmann']
12
+ s.email = 'philipp.ullmann@create.at'
13
+ s.homepage = 'http://github.com/create-philipp-ullmann/paranoid/'
14
+ s.summary = 'Enable soft delete of ActiveRecord records. Based off defunct ActsAsParanoid and IsParanoid'
15
+
16
+ s.rubyforge_project = 'paranoid_create'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ['lib']
22
+
23
+ s.add_development_dependency('rspec', ['>= 2.5.0'])
24
+ s.add_development_dependency('sqlite3-ruby', ['>= 1.3.3'])
25
+ s.add_development_dependency('activerecord', ['>= 3.0.5'])
26
+ end