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.
- data/README.markdown +8 -5
- data/VERSION +1 -1
- data/lib/permanent_records.rb +159 -143
- 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
|
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
|
-
|
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)
|
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.
|
1
|
+
3.0.3
|
data/lib/permanent_records.rb
CHANGED
@@ -1,16 +1,164 @@
|
|
1
1
|
module PermanentRecords
|
2
2
|
|
3
|
-
|
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
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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
|
96
|
-
|
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.
|
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:
|
93
|
+
hash: 1045959687738742071
|
94
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
96
96
|
requirements:
|