archival_record 2.0.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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