paper_trail 4.2.0 → 11.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install/USAGE +3 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  4. data/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  5. data/lib/generators/paper_trail/{templates/create_versions.rb → install/templates/create_versions.rb.erb} +10 -8
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  10. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +41 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +44 -0
  15. data/lib/paper_trail/cleaner.rb +18 -10
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +23 -31
  18. data/lib/paper_trail/events/base.rb +323 -0
  19. data/lib/paper_trail/events/create.rb +32 -0
  20. data/lib/paper_trail/events/destroy.rb +42 -0
  21. data/lib/paper_trail/events/update.rb +60 -0
  22. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +10 -1
  23. data/lib/paper_trail/frameworks/active_record.rb +2 -1
  24. data/lib/paper_trail/frameworks/cucumber.rb +6 -3
  25. data/lib/paper_trail/frameworks/rails/controller.rb +40 -25
  26. data/lib/paper_trail/frameworks/rails/engine.rb +39 -1
  27. data/lib/paper_trail/frameworks/rails.rb +3 -6
  28. data/lib/paper_trail/frameworks/rspec/helpers.rb +5 -1
  29. data/lib/paper_trail/frameworks/rspec.rb +22 -9
  30. data/lib/paper_trail/has_paper_trail.rb +37 -512
  31. data/lib/paper_trail/model_config.rb +251 -0
  32. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  33. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  34. data/lib/paper_trail/record_history.rb +4 -12
  35. data/lib/paper_trail/record_trail.rb +300 -0
  36. data/lib/paper_trail/reifier.rb +94 -234
  37. data/lib/paper_trail/request.rb +166 -0
  38. data/lib/paper_trail/serializers/json.rb +13 -14
  39. data/lib/paper_trail/serializers/yaml.rb +21 -15
  40. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  41. data/lib/paper_trail/version_concern.rb +218 -165
  42. data/lib/paper_trail/version_number.rb +14 -9
  43. data/lib/paper_trail.rb +138 -148
  44. metadata +94 -359
  45. data/.gitignore +0 -22
  46. data/.rspec +0 -2
  47. data/.travis.yml +0 -41
  48. data/CHANGELOG.md +0 -362
  49. data/CONTRIBUTING.md +0 -84
  50. data/Gemfile +0 -2
  51. data/MIT-LICENSE +0 -20
  52. data/README.md +0 -1535
  53. data/Rakefile +0 -30
  54. data/doc/bug_report_template.rb +0 -65
  55. data/gemfiles/ar3.gemfile +0 -61
  56. data/lib/generators/paper_trail/USAGE +0 -2
  57. data/lib/generators/paper_trail/install_generator.rb +0 -41
  58. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +0 -10
  59. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -11
  60. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -17
  61. data/lib/paper_trail/attributes_serialization.rb +0 -89
  62. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -7
  63. data/lib/paper_trail/frameworks/sinatra.rb +0 -38
  64. data/lib/paper_trail/version_association_concern.rb +0 -15
  65. data/paper_trail.gemspec +0 -59
  66. data/spec/generators/install_generator_spec.rb +0 -67
  67. data/spec/models/animal_spec.rb +0 -36
  68. data/spec/models/boolit_spec.rb +0 -48
  69. data/spec/models/callback_modifier_spec.rb +0 -96
  70. data/spec/models/fluxor_spec.rb +0 -19
  71. data/spec/models/gadget_spec.rb +0 -70
  72. data/spec/models/joined_version_spec.rb +0 -47
  73. data/spec/models/json_version_spec.rb +0 -103
  74. data/spec/models/kitchen/banana_spec.rb +0 -14
  75. data/spec/models/not_on_update_spec.rb +0 -19
  76. data/spec/models/post_with_status_spec.rb +0 -17
  77. data/spec/models/skipper_spec.rb +0 -46
  78. data/spec/models/thing_spec.rb +0 -11
  79. data/spec/models/version_spec.rb +0 -239
  80. data/spec/models/widget_spec.rb +0 -298
  81. data/spec/modules/paper_trail_spec.rb +0 -27
  82. data/spec/modules/version_concern_spec.rb +0 -32
  83. data/spec/modules/version_number_spec.rb +0 -44
  84. data/spec/paper_trail/config_spec.rb +0 -52
  85. data/spec/paper_trail_spec.rb +0 -66
  86. data/spec/rails_helper.rb +0 -34
  87. data/spec/requests/articles_spec.rb +0 -30
  88. data/spec/spec_helper.rb +0 -95
  89. data/spec/support/alt_db_init.rb +0 -59
  90. data/test/custom_json_serializer.rb +0 -13
  91. data/test/dummy/Rakefile +0 -7
  92. data/test/dummy/app/controllers/application_controller.rb +0 -20
  93. data/test/dummy/app/controllers/articles_controller.rb +0 -17
  94. data/test/dummy/app/controllers/test_controller.rb +0 -5
  95. data/test/dummy/app/controllers/widgets_controller.rb +0 -31
  96. data/test/dummy/app/helpers/application_helper.rb +0 -2
  97. data/test/dummy/app/models/animal.rb +0 -6
  98. data/test/dummy/app/models/article.rb +0 -16
  99. data/test/dummy/app/models/authorship.rb +0 -5
  100. data/test/dummy/app/models/book.rb +0 -9
  101. data/test/dummy/app/models/boolit.rb +0 -4
  102. data/test/dummy/app/models/callback_modifier.rb +0 -45
  103. data/test/dummy/app/models/cat.rb +0 -2
  104. data/test/dummy/app/models/chapter.rb +0 -9
  105. data/test/dummy/app/models/citation.rb +0 -5
  106. data/test/dummy/app/models/customer.rb +0 -4
  107. data/test/dummy/app/models/document.rb +0 -4
  108. data/test/dummy/app/models/dog.rb +0 -2
  109. data/test/dummy/app/models/editor.rb +0 -4
  110. data/test/dummy/app/models/editorship.rb +0 -5
  111. data/test/dummy/app/models/elephant.rb +0 -3
  112. data/test/dummy/app/models/fluxor.rb +0 -3
  113. data/test/dummy/app/models/foo_widget.rb +0 -2
  114. data/test/dummy/app/models/fruit.rb +0 -5
  115. data/test/dummy/app/models/gadget.rb +0 -3
  116. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  117. data/test/dummy/app/models/legacy_widget.rb +0 -4
  118. data/test/dummy/app/models/line_item.rb +0 -4
  119. data/test/dummy/app/models/not_on_update.rb +0 -4
  120. data/test/dummy/app/models/order.rb +0 -5
  121. data/test/dummy/app/models/paragraph.rb +0 -5
  122. data/test/dummy/app/models/person.rb +0 -38
  123. data/test/dummy/app/models/post.rb +0 -3
  124. data/test/dummy/app/models/post_with_status.rb +0 -8
  125. data/test/dummy/app/models/protected_widget.rb +0 -3
  126. data/test/dummy/app/models/quotation.rb +0 -5
  127. data/test/dummy/app/models/section.rb +0 -6
  128. data/test/dummy/app/models/skipper.rb +0 -6
  129. data/test/dummy/app/models/song.rb +0 -32
  130. data/test/dummy/app/models/thing.rb +0 -3
  131. data/test/dummy/app/models/translation.rb +0 -4
  132. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  133. data/test/dummy/app/models/widget.rb +0 -15
  134. data/test/dummy/app/models/wotsit.rb +0 -8
  135. data/test/dummy/app/versions/joined_version.rb +0 -5
  136. data/test/dummy/app/versions/json_version.rb +0 -3
  137. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  138. data/test/dummy/app/versions/post_version.rb +0 -3
  139. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  140. data/test/dummy/config/application.rb +0 -69
  141. data/test/dummy/config/boot.rb +0 -10
  142. data/test/dummy/config/database.mysql.yml +0 -19
  143. data/test/dummy/config/database.postgres.yml +0 -15
  144. data/test/dummy/config/database.sqlite.yml +0 -15
  145. data/test/dummy/config/environment.rb +0 -5
  146. data/test/dummy/config/environments/development.rb +0 -40
  147. data/test/dummy/config/environments/production.rb +0 -73
  148. data/test/dummy/config/environments/test.rb +0 -41
  149. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  150. data/test/dummy/config/initializers/inflections.rb +0 -10
  151. data/test/dummy/config/initializers/mime_types.rb +0 -5
  152. data/test/dummy/config/initializers/paper_trail.rb +0 -10
  153. data/test/dummy/config/initializers/secret_token.rb +0 -7
  154. data/test/dummy/config/initializers/session_store.rb +0 -8
  155. data/test/dummy/config/locales/en.yml +0 -5
  156. data/test/dummy/config/routes.rb +0 -4
  157. data/test/dummy/config.ru +0 -4
  158. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -287
  159. data/test/dummy/db/schema.rb +0 -246
  160. data/test/dummy/script/rails +0 -6
  161. data/test/functional/controller_test.rb +0 -91
  162. data/test/functional/enabled_for_controller_test.rb +0 -29
  163. data/test/functional/modular_sinatra_test.rb +0 -48
  164. data/test/functional/sinatra_test.rb +0 -49
  165. data/test/functional/thread_safety_test.rb +0 -48
  166. data/test/paper_trail_test.rb +0 -38
  167. data/test/test_helper.rb +0 -105
  168. data/test/time_travel_helper.rb +0 -15
  169. data/test/unit/associations_test.rb +0 -726
  170. data/test/unit/cleaner_test.rb +0 -182
  171. data/test/unit/inheritance_column_test.rb +0 -43
  172. data/test/unit/model_test.rb +0 -1373
  173. data/test/unit/protected_attrs_test.rb +0 -47
  174. data/test/unit/serializer_test.rb +0 -117
  175. data/test/unit/serializers/json_test.rb +0 -88
  176. data/test/unit/serializers/mixin_json_test.rb +0 -36
  177. data/test/unit/serializers/mixin_yaml_test.rb +0 -49
  178. data/test/unit/serializers/yaml_test.rb +0 -52
  179. data/test/unit/timestamp_test.rb +0 -43
  180. data/test/unit/version_test.rb +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e7937d44654d4f77422ffe61d4bc2d81b7ed4026
4
- data.tar.gz: 9ec1f8bc013795818775e58e1cd9c33e5dd36499
2
+ SHA256:
3
+ metadata.gz: f8f7237a39b0645932b394ee0332846be680f6a725ed02c7a126aa892d840ccc
4
+ data.tar.gz: b0356d622a6e6f3ba0938c4d9fe0ff211b347a0c65e2d3a6574bc8bf5fb04df6
5
5
  SHA512:
6
- metadata.gz: e546ad5e910cc2a08aeaaf0dca6754e05f3a89fbfb022b49a078025e09a623e359a552c2e52f3126c84662fad2cd804c34db709689211852ff43e32ff40db3a1
7
- data.tar.gz: 0ef05118041333567e076bc8407330917cc36ec5d23aec22fd6dced9367b10a466c4284eccddbb3f4d0675c4154165f24097eafe4acf0135e133c426a2b71d4b
6
+ metadata.gz: 46ad3d73231d4ab6c3d978925e7b838af2697b2b998b9a7948d078266fbdbec33b6dbdcbfb93751f2aa961be6a3499f499a71d56e3fbc6138174cb752feae41a
7
+ data.tar.gz: 9ec2fc044229898aa2c5ab70dc41b549b6896c61af42633af0760e6b99aadd375f5f7e39160ad713131ecb439b0f5aff8cb6696f276f4c5225eb40e371f43035
@@ -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
@@ -1,4 +1,6 @@
1
- class CreateVersions < ActiveRecord::Migration
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 %>
2
4
 
3
5
  # The largest text column available in all supported RDBMS is
4
6
  # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
@@ -7,12 +9,12 @@ class CreateVersions < ActiveRecord::Migration
7
9
  TEXT_BYTES = 1_073_741_823
8
10
 
9
11
  def change
10
- create_table :versions do |t|
11
- t.string :item_type, :null => false
12
- t.integer :item_id, :null => false
13
- t.string :event, :null => false
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
14
16
  t.string :whodunnit
15
- t.text :object, :limit => TEXT_BYTES
17
+ t.text :object, limit: TEXT_BYTES
16
18
 
17
19
  # Known issue in MySQL: fractional second precision
18
20
  # -------------------------------------------------
@@ -23,12 +25,12 @@ class CreateVersions < ActiveRecord::Migration
23
25
  # the `created_at` column.
24
26
  # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
25
27
  #
26
- # MySQL users should also upgrade to rails 4.2, which is the first
28
+ # MySQL users should also upgrade to at least rails 4.2, which is the first
27
29
  # version of ActiveRecord with support for fractional seconds in MySQL.
28
30
  # (https://github.com/rails/rails/pull/14359)
29
31
  #
30
32
  t.datetime :created_at
31
33
  end
32
- add_index :versions, [:item_type, :item_id]
34
+ add_index :versions, %i(item_type item_id)
33
35
  end
34
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,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
4
+ # Utilities for deleting version records.
2
5
  module Cleaner
3
6
  # Destroys all but the most recent version(s) for items on a given date
4
7
  # (or on all dates). Useful for deleting drafts.
@@ -6,7 +9,8 @@ module PaperTrail
6
9
  # Options:
7
10
  #
8
11
  # - :keeping - An `integer` indicating the number of versions to be kept for
9
- # each item per date. Defaults to `1`.
12
+ # each item per date. Defaults to `1`. The most recent matching versions
13
+ # are kept.
10
14
  # - :date - Should either be a `Date` object specifying which date to
11
15
  # destroy versions for or `:all`, which will specify that all dates
12
16
  # should be cleaned. Defaults to `:all`.
@@ -14,13 +18,13 @@ module PaperTrail
14
18
  # causes all items to be cleaned. Defaults to `nil`.
15
19
  #
16
20
  def clean_versions!(options = {})
17
- options = {:keeping => 1, :date => :all}.merge(options)
18
- gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
19
- group_versions_by_date(versions).each do |date, _versions|
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|
20
24
  # Remove the number of versions we wish to keep from the collection
21
25
  # of versions prior to destruction.
22
- _versions.pop(options[:keeping])
23
- _versions.map(&:destroy)
26
+ date_versions.pop(options[:keeping])
27
+ date_versions.map(&:destroy)
24
28
  end
25
29
  end
26
30
  end
@@ -30,10 +34,14 @@ module PaperTrail
30
34
  # Returns a hash of versions grouped by the `item_id` attribute formatted
31
35
  # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
32
36
  # set, versions will be narrowed to those pointing at items with those ids
33
- # that were created on specified date.
37
+ # that were created on specified date. Versions are returned in
38
+ # chronological order.
34
39
  def gather_versions(item_id = nil, date = :all)
35
- raise ArgumentError.new("`date` argument must receive a Timestamp or `:all`") unless date == :all || date.respond_to?(:to_date)
36
- 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)
37
45
  versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
38
46
 
39
47
  # If `versions` has not been converted to an ActiveRecord::Relation yet,
@@ -46,7 +54,7 @@ module PaperTrail
46
54
  # versions.
47
55
  # @api private
48
56
  def group_versions_by_date(versions)
49
- versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }
57
+ versions.group_by { |v| v.created_at.to_date }
50
58
  end
51
59
  end
52
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.1" # 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
@@ -1,47 +1,39 @@
1
- require 'singleton'
2
- require 'paper_trail/serializers/yaml'
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "paper_trail/serializers/yaml"
3
5
 
4
6
  module PaperTrail
7
+ # Global configuration affecting all threads. Some thread-specific
8
+ # configuration can be found in `paper_trail.rb`, others in `controller.rb`.
5
9
  class Config
6
10
  include Singleton
7
- attr_accessor :timestamp_field, :serializer, :version_limit
8
- attr_writer :track_associations
9
11
 
10
- def initialize
11
- @timestamp_field = :created_at
12
- @serializer = PaperTrail::Serializers::YAML
13
- end
12
+ attr_accessor(
13
+ :association_reify_error_behaviour,
14
+ :object_changes_adapter,
15
+ :serializer,
16
+ :version_limit,
17
+ :has_paper_trail_defaults
18
+ )
14
19
 
15
- def serialized_attributes
16
- ActiveSupport::Deprecation.warn(
17
- "PaperTrail.config.serialized_attributes is deprecated without " +
18
- "replacement and always returns false."
19
- )
20
- false
21
- end
22
-
23
- def serialized_attributes=(_)
24
- ActiveSupport::Deprecation.warn(
25
- "PaperTrail.config.serialized_attributes= is deprecated without " +
26
- "replacement and no longer has any effect."
27
- )
28
- end
20
+ def initialize
21
+ # Variables which affect all threads, whose access is synchronized.
22
+ @mutex = Mutex.new
23
+ @enabled = true
29
24
 
30
- def track_associations
31
- @track_associations.nil? ?
32
- PaperTrail::VersionAssociation.table_exists? :
33
- @track_associations
25
+ # Variables which affect all threads, whose access is *not* synchronized.
26
+ @serializer = PaperTrail::Serializers::YAML
27
+ @has_paper_trail_defaults = {}
34
28
  end
35
- alias_method :track_associations?, :track_associations
36
29
 
37
30
  # Indicates whether PaperTrail is on or off. Default: true.
38
31
  def enabled
39
- value = PaperTrail.paper_trail_store.fetch(:paper_trail_enabled, true)
40
- value.nil? ? true : value
32
+ @mutex.synchronize { !!@enabled }
41
33
  end
42
34
 
43
- def enabled= enable
44
- PaperTrail.paper_trail_store[:paper_trail_enabled] = enable
35
+ def enabled=(enable)
36
+ @mutex.synchronize { @enabled = enable }
45
37
  end
46
38
  end
47
39
  end