archival_record 2.0.2 → 3.0.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.gitlab-ci.yml +20 -0
  4. data/.rubocop.yml +7 -5
  5. data/.rubocop_todo.yml +1 -8
  6. data/Appraisals +8 -11
  7. data/CHANGELOG.md +15 -0
  8. data/Gemfile.lock +77 -43
  9. data/LICENSE +5 -2
  10. data/README.md +36 -12
  11. data/archival_record.gemspec +36 -31
  12. data/gemfiles/{rails_5.1.gemfile → rails_6.1.gemfile} +1 -2
  13. data/gemfiles/{rails_5.2.gemfile → rails_7.0.gemfile} +1 -2
  14. data/gemfiles/{rails_5.0.gemfile → rails_7.1.gemfile} +1 -2
  15. data/init.rb +2 -2
  16. data/lib/archival_record/version.rb +1 -1
  17. data/lib/archival_record.rb +5 -5
  18. data/lib/archival_record_core/archival_record.rb +38 -21
  19. data/lib/archival_record_core/archival_record_active_record_methods.rb +3 -2
  20. data/lib/archival_record_core/association_operation/base.rb +4 -2
  21. data/test/application_record_test.rb +1 -1
  22. data/test/archive_dependents_option_test.rb +54 -0
  23. data/test/basic_test.rb +5 -5
  24. data/test/bogus_relation_test.rb +22 -0
  25. data/test/deep_nesting_test.rb +1 -1
  26. data/test/fixtures/bogus_relation.rb +6 -0
  27. data/test/fixtures/explicit_act_on_dependents_archival.rb +9 -0
  28. data/test/fixtures/ignorable_dependent.rb +10 -0
  29. data/test/fixtures/ignore_dependents_archival.rb +9 -0
  30. data/test/fixtures/nonignorable_dependent.rb +10 -0
  31. data/test/polymorphic_test.rb +2 -2
  32. data/test/relations_test.rb +1 -1
  33. data/test/schema.rb +27 -0
  34. data/test/scope_test.rb +4 -10
  35. data/test/test_helper.rb +19 -25
  36. data/test/transaction_test.rb +2 -1
  37. metadata +55 -53
  38. data/.travis.yml +0 -23
  39. data/test/fixtures/callback_archival_4.rb +0 -19
  40. /data/test/fixtures/{callback_archival_5.rb → callback_archival.rb} +0 -0
@@ -3,17 +3,18 @@ module ArchivalRecordCore
3
3
 
4
4
  require "digest/md5"
5
5
 
6
- unless defined?(MissingArchivalColumnError) == "constant" && MissingArchivalColumnError.class == Class
6
+ unless defined?(MissingArchivalColumnError) == "constant" && MissingArchivalColumnError.instance_of?(Class)
7
7
  MissingArchivalColumnError = Class.new(ActiveRecord::ActiveRecordError)
8
8
  end
9
- unless defined?(CouldNotArchiveError) == "constant" && CouldNotArchiveError.class == Class
9
+ unless defined?(CouldNotArchiveError) == "constant" && CouldNotArchiveError.instance_of?(Class)
10
10
  CouldNotArchiveError = Class.new(ActiveRecord::ActiveRecordError)
11
11
  end
12
- unless defined?(CouldNotUnarchiveError) == "constant" && CouldNotUnarchiveError.class == Class
12
+ unless defined?(CouldNotUnarchiveError) == "constant" && CouldNotUnarchiveError.instance_of?(Class)
13
13
  CouldNotUnarchiveError = Class.new(ActiveRecord::ActiveRecordError)
14
14
  end
15
15
 
16
16
  def self.included(base)
17
+ super
17
18
  base.extend ActMethods
18
19
  end
19
20
 
@@ -24,6 +25,8 @@ module ArchivalRecordCore
24
25
 
25
26
  include InstanceMethods
26
27
 
28
+ setup_options(options)
29
+
27
30
  setup_validations(options)
28
31
 
29
32
  setup_scopes
@@ -33,10 +36,17 @@ module ArchivalRecordCore
33
36
 
34
37
  # Deprecated: Please use `archival_record` instead
35
38
  def acts_as_archival(options = {})
36
- ActiveSupport::Deprecation.warn("`acts_as_archival` is deprecated. Please use `archival_record` instead.")
39
+ ActiveSupport::Deprecation.new("3.0", "ArchivalRecord")
37
40
  archival_record(options)
38
41
  end
39
42
 
43
+ private def setup_options(options)
44
+ default_options = { readonly_when_archived: false, archive_dependents: true }
45
+ options.reverse_merge!(default_options)
46
+
47
+ define_method(:archive_dependents?) { options[:archive_dependents] }
48
+ end
49
+
40
50
  private def setup_validations(options)
41
51
  before_validation :raise_if_not_archival
42
52
  validate :readonly_when_archived if options[:readonly_when_archived]
@@ -45,9 +55,13 @@ module ArchivalRecordCore
45
55
  private def setup_scopes
46
56
  scope :archived, -> { where.not(archived_at: nil).where.not(archive_number: nil) }
47
57
  scope :unarchived, -> { where(archived_at: nil, archive_number: nil) }
48
- scope :archived_from_archive_number, (lambda do |head_archive_number|
49
- where(["archived_at IS NOT NULL AND archive_number = ?", head_archive_number])
50
- end)
58
+ scope :archived_from_archive_number,
59
+ (lambda do |head_archive_number|
60
+ table = arel_table
61
+ archive_at_check = table[:archived_at].not_eq(nil)
62
+ archive_number_check = table[:archive_number].eq(head_archive_number)
63
+ where(archive_at_check.and(archive_number_check))
64
+ end)
51
65
  end
52
66
 
53
67
  private def setup_callbacks
@@ -73,6 +87,12 @@ module ArchivalRecordCore
73
87
  private def define_callback_dsl_method(callbackable_type, action)
74
88
  # rubocop:disable Security/Eval
75
89
  eval <<-END_CALLBACKS, binding, __FILE__, __LINE__ + 1
90
+ # unless defined?(before_archive)
91
+ # def before_archive(*args, &blk)
92
+ # set_callback(:archive, :before, *args, &blk)
93
+ # end
94
+ # end
95
+
76
96
  unless defined?(#{callbackable_type}_#{action})
77
97
  def #{callbackable_type}_#{action}(*args, &blk)
78
98
  set_callback(:#{action}, :#{callbackable_type}, *args, &blk)
@@ -99,7 +119,8 @@ module ArchivalRecordCore
99
119
  missing_columns << "archived_at" unless respond_to?(:archived_at)
100
120
  return if missing_columns.blank?
101
121
 
102
- raise MissingArchivalColumnError.new("Add '#{missing_columns.join "', '"}' column(s) to '#{self.class.name}' to make it archival")
122
+ raise(MissingArchivalColumnError,
123
+ "Add '#{missing_columns.join "', '"}' column(s) to '#{self.class.name}' to make it archival")
103
124
  end
104
125
 
105
126
  def archived?
@@ -138,24 +159,20 @@ module ArchivalRecordCore
138
159
  AssociationOperation::Unarchive.new(self, head_archive_number).execute
139
160
  end
140
161
 
141
- private def execute_archival_action(action)
162
+ private def execute_archival_action(action, &block)
163
+ execution_result = false
142
164
  self.class.transaction do
143
- # rubocop: disable Style/RescueStandardError
144
- begin
145
- success = run_callbacks(action) { yield }
146
- return !!success
147
- rescue => e
148
- handle_archival_action_exception(e)
149
- end
150
- # rubocop: enable Style/RescueStandardError
165
+ execution_result = !!run_callbacks(action, &block)
166
+ rescue StandardError => e
167
+ handle_archival_action_exception(e)
151
168
  end
152
- false
169
+ execution_result
153
170
  end
154
171
 
155
172
  private def handle_archival_action_exception(exception)
156
- ActiveRecord::Base.logger.try(:debug, exception.message)
157
- ActiveRecord::Base.logger.try(:debug, exception.backtrace)
158
- raise ActiveRecord::Rollback
173
+ ActiveRecord::Base.logger.try(:error, exception.message)
174
+ ActiveRecord::Base.logger.try(:error, exception.backtrace)
175
+ raise(ActiveRecord::Rollback)
159
176
  end
160
177
 
161
178
  end
@@ -2,6 +2,7 @@ module ArchivalRecordCore
2
2
  module ArchivalRecordActiveRecordMethods
3
3
 
4
4
  def self.included(base)
5
+ super
5
6
  base.extend ARClassMethods
6
7
  base.send :include, ARInstanceMethods
7
8
  end
@@ -26,7 +27,7 @@ module ArchivalRecordCore
26
27
 
27
28
  def archive_all!
28
29
  error_message = "The #{klass} must implement 'act_on_archivals' in order to call `archive_all!`"
29
- raise NotImplementedError.new(error_message) unless archival?
30
+ raise(NotImplementedError, error_message) unless archival?
30
31
 
31
32
  head_archive_number = Digest::MD5.hexdigest("#{klass}#{Time.now.utc.to_i}")
32
33
  each { |record| record.archive!(head_archive_number) }.tap { reset }
@@ -34,7 +35,7 @@ module ArchivalRecordCore
34
35
 
35
36
  def unarchive_all!
36
37
  error_message = "The #{klass} must implement 'act_on_archivals' in order to call `unarchive_all!`"
37
- raise NotImplementedError.new(error_message) unless archival?
38
+ raise(NotImplementedError, error_message) unless archival?
38
39
 
39
40
  each(&:unarchive!).tap { reset }
40
41
  end
@@ -11,8 +11,10 @@ module ArchivalRecordCore
11
11
  end
12
12
 
13
13
  def execute
14
+ return unless model.archive_dependents?
15
+
14
16
  each_archivable_association do |association|
15
- act_on_association(association) if association_conditions_met? association
17
+ act_on_association(association) if association_conditions_met?(association)
16
18
  end
17
19
  end
18
20
 
@@ -45,7 +47,7 @@ module ArchivalRecordCore
45
47
  end
46
48
 
47
49
  def act_on_archivals(_scope)
48
- raise NotImplementedError.new("The #{self.class} hasn't implemented 'act_on_archivals(scope)'")
50
+ raise(NotImplementedError, "The #{self.class} hasn't implemented 'act_on_archivals(scope)'")
49
51
  end
50
52
 
51
53
  end
@@ -11,7 +11,7 @@ if defined?(ApplicationRecord)
11
11
  end
12
12
 
13
13
  test "unarchive unarchives archival records" do
14
- archival = ApplicationRecordRow.create!(archived_at: Time.now, archive_number: 1)
14
+ archival = ApplicationRecordRow.create!(archived_at: Time.now.utc, archive_number: 1)
15
15
  archival.unarchive!
16
16
  assert_not archival.reload.archived?
17
17
  end
@@ -0,0 +1,54 @@
1
+ require_relative "test_helper"
2
+
3
+ class ArchiveDependentsOptionTest < ActiveSupport::TestCase
4
+
5
+ test "archive_dependents option will leave dependent archival records alone when parent is archived" do
6
+ archival = IgnoreDependentsArchival.create!
7
+ child = archival.ignorable_dependents.create!
8
+
9
+ assert archival.archival?
10
+ assert child.archival?
11
+
12
+ archival.archive!
13
+
14
+ assert archival.reload.archived?
15
+ assert_not child.reload.archived?
16
+ end
17
+
18
+ test "archive_dependents option will leave dependent archival records alone when parent is unarchived" do
19
+ archival = IgnoreDependentsArchival.create!
20
+ child = archival.ignorable_dependents.create!
21
+
22
+ assert archival.archival?
23
+ assert child.archival?
24
+
25
+ # This is simulating an unlikely scenario where the option has been added after records have been
26
+ # archived as a set but we want to unarchive after adding the option.
27
+ archival.archive!
28
+ child.update!(archived_at: archival.archived_at, archive_number: archival.archive_number)
29
+
30
+ archival.unarchive!
31
+
32
+ assert_not archival.reload.archived?
33
+ assert child.reload.archived?
34
+ end
35
+
36
+ test "archive_dependents option will work normally if set to true" do
37
+ archival = ExplicitActOnDependentsArchival.create!
38
+ child = archival.nonignorable_dependents.create!
39
+
40
+ assert archival.archival?
41
+ assert child.archival?
42
+
43
+ archival.archive!
44
+
45
+ assert archival.reload.archived?
46
+ assert child.reload.archived?
47
+
48
+ archival.unarchive!
49
+
50
+ assert_not archival.reload.archived?
51
+ assert_not child.reload.archived?
52
+ end
53
+
54
+ end
data/test/basic_test.rb CHANGED
@@ -9,7 +9,7 @@ class BasicTest < ActiveSupport::TestCase
9
9
  end
10
10
 
11
11
  test "unarchive unarchives archival records" do
12
- archival = Archival.create!(archived_at: Time.now, archive_number: 1)
12
+ archival = Archival.create!(archived_at: Time.now.utc, archive_number: 1)
13
13
  archival.unarchive!
14
14
  assert_equal false, archival.reload.archived?
15
15
  end
@@ -26,23 +26,23 @@ class BasicTest < ActiveSupport::TestCase
26
26
  end
27
27
 
28
28
  test "unarchive returns true on success" do
29
- normal = Archival.create!(archived_at: Time.now, archive_number: "1")
29
+ normal = Archival.create!(archived_at: Time.now.utc, archive_number: "1")
30
30
  assert_equal true, normal.unarchive!
31
31
  end
32
32
 
33
33
  test "unarchive returns false on failure" do
34
- readonly = Archival.create!(archived_at: Time.now, archive_number: "1")
34
+ readonly = Archival.create!(archived_at: Time.now.utc, archive_number: "1")
35
35
  readonly.readonly!
36
36
  assert_equal false, readonly.unarchive!
37
37
  end
38
38
 
39
39
  test "archive sets archived_at to the time of archiving" do
40
40
  archival = Archival.create!
41
- before = DateTime.now
41
+ before = DateTime.now.utc
42
42
  sleep(0.001)
43
43
  archival.archive!
44
44
  sleep(0.001)
45
- after = DateTime.now
45
+ after = DateTime.now.utc
46
46
  assert before < archival.archived_at.to_datetime
47
47
  assert after > archival.archived_at.to_datetime
48
48
  end
@@ -0,0 +1,22 @@
1
+ require_relative "test_helper"
2
+
3
+ class BogusRelationTest < ActiveSupport::TestCase
4
+
5
+ test "does not successfully archive" do
6
+ archival = BogusRelation.create!
7
+ stub(ActiveRecord::Base.logger).error(
8
+ satisfy do |arg|
9
+ if arg.is_a?(String)
10
+ arg == "SQLite3::SQLException: no such column: bogus_relations.bogus_relation_id"
11
+ elsif arg.is_a?(Array)
12
+ arg.join.match?(%r{gems/activerecord}) # this is gonna be in the stack trace somewhere
13
+ else
14
+ raise "unexpected logging"
15
+ end
16
+ end
17
+ )
18
+ assert_not archival.archive!
19
+ assert_not archival.reload.archived?
20
+ end
21
+
22
+ end
@@ -18,7 +18,7 @@ class DeepNestingTest < ActiveSupport::TestCase
18
18
 
19
19
  test "unarchiving deeply nested items doesn't blow up" do
20
20
  archival_attributes = {
21
- archived_at: Time.now,
21
+ archived_at: Time.now.utc,
22
22
  archive_number: "test"
23
23
  }
24
24
  archival = Archival.create!(archival_attributes)
@@ -0,0 +1,6 @@
1
+ class BogusRelation < ApplicationRecord
2
+
3
+ archival_record
4
+ has_many :bogus_relations, dependent: :destroy
5
+
6
+ end
@@ -0,0 +1,9 @@
1
+ # archive_number - string
2
+ # archived_at - datetime
3
+ class ExplicitActOnDependentsArchival < ActiveRecord::Base
4
+
5
+ archival_record archive_dependents: true
6
+
7
+ has_many :nonignorable_dependents, dependent: :destroy
8
+
9
+ end
@@ -0,0 +1,10 @@
1
+ # ignore_dependents_archival_id - integer
2
+ # archive_number - string
3
+ # archived_at - datetime
4
+ class IgnorableDependent < ActiveRecord::Base
5
+
6
+ archival_record
7
+
8
+ belongs_to :ignore_dependents_archival
9
+
10
+ end
@@ -0,0 +1,9 @@
1
+ # archive_number - string
2
+ # archived_at - datetime
3
+ class IgnoreDependentsArchival < ActiveRecord::Base
4
+
5
+ archival_record archive_dependents: false
6
+
7
+ has_many :ignorable_dependents, dependent: :destroy
8
+
9
+ end
@@ -0,0 +1,10 @@
1
+ # explicit_act_on_dependents_archival_id - integer
2
+ # archive_number - string
3
+ # archived_at - datetime
4
+ class NonignorableDependent < ActiveRecord::Base
5
+
6
+ archival_record
7
+
8
+ belongs_to :explicit_act_on_dependents_archival
9
+
10
+ end
@@ -23,7 +23,7 @@ class PolymorphicTest < ActiveSupport::TestCase
23
23
  test "unarchive item with polymorphic association" do
24
24
  archive_attributes = {
25
25
  archive_number: "test",
26
- archived_at: Time.now
26
+ archived_at: Time.now.utc
27
27
  }
28
28
  archival = Archival.create!(archive_attributes)
29
29
  poly = archival.polys.create!(archive_attributes)
@@ -36,7 +36,7 @@ class PolymorphicTest < ActiveSupport::TestCase
36
36
  test "does not unarchive polymorphic association of different item with same id" do
37
37
  archive_attributes = {
38
38
  archive_number: "test",
39
- archived_at: Time.now
39
+ archived_at: Time.now.utc
40
40
  }
41
41
 
42
42
  archival = Archival.create!(archive_attributes)
@@ -34,7 +34,7 @@ class RelationsTest < ActiveSupport::TestCase
34
34
  assert parents.first.archivals.last.archived?
35
35
  end
36
36
 
37
- test "unarchive_all! unarchives all records in an AR Assocation" do
37
+ test "unarchive_all! unarchives all records in an AR association" do
38
38
  3.times { Archival.create! }
39
39
 
40
40
  archivals = Archival.all
data/test/schema.rb CHANGED
@@ -93,4 +93,31 @@ ActiveRecord::Schema.define(version: 1) do
93
93
  t.column :archive_number, :string
94
94
  t.column :archived_at, :datetime
95
95
  end
96
+
97
+ create_table :ignore_dependents_archivals, force: true do |t|
98
+ t.column :archive_number, :string
99
+ t.column :archived_at, :datetime
100
+ end
101
+
102
+ create_table :ignorable_dependents, force: true do |t|
103
+ t.column :ignore_dependents_archival_id, :integer
104
+ t.column :archive_number, :string
105
+ t.column :archived_at, :datetime
106
+ end
107
+
108
+ create_table :explicit_act_on_dependents_archivals, force: true do |t|
109
+ t.column :archive_number, :string
110
+ t.column :archived_at, :datetime
111
+ end
112
+
113
+ create_table :nonignorable_dependents, force: true do |t|
114
+ t.column :explicit_act_on_dependents_archival_id, :integer
115
+ t.column :archive_number, :string
116
+ t.column :archived_at, :datetime
117
+ end
118
+
119
+ create_table :bogus_relations, force: true do |t|
120
+ t.column :archive_number, :string
121
+ t.column :archived_at, :datetime
122
+ end
96
123
  end
data/test/scope_test.rb CHANGED
@@ -50,16 +50,10 @@ class ScopeTest < ActiveSupport::TestCase
50
50
  end
51
51
 
52
52
  test "table_name is set to 'legacy'" do
53
- archived_sql =
54
- if ActiveRecord.version >= Gem::Version.new("5.2.0")
55
- "SELECT \"legacy\".* FROM \"legacy\" " \
56
- 'WHERE "legacy"."archived_at" IS NOT NULL AND "legacy"."archive_number" IS NOT NULL'
57
- else
58
- "SELECT \"legacy\".* FROM \"legacy\" " \
59
- 'WHERE ("legacy"."archived_at" IS NOT NULL) AND ("legacy"."archive_number" IS NOT NULL)'
60
- end
61
- unarchived_sql = "SELECT \"legacy\".* FROM \"legacy\" " \
62
- 'WHERE "legacy"."archived_at" IS NULL AND "legacy"."archive_number" IS NULL'
53
+ archived_sql = 'SELECT "legacy".* FROM "legacy" ' \
54
+ 'WHERE "legacy"."archived_at" IS NOT NULL AND "legacy"."archive_number" IS NOT NULL'
55
+ unarchived_sql = 'SELECT "legacy".* FROM "legacy" ' \
56
+ 'WHERE "legacy"."archived_at" IS NULL AND "legacy"."archive_number" IS NULL'
63
57
  assert_equal archived_sql, ArchivalTableName.archived.to_sql
64
58
  assert_equal unarchived_sql, ArchivalTableName.unarchived.to_sql
65
59
  end
data/test/test_helper.rb CHANGED
@@ -1,4 +1,4 @@
1
- $LOAD_PATH.unshift(File.dirname(__FILE__) + "/../lib")
1
+ $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib")
2
2
 
3
3
  require "bundler/setup"
4
4
  require "minitest/autorun"
@@ -20,7 +20,7 @@ end
20
20
 
21
21
  def setup_logging
22
22
  require "logger"
23
- logfile = File.dirname(__FILE__) + "/debug.log"
23
+ logfile = "#{File.dirname(__FILE__)}/debug.log"
24
24
  ActiveRecord::Base.logger = Logger.new(logfile)
25
25
  end
26
26
 
@@ -40,37 +40,38 @@ def sqlite_config
40
40
  }
41
41
  end
42
42
 
43
+ def schema_file
44
+ "#{File.dirname(__FILE__)}/schema.rb"
45
+ end
46
+
43
47
  def create_test_tables
44
- schema_file = File.dirname(__FILE__) + "/schema.rb"
45
48
  puts "** Loading schema for SQLite"
46
49
  ActiveRecord::Base.establish_connection(sqlite_config)
47
50
  load(schema_file) if File.exist?(schema_file)
48
51
  end
49
52
 
50
- BASE_FIXTURE_CLASSES = %I[
53
+ FIXTURE_CLASSES = %I[
51
54
  another_polys_holder
55
+ application_record
56
+ application_record_row
52
57
  archival
53
- archival_kid
54
58
  archival_grandkid
59
+ archival_kid
55
60
  archival_table_name
61
+ bogus_relation
62
+ callback_archival
63
+ deprecated_warning_archival
64
+ explicit_act_on_dependents_archival
56
65
  exploder
66
+ ignorable_dependent
67
+ ignore_dependents_archival
57
68
  independent_archival
58
- missing_archived_at
59
69
  missing_archive_number
70
+ missing_archived_at
71
+ nonignorable_dependent
60
72
  plain
61
73
  poly
62
74
  readonly_when_archived
63
- deprecated_warning_archival
64
- ].freeze
65
-
66
- RAILS_4_FIXTURE_CLASSES = %I[
67
- callback_archival_4
68
- ].freeze
69
-
70
- RAILS_5_FIXTURE_CLASSES = %I[
71
- application_record
72
- application_record_row
73
- callback_archival_5
74
75
  ].freeze
75
76
 
76
77
  def require_test_classes
@@ -78,14 +79,7 @@ def require_test_classes
78
79
  inflect.irregular "poly", "polys"
79
80
  end
80
81
 
81
- fixtures = if ActiveRecord::VERSION::MAJOR >= 4
82
- RAILS_5_FIXTURE_CLASSES
83
- else
84
- RAILS_4_FIXTURE_CLASSES
85
- end
86
-
87
- fixtures += BASE_FIXTURE_CLASSES
88
- fixtures.each { |test_class_file| require_relative "fixtures/#{test_class_file}" }
82
+ FIXTURE_CLASSES.each { |test_class_file| require_relative "fixtures/#{test_class_file}" }
89
83
  end
90
84
 
91
85
  prepare_for_tests
@@ -11,7 +11,8 @@ class TransactionTest < ActiveSupport::TestCase
11
11
  end
12
12
  archival.archive!
13
13
 
14
- assert_not archival.archived?, "If this failed, you might be trying to test on a system that doesn't support nested transactions"
14
+ failure_reason = "If this failed, you might be trying to test on a system that doesn't support nested transactions"
15
+ assert_not archival.archived?, failure_reason
15
16
  assert_not exploder.reload.archived?
16
17
  end
17
18