paper_trail 4.0.0 → 11.1.0

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 (169) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +3 -1
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/lib/generators/paper_trail/install/USAGE +3 -0
  5. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  6. data/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  7. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
  8. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  9. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  10. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  11. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  12. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  13. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  14. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  15. data/lib/paper_trail/attribute_serializers/object_attribute.rb +41 -0
  16. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +44 -0
  17. data/lib/paper_trail/cleaner.rb +43 -18
  18. data/lib/paper_trail/compatibility.rb +51 -0
  19. data/lib/paper_trail/config.rb +24 -33
  20. data/lib/paper_trail/events/base.rb +323 -0
  21. data/lib/paper_trail/events/create.rb +32 -0
  22. data/lib/paper_trail/events/destroy.rb +42 -0
  23. data/lib/paper_trail/events/update.rb +60 -0
  24. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +10 -1
  25. data/lib/paper_trail/frameworks/active_record.rb +4 -3
  26. data/lib/paper_trail/frameworks/cucumber.rb +6 -3
  27. data/lib/paper_trail/frameworks/rails/controller.rb +53 -22
  28. data/lib/paper_trail/frameworks/rails/engine.rb +39 -1
  29. data/lib/paper_trail/frameworks/rails.rb +3 -6
  30. data/lib/paper_trail/frameworks/rspec/helpers.rb +5 -1
  31. data/lib/paper_trail/frameworks/rspec.rb +22 -9
  32. data/lib/paper_trail/has_paper_trail.rb +63 -490
  33. data/lib/paper_trail/model_config.rb +251 -0
  34. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  35. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  36. data/lib/paper_trail/record_history.rb +51 -0
  37. data/lib/paper_trail/record_trail.rb +300 -0
  38. data/lib/paper_trail/reifier.rb +130 -0
  39. data/lib/paper_trail/request.rb +166 -0
  40. data/lib/paper_trail/serializers/json.rb +13 -14
  41. data/lib/paper_trail/serializers/yaml.rb +21 -15
  42. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  43. data/lib/paper_trail/version_concern.rb +250 -332
  44. data/lib/paper_trail/version_number.rb +14 -9
  45. data/lib/paper_trail.rb +138 -142
  46. data/paper_trail.gemspec +66 -51
  47. metadata +127 -338
  48. data/.gitignore +0 -21
  49. data/.rspec +0 -2
  50. data/.travis.yml +0 -39
  51. data/CHANGELOG.md +0 -288
  52. data/README.md +0 -1445
  53. data/Rakefile +0 -30
  54. data/gemfiles/3.0.gemfile +0 -52
  55. data/lib/generators/paper_trail/USAGE +0 -2
  56. data/lib/generators/paper_trail/install_generator.rb +0 -41
  57. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +0 -10
  58. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -11
  59. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -17
  60. data/lib/generators/paper_trail/templates/create_versions.rb +0 -20
  61. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -7
  62. data/lib/paper_trail/frameworks/sinatra.rb +0 -37
  63. data/lib/paper_trail/version_association_concern.rb +0 -13
  64. data/spec/generators/install_generator_spec.rb +0 -67
  65. data/spec/models/animal_spec.rb +0 -19
  66. data/spec/models/boolit_spec.rb +0 -48
  67. data/spec/models/fluxor_spec.rb +0 -19
  68. data/spec/models/gadget_spec.rb +0 -70
  69. data/spec/models/joined_version_spec.rb +0 -47
  70. data/spec/models/json_version_spec.rb +0 -80
  71. data/spec/models/kitchen/banana_spec.rb +0 -14
  72. data/spec/models/not_on_update_spec.rb +0 -19
  73. data/spec/models/post_with_status_spec.rb +0 -17
  74. data/spec/models/skipper_spec.rb +0 -17
  75. data/spec/models/thing_spec.rb +0 -11
  76. data/spec/models/version_spec.rb +0 -239
  77. data/spec/models/widget_spec.rb +0 -298
  78. data/spec/modules/paper_trail_spec.rb +0 -27
  79. data/spec/modules/version_concern_spec.rb +0 -32
  80. data/spec/modules/version_number_spec.rb +0 -44
  81. data/spec/paper_trail_spec.rb +0 -66
  82. data/spec/rails_helper.rb +0 -34
  83. data/spec/requests/articles_spec.rb +0 -30
  84. data/spec/spec_helper.rb +0 -89
  85. data/spec/support/alt_db_init.rb +0 -59
  86. data/test/custom_json_serializer.rb +0 -13
  87. data/test/dummy/Rakefile +0 -7
  88. data/test/dummy/app/controllers/application_controller.rb +0 -20
  89. data/test/dummy/app/controllers/articles_controller.rb +0 -17
  90. data/test/dummy/app/controllers/test_controller.rb +0 -5
  91. data/test/dummy/app/controllers/widgets_controller.rb +0 -31
  92. data/test/dummy/app/helpers/application_helper.rb +0 -2
  93. data/test/dummy/app/models/animal.rb +0 -6
  94. data/test/dummy/app/models/article.rb +0 -16
  95. data/test/dummy/app/models/authorship.rb +0 -5
  96. data/test/dummy/app/models/book.rb +0 -9
  97. data/test/dummy/app/models/boolit.rb +0 -4
  98. data/test/dummy/app/models/cat.rb +0 -2
  99. data/test/dummy/app/models/customer.rb +0 -4
  100. data/test/dummy/app/models/document.rb +0 -4
  101. data/test/dummy/app/models/dog.rb +0 -2
  102. data/test/dummy/app/models/editor.rb +0 -4
  103. data/test/dummy/app/models/editorship.rb +0 -5
  104. data/test/dummy/app/models/elephant.rb +0 -3
  105. data/test/dummy/app/models/fluxor.rb +0 -3
  106. data/test/dummy/app/models/foo_widget.rb +0 -2
  107. data/test/dummy/app/models/fruit.rb +0 -5
  108. data/test/dummy/app/models/gadget.rb +0 -3
  109. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  110. data/test/dummy/app/models/legacy_widget.rb +0 -4
  111. data/test/dummy/app/models/line_item.rb +0 -4
  112. data/test/dummy/app/models/not_on_update.rb +0 -4
  113. data/test/dummy/app/models/order.rb +0 -5
  114. data/test/dummy/app/models/person.rb +0 -38
  115. data/test/dummy/app/models/post.rb +0 -3
  116. data/test/dummy/app/models/post_with_status.rb +0 -8
  117. data/test/dummy/app/models/protected_widget.rb +0 -3
  118. data/test/dummy/app/models/skipper.rb +0 -6
  119. data/test/dummy/app/models/song.rb +0 -32
  120. data/test/dummy/app/models/thing.rb +0 -3
  121. data/test/dummy/app/models/translation.rb +0 -4
  122. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  123. data/test/dummy/app/models/widget.rb +0 -15
  124. data/test/dummy/app/models/wotsit.rb +0 -8
  125. data/test/dummy/app/versions/joined_version.rb +0 -5
  126. data/test/dummy/app/versions/json_version.rb +0 -3
  127. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  128. data/test/dummy/app/versions/post_version.rb +0 -3
  129. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  130. data/test/dummy/config/application.rb +0 -69
  131. data/test/dummy/config/boot.rb +0 -10
  132. data/test/dummy/config/database.mysql.yml +0 -19
  133. data/test/dummy/config/database.postgres.yml +0 -15
  134. data/test/dummy/config/database.sqlite.yml +0 -15
  135. data/test/dummy/config/environment.rb +0 -5
  136. data/test/dummy/config/environments/development.rb +0 -40
  137. data/test/dummy/config/environments/production.rb +0 -73
  138. data/test/dummy/config/environments/test.rb +0 -41
  139. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  140. data/test/dummy/config/initializers/inflections.rb +0 -10
  141. data/test/dummy/config/initializers/mime_types.rb +0 -5
  142. data/test/dummy/config/initializers/paper_trail.rb +0 -8
  143. data/test/dummy/config/initializers/secret_token.rb +0 -7
  144. data/test/dummy/config/initializers/session_store.rb +0 -8
  145. data/test/dummy/config/locales/en.yml +0 -5
  146. data/test/dummy/config/routes.rb +0 -4
  147. data/test/dummy/config.ru +0 -4
  148. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -253
  149. data/test/dummy/db/schema.rb +0 -181
  150. data/test/dummy/script/rails +0 -6
  151. data/test/functional/controller_test.rb +0 -91
  152. data/test/functional/enabled_for_controller_test.rb +0 -29
  153. data/test/functional/modular_sinatra_test.rb +0 -48
  154. data/test/functional/sinatra_test.rb +0 -49
  155. data/test/functional/thread_safety_test.rb +0 -48
  156. data/test/paper_trail_test.rb +0 -38
  157. data/test/test_helper.rb +0 -69
  158. data/test/time_travel_helper.rb +0 -15
  159. data/test/unit/cleaner_test.rb +0 -182
  160. data/test/unit/inheritance_column_test.rb +0 -43
  161. data/test/unit/model_test.rb +0 -1905
  162. data/test/unit/protected_attrs_test.rb +0 -46
  163. data/test/unit/serializer_test.rb +0 -117
  164. data/test/unit/serializers/json_test.rb +0 -88
  165. data/test/unit/serializers/mixin_json_test.rb +0 -36
  166. data/test/unit/serializers/mixin_yaml_test.rb +0 -49
  167. data/test/unit/serializers/yaml_test.rb +0 -52
  168. data/test/unit/timestamp_test.rb +0 -43
  169. data/test/unit/version_test.rb +0 -101
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e6cf60ba027274553881419d2024a86547be00d2
4
- data.tar.gz: 57495d17ebfa11d24a17cf7d65ba5e67aca92816
2
+ SHA256:
3
+ metadata.gz: c312ab701b8ab7b26df37957b03b9f01b6ce6cbe0e5e25d4c478258f5bec1d6f
4
+ data.tar.gz: 62e94f6c2fa657d4c24f0fe6ecdf5abf3c8ae785e7b25f88611fc18e6d4324a7
5
5
  SHA512:
6
- metadata.gz: d9c462e7bb9dfdf1404de3fb8de7484de342ab0516e0e8eaae67e3059a51e01d0e7c9b1699433857c2045bee768256ab811783ccc8568318cf14dc39c50bd6ff
7
- data.tar.gz: 1e8c8098fa91e2cb9af8fae20268beceba18e885948a629e5fada7d84d2023159254da16875341436ce620365fb9f28432e4a37159ac7a9c8bc8883a2afef409
6
+ metadata.gz: b59d91302dde736a2476240eadb9306938db2521594cf55de792c9678324762c4a1ffedfa5a53c3fc0fd9e37a87bc1b6ea4e74e68535b80fe70342d48a221532
7
+ data.tar.gz: b4aba2c107556fd6fbf758d9373f147c9b9bb0d1e6059bb3e2ad28b77906e980c559bd3c888fe9734665182cbe294bf470e77527a5536e202b8f32a5b7993516
data/Gemfile CHANGED
@@ -1,2 +1,4 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
2
4
  gemspec
File without changes
@@ -0,0 +1,3 @@
1
+ Description:
2
+ Generates (but does not run) a migration to add a versions table. Also generates an initializer
3
+ file for configuring PaperTrail. See section 5.c. Generators in README.md for more information.
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../migration_generator"
4
+
5
+ module PaperTrail
6
+ # Installs PaperTrail in a rails app.
7
+ class InstallGenerator < MigrationGenerator
8
+ # Class names of MySQL adapters.
9
+ # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
10
+ # - `Mysql2Adapter` - Used by `mysql2` gem.
11
+ MYSQL_ADAPTERS = [
12
+ "ActiveRecord::ConnectionAdapters::MysqlAdapter",
13
+ "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
14
+ ].freeze
15
+
16
+ source_root File.expand_path("templates", __dir__)
17
+ class_option(
18
+ :with_changes,
19
+ type: :boolean,
20
+ default: false,
21
+ desc: "Store changeset (diff) with each version"
22
+ )
23
+
24
+ desc "Generates (but does not run) a migration to add a versions table." \
25
+ " See section 5.c. Generators in README.md for more information."
26
+
27
+ def create_migration_file
28
+ add_paper_trail_migration(
29
+ "create_versions",
30
+ item_type_options: item_type_options,
31
+ versions_table_options: versions_table_options
32
+ )
33
+ if options.with_changes?
34
+ add_paper_trail_migration("add_object_changes_to_versions")
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
41
+ # See https://github.com/paper-trail-gem/paper_trail/issues/651
42
+ def item_type_options
43
+ opt = { null: false }
44
+ opt[:limit] = 191 if mysql?
45
+ ", #{opt}"
46
+ end
47
+
48
+ def mysql?
49
+ MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
50
+ end
51
+
52
+ # Even modern versions of MySQL still use `latin1` as the default character
53
+ # encoding. Many users are not aware of this, and run into trouble when they
54
+ # try to use PaperTrail in apps that otherwise tend to use UTF-8. Postgres, by
55
+ # comparison, uses UTF-8 except in the unusual case where the OS is configured
56
+ # with a custom locale.
57
+ #
58
+ # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html
59
+ # - http://www.postgresql.org/docs/9.4/static/multibyte.html
60
+ #
61
+ # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had
62
+ # to be fixed later by introducing a new charset, `utf8mb4`.
63
+ #
64
+ # - https://mathiasbynens.be/notes/mysql-utf8mb4
65
+ # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
66
+ #
67
+ def versions_table_options
68
+ if mysql?
69
+ ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
70
+ else
71
+ ""
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,12 @@
1
+ # This migration adds the optional `object_changes` column, in which PaperTrail
2
+ # will store the `changes` diff for each update event. See the readme for
3
+ # details.
4
+ class AddObjectChangesToVersions < ActiveRecord::Migration<%= migration_version %>
5
+ # The largest text column available in all supported RDBMS.
6
+ # See `create_versions.rb` for details.
7
+ TEXT_BYTES = 1_073_741_823
8
+
9
+ def change
10
+ add_column :versions, :object_changes, :text, limit: TEXT_BYTES
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ # This migration creates the `versions` table, the only schema PT requires.
2
+ # All other migrations PT provides are optional.
3
+ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
4
+
5
+ # The largest text column available in all supported RDBMS is
6
+ # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
7
+ # so that MySQL will use `longtext` instead of `text`. Otherwise,
8
+ # when serializing very large objects, `text` might not be big enough.
9
+ TEXT_BYTES = 1_073_741_823
10
+
11
+ def change
12
+ create_table :versions<%= versions_table_options %> do |t|
13
+ t.string :item_type<%= item_type_options %>
14
+ t.bigint :item_id, null: false
15
+ t.string :event, null: false
16
+ t.string :whodunnit
17
+ t.text :object, limit: TEXT_BYTES
18
+
19
+ # Known issue in MySQL: fractional second precision
20
+ # -------------------------------------------------
21
+ #
22
+ # MySQL timestamp columns do not support fractional seconds unless
23
+ # defined with "fractional seconds precision". MySQL users should manually
24
+ # add fractional seconds precision to this migration, specifically, to
25
+ # the `created_at` column.
26
+ # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
27
+ #
28
+ # MySQL users should also upgrade to at least rails 4.2, which is the first
29
+ # version of ActiveRecord with support for fractional seconds in MySQL.
30
+ # (https://github.com/rails/rails/pull/14359)
31
+ #
32
+ t.datetime :created_at
33
+ end
34
+ add_index :versions, %i(item_type item_id)
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module PaperTrail
7
+ # Basic structure to support a generator that builds a migration
8
+ class MigrationGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ def self.next_migration_number(dirname)
12
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
13
+ end
14
+
15
+ protected
16
+
17
+ def add_paper_trail_migration(template, extra_options = {})
18
+ migration_dir = File.expand_path("db/migrate")
19
+ if self.class.migration_exists?(migration_dir, template)
20
+ ::Kernel.warn "Migration already exists: #{template}"
21
+ else
22
+ migration_template(
23
+ "#{template}.rb.erb",
24
+ "db/migrate/#{template}.rb",
25
+ { migration_version: migration_version }.merge(extra_options)
26
+ )
27
+ end
28
+ end
29
+
30
+ def migration_version
31
+ format(
32
+ "[%d.%d]",
33
+ ActiveRecord::VERSION::MAJOR,
34
+ ActiveRecord::VERSION::MINOR
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,4 @@
1
+ Description:
2
+ Generates (but does not run) a migration to update item_type for STI entries
3
+ in an existing versions table. See section 5.c. Generators in README.md for
4
+ more information.
@@ -0,0 +1,85 @@
1
+ # This migration updates existing `versions` that have `item_type` that refers to
2
+ # the base_class, and changes them to refer to the subclass instead.
3
+ class UpdateVersionsForItemSubtype < ActiveRecord::Migration<%= migration_version %>
4
+ include ActionView::Helpers::TextHelper
5
+ def up
6
+ <%=
7
+ # Returns class, column, range
8
+ def self.parse_custom_entry(text)
9
+ parts = text.split("):")
10
+ range = parts.last.split("..").map(&:to_i)
11
+ range = Range.new(range.first, range.last)
12
+ parts.first.split("(") + [range]
13
+ end
14
+ # Running:
15
+ # rails g paper_trail:update_item_subtype Animal(species):1..4 Plant(genus):42..1337
16
+ # results in:
17
+ # # Versions of item_type "Animal" with IDs between 1 and 4 will be updated based on `species`
18
+ # # Versions of item_type "Plant" with IDs between 42 and 1337 will be updated based on `genus`
19
+ # hints = {"Animal"=>{1..4=>"species"}, "Plant"=>{42..1337=>"genus"}}
20
+ hint_descriptions = ""
21
+ hints = args.inject(Hash.new{|h, k| h[k] = {}}) do |s, v|
22
+ klass, column, range = parse_custom_entry(v)
23
+ hint_descriptions << " # Versions of item_type \"#{klass}\" with IDs between #{
24
+ range.first} and #{range.last} will be updated based on \`#{column}\`\n"
25
+ s[klass][range] = column
26
+ s
27
+ end
28
+
29
+ unless hints.empty?
30
+ "#{hint_descriptions} hints = #{hints.inspect}\n"
31
+ end
32
+ %>
33
+ # Find all ActiveRecord models mentioned in existing versions
34
+ changes = Hash.new { |h, k| h[k] = [] }
35
+ model_names = PaperTrail::Version.select(:item_type).distinct
36
+ model_names.map(&:item_type).each do |model_name|
37
+ hint = hints[model_name] if defined?(hints)
38
+ begin
39
+ klass = model_name.constantize
40
+ # Actually implements an inheritance_column? (Usually "type")
41
+ has_inheritance_column = klass.columns.map(&:name).include?(klass.inheritance_column)
42
+ # Find domain of types stored in PaperTrail versions
43
+ PaperTrail::Version.where(item_type: model_name, item_subtype: nil).select(:id, :object, :object_changes).each do |obj|
44
+ if (object_detail = PaperTrail.serializer.load(obj.object || obj.object_changes))
45
+ is_found = false
46
+ subtype_name = nil
47
+ hint&.each do |k, v|
48
+ if k === obj.id && (subtype_name = object_detail[v])
49
+ break
50
+ end
51
+ end
52
+ if subtype_name.nil? && has_inheritance_column
53
+ subtype_name = object_detail[klass.inheritance_column]
54
+ end
55
+ if subtype_name
56
+ subtype_name = subtype_name.last if subtype_name.is_a?(Array)
57
+ if subtype_name != model_name
58
+ changes[subtype_name] << obj.id
59
+ end
60
+ end
61
+ end
62
+ end
63
+ rescue NameError => ex
64
+ say "Skipping reference to #{model_name}", subitem: true
65
+ end
66
+ end
67
+ changes.each do |k, v|
68
+ # Update in blocks of up to 100 at a time
69
+ block_of_ids = []
70
+ id_count = 0
71
+ num_updated = 0
72
+ v.sort.each do |id|
73
+ block_of_ids << id
74
+ if (id_count += 1) % 100 == 0
75
+ num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_subtype: k)
76
+ block_of_ids = []
77
+ end
78
+ end
79
+ num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_subtype: k)
80
+ if num_updated > 0
81
+ say "Associated #{pluralize(num_updated, 'record')} to #{k}", subitem: true
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../migration_generator"
4
+
5
+ module PaperTrail
6
+ # Updates STI entries for PaperTrail
7
+ class UpdateItemSubtypeGenerator < MigrationGenerator
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\
11
+ "existing versions table."
12
+
13
+ def create_migration_file
14
+ add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ Attribute Serializers
2
+ =====================
3
+
4
+ "Serialization" here refers to the preparation of data for insertion into a
5
+ database, particularly the `object` and `object_changes` columns in the
6
+ `versions` table.
7
+
8
+ Likewise, "deserialization" refers to any processing of data after they
9
+ have been read from the database, for example preparing the result of
10
+ `VersionConcern#changeset`.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/type_serializers/postgres_array_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Values returned by some Active Record serializers are
8
+ # not suited for writing JSON to a text column. This factory
9
+ # replaces certain default Active Record serializers
10
+ # with custom PaperTrail ones.
11
+ module AttributeSerializerFactory
12
+ AR_PG_ARRAY_CLASS = "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array"
13
+
14
+ def self.for(klass, attr)
15
+ active_record_serializer = klass.type_for_attribute(attr)
16
+ if active_record_serializer.class.name == AR_PG_ARRAY_CLASS
17
+ TypeSerializers::PostgresArraySerializer.new(
18
+ active_record_serializer.subtype,
19
+ active_record_serializer.delimiter
20
+ )
21
+ else
22
+ active_record_serializer
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/attribute_serializers/attribute_serializer_factory"
4
+
5
+ module PaperTrail
6
+ # :nodoc:
7
+ module AttributeSerializers
8
+ # The `CastAttributeSerializer` (de)serializes model attribute values. For
9
+ # example, the string "1.99" serializes into the integer `1` when assigned
10
+ # to an attribute of type `ActiveRecord::Type::Integer`.
11
+ #
12
+ # This implementation depends on the `type_for_attribute` method, which was
13
+ # introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2.
14
+ class CastAttributeSerializer
15
+ def initialize(klass)
16
+ @klass = klass
17
+ end
18
+
19
+ private
20
+
21
+ # Returns a hash mapping attributes to hashes that map strings to
22
+ # integers. Example:
23
+ #
24
+ # ```
25
+ # { "status" => { "draft"=>0, "published"=>1, "archived"=>2 } }
26
+ # ```
27
+ #
28
+ # ActiveRecord::Enum was added in AR 4.1
29
+ # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
30
+ def defined_enums
31
+ @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
32
+ end
33
+ end
34
+
35
+ # Uses AR 5's `serialize` and `deserialize`.
36
+ class CastAttributeSerializer
37
+ def serialize(attr, val)
38
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
39
+ end
40
+
41
+ def deserialize(attr, val)
42
+ if defined_enums[attr] && val.is_a?(::String)
43
+ # Because PT 4 used to save the string version of enums to `object_changes`
44
+ val
45
+ else
46
+ AttributeSerializerFactory.for(@klass, attr).deserialize(val)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/attribute_serializers/cast_attribute_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Serialize or deserialize the `version.object` column.
8
+ class ObjectAttribute
9
+ def initialize(model_class)
10
+ @model_class = model_class
11
+ end
12
+
13
+ def serialize(attributes)
14
+ alter(attributes, :serialize)
15
+ end
16
+
17
+ def deserialize(attributes)
18
+ alter(attributes, :deserialize)
19
+ end
20
+
21
+ private
22
+
23
+ # Modifies `attributes` in place.
24
+ # TODO: Return a new hash instead.
25
+ def alter(attributes, serialization_method)
26
+ # Don't serialize before values before inserting into columns of type
27
+ # `JSON` on `PostgreSQL` databases.
28
+ return attributes if object_col_is_json?
29
+
30
+ serializer = CastAttributeSerializer.new(@model_class)
31
+ attributes.each do |key, value|
32
+ attributes[key] = serializer.send(serialization_method, key, value)
33
+ end
34
+ end
35
+
36
+ def object_col_is_json?
37
+ @model_class.paper_trail.version_class.object_col_is_json?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/attribute_serializers/cast_attribute_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Serialize or deserialize the `version.object_changes` column.
8
+ class ObjectChangesAttribute
9
+ def initialize(item_class)
10
+ @item_class = item_class
11
+ end
12
+
13
+ def serialize(changes)
14
+ alter(changes, :serialize)
15
+ end
16
+
17
+ def deserialize(changes)
18
+ alter(changes, :deserialize)
19
+ end
20
+
21
+ private
22
+
23
+ # Modifies `changes` in place.
24
+ # TODO: Return a new hash instead.
25
+ def alter(changes, serialization_method)
26
+ # Don't serialize before values before inserting into columns of type
27
+ # `JSON` on `PostgreSQL` databases.
28
+ return changes if object_changes_col_is_json?
29
+
30
+ serializer = CastAttributeSerializer.new(@item_class)
31
+ changes.clone.each do |key, change|
32
+ # `change` is an Array with two elements, representing before and after.
33
+ changes[key] = Array(change).map do |value|
34
+ serializer.send(serialization_method, key, value)
35
+ end
36
+ end
37
+ end
38
+
39
+ def object_changes_col_is_json?
40
+ @item_class.paper_trail.version_class.object_changes_col_is_json?
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,35 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
4
+ # Utilities for deleting version records.
2
5
  module Cleaner
3
- # Destroys all but the most recent version(s) for items on a given date (or on all dates). Useful for deleting drafts.
6
+ # Destroys all but the most recent version(s) for items on a given date
7
+ # (or on all dates). Useful for deleting drafts.
4
8
  #
5
9
  # Options:
6
- # :keeping An `integer` indicating the number of versions to be kept for each item per date.
7
- # Defaults to `1`.
8
- # :date Should either be a `Date` object specifying which date to destroy versions for or `:all`,
9
- # which will specify that all dates should be cleaned. Defaults to `:all`.
10
- # :item_id The `id` for the item to be cleaned on, or `nil`, which causes all items to be cleaned.
11
- # Defaults to `nil`.
10
+ #
11
+ # - :keeping - An `integer` indicating the number of versions to be kept for
12
+ # each item per date. Defaults to `1`. The most recent matching versions
13
+ # are kept.
14
+ # - :date - Should either be a `Date` object specifying which date to
15
+ # destroy versions for or `:all`, which will specify that all dates
16
+ # should be cleaned. Defaults to `:all`.
17
+ # - :item_id - The `id` for the item to be cleaned on, or `nil`, which
18
+ # causes all items to be cleaned. Defaults to `nil`.
19
+ #
12
20
  def clean_versions!(options = {})
13
- options = {:keeping => 1, :date => :all}.merge(options)
14
- gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
15
- versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }.each do |date, _versions|
16
- # remove the number of versions we wish to keep from the collection of versions prior to destruction
17
- _versions.pop(options[:keeping])
18
- _versions.map(&:destroy)
21
+ options = { keeping: 1, date: :all }.merge(options)
22
+ gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions|
23
+ group_versions_by_date(item_versions).each do |_date, date_versions|
24
+ # Remove the number of versions we wish to keep from the collection
25
+ # of versions prior to destruction.
26
+ date_versions.pop(options[:keeping])
27
+ date_versions.map(&:destroy)
19
28
  end
20
29
  end
21
30
  end
22
31
 
23
32
  private
24
33
 
25
- # Returns a hash of versions grouped by the `item_id` attribute formatted like this: {:item_id => PaperTrail::Version}.
26
- # If `item_id` or `date` is set, versions will be narrowed to those pointing at items with those ids that were created on specified date.
34
+ # Returns a hash of versions grouped by the `item_id` attribute formatted
35
+ # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
36
+ # set, versions will be narrowed to those pointing at items with those ids
37
+ # that were created on specified date. Versions are returned in
38
+ # chronological order.
27
39
  def gather_versions(item_id = nil, date = :all)
28
- raise ArgumentError.new("`date` argument must receive a Timestamp or `:all`") unless date == :all || date.respond_to?(:to_date)
29
- versions = item_id ? PaperTrail::Version.where(:item_id => item_id) : PaperTrail::Version
40
+ unless date == :all || date.respond_to?(:to_date)
41
+ raise ArgumentError, "Expected date to be a Timestamp or :all"
42
+ end
43
+ versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version
44
+ versions = versions.order(PaperTrail::Version.timestamp_sort_order)
30
45
  versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
31
- versions = PaperTrail::Version.all if versions == PaperTrail::Version # if versions has not been converted to an ActiveRecord::Relation yet, do so now
46
+
47
+ # If `versions` has not been converted to an ActiveRecord::Relation yet,
48
+ # do so now.
49
+ versions = PaperTrail::Version.all if versions == PaperTrail::Version
32
50
  versions.group_by(&:item_id)
33
51
  end
52
+
53
+ # Given an array of versions, returns a hash mapping dates to arrays of
54
+ # versions.
55
+ # @api private
56
+ def group_versions_by_date(versions)
57
+ versions.group_by { |v| v.created_at.to_date }
58
+ end
34
59
  end
35
60
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Rails does not follow SemVer, makes breaking changes in minor versions.
5
+ # Breaking changes are expected, and are generally good for the rails
6
+ # ecosystem. However, they often require dozens of hours to fix, even with the
7
+ # [help of experts](https://github.com/paper-trail-gem/paper_trail/pull/899).
8
+ #
9
+ # It is not safe to assume that a new version of rails will be compatible with
10
+ # PaperTrail. PT is only compatible with the versions of rails that it is
11
+ # tested against. See `.travis.yml`.
12
+ #
13
+ # However, as of
14
+ # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
15
+ # gemspec allows installation with newer, incompatible rails versions. We hope
16
+ # this will make it easier for contributors to work on compatibility with
17
+ # newer rails versions. Most PT users should avoid incompatible rails
18
+ # versions.
19
+ module Compatibility
20
+ ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
21
+ ACTIVERECORD_LT = "< 6.2" # not enforced in gemspec
22
+
23
+ E_INCOMPATIBLE_AR = <<-EOS
24
+ PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
25
+ contributors to install incompatible versions of ActiveRecord, and this
26
+ warning can be silenced with an environment variable, but this is a bad
27
+ idea for normal use. Please install a compatible version of ActiveRecord
28
+ instead (%s). Please see the discussion in paper_trail/compatibility.rb
29
+ for details.
30
+ EOS
31
+
32
+ # Normal users need a warning if they accidentally install an incompatible
33
+ # version of ActiveRecord. Contributors can silence this warning with an
34
+ # environment variable.
35
+ def self.check_activerecord(ar_version)
36
+ raise ::TypeError unless ar_version.instance_of?(::Gem::Version)
37
+ return if ::ENV["PT_SILENCE_AR_COMPAT_WARNING"].present?
38
+ req = ::Gem::Requirement.new([ACTIVERECORD_GTE, ACTIVERECORD_LT])
39
+ unless req.satisfied_by?(ar_version)
40
+ ::Kernel.warn(
41
+ format(
42
+ E_INCOMPATIBLE_AR,
43
+ ::PaperTrail.gem_version,
44
+ ar_version,
45
+ req
46
+ )
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end