permanent_records 3.0.2 → 3.0.3

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.
Files changed (4) hide show
  1. data/README.markdown +8 -5
  2. data/VERSION +1 -1
  3. data/lib/permanent_records.rb +159 -143
  4. metadata +2 -2
data/README.markdown CHANGED
@@ -5,7 +5,7 @@
5
5
  This gem prevents any of your ActiveRecord data from being destroyed.
6
6
  Any model that you've given a "deleted_at" datetime column will have that column set rather than let the record be deleted.
7
7
 
8
- ## Compatibility: This gem works with Rails versions 1, 2, and 3
8
+ ## Compatibility: This gem is for Rails 3.x
9
9
 
10
10
  ## Does it make a lot of sense?
11
11
 
@@ -66,8 +66,6 @@ And if you had dependent records that were set to be destroyed along with the pa
66
66
 
67
67
  ## Can I use default scopes?
68
68
 
69
- In Rails 3, yes.
70
-
71
69
  ```ruby
72
70
  default_scope where(:deleted_at => nil)
73
71
  ```
@@ -80,8 +78,13 @@ If you use such a default scope, you will need to simulate the `deleted` scope w
80
78
  end
81
79
  ```
82
80
 
83
- Rails 2 provides no practical means of overriding default scopes (aside from using something like `Model.with_exclusive_scope { find(id) }`), so you'll need to implement those yourself if you need them.
81
+ ## Productionizing
82
+
83
+ If you operate a system where destroying or reviving a record takes more
84
+ than about 3 seconds then you'll want to customize
85
+ `PermanentRecords.dependent_record_window = 10.seconds` or some other
86
+ value that works for you.
84
87
 
85
88
  Patches welcome, forks celebrated.
86
89
 
87
- Copyright (c) 2010 Jack Danger Canty @ [http://jåck.com](http://jåck.com) released under the MIT license
90
+ Copyright (c) 2013 Jack Danger Canty @ [http://jåck.com](http://jåck.com) released under the MIT license
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.0.3
@@ -1,16 +1,164 @@
1
1
  module PermanentRecords
2
2
 
3
- def self.included(base)
3
+ # This module defines the public api that you can
4
+ # use in your model instances.
5
+ #
6
+ # * is_permanent? #=> true/false, depending if you have a deleted_at column
7
+ # * deleted? #=> true/false, depending if you've called .destroy
8
+ # * destroy #=> sets deleted_at, your record is now in the .destroyed scope
9
+ # * revice #=> undo the destroy
10
+ module ActiveRecord
11
+ def self.included(base)
12
+
13
+ base.extend Scopes
14
+ base.extend IsPermanent
15
+
16
+ base.instance_eval do
17
+ define_model_callbacks :revive
18
+
19
+ before_revive :revive_destroyed_dependent_records
20
+ end
21
+ end
22
+
23
+ def is_permanent?
24
+ respond_to?(:deleted_at)
25
+ end
26
+
27
+ def deleted?
28
+ deleted_at if is_permanent?
29
+ end
30
+
31
+ def revive
32
+ _run_revive_callbacks { set_deleted_at nil }
33
+ self
34
+ end
4
35
 
5
- base.extend Scopes
6
- base.extend IsPermanent
36
+ def destroy(force = nil)
37
+ if !is_permanent? || PermanentRecords.should_force_destroy?(force)
38
+ return permanently_delete_records_after { super() }
39
+ end
40
+ destroy_with_permanent_records force
41
+ end
7
42
 
8
- base.instance_eval do
9
- define_model_callbacks :revive
43
+ private
44
+
45
+ def set_deleted_at(value, force = nil)
46
+ return self unless is_permanent?
47
+ record = self.class.unscoped.find(id)
48
+ record.deleted_at = value
49
+ begin
50
+ # we call save! instead of update_attribute so an ActiveRecord::RecordInvalid
51
+ # error will be raised if the record isn't valid. (This prevents reviving records that
52
+ # disregard validation constraints,)
53
+ if PermanentRecords.should_ignore_validations?(force)
54
+ record.save(:validate => false)
55
+ else
56
+ record.save!
57
+ end
58
+ @attributes, @attributes_cache = record.attributes, record.attributes
59
+ rescue Exception => e
60
+ # trigger dependent record destruction (they were revived before this record,
61
+ # which cannot be revived due to validations)
62
+ record.destroy
63
+ raise e
64
+ end
65
+ end
10
66
 
11
- before_revive :revive_destroyed_dependent_records
67
+ def destroy_with_permanent_records(force = nil)
68
+ _run_destroy_callbacks do
69
+ deleted? || new_record? ? save : set_deleted_at(Time.now, force)
70
+ end
71
+ self
12
72
  end
13
73
 
74
+ def revive_destroyed_dependent_records
75
+ self.class.reflections.select do |name, reflection|
76
+ 'destroy' == reflection.options[:dependent].to_s && reflection.klass.is_permanent?
77
+ end.each do |name, reflection|
78
+ cardinality = reflection.macro.to_s.gsub('has_', '')
79
+ if cardinality == 'many'
80
+ records = send(name).unscoped.find(
81
+ :all,
82
+ :conditions => [
83
+ "#{reflection.quoted_table_name}.deleted_at > ?" +
84
+ " AND " +
85
+ "#{reflection.quoted_table_name}.deleted_at < ?",
86
+ deleted_at - PermanentRecords.dependent_record_window,
87
+ deleted_at + PermanentRecords.dependent_record_window
88
+ ]
89
+ )
90
+ elsif cardinality == 'one' or cardinality == 'belongs_to'
91
+ self.class.unscoped do
92
+ records = [] << send(name)
93
+ end
94
+ end
95
+ [records].flatten.compact.each do |dependent|
96
+ dependent.revive
97
+ end
98
+
99
+ # and update the reflection cache
100
+ send(name, :reload)
101
+ end
102
+ end
103
+
104
+ def attempt_notifying_observers(callback)
105
+ begin
106
+ notify_observers(callback)
107
+ rescue NoMethodError => e
108
+ # do nothing: this model isn't being observed
109
+ end
110
+ end
111
+
112
+ # return the records corresponding to an association with the `:dependent => :destroy` option
113
+ def get_dependent_records
114
+ dependent_records = {}
115
+
116
+ # check which dependent records are to be destroyed
117
+ klass = self.class
118
+ klass.reflections.each do |key, reflection|
119
+ if reflection.options[:dependent] == :destroy
120
+ next unless records = self.send(key) # skip if there are no dependent record instances
121
+ if records.respond_to? :size
122
+ next unless records.size > 0 # skip if there are no dependent record instances
123
+ else
124
+ records = [] << records
125
+ end
126
+ dependent_record = records.first
127
+ next if dependent_record.nil?
128
+ dependent_records[dependent_record.class] = records.map(&:id)
129
+ end
130
+ end
131
+ dependent_records
132
+ end
133
+
134
+ # If we force the destruction of the record, we will need to force the destruction of dependent records if the
135
+ # user specified `:dependent => :destroy` in the model.
136
+ # By default, the call to super/destroy_with_permanent_records (i.e. the &block param) will only soft delete
137
+ # the dependent records; we keep track of the dependent records
138
+ # that have `:dependent => :destroy` and call destroy(force) on them after the call to super
139
+ def permanently_delete_records_after(&block)
140
+ dependent_records = get_dependent_records
141
+ result = block.call
142
+ if result
143
+ permanently_delete_records(dependent_records)
144
+ end
145
+ result
146
+ end
147
+
148
+ # permanently delete the records (i.e. remove from database)
149
+ def permanently_delete_records(dependent_records)
150
+ dependent_records.each do |klass, ids|
151
+ ids.each do |id|
152
+ record = begin
153
+ klass.unscoped.find id
154
+ rescue ::ActiveRecord::RecordNotFound
155
+ next # the record has already been deleted, possibly due to another association with `:dependent => :destroy`
156
+ end
157
+ record.deleted_at = nil
158
+ record.destroy(:force)
159
+ end
160
+ end
161
+ end
14
162
  end
15
163
 
16
164
  module Scopes
@@ -29,26 +177,6 @@ module PermanentRecords
29
177
  end
30
178
  end
31
179
 
32
- def is_permanent?
33
- respond_to?(:deleted_at)
34
- end
35
-
36
- def deleted?
37
- deleted_at if is_permanent?
38
- end
39
-
40
- def revive
41
- _run_revive_callbacks { set_deleted_at nil }
42
- self
43
- end
44
-
45
- def destroy(force = nil)
46
- if !is_permanent? || PermanentRecords.should_force_destroy?(force)
47
- return permanently_delete_records_after { super() }
48
- end
49
- destroy_with_permanent_records force
50
- end
51
-
52
180
  def self.should_force_destroy?(force)
53
181
  if Hash === force
54
182
  force[:force]
@@ -61,126 +189,14 @@ module PermanentRecords
61
189
  Hash === force && false == force[:validate]
62
190
  end
63
191
 
64
- protected
65
-
66
- def set_deleted_at(value, force = nil)
67
- return self unless is_permanent?
68
- record = self.class.unscoped.find(id)
69
- record.deleted_at = value
70
- begin
71
- # we call save! instead of update_attribute so an ActiveRecord::RecordInvalid
72
- # error will be raised if the record isn't valid. (This prevents reviving records that
73
- # disregard validation constraints,)
74
- if PermanentRecords.should_ignore_validations?(force)
75
- record.save(:validate => false)
76
- else
77
- record.save!
78
- end
79
- @attributes, @attributes_cache = record.attributes, record.attributes
80
- rescue Exception => e
81
- # trigger dependent record destruction (they were revived before this record,
82
- # which cannot be revived due to validations)
83
- record.destroy
84
- raise e
85
- end
86
- end
87
-
88
- def destroy_with_permanent_records(force = nil)
89
- _run_destroy_callbacks do
90
- deleted? || new_record? ? save : set_deleted_at(Time.now, force)
91
- end
92
- self
192
+ def self.dependent_record_window
193
+ @dependent_record_window || 3.seconds
93
194
  end
94
195
 
95
- def revive_destroyed_dependent_records
96
- self.class.reflections.select do |name, reflection|
97
- 'destroy' == reflection.options[:dependent].to_s && reflection.klass.is_permanent?
98
- end.each do |name, reflection|
99
- cardinality = reflection.macro.to_s.gsub('has_', '')
100
- if cardinality == 'many'
101
- records = send(name).unscoped.find(
102
- :all,
103
- :conditions => [
104
- "#{reflection.quoted_table_name}.deleted_at > ?" +
105
- " AND " +
106
- "#{reflection.quoted_table_name}.deleted_at < ?",
107
- deleted_at - 3.seconds,
108
- deleted_at + 3.seconds
109
- ]
110
- )
111
- elsif cardinality == 'one' or cardinality == 'belongs_to'
112
- self.class.unscoped do
113
- records = [] << send(name)
114
- end
115
- end
116
- [records].flatten.compact.each do |dependent|
117
- dependent.revive
118
- end
119
-
120
- # and update the reflection cache
121
- send(name, :reload)
122
- end
123
- end
124
-
125
- def attempt_notifying_observers(callback)
126
- begin
127
- notify_observers(callback)
128
- rescue NoMethodError => e
129
- # do nothing: this model isn't being observed
130
- end
131
- end
132
-
133
- # return the records corresponding to an association with the `:dependent => :destroy` option
134
- def get_dependent_records
135
- dependent_records = {}
136
-
137
- # check which dependent records are to be destroyed
138
- klass = self.class
139
- klass.reflections.each do |key, reflection|
140
- if reflection.options[:dependent] == :destroy
141
- next unless records = self.send(key) # skip if there are no dependent record instances
142
- if records.respond_to? :size
143
- next unless records.size > 0 # skip if there are no dependent record instances
144
- else
145
- records = [] << records
146
- end
147
- dependent_record = records.first
148
- next if dependent_record.nil?
149
- dependent_records[dependent_record.class] = records.map(&:id)
150
- end
151
- end
152
- dependent_records
153
- end
154
-
155
- # If we force the destruction of the record, we will need to force the destruction of dependent records if the
156
- # user specified `:dependent => :destroy` in the model.
157
- # By default, the call to super/destroy_with_permanent_records (i.e. the &block param) will only soft delete
158
- # the dependent records; we keep track of the dependent records
159
- # that have `:dependent => :destroy` and call destroy(force) on them after the call to super
160
- def permanently_delete_records_after(&block)
161
- dependent_records = get_dependent_records
162
- result = block.call
163
- if result
164
- permanently_delete_records(dependent_records)
165
- end
166
- result
167
- end
168
-
169
- # permanently delete the records (i.e. remove from database)
170
- def permanently_delete_records(dependent_records)
171
- dependent_records.each do |klass, ids|
172
- ids.each do |id|
173
- record = begin
174
- klass.unscoped.find id
175
- rescue ActiveRecord::RecordNotFound
176
- next # the record has already been deleted, possibly due to another association with `:dependent => :destroy`
177
- end
178
- record.deleted_at = nil
179
- record.destroy(:force)
180
- end
181
- end
196
+ def self.dependent_record_window=(time_value)
197
+ @dependent_record_window = time_value
182
198
  end
183
199
  end
184
200
 
185
- ActiveRecord::Base.send :include, PermanentRecords
201
+ ActiveRecord::Base.send :include, PermanentRecords::ActiveRecord
186
202
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: permanent_records
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -90,7 +90,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
90
  version: '0'
91
91
  segments:
92
92
  - 0
93
- hash: 1685930428738744402
93
+ hash: 1045959687738742071
94
94
  required_rubygems_version: !ruby/object:Gem::Requirement
95
95
  none: false
96
96
  requirements: