paper_trail 4.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +105 -0
  3. data/.github/ISSUE_TEMPLATE.md +13 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +100 -0
  6. data/.rubocop_todo.yml +14 -0
  7. data/.travis.yml +11 -10
  8. data/Appraisals +37 -0
  9. data/CHANGELOG.md +173 -8
  10. data/Gemfile +1 -1
  11. data/README.md +641 -470
  12. data/Rakefile +19 -19
  13. data/doc/bug_report_template.rb +71 -0
  14. data/doc/warning_about_not_setting_whodunnit.md +32 -0
  15. data/gemfiles/ar3.gemfile +18 -0
  16. data/gemfiles/ar4.gemfile +7 -0
  17. data/gemfiles/ar5.gemfile +13 -0
  18. data/lib/generators/paper_trail/install_generator.rb +26 -18
  19. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +3 -1
  20. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
  21. data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
  22. data/lib/generators/paper_trail/templates/create_versions.rb +53 -5
  23. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  24. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +58 -0
  25. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
  26. data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
  27. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
  28. data/lib/paper_trail/cleaner.rb +41 -18
  29. data/lib/paper_trail/config.rb +42 -26
  30. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
  31. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
  32. data/lib/paper_trail/frameworks/active_record.rb +2 -2
  33. data/lib/paper_trail/frameworks/cucumber.rb +1 -0
  34. data/lib/paper_trail/frameworks/rails/controller.rb +50 -14
  35. data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
  36. data/lib/paper_trail/frameworks/rails.rb +2 -7
  37. data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
  38. data/lib/paper_trail/frameworks/rspec.rb +5 -5
  39. data/lib/paper_trail/frameworks/sinatra.rb +8 -5
  40. data/lib/paper_trail/has_paper_trail.rb +381 -221
  41. data/lib/paper_trail/record_history.rb +57 -0
  42. data/lib/paper_trail/reifier.rb +450 -0
  43. data/lib/paper_trail/serializers/json.rb +7 -7
  44. data/lib/paper_trail/serializers/yaml.rb +31 -12
  45. data/lib/paper_trail/version_association_concern.rb +6 -2
  46. data/lib/paper_trail/version_concern.rb +200 -287
  47. data/lib/paper_trail/version_number.rb +6 -9
  48. data/lib/paper_trail.rb +169 -137
  49. data/paper_trail.gemspec +41 -43
  50. data/spec/generators/install_generator_spec.rb +24 -25
  51. data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
  52. data/spec/models/animal_spec.rb +23 -6
  53. data/spec/models/boolit_spec.rb +8 -8
  54. data/spec/models/callback_modifier_spec.rb +96 -0
  55. data/spec/models/car_spec.rb +13 -0
  56. data/spec/models/fluxor_spec.rb +3 -3
  57. data/spec/models/gadget_spec.rb +19 -19
  58. data/spec/models/joined_version_spec.rb +3 -3
  59. data/spec/models/json_version_spec.rb +50 -28
  60. data/spec/models/kitchen/banana_spec.rb +3 -3
  61. data/spec/models/not_on_update_spec.rb +7 -4
  62. data/spec/models/post_with_status_spec.rb +13 -3
  63. data/spec/models/skipper_spec.rb +40 -11
  64. data/spec/models/thing_spec.rb +4 -4
  65. data/spec/models/truck_spec.rb +5 -0
  66. data/spec/models/vehicle_spec.rb +5 -0
  67. data/spec/models/version_spec.rb +103 -59
  68. data/spec/models/widget_spec.rb +86 -55
  69. data/spec/modules/paper_trail_spec.rb +2 -2
  70. data/spec/modules/version_concern_spec.rb +11 -12
  71. data/spec/modules/version_number_spec.rb +3 -4
  72. data/spec/paper_trail/config_spec.rb +33 -0
  73. data/spec/paper_trail_spec.rb +16 -14
  74. data/spec/rails_helper.rb +10 -9
  75. data/spec/requests/articles_spec.rb +11 -7
  76. data/spec/spec_helper.rb +42 -17
  77. data/spec/support/alt_db_init.rb +8 -13
  78. data/test/custom_json_serializer.rb +3 -3
  79. data/test/dummy/Rakefile +2 -2
  80. data/test/dummy/app/controllers/application_controller.rb +21 -8
  81. data/test/dummy/app/controllers/articles_controller.rb +11 -8
  82. data/test/dummy/app/controllers/widgets_controller.rb +13 -12
  83. data/test/dummy/app/models/animal.rb +1 -1
  84. data/test/dummy/app/models/article.rb +19 -11
  85. data/test/dummy/app/models/authorship.rb +1 -1
  86. data/test/dummy/app/models/bar_habtm.rb +4 -0
  87. data/test/dummy/app/models/book.rb +4 -4
  88. data/test/dummy/app/models/boolit.rb +1 -1
  89. data/test/dummy/app/models/callback_modifier.rb +45 -0
  90. data/test/dummy/app/models/car.rb +3 -0
  91. data/test/dummy/app/models/chapter.rb +9 -0
  92. data/test/dummy/app/models/citation.rb +5 -0
  93. data/test/dummy/app/models/customer.rb +1 -1
  94. data/test/dummy/app/models/document.rb +2 -2
  95. data/test/dummy/app/models/editor.rb +1 -1
  96. data/test/dummy/app/models/foo_habtm.rb +5 -0
  97. data/test/dummy/app/models/fruit.rb +2 -2
  98. data/test/dummy/app/models/gadget.rb +1 -1
  99. data/test/dummy/app/models/kitchen/banana.rb +1 -1
  100. data/test/dummy/app/models/legacy_widget.rb +2 -2
  101. data/test/dummy/app/models/line_item.rb +1 -1
  102. data/test/dummy/app/models/not_on_update.rb +1 -1
  103. data/test/dummy/app/models/paragraph.rb +5 -0
  104. data/test/dummy/app/models/person.rb +6 -6
  105. data/test/dummy/app/models/post.rb +1 -1
  106. data/test/dummy/app/models/post_with_status.rb +1 -1
  107. data/test/dummy/app/models/quotation.rb +5 -0
  108. data/test/dummy/app/models/section.rb +6 -0
  109. data/test/dummy/app/models/skipper.rb +2 -2
  110. data/test/dummy/app/models/song.rb +13 -4
  111. data/test/dummy/app/models/thing.rb +2 -2
  112. data/test/dummy/app/models/translation.rb +2 -2
  113. data/test/dummy/app/models/truck.rb +4 -0
  114. data/test/dummy/app/models/vehicle.rb +4 -0
  115. data/test/dummy/app/models/whatchamajigger.rb +1 -1
  116. data/test/dummy/app/models/widget.rb +7 -6
  117. data/test/dummy/app/versions/joined_version.rb +4 -3
  118. data/test/dummy/app/versions/json_version.rb +1 -1
  119. data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
  120. data/test/dummy/app/versions/post_version.rb +2 -2
  121. data/test/dummy/config/application.rb +20 -9
  122. data/test/dummy/config/boot.rb +5 -5
  123. data/test/dummy/config/database.postgres.yml +1 -1
  124. data/test/dummy/config/environment.rb +1 -1
  125. data/test/dummy/config/environments/development.rb +4 -3
  126. data/test/dummy/config/environments/production.rb +3 -2
  127. data/test/dummy/config/environments/test.rb +15 -5
  128. data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
  129. data/test/dummy/config/initializers/paper_trail.rb +4 -3
  130. data/test/dummy/config/initializers/secret_token.rb +3 -1
  131. data/test/dummy/config/initializers/session_store.rb +1 -1
  132. data/test/dummy/config/routes.rb +2 -2
  133. data/test/dummy/config.ru +1 -1
  134. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +148 -68
  135. data/test/dummy/db/schema.rb +119 -31
  136. data/test/dummy/script/rails +6 -4
  137. data/test/functional/controller_test.rb +34 -35
  138. data/test/functional/enabled_for_controller_test.rb +6 -7
  139. data/test/functional/modular_sinatra_test.rb +43 -38
  140. data/test/functional/sinatra_test.rb +49 -40
  141. data/test/functional/thread_safety_test.rb +4 -6
  142. data/test/paper_trail_test.rb +15 -14
  143. data/test/test_helper.rb +78 -18
  144. data/test/time_travel_helper.rb +1 -15
  145. data/test/unit/associations_test.rb +1016 -0
  146. data/test/unit/cleaner_test.rb +66 -60
  147. data/test/unit/inheritance_column_test.rb +19 -19
  148. data/test/unit/model_test.rb +646 -1071
  149. data/test/unit/protected_attrs_test.rb +19 -14
  150. data/test/unit/serializer_test.rb +44 -43
  151. data/test/unit/serializers/json_test.rb +28 -21
  152. data/test/unit/serializers/mixin_json_test.rb +15 -14
  153. data/test/unit/serializers/mixin_yaml_test.rb +20 -16
  154. data/test/unit/serializers/yaml_test.rb +16 -14
  155. data/test/unit/timestamp_test.rb +10 -12
  156. data/test/unit/version_test.rb +88 -70
  157. metadata +166 -72
  158. data/gemfiles/3.0.gemfile +0 -52
data/Rakefile CHANGED
@@ -1,30 +1,30 @@
1
- require 'bundler'
1
+ require "bundler"
2
2
  Bundler::GemHelper.install_tasks
3
3
 
4
- desc 'Set a relevant database.yml for testing'
4
+ desc "Set a relevant database.yml for testing"
5
5
  task :prepare do
6
6
  ENV["DB"] ||= "sqlite"
7
- if RUBY_VERSION >= '1.9'
8
- FileUtils.cp "test/dummy/config/database.#{ENV["DB"]}.yml", "test/dummy/config/database.yml"
9
- else
10
- require 'ftools'
11
- File.cp "test/dummy/config/database.#{ENV["DB"]}.yml", "test/dummy/config/database.yml"
12
- end
7
+ FileUtils.cp "test/dummy/config/database.#{ENV['DB']}.yml", "test/dummy/config/database.yml"
13
8
  end
14
9
 
15
-
16
- require 'rake/testtask'
17
- desc 'Run tests on PaperTrail with Test::Unit.'
10
+ require "rake/testtask"
11
+ desc "Run tests on PaperTrail with Test::Unit."
18
12
  Rake::TestTask.new(:test) do |t|
19
- t.libs << 'lib'
20
- t.libs << 'test'
21
- t.pattern = 'test/**/*_test.rb'
13
+ t.libs << "lib"
14
+ t.libs << "test"
15
+ t.pattern = "test/**/*_test.rb"
22
16
  t.verbose = false
23
17
  end
24
18
 
25
- require 'rspec/core/rake_task'
26
- desc 'Run tests on PaperTrail with RSpec'
27
- RSpec::Core::RakeTask.new(:spec)
19
+ require "rspec/core/rake_task"
20
+ desc "Run tests on PaperTrail with RSpec"
21
+ task(:spec).clear
22
+ RSpec::Core::RakeTask.new(:spec) do |t|
23
+ t.verbose = false # hide list of specs bit.ly/1nVq3Jn
24
+ end
25
+
26
+ require "rubocop/rake_task"
27
+ RuboCop::RakeTask.new
28
28
 
29
- desc 'Default: run all available test suites'
30
- task :default => [:prepare, :test, :spec]
29
+ desc "Default: run all available test suites"
30
+ task default: [:rubocop, :prepare, :test, :spec]
@@ -0,0 +1,71 @@
1
+ # Use this template to report PaperTrail bugs.
2
+ # It is based on the ActiveRecord template.
3
+ # https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb
4
+ begin
5
+ require "bundler/inline"
6
+ rescue LoadError => e
7
+ $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
8
+ raise e
9
+ end
10
+
11
+ gemfile(true) do
12
+ ruby "2.2.3"
13
+ source "https://rubygems.org"
14
+ gem "activerecord", "4.2.0"
15
+ gem "minitest", "5.8.3"
16
+ gem "paper_trail", "4.0.0", require: false
17
+ gem "sqlite3"
18
+ end
19
+
20
+ require "active_record"
21
+ require "minitest/autorun"
22
+ require "logger"
23
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
24
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
25
+
26
+ ActiveRecord::Schema.define do
27
+ create_table :users, force: true do |t|
28
+ t.text :first_name, null: false
29
+ t.timestamps null: false
30
+ end
31
+ create_table :versions do |t|
32
+ t.string :item_type, null: false
33
+ t.integer :item_id, null: false
34
+ t.string :event, null: false
35
+ t.string :whodunnit
36
+ t.text :object, limit: 1_073_741_823
37
+ t.text :object_changes, limit: 1_073_741_823
38
+ t.integer :transaction_id
39
+ t.datetime :created_at
40
+ end
41
+ add_index :versions, [:item_type, :item_id]
42
+ add_index :versions, [:transaction_id]
43
+
44
+ create_table :version_associations do |t|
45
+ t.integer :version_id
46
+ t.string :foreign_key_name, null: false
47
+ t.integer :foreign_key_id
48
+ end
49
+ add_index :version_associations, [:version_id]
50
+ add_index :version_associations, [:foreign_key_name, :foreign_key_id],
51
+ name: "index_version_associations_on_foreign_key"
52
+ end
53
+
54
+ # Require `paper_trail.rb` after the `version_associations` table
55
+ # exists so that PT will track associations.
56
+ require "paper_trail"
57
+
58
+ # Include your models here. Please only include the minimum code necessary to
59
+ # reproduce your issue.
60
+ class User < ActiveRecord::Base
61
+ has_paper_trail
62
+ end
63
+
64
+ # Please write a test that demonstrates your issue by failing.
65
+ class BugTest < ActiveSupport::TestCase
66
+ def test_1
67
+ assert_difference(-> { PaperTrail::Version.count }, +1) {
68
+ User.create(first_name: "Jane")
69
+ }
70
+ end
71
+ end
@@ -0,0 +1,32 @@
1
+ # The warning about not setting whodunnit
2
+
3
+ After upgrading to PaperTrail 5, you see this warning:
4
+
5
+ > user_for_paper_trail is present, but whodunnit has not been set. PaperTrail no
6
+ > longer adds the set_paper_trail_whodunnit before_filter for you. Please add this
7
+ > before_filter to your ApplicationController to continue recording whodunnit.
8
+
9
+ ## You want to track whodunnit
10
+
11
+ Add the set_paper_trail_whodunnit before_filter to your ApplicationController.
12
+ See the PaperTrail readme for an example (https://git.io/vrsbt).
13
+
14
+ ## You don't want to track whodunnit
15
+
16
+ If you no longer want to track whodunnit, you may disable this
17
+ warning by overriding user_for_paper_trail to return nil.
18
+
19
+ ```
20
+ # in application_controller.rb
21
+ def user_for_paper_trail
22
+ nil # disable whodunnit tracking
23
+ end
24
+ ```
25
+
26
+ ## You just want the warning to go away
27
+
28
+ To disable this warning for any other reason, use `skip_after_action`.
29
+
30
+ ```
31
+ skip_after_action :warn_about_not_setting_whodunnit
32
+ ```
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 3.2.22"
6
+ gem "i18n", "~> 0.6.11"
7
+ gem "request_store", "~> 1.1.0"
8
+
9
+ group :development, :test do
10
+ gem "railties", "~> 3.2.22"
11
+ gem "test-unit", "~> 3.1.5"
12
+
13
+ platforms :ruby do
14
+ gem "mysql2", "~> 0.3.20"
15
+ end
16
+ end
17
+
18
+ gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.2"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "5.0.0.beta3"
6
+ gem "activemodel", "5.0.0.beta3"
7
+ gem "actionpack", "5.0.0.beta3"
8
+ gem "railties", "5.0.0.beta3"
9
+ gem "rspec-rails", "3.5.0.beta3"
10
+ gem "rails-controller-testing"
11
+ gem "sinatra", :github => "sinatra/sinatra"
12
+
13
+ gemspec :path => "../"
@@ -1,24 +1,33 @@
1
- require 'rails/generators'
2
- require 'rails/generators/active_record'
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
3
 
4
4
  module PaperTrail
5
+ # Installs PaperTrail in a rails app.
5
6
  class InstallGenerator < ::Rails::Generators::Base
6
7
  include ::Rails::Generators::Migration
7
8
 
8
- source_root File.expand_path('../templates', __FILE__)
9
- class_option :with_changes, :type => :boolean, :default => false,
10
- :desc => "Store changeset (diff) with each version"
11
- class_option :with_associations, :type => :boolean, :default => false,
12
- :desc => "Store transactional IDs to support association restoration"
9
+ source_root File.expand_path("../templates", __FILE__)
10
+ class_option(
11
+ :with_changes,
12
+ type: :boolean,
13
+ default: false,
14
+ desc: "Store changeset (diff) with each version"
15
+ )
16
+ class_option(
17
+ :with_associations,
18
+ type: :boolean,
19
+ default: false,
20
+ desc: "Store transactional IDs to support association restoration"
21
+ )
13
22
 
14
- desc 'Generates (but does not run) a migration to add a versions table.'
23
+ desc "Generates (but does not run) a migration to add a versions table."
15
24
 
16
25
  def create_migration_file
17
- add_paper_trail_migration('create_versions')
18
- add_paper_trail_migration('add_object_changes_to_versions') if options.with_changes?
26
+ add_paper_trail_migration("create_versions")
27
+ add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes?
19
28
  if options.with_associations?
20
- add_paper_trail_migration('create_version_associations')
21
- add_paper_trail_migration('add_transaction_id_column_to_versions')
29
+ add_paper_trail_migration("create_version_associations")
30
+ add_paper_trail_migration("add_transaction_id_column_to_versions")
22
31
  end
23
32
  end
24
33
 
@@ -27,14 +36,13 @@ module PaperTrail
27
36
  end
28
37
 
29
38
  protected
30
- def add_paper_trail_migration(template)
31
- migration_dir = File.expand_path('db/migrate')
32
39
 
33
- unless self.class.migration_exists?(migration_dir, template)
34
- migration_template "#{template}.rb", "db/migrate/#{template}.rb"
40
+ def add_paper_trail_migration(template)
41
+ migration_dir = File.expand_path("db/migrate")
42
+ if self.class.migration_exists?(migration_dir, template)
43
+ ::Kernel.warn "Migration already exists: #{template}"
35
44
  else
36
- warn("ALERT: Migration already exists named '#{template}'." +
37
- " Please check your migrations directory before re-running")
45
+ migration_template "#{template}.rb", "db/migrate/#{template}.rb"
38
46
  end
39
47
  end
40
48
  end
@@ -1,5 +1,7 @@
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.
1
4
  class AddObjectChangesToVersions < ActiveRecord::Migration
2
-
3
5
  # The largest text column available in all supported RDBMS.
4
6
  # See `create_versions.rb` for details.
5
7
  TEXT_BYTES = 1_073_741_823
@@ -1,3 +1,5 @@
1
+ # This migration and CreateVersionAssociations provide the necessary
2
+ # schema for tracking associations.
1
3
  class AddTransactionIdColumnToVersions < ActiveRecord::Migration
2
4
  def self.up
3
5
  add_column :versions, :transaction_id, :integer
@@ -1,17 +1,22 @@
1
+ # This migration and AddTransactionIdColumnToVersions provide the necessary
2
+ # schema for tracking associations.
1
3
  class CreateVersionAssociations < ActiveRecord::Migration
2
4
  def self.up
3
5
  create_table :version_associations do |t|
4
6
  t.integer :version_id
5
- t.string :foreign_key_name, :null => false
7
+ t.string :foreign_key_name, null: false
6
8
  t.integer :foreign_key_id
7
9
  end
8
10
  add_index :version_associations, [:version_id]
9
- add_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_version_associations_on_foreign_key'
11
+ add_index :version_associations,
12
+ [:foreign_key_name, :foreign_key_id],
13
+ name: "index_version_associations_on_foreign_key"
10
14
  end
11
15
 
12
16
  def self.down
13
17
  remove_index :version_associations, [:version_id]
14
- remove_index :version_associations, :name => 'index_version_associations_on_foreign_key'
18
+ remove_index :version_associations,
19
+ name: "index_version_associations_on_foreign_key"
15
20
  drop_table :version_associations
16
21
  end
17
- end
22
+ end
@@ -1,4 +1,13 @@
1
+ # This migration creates the `versions` table, the only schema PT requires.
2
+ # All other migrations PT provides are optional.
1
3
  class CreateVersions < ActiveRecord::Migration
4
+ # Class names of MySQL adapters.
5
+ # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
6
+ # - `Mysql2Adapter` - Used by `mysql2` gem.
7
+ MYSQL_ADAPTERS = [
8
+ "ActiveRecord::ConnectionAdapters::MysqlAdapter",
9
+ "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
10
+ ].freeze
2
11
 
3
12
  # The largest text column available in all supported RDBMS is
4
13
  # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
@@ -7,14 +16,53 @@ class CreateVersions < ActiveRecord::Migration
7
16
  TEXT_BYTES = 1_073_741_823
8
17
 
9
18
  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
19
+ create_table :versions, versions_table_options do |t|
20
+ t.string :item_type, null: false
21
+ t.integer :item_id, null: false
22
+ t.string :event, null: false
14
23
  t.string :whodunnit
15
- t.text :object, :limit => TEXT_BYTES
24
+ t.text :object, limit: TEXT_BYTES
25
+
26
+ # Known issue in MySQL: fractional second precision
27
+ # -------------------------------------------------
28
+ #
29
+ # MySQL timestamp columns do not support fractional seconds unless
30
+ # defined with "fractional seconds precision". MySQL users should manually
31
+ # add fractional seconds precision to this migration, specifically, to
32
+ # the `created_at` column.
33
+ # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
34
+ #
35
+ # MySQL users should also upgrade to rails 4.2, which is the first
36
+ # version of ActiveRecord with support for fractional seconds in MySQL.
37
+ # (https://github.com/rails/rails/pull/14359)
38
+ #
16
39
  t.datetime :created_at
17
40
  end
18
41
  add_index :versions, [:item_type, :item_id]
19
42
  end
43
+
44
+ private
45
+
46
+ # Even modern versions of MySQL still use `latin1` as the default character
47
+ # encoding. Many users are not aware of this, and run into trouble when they
48
+ # try to use PaperTrail in apps that otherwise tend to use UTF-8. Postgres, by
49
+ # comparison, uses UTF-8 except in the unusual case where the OS is configured
50
+ # with a custom locale.
51
+ #
52
+ # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html
53
+ # - http://www.postgresql.org/docs/9.4/static/multibyte.html
54
+ #
55
+ # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had
56
+ # to be fixed later by introducing a new charset, `utf8mb4`.
57
+ #
58
+ # - https://mathiasbynens.be/notes/mysql-utf8mb4
59
+ # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
60
+ #
61
+ def versions_table_options
62
+ if MYSQL_ADAPTERS.include?(connection.class.name)
63
+ { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }
64
+ else
65
+ {}
66
+ end
67
+ end
20
68
  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,58 @@
1
+ module PaperTrail
2
+ # :nodoc:
3
+ module AttributeSerializers
4
+ # The `CastAttributeSerializer` (de)serializes model attribute values. For
5
+ # example, the string "1.99" serializes into the integer `1` when assigned
6
+ # to an attribute of type `ActiveRecord::Type::Integer`.
7
+ #
8
+ # This implementation depends on the `type_for_attribute` method, which was
9
+ # introduced in rails 4.2. In older versions of rails, we shim this method
10
+ # with `LegacyActiveRecordShim`.
11
+ if ::ActiveRecord::VERSION::MAJOR >= 5
12
+ # This implementation uses AR 5's `serialize` and `deserialize`.
13
+ class CastAttributeSerializer
14
+ def initialize(klass)
15
+ @klass = klass
16
+ end
17
+
18
+ def serialize(attr, val)
19
+ @klass.type_for_attribute(attr).serialize(val)
20
+ end
21
+
22
+ def deserialize(attr, val)
23
+ @klass.type_for_attribute(attr).deserialize(val)
24
+ end
25
+ end
26
+ else
27
+ # This implementation uses AR 4.2's `type_cast_for_database`. For
28
+ # versions of AR < 4.2 we provide an implementation of
29
+ # `type_cast_for_database` in our shim attribute type classes,
30
+ # `NoOpAttribute` and `SerializedAttribute`.
31
+ class CastAttributeSerializer
32
+ def initialize(klass)
33
+ @klass = klass
34
+ end
35
+
36
+ def serialize(attr, val)
37
+ val = defined_enums[attr][val] if defined_enums[attr]
38
+ @klass.type_for_attribute(attr).type_cast_for_database(val)
39
+ end
40
+
41
+ def deserialize(attr, val)
42
+ val = @klass.type_for_attribute(attr).type_cast_from_database(val)
43
+ if defined_enums[attr]
44
+ defined_enums[attr].key(val)
45
+ else
46
+ val
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def defined_enums
53
+ @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ module PaperTrail
2
+ module AttributeSerializers
3
+ # Included into model if AR version is < 4.2. Backport Rails 4.2 and later's
4
+ # `type_for_attribute` so we can build on a common interface.
5
+ module LegacyActiveRecordShim
6
+ # An attribute which needs no processing. It is part of our backport (shim)
7
+ # of rails 4.2's attribute API. See `type_for_attribute` below.
8
+ class NoOpAttribute
9
+ def type_cast_for_database(value)
10
+ value
11
+ end
12
+
13
+ def type_cast_from_database(data)
14
+ data
15
+ end
16
+ end
17
+ NO_OP_ATTRIBUTE = NoOpAttribute.new
18
+
19
+ # An attribute which requires manual (de)serialization to/from what we get
20
+ # from the database. It is part of our backport (shim) of rails 4.2's
21
+ # attribute API. See `type_for_attribute` below.
22
+ class SerializedAttribute
23
+ def initialize(coder)
24
+ @coder = coder.respond_to?(:dump) ? coder : PaperTrail.serializer
25
+ end
26
+
27
+ def type_cast_for_database(value)
28
+ @coder.dump(value)
29
+ end
30
+
31
+ def type_cast_from_database(data)
32
+ @coder.load(data)
33
+ end
34
+ end
35
+
36
+ def type_for_attribute(attr_name)
37
+ serialized_attribute_types[attr_name.to_s] || NO_OP_ATTRIBUTE
38
+ end
39
+
40
+ def serialized_attribute_types
41
+ @attribute_types ||= Hash[serialized_attributes.map do |attr_name, coder|
42
+ [attr_name, SerializedAttribute.new(coder)]
43
+ end]
44
+ end
45
+ private :serialized_attribute_types
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ require "paper_trail/attribute_serializers/cast_attribute_serializer"
2
+
3
+ module PaperTrail
4
+ module AttributeSerializers
5
+ # Serialize or deserialize the `version.object` column.
6
+ class ObjectAttribute
7
+ def initialize(model_class)
8
+ @model_class = model_class
9
+ end
10
+
11
+ def serialize(attributes)
12
+ alter(attributes, :serialize)
13
+ end
14
+
15
+ def deserialize(attributes)
16
+ alter(attributes, :deserialize)
17
+ end
18
+
19
+ private
20
+
21
+ # Modifies `attributes` in place.
22
+ # TODO: Return a new hash instead.
23
+ def alter(attributes, serialization_method)
24
+ # Don't serialize before values before inserting into columns of type
25
+ # `JSON` on `PostgreSQL` databases.
26
+ return attributes if object_col_is_json?
27
+
28
+ serializer = CastAttributeSerializer.new(@model_class)
29
+ attributes.each do |key, value|
30
+ attributes[key] = serializer.send(serialization_method, key, value)
31
+ end
32
+ end
33
+
34
+ def object_col_is_json?
35
+ @model_class.paper_trail_version_class.object_col_is_json?
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ require "paper_trail/attribute_serializers/cast_attribute_serializer"
2
+
3
+ module PaperTrail
4
+ module AttributeSerializers
5
+ # Serialize or deserialize the `version.object_changes` column.
6
+ class ObjectChangesAttribute
7
+ def initialize(item_class)
8
+ @item_class = item_class
9
+ end
10
+
11
+ def serialize(changes)
12
+ alter(changes, :serialize)
13
+ end
14
+
15
+ def deserialize(changes)
16
+ alter(changes, :deserialize)
17
+ end
18
+
19
+ private
20
+
21
+ # Modifies `changes` in place.
22
+ # TODO: Return a new hash instead.
23
+ def alter(changes, serialization_method)
24
+ # Don't serialize before values before inserting into columns of type
25
+ # `JSON` on `PostgreSQL` databases.
26
+ return changes if object_changes_col_is_json?
27
+
28
+ serializer = CastAttributeSerializer.new(@item_class)
29
+ changes.clone.each do |key, change|
30
+ # `change` is an Array with two elements, representing before and after.
31
+ changes[key] = Array(change).map do |value|
32
+ serializer.send(serialization_method, key, value)
33
+ end
34
+ end
35
+ end
36
+
37
+ def object_changes_col_is_json?
38
+ @item_class.paper_trail_version_class.object_changes_col_is_json?
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,35 +1,58 @@
1
1
  module PaperTrail
2
+ # Utilities for deleting version records.
2
3
  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.
4
+ # Destroys all but the most recent version(s) for items on a given date
5
+ # (or on all dates). Useful for deleting drafts.
4
6
  #
5
7
  # 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`.
8
+ #
9
+ # - :keeping - An `integer` indicating the number of versions to be kept for
10
+ # each item per date. Defaults to `1`. The most recent matching versions
11
+ # are kept.
12
+ # - :date - Should either be a `Date` object specifying which date to
13
+ # destroy versions for or `:all`, which will specify that all dates
14
+ # should be cleaned. Defaults to `:all`.
15
+ # - :item_id - The `id` for the item to be cleaned on, or `nil`, which
16
+ # causes all items to be cleaned. Defaults to `nil`.
17
+ #
12
18
  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)
19
+ options = { keeping: 1, date: :all }.merge(options)
20
+ gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions|
21
+ group_versions_by_date(item_versions).each do |_date, date_versions|
22
+ # Remove the number of versions we wish to keep from the collection
23
+ # of versions prior to destruction.
24
+ date_versions.pop(options[:keeping])
25
+ date_versions.map(&:destroy)
19
26
  end
20
27
  end
21
28
  end
22
29
 
23
30
  private
24
31
 
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.
32
+ # Returns a hash of versions grouped by the `item_id` attribute formatted
33
+ # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
34
+ # set, versions will be narrowed to those pointing at items with those ids
35
+ # that were created on specified date. Versions are returned in
36
+ # chronological order.
27
37
  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
38
+ unless date == :all || date.respond_to?(:to_date)
39
+ raise ArgumentError, "Expected date to be a Timestamp or :all"
40
+ end
41
+ versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version
42
+ versions = versions.order(PaperTrail::Version.timestamp_sort_order)
30
43
  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
44
+
45
+ # If `versions` has not been converted to an ActiveRecord::Relation yet,
46
+ # do so now.
47
+ versions = PaperTrail::Version.all if versions == PaperTrail::Version
32
48
  versions.group_by(&:item_id)
33
49
  end
50
+
51
+ # Given an array of versions, returns a hash mapping dates to arrays of
52
+ # versions.
53
+ # @api private
54
+ def group_versions_by_date(versions)
55
+ versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }
56
+ end
34
57
  end
35
58
  end