misterbojangles-rails3_acts_as_paranoid 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Gonçalo Silva
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,113 @@
1
+ # ActsAsParanoid
2
+
3
+ A simple plugin which hides records instead of deleting them, being able to recover them.
4
+
5
+ ## Credits
6
+
7
+ This plugin was inspired by [acts_as_paranoid](http://github.com/technoweenie/acts_as_paranoid) and [acts_as_active](http://github.com/fernandoluizao/acts_as_active).
8
+
9
+ While porting it to Rails 3, I decided to apply the ideas behind those plugins to an unified solution while removing a **lot** of the complexity found in them. I eventually ended up writing a new plugin from scratch.
10
+
11
+ ## Usage
12
+
13
+ You can enable ActsAsParanoid like this:
14
+
15
+ class Paranoiac < ActiveRecord::Base
16
+ acts_as_paranoid
17
+ end
18
+
19
+ ### Options
20
+
21
+ You can also specify the name of the column to store it's *deletion* and the type of data it holds:
22
+
23
+ - :column => 'deleted_at'
24
+ - :type => 'time'
25
+
26
+ The values shown are the defaults. While *column* can be anything (as long as it exists in your database), *type* is restricted to "boolean" or "time".
27
+
28
+ ### Filtering
29
+
30
+ If a record is deleted by ActsAsParanoid, it won't be retrieved when accessing the database. So, `Paranoiac.all` will **not** include the deleted_records. if you want to access them, you have 2 choices:
31
+
32
+ Paranoiac.only_deleted # retrieves the deleted records
33
+ Paranoiac.with_deleted # retrieves all records, deleted or not
34
+
35
+ ### Real deletion
36
+
37
+ In order to really delete a record, just use:
38
+
39
+ paranoiac.destroy!
40
+ Paranoiac.delete_all!(conditions)
41
+
42
+ You can also definitively delete a record by calling `destroy` or `delete_all` on it twice. If a record was already deleted (hidden by ActsAsParanoid) and you delete it again, it will be removed from the database. Take this example:
43
+
44
+ Paranoiac.first.destroy # does NOT delete the first record, just hides it
45
+ Paranoiac.only_deleted.destroy # deletes the first record from the database
46
+
47
+ ### Recovery
48
+
49
+ Recovery is easy. Just invoke `recover` on it, like this:
50
+
51
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover
52
+
53
+ All associations marked as `:dependent => :destroy` are also recursively recovered. If you would like to disable this behavior, you can call `recover` with the `recursive` option:
54
+
55
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recursive => false)
56
+
57
+ If you would like to change the default behavior for a model, you can use the `recover_dependent_associations` option
58
+
59
+ class Paranoiac < ActiveRecord::Base
60
+ acts_as_paranoid :recover_dependent_associations => false
61
+ end
62
+
63
+ By default when using timestamp fields to mark deletion, dependent records will be recovered if they were deleted within 5 seconds of the object upon which they depend. This restores the objects to the state before the recursive deletion without restoring other objects that were deleted earlier. This window can be changed with the `dependent_recovery_window` option
64
+
65
+ class Paranoiac < ActiveRecord::Base
66
+ acts_as_paranoid
67
+ has_many :paranoids, :dependent => :destroy
68
+ end
69
+
70
+ class Paranoid < ActiveRecord::Base
71
+ belongs_to :paranoic
72
+
73
+ # Paranoid objects will be recovered alongside Paranoic objects
74
+ # if they were deleted within 1 minute of the Paranoic object
75
+ acts_as_paranoid :dependent_recovery_window => 1.minute
76
+ end
77
+
78
+ or in the recover statement
79
+
80
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recovery_window => 30.seconds)
81
+
82
+ ### Validation
83
+ ActiveRecord's built-in uniqueness validation does not account for records deleted by ActsAsParanoid. If you want to check for uniqueness among non-deleted records only, use the macro `validates_as_paranoid` in your model. Then, instead of using `validates_uniqueness_of`, use `validates_uniqueness_of_without_deleted`. This will keep deleted records from counting against the uniqueness check.
84
+
85
+ class Paranoiac < ActiveRecord::Base
86
+ acts_as_paranoid
87
+ validates_as_paranoid
88
+ validates_uniqueness_of_without_deleted :name
89
+ end
90
+
91
+ Paranoiac.create(:name => 'foo').destroy
92
+ Paranoiac.new(:name => 'foo').valid? #=> true
93
+
94
+
95
+ ### Status
96
+ Once you retrieve data using `with_deleted` scope you can check deletion status using `deleted?` helper:
97
+
98
+ Paranoiac.create(:name => 'foo').destroy
99
+ Paranoiac.with_deleted.first.deleted? #=> true
100
+
101
+ ## Caveats
102
+
103
+ Watch out for these caveats:
104
+
105
+ - You cannot use default\_scope in your model. It is possible to work around this caveat, but it's not pretty. Have a look at [this article](http://joshuaclayton.github.com/code/default_scope/activerecord/is_paranoid/multiple-default-scopes.html) if you really need to have your own default scope.
106
+ - You cannot use scopes named `with_deleted` and `only_deleted`
107
+
108
+ ## Acknowledgements
109
+
110
+ * To [cheerfulstoic](https://github.com/cheerfulstoic) for adding recursive recovery
111
+ * To [Jonathan Vaught](https://github.com/gravelpup) for adding paranoid validations
112
+
113
+ Copyright © 2010 Gonçalo Silva, released under the MIT license
@@ -0,0 +1,152 @@
1
+ require 'active_record'
2
+ require 'validations/uniqueness_without_deleted'
3
+
4
+ class Object
5
+ class << self
6
+ def is_paranoid?
7
+ false
8
+ end
9
+ end
10
+ end
11
+
12
+ module ActsAsParanoid
13
+ def acts_as_paranoid(options = {})
14
+ raise ArgumentError, "Hash expected, got #{options.class.name}" if not options.is_a?(Hash) and not options.empty?
15
+
16
+ configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 5.minutes }
17
+ configuration.update(options) unless options.nil?
18
+
19
+ type = case configuration[:column_type]
20
+ when "time" then "Time.now"
21
+ when "boolean" then "true"
22
+ else
23
+ raise ArgumentError, "'time' or 'boolean' expected for :column_type option, got #{configuration[:column_type]}"
24
+ end
25
+
26
+ column_reference = "#{self.table_name}.#{configuration[:column]}"
27
+
28
+ class_eval <<-EOV
29
+ default_scope where("#{column_reference} IS ?", nil)
30
+
31
+ class << self
32
+ def is_paranoid?
33
+ true
34
+ end
35
+
36
+ def with_deleted
37
+ self.unscoped.where("") #self.unscoped.reload
38
+ end
39
+
40
+ def only_deleted
41
+ self.unscoped.where("#{column_reference} IS NOT ?", nil)
42
+ end
43
+
44
+ def delete_all!(conditions = nil)
45
+ self.unscoped.delete_all!(conditions)
46
+ end
47
+
48
+ def delete_all(conditions = nil)
49
+ update_all ["#{configuration[:column]} = ?", #{type}], conditions
50
+ end
51
+
52
+ def paranoid_column
53
+ :"#{configuration[:column]}"
54
+ end
55
+
56
+ def paranoid_column_type
57
+ :"#{configuration[:column_type]}"
58
+ end
59
+
60
+ def dependent_associations
61
+ self.reflect_on_all_associations.select {|a| a.options[:dependent] == :destroy }
62
+ end
63
+ end
64
+
65
+ def paranoid_value
66
+ self.send(self.class.paranoid_column)
67
+ end
68
+
69
+ def destroy!
70
+ before_destroy() if respond_to?(:before_destroy)
71
+
72
+ #{self.name}.delete_all!(:id => self)
73
+
74
+ after_destroy() if respond_to?(:after_destroy)
75
+ end
76
+
77
+ def destroy
78
+ run_callbacks :destroy do
79
+ if paranoid_value == nil
80
+ #{self.name}.delete_all(:id => self.id)
81
+ else
82
+ #{self.name}.delete_all!(:id => self.id)
83
+ end
84
+ end
85
+ end
86
+
87
+ def recover(options = {})
88
+ options = {
89
+ :recursive => #{configuration[:recover_dependent_associations]},
90
+ :recovery_window => #{configuration[:dependent_recovery_window]}
91
+ }.merge(options)
92
+
93
+ self.class.transaction do
94
+ recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
95
+
96
+ self.update_attributes(self.class.paranoid_column.to_sym => nil)
97
+ end
98
+ end
99
+
100
+ def recover_dependent_associations(window, options)
101
+ self.class.dependent_associations.each do |association|
102
+ if association.collection? && self.send(association.name).is_paranoid?
103
+ self.send(association.name).unscoped do
104
+ self.send(association.name).deleted_around(paranoid_value, window).each do |object|
105
+ object.recover(options) if object.respond_to?(:recover)
106
+ end
107
+ end
108
+ elsif association.macro == :has_one && association.klass.is_paranoid?
109
+ association.klass.unscoped do
110
+ object = association.klass.deleted_around(paranoid_value, window).send('find_by_'+association.primary_key_name, self.id)
111
+ object.recover(options) if object && object.respond_to?(:recover)
112
+ end
113
+ elsif association.klass.is_paranoid?
114
+ association.klass.unscoped do
115
+ id = self.send(association.primary_key_name)
116
+ object = association.klass.deleted_around(paranoid_value, window).find_by_id(id)
117
+ object.recover(options) if object && object.respond_to?(:recover)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def deleted?
124
+ !self.#{configuration[:column]}.nil?
125
+ end
126
+
127
+ scope :deleted_around, lambda {|value, window|
128
+ if self.class.is_paranoid?
129
+ if self.class.paranoid_column_type == 'time' && ![true, false].include?(value)
130
+ self.where("\#{self.class.paranoid_column} > ? AND \#{self.class.paranoid_column} < ?", (value - window), (value + window))
131
+ else
132
+ self.only_deleted
133
+ end
134
+ end
135
+ }
136
+
137
+ ActiveRecord::Relation.class_eval do
138
+ alias_method :delete_all!, :delete_all
139
+ alias_method :destroy!, :destroy
140
+ end
141
+ EOV
142
+ end
143
+
144
+ def validates_as_paranoid
145
+ class_eval <<-EOV
146
+ send :extend, ParanoidValidations::ClassMethods
147
+ EOV
148
+ end
149
+ end
150
+
151
+ # Extend ActiveRecord's functionality
152
+ ActiveRecord::Base.send :extend, ActsAsParanoid
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ParanoidValidations
4
+ class UniquenessWithoutDeletedValidator < ActiveRecord::Validations::UniquenessValidator
5
+ def validate_each(record, attribute, value)
6
+ finder_class = find_finder_class_for(record)
7
+
8
+ if value && record.class.serialized_attributes.key?(attribute.to_s)
9
+ value = YAML.dump value
10
+ end
11
+
12
+ sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value)
13
+
14
+ # This is the only changed line from the base class version - it does finder_class.unscoped
15
+ relation = finder_class.where(sql, *params)
16
+
17
+ Array.wrap(options[:scope]).each do |scope_item|
18
+ scope_value = record.send(scope_item)
19
+ relation = relation.where(scope_item => scope_value)
20
+ end
21
+
22
+ if record.persisted?
23
+ # TODO : This should be in Arel
24
+ relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
25
+ end
26
+
27
+ if relation.exists?
28
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
29
+ end
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ def validates_uniqueness_of_without_deleted(*attr_names)
35
+ validates_with UniquenessWithoutDeletedValidator, _merge_attributes(attr_names)
36
+ end
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: misterbojangles-rails3_acts_as_paranoid
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 4
10
+ version: 0.0.4
11
+ platform: ruby
12
+ authors:
13
+ - "Gon\xC3\xA7alo Silva"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-02 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ version: "3.0"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ description: Active Record (>=3.0) plugin which allows you to hide and restore records without actually deleting them. Check its GitHub page for more in-depth information.
37
+ email:
38
+ - goncalossilva@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - lib/rails3_acts_as_paranoid.rb
47
+ - lib/validations/uniqueness_without_deleted.rb
48
+ - LICENSE
49
+ - README.markdown
50
+ has_rdoc: true
51
+ homepage: http://github.com/goncalossilva/rails3_acts_as_paranoid
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options: []
56
+
57
+ require_paths:
58
+ - lib
59
+ - lib/validations
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 21
75
+ segments:
76
+ - 1
77
+ - 3
78
+ - 7
79
+ version: 1.3.7
80
+ requirements: []
81
+
82
+ rubyforge_project: misterbojangles-rails3_acts_as_paranoid
83
+ rubygems_version: 1.6.0
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Active Record (>=3.0) plugin which allows you to hide and restore records without actually deleting them.
87
+ test_files: []
88
+