permanent_records 4.1.0 → 4.1.1

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.
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: