rails3_acts_as_paranoid 0.0.9

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,122 @@
1
+ # ActsAsParanoid
2
+
3
+ A simple plugin which hides records instead of deleting them, being able to recover them.
4
+
5
+ This branch targets Rails 3.0.X. If you're working with another version, switch to the corresponding branch.
6
+
7
+ ## Credits
8
+
9
+ 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).
10
+
11
+ 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.
12
+
13
+ ## Usage
14
+
15
+ You can enable ActsAsParanoid like this:
16
+
17
+ class Paranoiac < ActiveRecord::Base
18
+ acts_as_paranoid
19
+ end
20
+
21
+ ### Options
22
+
23
+ You can also specify the name of the column to store it's *deletion* and the type of data it holds:
24
+
25
+ - :column => 'deleted_at'
26
+ - :type => 'time'
27
+
28
+ The values shown are the defaults. While *column* can be anything (as long as it exists in your database), *type* is restricted to "boolean", "time" or "string".
29
+
30
+ If your column type is a "string", you can also specify which value to use when marking an object as deleted by passing `:deleted_value` (default is "deleted").
31
+
32
+ ### Filtering
33
+
34
+ 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:
35
+
36
+ Paranoiac.only_deleted # retrieves the deleted records
37
+ Paranoiac.with_deleted # retrieves all records, deleted or not
38
+
39
+ ### Real deletion
40
+
41
+ In order to really delete a record, just use:
42
+
43
+ paranoiac.destroy!
44
+ Paranoiac.delete_all!(conditions)
45
+
46
+ 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:
47
+
48
+ Paranoiac.first.destroy # does NOT delete the first record, just hides it
49
+ Paranoiac.only_deleted.destroy # deletes the first record from the database
50
+
51
+ ### Recovery
52
+
53
+ Recovery is easy. Just invoke `recover` on it, like this:
54
+
55
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover
56
+
57
+ 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:
58
+
59
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recursive => false)
60
+
61
+ If you would like to change the default behavior for a model, you can use the `recover_dependent_associations` option
62
+
63
+ class Paranoiac < ActiveRecord::Base
64
+ acts_as_paranoid :recover_dependent_associations => false
65
+ end
66
+
67
+ 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
68
+
69
+ class Paranoiac < ActiveRecord::Base
70
+ acts_as_paranoid
71
+ has_many :paranoids, :dependent => :destroy
72
+ end
73
+
74
+ class Paranoid < ActiveRecord::Base
75
+ belongs_to :paranoic
76
+
77
+ # Paranoid objects will be recovered alongside Paranoic objects
78
+ # if they were deleted within 1 minute of the Paranoic object
79
+ acts_as_paranoid :dependent_recovery_window => 1.minute
80
+ end
81
+
82
+ or in the recover statement
83
+
84
+ Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(:recovery_window => 30.seconds)
85
+
86
+ ### Validation
87
+ 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.
88
+
89
+ class Paranoiac < ActiveRecord::Base
90
+ acts_as_paranoid
91
+ validates_as_paranoid
92
+ validates_uniqueness_of_without_deleted :name
93
+ end
94
+
95
+ Paranoiac.create(:name => 'foo').destroy
96
+ Paranoiac.new(:name => 'foo').valid? #=> true
97
+
98
+
99
+ ### Status
100
+ Once you retrieve data using `with_deleted` scope you can check deletion status using `deleted?` helper:
101
+
102
+ Paranoiac.create(:name => 'foo').destroy
103
+ Paranoiac.with_deleted.first.deleted? #=> true
104
+
105
+ ## Caveats
106
+
107
+ Watch out for these caveats:
108
+
109
+ - 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.
110
+ - You cannot use scopes named `with_deleted`, `only_deleted` and `paranoid_deleted_around_time`
111
+ - `unscoped` will return all records, deleted or not
112
+
113
+ ## Acknowledgements
114
+
115
+ * To [cheerfulstoic](https://github.com/cheerfulstoic) for adding recursive recovery
116
+ * To [Jonathan Vaught](https://github.com/gravelpup) for adding paranoid validations
117
+ * To [Geoffrey Hichborn](https://github.com/phene) for improving the overral code quality and adding support for after_commit
118
+ * To [flah00](https://github.com/flah00) for adding support for STI-based associations (with :dependent)
119
+ * To [vikramdhillon](https://github.com/vikramdhillon) for the idea and
120
+ initial implementation of support for string column type
121
+
122
+ Copyright © 2010 Gonçalo Silva, released under the MIT license
@@ -0,0 +1,203 @@
1
+ require 'active_record'
2
+ require 'validations/uniqueness_without_deleted'
3
+
4
+ module ActsAsParanoid
5
+
6
+ def paranoid?
7
+ self.included_modules.include?(InstanceMethods)
8
+ end
9
+
10
+ def validates_as_paranoid
11
+ extend ParanoidValidations::ClassMethods
12
+ end
13
+
14
+ def acts_as_paranoid(options = {})
15
+ raise ArgumentError, "Hash expected, got #{options.class.name}" if not options.is_a?(Hash) and not options.empty?
16
+
17
+ class_attribute :paranoid_configuration, :paranoid_column_reference
18
+
19
+ self.paranoid_configuration = { :column => "deleted_at", :column_type => "time", :recover_dependent_associations => true, :dependent_recovery_window => 2.minutes }
20
+ self.paranoid_configuration.merge!({ :deleted_value => "deleted" }) if options[:column_type] == "string"
21
+ self.paranoid_configuration.merge!(options) # user options
22
+
23
+ raise ArgumentError, "'time', 'boolean' or 'string' expected for :column_type option, got #{paranoid_configuration[:column_type]}" unless ['time', 'boolean', 'string'].include? paranoid_configuration[:column_type]
24
+
25
+ self.paranoid_column_reference = "#{self.table_name}.#{paranoid_configuration[:column]}"
26
+
27
+ return if paranoid?
28
+
29
+ ActiveRecord::Relation.class_eval do
30
+ alias_method :delete_all!, :delete_all
31
+ alias_method :destroy!, :destroy
32
+ end
33
+
34
+ ActiveRecord::Reflection::AssociationReflection.class_eval do
35
+ alias_method :foreign_key, :primary_key_name unless respond_to?(:foreign_key)
36
+ end
37
+
38
+ # Magic!
39
+ default_scope where("#{paranoid_column_reference} IS ?", nil)
40
+
41
+ scope :paranoid_deleted_around_time, lambda {|value, window|
42
+ if self.class.respond_to?(:paranoid?) && self.class.paranoid?
43
+ if self.class.paranoid_column_type == 'time' && ![true, false].include?(value)
44
+ self.where("#{self.class.paranoid_column} > ? AND #{self.class.paranoid_column} < ?", (value - window), (value + window))
45
+ else
46
+ self.only_deleted
47
+ end
48
+ end if paranoid_configuration[:column_type] == 'time'
49
+ }
50
+
51
+ include InstanceMethods
52
+ extend ClassMethods
53
+ end
54
+
55
+ module ClassMethods
56
+ def self.extended(base)
57
+ base.define_callbacks :recover
58
+ end
59
+
60
+ def before_recover(method)
61
+ set_callback :recover, :before, method
62
+ end
63
+
64
+ def after_recover(method)
65
+ set_callback :recover, :after, method
66
+ end
67
+
68
+ def with_deleted
69
+ self.unscoped
70
+ end
71
+
72
+ def only_deleted
73
+ self.unscoped.where("#{paranoid_column_reference} IS NOT ?", nil)
74
+ end
75
+
76
+ def delete_all!(conditions = nil)
77
+ self.unscoped.delete_all!(conditions)
78
+ end
79
+
80
+ def delete_all(conditions = nil)
81
+ update_all ["#{paranoid_configuration[:column]} = ?", delete_now_value], conditions
82
+ end
83
+
84
+ def paranoid_column
85
+ paranoid_configuration[:column].to_sym
86
+ end
87
+
88
+ def paranoid_column_type
89
+ paranoid_configuration[:column_type].to_sym
90
+ end
91
+
92
+ def dependent_associations
93
+ self.reflect_on_all_associations.select {|a| [:destroy, :delete_all].include?(a.options[:dependent]) }
94
+ end
95
+
96
+ def delete_now_value
97
+ case paranoid_configuration[:column_type]
98
+ when "time" then Time.now
99
+ when "boolean" then true
100
+ when "string" then paranoid_configuration[:deleted_value]
101
+ end
102
+ end
103
+ end
104
+
105
+ module InstanceMethods
106
+
107
+ def paranoid_value
108
+ self.send(self.class.paranoid_column)
109
+ end
110
+
111
+ def destroy!
112
+ with_transaction_returning_status do
113
+ run_callbacks :destroy do
114
+ act_on_dependent_destroy_associations
115
+ self.class.delete_all!(self.class.primary_key.to_sym => self.id)
116
+ self.paranoid_value = self.class.delete_now_value
117
+ freeze
118
+ end
119
+ end
120
+ end
121
+
122
+ def destroy
123
+ if paranoid_value.nil?
124
+ with_transaction_returning_status do
125
+ run_callbacks :destroy do
126
+ self.class.delete_all(self.class.primary_key.to_sym => self.id)
127
+ self.paranoid_value = self.class.delete_now_value
128
+ self
129
+ end
130
+ end
131
+ else
132
+ destroy!
133
+ end
134
+ end
135
+
136
+ def recover(options={})
137
+ options = {
138
+ :recursive => self.class.paranoid_configuration[:recover_dependent_associations],
139
+ :recovery_window => self.class.paranoid_configuration[:dependent_recovery_window]
140
+ }.merge(options)
141
+
142
+ self.class.transaction do
143
+ run_callbacks :recover do
144
+ recover_dependent_associations(options[:recovery_window], options) if options[:recursive]
145
+
146
+ self.paranoid_value = nil
147
+ self.save
148
+ end
149
+ end
150
+ end
151
+
152
+ def recover_dependent_associations(window, options)
153
+ self.class.dependent_associations.each do |association|
154
+ if association.collection? && self.send(association.name).paranoid?
155
+ self.send(association.name).unscoped do
156
+ self.send(association.name).paranoid_deleted_around_time(paranoid_value, window).each do |object|
157
+ object.recover(options) if object.respond_to?(:recover)
158
+ end
159
+ end
160
+ elsif association.macro == :has_one && association.klass.paranoid?
161
+ association.klass.unscoped do
162
+ object = association.klass.paranoid_deleted_around_time(paranoid_value, window).send('find_by_'+association.foreign_key, self.id)
163
+ object.recover(options) if object && object.respond_to?(:recover)
164
+ end
165
+ elsif association.klass.paranoid?
166
+ association.klass.unscoped do
167
+ id = self.send(association.foreign_key)
168
+ object = association.klass.paranoid_deleted_around_time(paranoid_value, window).find_by_id(id)
169
+ object.recover(options) if object && object.respond_to?(:recover)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def act_on_dependent_destroy_associations
176
+ self.class.dependent_associations.each do |association|
177
+ if association.collection? && self.send(association.name).paranoid?
178
+ association.klass.with_deleted.instance_eval("find_all_by_#{association.foreign_key}(#{self.id.to_json})").each do |object|
179
+ object.destroy!
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ def deleted?
186
+ !paranoid_value.nil?
187
+ end
188
+ alias_method :destroyed?, :deleted?
189
+
190
+ private
191
+ def paranoid_value=(value)
192
+ self.send("#{self.class.paranoid_column}=", value)
193
+ end
194
+
195
+ end
196
+
197
+ end
198
+
199
+ # Extend ActiveRecord's functionality
200
+ ActiveRecord::Base.send :extend, ActsAsParanoid
201
+
202
+ # Push the recover callback onto the activerecord callback list
203
+ ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
@@ -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,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails3_acts_as_paranoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Goncalo Silva
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-06 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70156244254600 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70156244254600
25
+ description: Active Record (~>3.0) plugin which allows you to hide and restore records
26
+ without actually deleting them. Check its GitHub page for more in-depth information.
27
+ email:
28
+ - goncalossilva@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/rails3_acts_as_paranoid.rb
34
+ - lib/validations/uniqueness_without_deleted.rb
35
+ - LICENSE
36
+ - README.markdown
37
+ homepage: http://github.com/goncalossilva/rails3_acts_as_paranoid
38
+ licenses: []
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ segments:
50
+ - 0
51
+ hash: -960527913778585172
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: 1.3.6
58
+ requirements: []
59
+ rubyforge_project: rails3_acts_as_paranoid
60
+ rubygems_version: 1.8.10
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Active Record (~>3.0) plugin which allows you to hide and restore records
64
+ without actually deleting them.
65
+ test_files: []