permanent_records 4.1.0 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b06f61ce3509014ff0b75f3d8cc1377a96c46ba3
4
- data.tar.gz: de86eadce595b382db87f524ecadf2555b6b2f77
3
+ metadata.gz: 8fc77ef2eb0c9943d477c2bb228a11d31c1fd0a7
4
+ data.tar.gz: 748645f6c54d8f067c20c494ed451d9cdccaffd3
5
5
  SHA512:
6
- metadata.gz: a57f2492aa6117680c5b018e7887a840daf17ebd2b11b857ab7f6ef77858e223ab078ee0eb1b075873fef321135c097a8870349b2c1dcaa9ac23a35cc657ea71
7
- data.tar.gz: f188ea9d53635618d4baa67daedf8afe5be7b6f7021952482e41d483f91343b063b07bfd8d976b0da30cc5af9c10054b0426487537cc8945ebc4051b7a676c4b
6
+ metadata.gz: c72d7ef29d6a675e511d67194a265c6b2b183a7d629deed3fc63b9e911a75dbd8b59c2a7e57c81cd15d9c12af6a8f4a6115b9fe5b6cbf059139c3ee8465c7dcf
7
+ data.tar.gz: 925ce8fe43fae831d5bd73291bce29d9d0d6747c89d6a08e2d2b49239b27c328352576c73a4f3afb4c1bdae82974ba955a94fe3e7ee9c3e6d262c23eaa0421f8
data/.travis.yml CHANGED
@@ -2,6 +2,7 @@ language: ruby
2
2
  rvm:
3
3
  - 2.0.0
4
4
  - 2.2.2
5
+ - 2.3.0
5
6
  env:
6
7
  - AR_TEST_VERSION: 4.2.0
7
8
  - AR_TEST_VERSION: 4.2.5
data/Rakefile CHANGED
@@ -1,16 +1,19 @@
1
1
  require 'bundler'
2
2
  require 'yaml'
3
+ require 'English'
3
4
  Bundler::GemHelper.install_tasks
4
5
 
5
- $config = YAML::load_file File.expand_path('spec/support/database.yml', File.dirname(__FILE__))
6
+ CONFIG = YAML.load_file(
7
+ File.expand_path('spec/support/database.yml', File.dirname(__FILE__))
8
+ )
6
9
 
7
10
  def test_database_exists?
8
- system "psql -l | grep -q #{$config['test'][:database]}"
9
- $?.success?
11
+ system "psql -l | grep -q #{CONFIG['test'][:database]}"
12
+ $CHILD_STATUS.success?
10
13
  end
11
14
 
12
15
  def create_test_database
13
- system "createdb #{$config['test'][:database]}"
16
+ system "createdb #{CONFIG['test'][:database]}"
14
17
  end
15
18
 
16
19
  namespace :db do
@@ -19,9 +22,13 @@ namespace :db do
19
22
  end
20
23
  end
21
24
 
22
- task :default => [:spec]
25
+ require 'rubocop/rake_task'
26
+ RuboCop::RakeTask.new
23
27
 
24
28
  desc 'Run all tests'
25
- task :spec => 'db:create' do
29
+ task spec: 'db:create' do
26
30
  exec 'rspec'
27
31
  end
32
+
33
+ task default: [:spec, :rubocop]
34
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.1.0
1
+ 4.1.1
data/ci CHANGED
@@ -3,5 +3,5 @@ for version in `grep AR_TEST_VERSION .travis.yml | awk '{print $3}'`; do
3
3
  export AR_TEST_VERSION=$version
4
4
  echo "Testing against ActiveRecord $version"
5
5
  bundle
6
- bundle exec rspec
6
+ bundle exec rake
7
7
  done
@@ -1,40 +1,47 @@
1
+ # PermanentRecords works with ActiveRecord to set deleted_at columns with a
2
+ # timestamp reflecting when a record was 'deleted' instead of actually deleting
3
+ # the record. All dependent records and associations are treated exactly as
4
+ # you'd expect: If there's a deleted_at column then the record is preserved,
5
+ # otherwise it's deleted.
1
6
  module PermanentRecords
2
-
3
7
  # This module defines the public api that you can
4
8
  # use in your model instances.
5
9
  #
6
10
  # * is_permanent? #=> true/false, depending if you have a deleted_at column
7
11
  # * deleted? #=> true/false, depending if you've called .destroy
8
- # * destroy #=> sets deleted_at, your record is now in the .destroyed scope
12
+ # * destroy #=> sets deleted_at, your record is now in
13
+ # the .destroyed scope
9
14
  # * revive #=> undo the destroy
10
- module ActiveRecord
15
+ module ActiveRecord # rubocop:disable Metrics/ModuleLength
11
16
  def self.included(base)
12
-
13
17
  base.extend Scopes
14
18
  base.extend IsPermanent
15
19
 
16
20
  base.instance_eval do
17
21
  define_model_callbacks :revive
18
-
19
- before_revive :revive_destroyed_dependent_records
20
22
  end
21
23
  end
22
24
 
23
- def is_permanent?
25
+ def is_permanent? # rubocop:disable Style/PredicateName
24
26
  respond_to?(:deleted_at)
25
27
  end
26
28
 
27
29
  def deleted?
28
30
  if is_permanent?
29
- !!deleted_at
31
+ !!deleted_at # rubocop:disable Style/DoubleNegation
30
32
  else
31
33
  destroyed?
32
34
  end
33
35
  end
34
36
 
35
- def revive(validate = nil)
37
+ def revive(options = nil)
36
38
  with_transaction_returning_status do
37
- run_callbacks(:revive) { set_deleted_at(nil, validate) }
39
+ if PermanentRecords.should_revive_parent_first?(options)
40
+ revival.reverse
41
+ else
42
+ revival
43
+ end.each { |p| p.call(options) }
44
+
38
45
  self
39
46
  end
40
47
  end
@@ -51,23 +58,46 @@ module PermanentRecords
51
58
 
52
59
  private
53
60
 
61
+ def revival
62
+ [
63
+ lambda do |validate|
64
+ revive_destroyed_dependent_records(validate)
65
+ end,
66
+ lambda do |validate|
67
+ run_callbacks(:revive) { set_deleted_at(nil, validate) }
68
+ end
69
+ ]
70
+ end
71
+
72
+ # rubocop:disable Style/AccessorMethodName
73
+ def get_deleted_record
74
+ # Looking for parent on STI case
75
+ if respond_to?(:parent_id) && parent_id.present?
76
+ self.class.unscoped.find(parent_id)
77
+ else
78
+ self.class.unscoped.find(id)
79
+ end
80
+ end
81
+
82
+ # rubocop:disable Metrics/MethodLength
54
83
  def set_deleted_at(value, force = nil)
55
84
  return self unless is_permanent?
56
- record = self.class.unscoped.find(id)
85
+ record = get_deleted_record
57
86
  record.deleted_at = value
58
87
  begin
59
- # we call save! instead of update_attribute so an ActiveRecord::RecordInvalid
60
- # error will be raised if the record isn't valid. (This prevents reviving records that
61
- # disregard validation constraints,)
88
+ # we call save! instead of update_attribute so an
89
+ # ActiveRecord::RecordInvalid error will be raised if the record isn't
90
+ # valid. (This prevents reviving records that disregard validation
91
+ # constraints,)
62
92
  if PermanentRecords.should_ignore_validations?(force)
63
- record.save(:validate => false)
93
+ record.save(validate: false)
64
94
  else
65
95
  record.save!
66
96
  end
67
97
  @attributes = record.instance_variable_get('@attributes')
68
- rescue Exception => e
69
- # trigger dependent record destruction (they were revived before this record,
70
- # which cannot be revived due to validations)
98
+ rescue => e
99
+ # trigger dependent record destruction (they were revived before this
100
+ # record, which cannot be revived due to validations)
71
101
  record.destroy
72
102
  raise e
73
103
  end
@@ -81,28 +111,36 @@ module PermanentRecords
81
111
  deleted? ? self : false
82
112
  end
83
113
 
84
- def revive_destroyed_dependent_records
85
- self.class.reflections.select do |name, reflection|
86
- 'destroy' == reflection.options[:dependent].to_s && reflection.klass.is_permanent?
87
- end.each do |name, reflection|
88
- cardinality = reflection.macro.to_s.gsub('has_', '')
89
- if cardinality == 'many'
90
- records = send(name).unscoped.where(
91
- [
92
- "#{reflection.quoted_table_name}.deleted_at > ?" +
93
- " AND " +
94
- "#{reflection.quoted_table_name}.deleted_at < ?",
95
- deleted_at - PermanentRecords.dependent_record_window,
96
- deleted_at + PermanentRecords.dependent_record_window
97
- ]
98
- )
99
- elsif cardinality == 'one' or cardinality == 'belongs_to'
100
- self.class.unscoped do
101
- records = [] << send(name)
114
+ def add_record_window(_request, name, reflection)
115
+ send(name).unscope(where: :deleted_at).where(
116
+ [
117
+ "#{reflection.quoted_table_name}.deleted_at > ?" \
118
+ ' AND ' \
119
+ "#{reflection.quoted_table_name}.deleted_at < ?",
120
+ deleted_at - PermanentRecords.dependent_record_window,
121
+ deleted_at + PermanentRecords.dependent_record_window
122
+ ]
123
+ )
124
+ end
125
+
126
+ # TODO: Feel free to refactor this without polluting the ActiveRecord
127
+ # namespace.
128
+ # rubocop:disable Metrics/AbcSize
129
+ def revive_destroyed_dependent_records(force = nil)
130
+ PermanentRecords.dependent_permanent_reflections(self.class)
131
+ .each do |name, reflection|
132
+ cardinality = reflection.macro.to_s.gsub('has_', '').to_sym
133
+ case cardinality
134
+ when :many
135
+ if deleted_at
136
+ add_record_window(send(name), name, reflection)
137
+ else
138
+ send(name)
102
139
  end
103
- end
104
- [records].flatten.compact.each do |dependent|
105
- dependent.revive
140
+ when :one, :belongs_to
141
+ self.class.unscoped { Array(send(name)) }
142
+ end.to_a.flatten.compact.each do |dependent|
143
+ dependent.revive(force)
106
144
  end
107
145
 
108
146
  # and update the reflection cache
@@ -111,46 +149,34 @@ module PermanentRecords
111
149
  end
112
150
 
113
151
  def attempt_notifying_observers(callback)
114
- begin
115
- notify_observers(callback)
116
- rescue NoMethodError => e
117
- # do nothing: this model isn't being observed
118
- end
152
+ notify_observers(callback)
153
+ rescue NoMethodError # rubocop:disable Lint/HandleExceptions
154
+ # do nothing: this model isn't being observed
119
155
  end
120
156
 
121
- # return the records corresponding to an association with the `:dependent => :destroy` option
122
- def get_dependent_records
123
- dependent_records = {}
124
-
157
+ # return the records corresponding to an association with the `:dependent
158
+ # => :destroy` option
159
+ def dependent_record_ids
125
160
  # check which dependent records are to be destroyed
126
- klass = self.class
127
- klass.reflections.each do |key, reflection|
128
- if reflection.options[:dependent] == :destroy
129
- next unless records = self.send(key) # skip if there are no dependent record instances
130
- if records.respond_to? :size
131
- next unless records.size > 0 # skip if there are no dependent record instances
132
- else
133
- records = [] << records
134
- end
135
- dependent_record = records.first
136
- next if dependent_record.nil?
137
- dependent_records[dependent_record.class] = records.map(&:id)
138
- end
139
- end
140
- dependent_records
141
- end
142
-
143
- # If we force the destruction of the record, we will need to force the destruction of dependent records if the
144
- # user specified `:dependent => :destroy` in the model.
145
- # By default, the call to super/destroy_with_permanent_records (i.e. the &block param) will only soft delete
146
- # the dependent records; we keep track of the dependent records
147
- # that have `:dependent => :destroy` and call destroy(force) on them after the call to super
148
- def permanently_delete_records_after(&block)
149
- dependent_records = get_dependent_records
150
- result = block.call
151
- if result
152
- permanently_delete_records(dependent_records)
161
+ PermanentRecords.dependent_reflections(self.class)
162
+ .reduce({}) do |records, (key, _)|
163
+ found = Array(send(key)).compact
164
+ next records unless found.size > 0
165
+ records.update found.first.class => found.map(&:id)
153
166
  end
167
+ end
168
+
169
+ # If we force the destruction of the record, we will need to force the
170
+ # destruction of dependent records if the user specified `:dependent =>
171
+ # :destroy` in the model. By default, the call to
172
+ # super/destroy_with_permanent_records (i.e. the &block param) will only
173
+ # soft delete the dependent records; we keep track of the dependent records
174
+ # that have `:dependent => :destroy` and call destroy(force) on them after
175
+ # the call to super
176
+ def permanently_delete_records_after(&_block)
177
+ dependent_records = dependent_record_ids
178
+ result = yield
179
+ permanently_delete_records(dependent_records) if result
154
180
  result
155
181
  end
156
182
 
@@ -158,11 +184,8 @@ module PermanentRecords
158
184
  def permanently_delete_records(dependent_records)
159
185
  dependent_records.each do |klass, ids|
160
186
  ids.each do |id|
161
- record = begin
162
- klass.unscoped.find id
163
- rescue ::ActiveRecord::RecordNotFound
164
- next # the record has already been deleted, possibly due to another association with `:dependent => :destroy`
165
- end
187
+ record = klass.unscoped.where(klass.primary_key => id).first
188
+ next unless record
166
189
  record.deleted_at = nil
167
190
  record.destroy(:force)
168
191
  end
@@ -170,6 +193,7 @@ module PermanentRecords
170
193
  end
171
194
  end
172
195
 
196
+ # ActiveRelation scopes
173
197
  module Scopes
174
198
  def deleted
175
199
  where arel_table[:deleted_at].not_eq(nil)
@@ -180,22 +204,27 @@ module PermanentRecords
180
204
  end
181
205
  end
182
206
 
207
+ # Included into ActiveRecord for all models
183
208
  module IsPermanent
184
- def is_permanent?
185
- columns.detect {|c| 'deleted_at' == c.name}
209
+ def is_permanent? # rubocop:disable Style/PredicateName
210
+ columns.detect { |c| 'deleted_at' == c.name }
186
211
  end
187
212
  end
188
213
 
189
214
  def self.should_force_destroy?(force)
190
- if Hash === force
215
+ if force.is_a?(Hash)
191
216
  force[:force]
192
217
  else
193
218
  :force == force
194
219
  end
195
220
  end
196
221
 
222
+ def self.should_revive_parent_first?(order)
223
+ order.is_a?(Hash) && true == order[:reverse]
224
+ end
225
+
197
226
  def self.should_ignore_validations?(force)
198
- Hash === force && false == force[:validate]
227
+ force.is_a?(Hash) && false == force[:validate]
199
228
  end
200
229
 
201
230
  def self.dependent_record_window
@@ -205,7 +234,19 @@ module PermanentRecords
205
234
  def self.dependent_record_window=(time_value)
206
235
  @dependent_record_window = time_value
207
236
  end
237
+
238
+ def self.dependent_reflections(klass)
239
+ klass.reflections.select do |_, reflection|
240
+ # skip if there are no dependent record instances
241
+ reflection.options[:dependent] == :destroy
242
+ end
243
+ end
244
+
245
+ def self.dependent_permanent_reflections(klass)
246
+ dependent_reflections(klass).select do |_name, reflection|
247
+ reflection.klass.is_permanent?
248
+ end
249
+ end
208
250
  end
209
251
 
210
252
  ActiveRecord::Base.send :include, PermanentRecords::ActiveRecord
211
-
@@ -1,20 +1,27 @@
1
1
  # encoding: utf-8
2
2
  Gem::Specification.new do |s|
3
- s.name = "permanent_records"
3
+ s.name = 'permanent_records'
4
4
  s.version = File.read('VERSION')
5
5
  s.license = 'MIT'
6
6
 
7
- s.authors = ["Jack Danger Canty", "David Sulc", "Joe Nelson", "Trond Arve Nordheim", "Josh Teneycke", "Maximilian Herold", "Hugh Evans"]
8
- s.summary = "Soft-delete your ActiveRecord records"
9
- s.description = "Never Lose Data. Rather than deleting rows this sets Record#deleted_at and gives you all the scopes you need to work with your data."
10
- s.email = "github@jackcanty.com"
7
+ s.authors = ['Jack Danger Canty', 'David Sulc', 'Joe Nelson',
8
+ 'Trond Arve Nordheim', 'Josh Teneycke', 'Maximilian Herold',
9
+ 'Hugh Evans']
10
+ s.summary = 'Soft-delete your ActiveRecord records'
11
+ s.description = <<-EOS
12
+ Never Lose Data. Rather than deleting rows this sets Record#deleted_at and
13
+ gives you all the scopes you need to work with your data.
14
+ EOS
15
+ s.email = 'github@jackcanty.com'
11
16
  s.extra_rdoc_files = [
12
- "LICENSE",
13
- "README.md"
17
+ 'LICENSE',
18
+ 'README.md'
14
19
  ]
15
- s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
- s.homepage = "https://github.com/JackDanger/permanent_records"
17
- s.require_paths = ["lib"]
20
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ f.match(%r{^(test|spec|features)/})
22
+ end
23
+ s.homepage = 'https://github.com/JackDanger/permanent_records'
24
+ s.require_paths = ['lib']
18
25
 
19
26
  # For testing against multiple AR versions
20
27
  ver = ENV['AR_TEST_VERSION']
@@ -22,10 +29,10 @@ Gem::Specification.new do |s|
22
29
 
23
30
  s.add_runtime_dependency('activerecord', ver || '>= 4.2.0')
24
31
  s.add_runtime_dependency('activesupport', ver || '>= 4.2.0')
25
- s.add_development_dependency('rake') # For Travis-ci
26
- s.add_development_dependency('sqlite3')
27
- s.add_development_dependency('pry-byebug')
28
32
  s.add_development_dependency('database_cleaner', '>= 1.5.1')
33
+ s.add_development_dependency('pry-byebug')
34
+ s.add_development_dependency('rake') # For Travis-ci
29
35
  s.add_development_dependency('rspec', '~> 2.14.1')
36
+ s.add_development_dependency('rubocop')
37
+ s.add_development_dependency('sqlite3')
30
38
  end
31
-
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: 4.1.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Danger Canty
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2015-12-14 00:00:00.000000000 Z
17
+ date: 2016-02-02 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activerecord
@@ -45,21 +45,21 @@ dependencies:
45
45
  - !ruby/object:Gem::Version
46
46
  version: 4.2.0
47
47
  - !ruby/object:Gem::Dependency
48
- name: rake
48
+ name: database_cleaner
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: 1.5.1
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: 1.5.1
61
61
  - !ruby/object:Gem::Dependency
62
- name: sqlite3
62
+ name: pry-byebug
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
@@ -73,7 +73,7 @@ dependencies:
73
73
  - !ruby/object:Gem::Version
74
74
  version: '0'
75
75
  - !ruby/object:Gem::Dependency
76
- name: pry-byebug
76
+ name: rake
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
@@ -87,35 +87,50 @@ dependencies:
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  - !ruby/object:Gem::Dependency
90
- name: database_cleaner
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 2.14.1
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 2.14.1
103
+ - !ruby/object:Gem::Dependency
104
+ name: rubocop
91
105
  requirement: !ruby/object:Gem::Requirement
92
106
  requirements:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
- version: 1.5.1
109
+ version: '0'
96
110
  type: :development
97
111
  prerelease: false
98
112
  version_requirements: !ruby/object:Gem::Requirement
99
113
  requirements:
100
114
  - - ">="
101
115
  - !ruby/object:Gem::Version
102
- version: 1.5.1
116
+ version: '0'
103
117
  - !ruby/object:Gem::Dependency
104
- name: rspec
118
+ name: sqlite3
105
119
  requirement: !ruby/object:Gem::Requirement
106
120
  requirements:
107
- - - "~>"
121
+ - - ">="
108
122
  - !ruby/object:Gem::Version
109
- version: 2.14.1
123
+ version: '0'
110
124
  type: :development
111
125
  prerelease: false
112
126
  version_requirements: !ruby/object:Gem::Requirement
113
127
  requirements:
114
- - - "~>"
128
+ - - ">="
115
129
  - !ruby/object:Gem::Version
116
- version: 2.14.1
117
- description: Never Lose Data. Rather than deleting rows this sets Record#deleted_at
118
- and gives you all the scopes you need to work with your data.
130
+ version: '0'
131
+ description: |
132
+ Never Lose Data. Rather than deleting rows this sets Record#deleted_at and
133
+ gives you all the scopes you need to work with your data.
119
134
  email: github@jackcanty.com
120
135
  executables: []
121
136
  extensions: []
@@ -155,8 +170,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
170
  version: '0'
156
171
  requirements: []
157
172
  rubyforge_project:
158
- rubygems_version: 2.4.5
173
+ rubygems_version: 2.5.1
159
174
  signing_key:
160
175
  specification_version: 4
161
176
  summary: Soft-delete your ActiveRecord records
162
177
  test_files: []
178
+ has_rdoc: