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.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +105 -0
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +100 -0
- data/.rubocop_todo.yml +14 -0
- data/.travis.yml +11 -10
- data/Appraisals +37 -0
- data/CHANGELOG.md +173 -8
- data/Gemfile +1 -1
- data/README.md +641 -470
- data/Rakefile +19 -19
- data/doc/bug_report_template.rb +71 -0
- data/doc/warning_about_not_setting_whodunnit.md +32 -0
- data/gemfiles/ar3.gemfile +18 -0
- data/gemfiles/ar4.gemfile +7 -0
- data/gemfiles/ar5.gemfile +13 -0
- data/lib/generators/paper_trail/install_generator.rb +26 -18
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +3 -1
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
- data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
- data/lib/generators/paper_trail/templates/create_versions.rb +53 -5
- data/lib/paper_trail/attribute_serializers/README.md +10 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +58 -0
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
- data/lib/paper_trail/cleaner.rb +41 -18
- data/lib/paper_trail/config.rb +42 -26
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
- data/lib/paper_trail/frameworks/active_record.rb +2 -2
- data/lib/paper_trail/frameworks/cucumber.rb +1 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +50 -14
- data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
- data/lib/paper_trail/frameworks/rails.rb +2 -7
- data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
- data/lib/paper_trail/frameworks/rspec.rb +5 -5
- data/lib/paper_trail/frameworks/sinatra.rb +8 -5
- data/lib/paper_trail/has_paper_trail.rb +381 -221
- data/lib/paper_trail/record_history.rb +57 -0
- data/lib/paper_trail/reifier.rb +450 -0
- data/lib/paper_trail/serializers/json.rb +7 -7
- data/lib/paper_trail/serializers/yaml.rb +31 -12
- data/lib/paper_trail/version_association_concern.rb +6 -2
- data/lib/paper_trail/version_concern.rb +200 -287
- data/lib/paper_trail/version_number.rb +6 -9
- data/lib/paper_trail.rb +169 -137
- data/paper_trail.gemspec +41 -43
- data/spec/generators/install_generator_spec.rb +24 -25
- data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
- data/spec/models/animal_spec.rb +23 -6
- data/spec/models/boolit_spec.rb +8 -8
- data/spec/models/callback_modifier_spec.rb +96 -0
- data/spec/models/car_spec.rb +13 -0
- data/spec/models/fluxor_spec.rb +3 -3
- data/spec/models/gadget_spec.rb +19 -19
- data/spec/models/joined_version_spec.rb +3 -3
- data/spec/models/json_version_spec.rb +50 -28
- data/spec/models/kitchen/banana_spec.rb +3 -3
- data/spec/models/not_on_update_spec.rb +7 -4
- data/spec/models/post_with_status_spec.rb +13 -3
- data/spec/models/skipper_spec.rb +40 -11
- data/spec/models/thing_spec.rb +4 -4
- data/spec/models/truck_spec.rb +5 -0
- data/spec/models/vehicle_spec.rb +5 -0
- data/spec/models/version_spec.rb +103 -59
- data/spec/models/widget_spec.rb +86 -55
- data/spec/modules/paper_trail_spec.rb +2 -2
- data/spec/modules/version_concern_spec.rb +11 -12
- data/spec/modules/version_number_spec.rb +3 -4
- data/spec/paper_trail/config_spec.rb +33 -0
- data/spec/paper_trail_spec.rb +16 -14
- data/spec/rails_helper.rb +10 -9
- data/spec/requests/articles_spec.rb +11 -7
- data/spec/spec_helper.rb +42 -17
- data/spec/support/alt_db_init.rb +8 -13
- data/test/custom_json_serializer.rb +3 -3
- data/test/dummy/Rakefile +2 -2
- data/test/dummy/app/controllers/application_controller.rb +21 -8
- data/test/dummy/app/controllers/articles_controller.rb +11 -8
- data/test/dummy/app/controllers/widgets_controller.rb +13 -12
- data/test/dummy/app/models/animal.rb +1 -1
- data/test/dummy/app/models/article.rb +19 -11
- data/test/dummy/app/models/authorship.rb +1 -1
- data/test/dummy/app/models/bar_habtm.rb +4 -0
- data/test/dummy/app/models/book.rb +4 -4
- data/test/dummy/app/models/boolit.rb +1 -1
- data/test/dummy/app/models/callback_modifier.rb +45 -0
- data/test/dummy/app/models/car.rb +3 -0
- data/test/dummy/app/models/chapter.rb +9 -0
- data/test/dummy/app/models/citation.rb +5 -0
- data/test/dummy/app/models/customer.rb +1 -1
- data/test/dummy/app/models/document.rb +2 -2
- data/test/dummy/app/models/editor.rb +1 -1
- data/test/dummy/app/models/foo_habtm.rb +5 -0
- data/test/dummy/app/models/fruit.rb +2 -2
- data/test/dummy/app/models/gadget.rb +1 -1
- data/test/dummy/app/models/kitchen/banana.rb +1 -1
- data/test/dummy/app/models/legacy_widget.rb +2 -2
- data/test/dummy/app/models/line_item.rb +1 -1
- data/test/dummy/app/models/not_on_update.rb +1 -1
- data/test/dummy/app/models/paragraph.rb +5 -0
- data/test/dummy/app/models/person.rb +6 -6
- data/test/dummy/app/models/post.rb +1 -1
- data/test/dummy/app/models/post_with_status.rb +1 -1
- data/test/dummy/app/models/quotation.rb +5 -0
- data/test/dummy/app/models/section.rb +6 -0
- data/test/dummy/app/models/skipper.rb +2 -2
- data/test/dummy/app/models/song.rb +13 -4
- data/test/dummy/app/models/thing.rb +2 -2
- data/test/dummy/app/models/translation.rb +2 -2
- data/test/dummy/app/models/truck.rb +4 -0
- data/test/dummy/app/models/vehicle.rb +4 -0
- data/test/dummy/app/models/whatchamajigger.rb +1 -1
- data/test/dummy/app/models/widget.rb +7 -6
- data/test/dummy/app/versions/joined_version.rb +4 -3
- data/test/dummy/app/versions/json_version.rb +1 -1
- data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
- data/test/dummy/app/versions/post_version.rb +2 -2
- data/test/dummy/config/application.rb +20 -9
- data/test/dummy/config/boot.rb +5 -5
- data/test/dummy/config/database.postgres.yml +1 -1
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/development.rb +4 -3
- data/test/dummy/config/environments/production.rb +3 -2
- data/test/dummy/config/environments/test.rb +15 -5
- data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
- data/test/dummy/config/initializers/paper_trail.rb +4 -3
- data/test/dummy/config/initializers/secret_token.rb +3 -1
- data/test/dummy/config/initializers/session_store.rb +1 -1
- data/test/dummy/config/routes.rb +2 -2
- data/test/dummy/config.ru +1 -1
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +148 -68
- data/test/dummy/db/schema.rb +119 -31
- data/test/dummy/script/rails +6 -4
- data/test/functional/controller_test.rb +34 -35
- data/test/functional/enabled_for_controller_test.rb +6 -7
- data/test/functional/modular_sinatra_test.rb +43 -38
- data/test/functional/sinatra_test.rb +49 -40
- data/test/functional/thread_safety_test.rb +4 -6
- data/test/paper_trail_test.rb +15 -14
- data/test/test_helper.rb +78 -18
- data/test/time_travel_helper.rb +1 -15
- data/test/unit/associations_test.rb +1016 -0
- data/test/unit/cleaner_test.rb +66 -60
- data/test/unit/inheritance_column_test.rb +19 -19
- data/test/unit/model_test.rb +646 -1071
- data/test/unit/protected_attrs_test.rb +19 -14
- data/test/unit/serializer_test.rb +44 -43
- data/test/unit/serializers/json_test.rb +28 -21
- data/test/unit/serializers/mixin_json_test.rb +15 -14
- data/test/unit/serializers/mixin_yaml_test.rb +20 -16
- data/test/unit/serializers/yaml_test.rb +16 -14
- data/test/unit/timestamp_test.rb +10 -12
- data/test/unit/version_test.rb +88 -70
- metadata +166 -72
- data/gemfiles/3.0.gemfile +0 -52
data/Rakefile
CHANGED
@@ -1,30 +1,30 @@
|
|
1
|
-
require
|
1
|
+
require "bundler"
|
2
2
|
Bundler::GemHelper.install_tasks
|
3
3
|
|
4
|
-
desc
|
4
|
+
desc "Set a relevant database.yml for testing"
|
5
5
|
task :prepare do
|
6
6
|
ENV["DB"] ||= "sqlite"
|
7
|
-
|
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
|
-
|
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 <<
|
20
|
-
t.libs <<
|
21
|
-
t.pattern =
|
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
|
26
|
-
desc
|
27
|
-
|
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
|
30
|
-
task :
|
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,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
|
2
|
-
require
|
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(
|
9
|
-
class_option
|
10
|
-
:
|
11
|
-
|
12
|
-
:
|
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
|
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(
|
18
|
-
add_paper_trail_migration(
|
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(
|
21
|
-
add_paper_trail_migration(
|
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
|
-
|
34
|
-
|
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
|
-
|
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,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, :
|
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,
|
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,
|
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, :
|
12
|
-
t.integer :item_id, :
|
13
|
-
t.string :event, :
|
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,
|
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
|
data/lib/paper_trail/cleaner.rb
CHANGED
@@ -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
|
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
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# :
|
11
|
-
#
|
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 = {:
|
14
|
-
gather_versions(options[:item_id], options[:date]).each do |
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
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
|
26
|
-
# If `item_id` or `date` is
|
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
|
-
|
29
|
-
|
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
|
-
|
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
|