paper_trail 5.2.3 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/generators/paper_trail/install/USAGE +3 -0
- data/lib/generators/paper_trail/install/install_generator.rb +75 -0
- data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
- data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
- data/lib/generators/paper_trail/migration_generator.rb +38 -0
- data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
- data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
- data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
- data/lib/paper_trail.rb +82 -130
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
- data/lib/paper_trail/cleaner.rb +3 -1
- data/lib/paper_trail/compatibility.rb +51 -0
- data/lib/paper_trail/config.rb +11 -49
- data/lib/paper_trail/events/base.rb +323 -0
- data/lib/paper_trail/events/create.rb +32 -0
- data/lib/paper_trail/events/destroy.rb +42 -0
- data/lib/paper_trail/events/update.rb +60 -0
- data/lib/paper_trail/frameworks/active_record.rb +2 -1
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
- data/lib/paper_trail/frameworks/cucumber.rb +5 -3
- data/lib/paper_trail/frameworks/rails.rb +2 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
- data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
- data/lib/paper_trail/frameworks/rspec.rb +17 -4
- data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
- data/lib/paper_trail/has_paper_trail.rb +22 -310
- data/lib/paper_trail/model_config.rb +157 -109
- data/lib/paper_trail/queries/versions/where_object.rb +65 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
- data/lib/paper_trail/record_history.rb +3 -9
- data/lib/paper_trail/record_trail.rb +169 -319
- data/lib/paper_trail/reifier.rb +53 -374
- data/lib/paper_trail/request.rb +166 -0
- data/lib/paper_trail/serializers/json.rb +9 -10
- data/lib/paper_trail/serializers/yaml.rb +15 -28
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
- data/lib/paper_trail/version_concern.rb +160 -155
- data/lib/paper_trail/version_number.rb +12 -4
- metadata +77 -372
- data/.github/CONTRIBUTING.md +0 -109
- data/.github/ISSUE_TEMPLATE.md +0 -13
- data/.gitignore +0 -23
- data/.rspec +0 -2
- data/.rubocop.yml +0 -99
- data/.rubocop_todo.yml +0 -22
- data/.travis.yml +0 -41
- data/Appraisals +0 -38
- data/CHANGELOG.md +0 -560
- data/Gemfile +0 -2
- data/MIT-LICENSE +0 -20
- data/README.md +0 -1654
- data/Rakefile +0 -30
- data/doc/bug_report_template.rb +0 -69
- data/doc/warning_about_not_setting_whodunnit.md +0 -32
- data/gemfiles/ar3.gemfile +0 -19
- data/gemfiles/ar4.gemfile +0 -8
- data/gemfiles/ar5.gemfile +0 -9
- data/lib/generators/paper_trail/USAGE +0 -2
- data/lib/generators/paper_trail/default_initializer.rb +0 -0
- data/lib/generators/paper_trail/install_generator.rb +0 -57
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
- data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
- data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
- data/lib/paper_trail/frameworks/sinatra.rb +0 -40
- data/lib/paper_trail/version_association_concern.rb +0 -17
- data/paper_trail.gemspec +0 -56
- data/spec/generators/install_generator_spec.rb +0 -66
- data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
- data/spec/models/animal_spec.rb +0 -36
- data/spec/models/boolit_spec.rb +0 -48
- data/spec/models/callback_modifier_spec.rb +0 -96
- data/spec/models/car_spec.rb +0 -13
- data/spec/models/custom_primary_key_record_spec.rb +0 -18
- data/spec/models/fluxor_spec.rb +0 -17
- data/spec/models/gadget_spec.rb +0 -68
- data/spec/models/joined_version_spec.rb +0 -47
- data/spec/models/json_version_spec.rb +0 -102
- data/spec/models/kitchen/banana_spec.rb +0 -14
- data/spec/models/not_on_update_spec.rb +0 -22
- data/spec/models/post_with_status_spec.rb +0 -50
- data/spec/models/skipper_spec.rb +0 -46
- data/spec/models/thing_spec.rb +0 -11
- data/spec/models/truck_spec.rb +0 -5
- data/spec/models/vehicle_spec.rb +0 -5
- data/spec/models/version_spec.rb +0 -272
- data/spec/models/widget_spec.rb +0 -343
- data/spec/modules/paper_trail_spec.rb +0 -27
- data/spec/modules/version_concern_spec.rb +0 -31
- data/spec/modules/version_number_spec.rb +0 -43
- data/spec/paper_trail/config_spec.rb +0 -33
- data/spec/paper_trail_spec.rb +0 -79
- data/spec/rails_helper.rb +0 -34
- data/spec/requests/articles_spec.rb +0 -34
- data/spec/spec_helper.rb +0 -114
- data/spec/support/alt_db_init.rb +0 -54
- data/test/custom_json_serializer.rb +0 -13
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -33
- data/test/dummy/app/controllers/articles_controller.rb +0 -20
- data/test/dummy/app/controllers/test_controller.rb +0 -5
- data/test/dummy/app/controllers/widgets_controller.rb +0 -32
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/animal.rb +0 -6
- data/test/dummy/app/models/article.rb +0 -24
- data/test/dummy/app/models/authorship.rb +0 -5
- data/test/dummy/app/models/bar_habtm.rb +0 -4
- data/test/dummy/app/models/book.rb +0 -9
- data/test/dummy/app/models/boolit.rb +0 -4
- data/test/dummy/app/models/callback_modifier.rb +0 -45
- data/test/dummy/app/models/car.rb +0 -3
- data/test/dummy/app/models/cat.rb +0 -2
- data/test/dummy/app/models/chapter.rb +0 -9
- data/test/dummy/app/models/citation.rb +0 -5
- data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
- data/test/dummy/app/models/customer.rb +0 -4
- data/test/dummy/app/models/document.rb +0 -4
- data/test/dummy/app/models/dog.rb +0 -2
- data/test/dummy/app/models/editor.rb +0 -4
- data/test/dummy/app/models/editorship.rb +0 -5
- data/test/dummy/app/models/elephant.rb +0 -3
- data/test/dummy/app/models/fluxor.rb +0 -3
- data/test/dummy/app/models/foo_habtm.rb +0 -5
- data/test/dummy/app/models/foo_widget.rb +0 -2
- data/test/dummy/app/models/fruit.rb +0 -5
- data/test/dummy/app/models/gadget.rb +0 -3
- data/test/dummy/app/models/kitchen/banana.rb +0 -5
- data/test/dummy/app/models/legacy_widget.rb +0 -4
- data/test/dummy/app/models/line_item.rb +0 -4
- data/test/dummy/app/models/not_on_update.rb +0 -4
- data/test/dummy/app/models/order.rb +0 -5
- data/test/dummy/app/models/paragraph.rb +0 -5
- data/test/dummy/app/models/person.rb +0 -38
- data/test/dummy/app/models/post.rb +0 -3
- data/test/dummy/app/models/post_with_status.rb +0 -8
- data/test/dummy/app/models/protected_widget.rb +0 -3
- data/test/dummy/app/models/quotation.rb +0 -5
- data/test/dummy/app/models/section.rb +0 -6
- data/test/dummy/app/models/skipper.rb +0 -6
- data/test/dummy/app/models/song.rb +0 -41
- data/test/dummy/app/models/thing.rb +0 -3
- data/test/dummy/app/models/translation.rb +0 -4
- data/test/dummy/app/models/truck.rb +0 -4
- data/test/dummy/app/models/vehicle.rb +0 -4
- data/test/dummy/app/models/whatchamajigger.rb +0 -4
- data/test/dummy/app/models/widget.rb +0 -16
- data/test/dummy/app/models/wotsit.rb +0 -8
- data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
- data/test/dummy/app/versions/joined_version.rb +0 -6
- data/test/dummy/app/versions/json_version.rb +0 -3
- data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
- data/test/dummy/app/versions/post_version.rb +0 -3
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -80
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.mysql.yml +0 -19
- data/test/dummy/config/database.postgres.yml +0 -15
- data/test/dummy/config/database.sqlite.yml +0 -15
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -74
- data/test/dummy/config/environments/test.rb +0 -51
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/paper_trail.rb +0 -9
- data/test/dummy/config/initializers/secret_token.rb +0 -9
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -4
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
- data/test/dummy/db/schema.rb +0 -288
- data/test/dummy/script/rails +0 -8
- data/test/functional/controller_test.rb +0 -90
- data/test/functional/enabled_for_controller_test.rb +0 -28
- data/test/functional/modular_sinatra_test.rb +0 -46
- data/test/functional/sinatra_test.rb +0 -51
- data/test/functional/thread_safety_test.rb +0 -46
- data/test/test_helper.rb +0 -127
- data/test/time_travel_helper.rb +0 -1
- data/test/unit/associations_test.rb +0 -1016
- data/test/unit/cleaner_test.rb +0 -188
- data/test/unit/inheritance_column_test.rb +0 -43
- data/test/unit/model_test.rb +0 -1489
- data/test/unit/protected_attrs_test.rb +0 -52
- data/test/unit/serializer_test.rb +0 -119
- data/test/unit/serializers/json_test.rb +0 -95
- data/test/unit/serializers/mixin_json_test.rb +0 -37
- data/test/unit/serializers/mixin_yaml_test.rb +0 -53
- data/test/unit/serializers/yaml_test.rb +0 -54
- data/test/unit/timestamp_test.rb +0 -41
- data/test/unit/version_test.rb +0 -119
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "request_store"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
# Manages variables that affect the current HTTP request, such as `whodunnit`.
|
7
|
+
#
|
8
|
+
# Please do not use `PaperTrail::Request` directly, use `PaperTrail.request`.
|
9
|
+
# Currently, `Request` is a `Module`, but in the future it is quite possible
|
10
|
+
# we may make it a `Class`. If we make such a choice, we will not provide any
|
11
|
+
# warning and will not treat it as a breaking change. You've been warned :)
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
module Request
|
15
|
+
class InvalidOption < RuntimeError
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Sets any data from the controller that you want PaperTrail to store.
|
20
|
+
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
|
21
|
+
#
|
22
|
+
# PaperTrail.request.controller_info = { ip: request_user_ip }
|
23
|
+
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def controller_info=(value)
|
27
|
+
store[:controller_info] = value
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the data from the controller that you want PaperTrail to store.
|
31
|
+
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
|
32
|
+
#
|
33
|
+
# PaperTrail.request.controller_info = { ip: request_user_ip }
|
34
|
+
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def controller_info
|
38
|
+
store[:controller_info]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Switches PaperTrail off for the given model.
|
42
|
+
# @api public
|
43
|
+
def disable_model(model_class)
|
44
|
+
enabled_for_model(model_class, false)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Switches PaperTrail on for the given model.
|
48
|
+
# @api public
|
49
|
+
def enable_model(model_class)
|
50
|
+
enabled_for_model(model_class, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets whether PaperTrail is enabled or disabled for the current request.
|
54
|
+
# @api public
|
55
|
+
def enabled=(value)
|
56
|
+
store[:enabled] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
|
60
|
+
# See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
|
61
|
+
# @api public
|
62
|
+
def enabled?
|
63
|
+
!!store[:enabled]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sets whether PaperTrail is enabled or disabled for this model in the
|
67
|
+
# current request.
|
68
|
+
# @api public
|
69
|
+
def enabled_for_model(model, value)
|
70
|
+
store[:"enabled_for_#{model}"] = value
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns `true` if PaperTrail is enabled for this model in the current
|
74
|
+
# request, `false` otherwise.
|
75
|
+
# @api public
|
76
|
+
def enabled_for_model?(model)
|
77
|
+
model.include?(::PaperTrail::Model::InstanceMethods) &&
|
78
|
+
!!store.fetch(:"enabled_for_#{model}", true)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def merge(options)
|
83
|
+
options.to_h.each do |k, v|
|
84
|
+
store[k] = v
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
def set(options)
|
90
|
+
store.clear
|
91
|
+
merge(options)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a deep copy of the internal hash from our RequestStore. Keys are
|
95
|
+
# all symbols. Values are mostly primitives, but whodunnit can be a Proc.
|
96
|
+
# We cannot use Marshal.dump here because it doesn't support Proc. It is
|
97
|
+
# unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
|
98
|
+
# @api private
|
99
|
+
def to_h
|
100
|
+
store.deep_dup
|
101
|
+
end
|
102
|
+
|
103
|
+
# Temporarily set `options` and execute a block.
|
104
|
+
# @api private
|
105
|
+
def with(options)
|
106
|
+
return unless block_given?
|
107
|
+
validate_public_options(options)
|
108
|
+
before = to_h
|
109
|
+
merge(options)
|
110
|
+
yield
|
111
|
+
ensure
|
112
|
+
set(before)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Sets who is responsible for any changes that occur during request. You
|
116
|
+
# would normally use this in a migration or on the console, when working
|
117
|
+
# with models directly.
|
118
|
+
#
|
119
|
+
# `value` is usually a string, the name of a person, but you can set
|
120
|
+
# anything that responds to `to_s`. You can also set a Proc, which will
|
121
|
+
# not be evaluated until `whodunnit` is called later, usually right before
|
122
|
+
# inserting a `Version` record.
|
123
|
+
#
|
124
|
+
# @api public
|
125
|
+
def whodunnit=(value)
|
126
|
+
store[:whodunnit] = value
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns who is reponsible for any changes that occur during request.
|
130
|
+
#
|
131
|
+
# @api public
|
132
|
+
def whodunnit
|
133
|
+
who = store[:whodunnit]
|
134
|
+
who.respond_to?(:call) ? who.call : who
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Returns a Hash, initializing with default values if necessary.
|
140
|
+
# @api private
|
141
|
+
def store
|
142
|
+
RequestStore.store[:paper_trail] ||= {
|
143
|
+
enabled: true
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
# Provide a helpful error message if someone has a typo in one of their
|
148
|
+
# option keys. We don't validate option values here. That's traditionally
|
149
|
+
# been handled with casting (`to_s`, `!!`) in the accessor method.
|
150
|
+
# @api private
|
151
|
+
def validate_public_options(options)
|
152
|
+
options.each do |k, _v|
|
153
|
+
case k
|
154
|
+
when :controller_info,
|
155
|
+
/enabled_for_/,
|
156
|
+
:enabled,
|
157
|
+
:whodunnit
|
158
|
+
next
|
159
|
+
else
|
160
|
+
raise InvalidOption, "Invalid option: #{k}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PaperTrail
|
4
4
|
module Serializers
|
@@ -32,15 +32,14 @@ module PaperTrail
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
or(arel_field.matches("%\"#{field}\":[%,#{json_value}]%"))
|
35
|
+
def where_object_changes_condition(*)
|
36
|
+
raise <<-STR.squish.freeze
|
37
|
+
where_object_changes no longer supports reading JSON from a text
|
38
|
+
column. The old implementation was inaccurate, returning more records
|
39
|
+
than you wanted. This feature was deprecated in 7.1.0 and removed in
|
40
|
+
8.0.0. The json and jsonb datatypes are still supported. See the
|
41
|
+
discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
|
42
|
+
STR
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "yaml"
|
2
4
|
|
3
5
|
module PaperTrail
|
@@ -10,7 +12,12 @@ module PaperTrail
|
|
10
12
|
::YAML.load string
|
11
13
|
end
|
12
14
|
|
15
|
+
# @param object (Hash | HashWithIndifferentAccess) - Coming from
|
16
|
+
# `recordable_object` `object` will be a plain `Hash`. However, due to
|
17
|
+
# recent [memory optimizations](https://git.io/fjeYv), when coming from
|
18
|
+
# `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
|
13
19
|
def dump(object)
|
20
|
+
object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
|
14
21
|
::YAML.dump object
|
15
22
|
end
|
16
23
|
|
@@ -22,34 +29,14 @@ module PaperTrail
|
|
22
29
|
|
23
30
|
# Returns a SQL LIKE condition to be used to match the given field and
|
24
31
|
# value in the serialized `object_changes`.
|
25
|
-
def where_object_changes_condition(
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
when :syck
|
34
|
-
# Syck adds extra spaces into array dumps
|
35
|
-
m1 = "%\n#{field}: \n%- #{value}\n%"
|
36
|
-
m2 = "%\n#{field}: \n-%\n- #{value}\n%"
|
37
|
-
else
|
38
|
-
raise "Unknown yaml engine"
|
39
|
-
end
|
40
|
-
arel_field.matches(m1).or(arel_field.matches(m2))
|
41
|
-
end
|
42
|
-
|
43
|
-
# Returns a symbol identifying the YAML engine. Syck was removed from
|
44
|
-
# the ruby stdlib in ruby 2.0, but is still available as a gem.
|
45
|
-
# @api private
|
46
|
-
def yaml_engine_id
|
47
|
-
if (defined?(::YAML::ENGINE) && ::YAML::ENGINE.yamler == "psych") ||
|
48
|
-
(defined?(::Psych) && ::YAML == ::Psych)
|
49
|
-
:psych
|
50
|
-
else
|
51
|
-
:syck
|
52
|
-
end
|
32
|
+
def where_object_changes_condition(*)
|
33
|
+
raise <<-STR.squish.freeze
|
34
|
+
where_object_changes no longer supports reading YAML from a text
|
35
|
+
column. The old implementation was inaccurate, returning more records
|
36
|
+
than you wanted. This feature was deprecated in 8.1.0 and removed in
|
37
|
+
9.0.0. The json and jsonb datatypes are still supported. See
|
38
|
+
discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
|
39
|
+
STR
|
53
40
|
end
|
54
41
|
end
|
55
42
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
module TypeSerializers
|
5
|
+
# Provides an alternative method of serialization
|
6
|
+
# and deserialization of PostgreSQL array columns.
|
7
|
+
class PostgresArraySerializer
|
8
|
+
def initialize(subtype, delimiter)
|
9
|
+
@subtype = subtype
|
10
|
+
@delimiter = delimiter
|
11
|
+
end
|
12
|
+
|
13
|
+
def serialize(array)
|
14
|
+
return serialize_with_ar(array) if active_record_pre_502?
|
15
|
+
array
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(array)
|
19
|
+
return deserialize_with_ar(array) if active_record_pre_502?
|
20
|
+
|
21
|
+
case array
|
22
|
+
# Needed for legacy reasons. If serialized array is a string
|
23
|
+
# then it was serialized with Rails < 5.0.2.
|
24
|
+
when ::String then deserialize_with_ar(array)
|
25
|
+
else array
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def active_record_pre_502?
|
32
|
+
::ActiveRecord.gem_version < Gem::Version.new("5.0.2")
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize_with_ar(array)
|
36
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
|
37
|
+
new(@subtype, @delimiter).
|
38
|
+
serialize(array)
|
39
|
+
end
|
40
|
+
|
41
|
+
def deserialize_with_ar(array)
|
42
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
|
43
|
+
new(@subtype, @delimiter).
|
44
|
+
deserialize(array)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,5 +1,8 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
4
|
+
require "paper_trail/queries/versions/where_object"
|
5
|
+
require "paper_trail/queries/versions/where_object_changes"
|
3
6
|
|
4
7
|
module PaperTrail
|
5
8
|
# Originally, PaperTrail did not provide this module, and all of this
|
@@ -10,38 +13,22 @@ module PaperTrail
|
|
10
13
|
extend ::ActiveSupport::Concern
|
11
14
|
|
12
15
|
included do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# DB is not initialized prior to test runs such as when we run on Travis
|
18
|
-
# CI (there won't be a db in `test/dummy/db/`).
|
19
|
-
if PaperTrail.config.track_associations?
|
20
|
-
has_many :version_associations, dependent: :destroy
|
16
|
+
if ::ActiveRecord.gem_version >= Gem::Version.new("5.0")
|
17
|
+
belongs_to :item, polymorphic: true, optional: true
|
18
|
+
else
|
19
|
+
belongs_to :item, polymorphic: true
|
21
20
|
end
|
22
21
|
|
23
22
|
validates_presence_of :event
|
24
|
-
|
25
|
-
if PaperTrail.active_record_protected_attributes?
|
26
|
-
attr_accessible(
|
27
|
-
:item_type,
|
28
|
-
:item_id,
|
29
|
-
:event,
|
30
|
-
:whodunnit,
|
31
|
-
:object,
|
32
|
-
:object_changes,
|
33
|
-
:transaction_id,
|
34
|
-
:created_at
|
35
|
-
)
|
36
|
-
end
|
37
|
-
|
38
23
|
after_create :enforce_version_limit!
|
39
|
-
|
40
|
-
scope :within_transaction, ->(id) { where transaction_id: id }
|
41
24
|
end
|
42
25
|
|
43
26
|
# :nodoc:
|
44
27
|
module ClassMethods
|
28
|
+
def item_subtype_column_present?
|
29
|
+
column_names.include?("item_subtype")
|
30
|
+
end
|
31
|
+
|
45
32
|
def with_item_keys(item_type, item_id)
|
46
33
|
where item_type: item_type, item_id: item_id
|
47
34
|
end
|
@@ -62,128 +49,152 @@ module PaperTrail
|
|
62
49
|
where "event <> ?", "create"
|
63
50
|
end
|
64
51
|
|
65
|
-
# Returns versions after `obj`.
|
66
|
-
#
|
67
|
-
# @param obj - a `Version` or a timestamp
|
68
|
-
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
69
|
-
# Default: false.
|
70
|
-
# @return `ActiveRecord::Relation`
|
71
|
-
# @api public
|
72
|
-
def subsequent(obj, timestamp_arg = false)
|
73
|
-
if timestamp_arg != true && primary_key_is_int?
|
74
|
-
return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
|
75
|
-
end
|
76
|
-
|
77
|
-
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
78
|
-
where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(timestamp_sort_order)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Returns versions before `obj`.
|
82
|
-
#
|
83
|
-
# @param obj - a `Version` or a timestamp
|
84
|
-
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
85
|
-
# Default: false.
|
86
|
-
# @return `ActiveRecord::Relation`
|
87
|
-
# @api public
|
88
|
-
def preceding(obj, timestamp_arg = false)
|
89
|
-
if timestamp_arg != true && primary_key_is_int?
|
90
|
-
return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
|
91
|
-
end
|
92
|
-
|
93
|
-
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
94
|
-
where(arel_table[PaperTrail.timestamp_field].lt(obj)).
|
95
|
-
order(timestamp_sort_order("desc"))
|
96
|
-
end
|
97
|
-
|
98
52
|
def between(start_time, end_time)
|
99
53
|
where(
|
100
|
-
arel_table[
|
101
|
-
and(arel_table[
|
54
|
+
arel_table[:created_at].gt(start_time).
|
55
|
+
and(arel_table[:created_at].lt(end_time))
|
102
56
|
).order(timestamp_sort_order)
|
103
57
|
end
|
104
58
|
|
105
59
|
# Defaults to using the primary key as the secondary sort order if
|
106
60
|
# possible.
|
107
61
|
def timestamp_sort_order(direction = "asc")
|
108
|
-
[arel_table[
|
62
|
+
[arel_table[:created_at].send(direction.downcase)].tap do |array|
|
109
63
|
array << arel_table[primary_key].send(direction.downcase) if primary_key_is_int?
|
110
64
|
end
|
111
65
|
end
|
112
66
|
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
67
|
+
# Given a hash of attributes like `name: 'Joan'`, query the
|
68
|
+
# `versions.objects` column.
|
69
|
+
#
|
70
|
+
# ```
|
71
|
+
# SELECT "versions".*
|
72
|
+
# FROM "versions"
|
73
|
+
# WHERE ("versions"."object" LIKE '%
|
74
|
+
# name: Joan
|
75
|
+
# %')
|
76
|
+
# ```
|
77
|
+
#
|
78
|
+
# This is useful for finding versions where a given attribute had a given
|
79
|
+
# value. Imagine, in the example above, that Joan had changed her name
|
80
|
+
# and we wanted to find the versions before that change.
|
81
|
+
#
|
82
|
+
# Based on the data type of the `object` column, the appropriate SQL
|
83
|
+
# operator is used. For example, a text column will use `like`, and a
|
84
|
+
# jsonb column will use `@>`.
|
85
|
+
#
|
116
86
|
# @api public
|
117
87
|
def where_object(args = {})
|
118
88
|
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
|
119
|
-
|
120
|
-
if columns_hash["object"].type == :jsonb
|
121
|
-
where("object @> ?", args.to_json)
|
122
|
-
elsif columns_hash["object"].type == :json
|
123
|
-
predicates = []
|
124
|
-
values = []
|
125
|
-
args.each do |field, value|
|
126
|
-
predicates.push "object->>? = ?"
|
127
|
-
values.concat([field, value.to_s])
|
128
|
-
end
|
129
|
-
sql = predicates.join(" and ")
|
130
|
-
where(sql, *values)
|
131
|
-
else
|
132
|
-
arel_field = arel_table[:object]
|
133
|
-
where_conditions = args.map { |field, value|
|
134
|
-
PaperTrail.serializer.where_object_condition(arel_field, field, value)
|
135
|
-
}
|
136
|
-
where_conditions = where_conditions.reduce { |a, e| a.and(e) }
|
137
|
-
where(where_conditions)
|
138
|
-
end
|
89
|
+
Queries::Versions::WhereObject.new(self, args).execute
|
139
90
|
end
|
140
91
|
|
141
|
-
#
|
142
|
-
#
|
92
|
+
# Given a hash of attributes like `name: 'Joan'`, query the
|
93
|
+
# `versions.objects_changes` column.
|
94
|
+
#
|
95
|
+
# ```
|
96
|
+
# SELECT "versions".*
|
97
|
+
# FROM "versions"
|
98
|
+
# WHERE .. ("versions"."object_changes" LIKE '%
|
99
|
+
# name:
|
100
|
+
# - Joan
|
101
|
+
# %' OR "versions"."object_changes" LIKE '%
|
102
|
+
# name:
|
103
|
+
# -%
|
104
|
+
# - Joan
|
105
|
+
# %')
|
106
|
+
# ```
|
107
|
+
#
|
108
|
+
# This is useful for finding versions immediately before and after a given
|
109
|
+
# attribute had a given value. Imagine, in the example above, that someone
|
110
|
+
# changed their name to Joan and we wanted to find the versions
|
111
|
+
# immediately before and after that change.
|
112
|
+
#
|
113
|
+
# Based on the data type of the `object` column, the appropriate SQL
|
114
|
+
# operator is used. For example, a text column will use `like`, and a
|
115
|
+
# jsonb column will use `@>`.
|
116
|
+
#
|
143
117
|
# @api public
|
144
118
|
def where_object_changes(args = {})
|
145
119
|
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
|
146
|
-
|
147
|
-
if columns_hash["object_changes"].type == :jsonb
|
148
|
-
args.each { |field, value| args[field] = [value] }
|
149
|
-
where("object_changes @> ?", args.to_json)
|
150
|
-
elsif columns_hash["object"].type == :json
|
151
|
-
predicates = []
|
152
|
-
values = []
|
153
|
-
args.each do |field, value|
|
154
|
-
predicates.push(
|
155
|
-
"((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
|
156
|
-
)
|
157
|
-
values.concat([field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%"])
|
158
|
-
end
|
159
|
-
sql = predicates.join(" and ")
|
160
|
-
where(sql, *values)
|
161
|
-
else
|
162
|
-
arel_field = arel_table[:object_changes]
|
163
|
-
where_conditions = args.map { |field, value|
|
164
|
-
PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
|
165
|
-
}
|
166
|
-
where_conditions = where_conditions.reduce { |a, e| a.and(e) }
|
167
|
-
where(where_conditions)
|
168
|
-
end
|
120
|
+
Queries::Versions::WhereObjectChanges.new(self, args).execute
|
169
121
|
end
|
170
122
|
|
171
123
|
def primary_key_is_int?
|
172
124
|
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
|
173
|
-
rescue
|
125
|
+
rescue StandardError # TODO: Rescue something more specific
|
174
126
|
true
|
175
127
|
end
|
176
128
|
|
177
129
|
# Returns whether the `object` column is using the `json` type supported
|
178
130
|
# by PostgreSQL.
|
179
131
|
def object_col_is_json?
|
180
|
-
[
|
132
|
+
%i[json jsonb].include?(columns_hash["object"].type)
|
181
133
|
end
|
182
134
|
|
183
135
|
# Returns whether the `object_changes` column is using the `json` type
|
184
136
|
# supported by PostgreSQL.
|
185
137
|
def object_changes_col_is_json?
|
186
|
-
[
|
138
|
+
%i[json jsonb].include?(columns_hash["object_changes"].try(:type))
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns versions before `obj`.
|
142
|
+
#
|
143
|
+
# @param obj - a `Version` or a timestamp
|
144
|
+
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
145
|
+
# Default: false.
|
146
|
+
# @return `ActiveRecord::Relation`
|
147
|
+
# @api public
|
148
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
149
|
+
def preceding(obj, timestamp_arg = false)
|
150
|
+
if timestamp_arg != true && primary_key_is_int?
|
151
|
+
preceding_by_id(obj)
|
152
|
+
else
|
153
|
+
preceding_by_timestamp(obj)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
157
|
+
|
158
|
+
# Returns versions after `obj`.
|
159
|
+
#
|
160
|
+
# @param obj - a `Version` or a timestamp
|
161
|
+
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
162
|
+
# Default: false.
|
163
|
+
# @return `ActiveRecord::Relation`
|
164
|
+
# @api public
|
165
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
166
|
+
def subsequent(obj, timestamp_arg = false)
|
167
|
+
if timestamp_arg != true && primary_key_is_int?
|
168
|
+
subsequent_by_id(obj)
|
169
|
+
else
|
170
|
+
subsequent_by_timestamp(obj)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# @api private
|
178
|
+
def preceding_by_id(obj)
|
179
|
+
where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @api private
|
183
|
+
def preceding_by_timestamp(obj)
|
184
|
+
obj = obj.send(:created_at) if obj.is_a?(self)
|
185
|
+
where(arel_table[:created_at].lt(obj)).
|
186
|
+
order(timestamp_sort_order("desc"))
|
187
|
+
end
|
188
|
+
|
189
|
+
# @api private
|
190
|
+
def subsequent_by_id(version)
|
191
|
+
where(arel_table[primary_key].gt(version.id)).order(arel_table[primary_key].asc)
|
192
|
+
end
|
193
|
+
|
194
|
+
# @api private
|
195
|
+
def subsequent_by_timestamp(obj)
|
196
|
+
obj = obj.send(:created_at) if obj.is_a?(self)
|
197
|
+
where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order)
|
187
198
|
end
|
188
199
|
end
|
189
200
|
|
@@ -198,18 +209,8 @@ module PaperTrail
|
|
198
209
|
|
199
210
|
# Restore the item from this version.
|
200
211
|
#
|
201
|
-
# Optionally this can also restore all :has_one and :has_many (including
|
202
|
-
# has_many :through) associations as they were "at the time", if they are
|
203
|
-
# also being versioned by PaperTrail.
|
204
|
-
#
|
205
212
|
# Options:
|
206
213
|
#
|
207
|
-
# - :has_one
|
208
|
-
# - `true` - Also reify has_one associations.
|
209
|
-
# - `false - Default.
|
210
|
-
# - :has_many
|
211
|
-
# - `true` - Also reify has_many and has_many :through associations.
|
212
|
-
# - `false` - Default.
|
213
214
|
# - :mark_for_destruction
|
214
215
|
# - `true` - Mark the has_one/has_many associations that did not exist in
|
215
216
|
# the reified version for destruction, instead of removing them.
|
@@ -224,10 +225,11 @@ module PaperTrail
|
|
224
225
|
# - `:preserve` - Attributes undefined in version record are not modified.
|
225
226
|
#
|
226
227
|
def reify(options = {})
|
227
|
-
|
228
|
-
|
229
|
-
::PaperTrail::Reifier.reify(self, options)
|
228
|
+
unless self.class.column_names.include? "object"
|
229
|
+
raise "reify can't be called without an object column"
|
230
230
|
end
|
231
|
+
return nil if object.nil?
|
232
|
+
::PaperTrail::Reifier.reify(self, options)
|
231
233
|
end
|
232
234
|
|
233
235
|
# Returns what changed in this version of the item.
|
@@ -243,11 +245,6 @@ module PaperTrail
|
|
243
245
|
@paper_trail_originator ||= previous.try(:whodunnit)
|
244
246
|
end
|
245
247
|
|
246
|
-
def originator
|
247
|
-
::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
|
248
|
-
paper_trail_originator
|
249
|
-
end
|
250
|
-
|
251
248
|
# Returns who changed the item from the state it had in this version. This
|
252
249
|
# is an alias for `whodunnit`.
|
253
250
|
def terminator
|
@@ -255,13 +252,6 @@ module PaperTrail
|
|
255
252
|
end
|
256
253
|
alias version_author terminator
|
257
254
|
|
258
|
-
def sibling_versions(reload = false)
|
259
|
-
if reload || @sibling_versions.nil?
|
260
|
-
@sibling_versions = self.class.with_item_keys(item_type, item_id)
|
261
|
-
end
|
262
|
-
@sibling_versions
|
263
|
-
end
|
264
|
-
|
265
255
|
def next
|
266
256
|
@next ||= sibling_versions.subsequent(self).first
|
267
257
|
end
|
@@ -271,8 +261,9 @@ module PaperTrail
|
|
271
261
|
end
|
272
262
|
|
273
263
|
# Returns an integer representing the chronological position of the
|
274
|
-
# version among its siblings
|
275
|
-
#
|
264
|
+
# version among its siblings. The "create" event, for example, has an index
|
265
|
+
# of 0.
|
266
|
+
#
|
276
267
|
# @api public
|
277
268
|
def index
|
278
269
|
@index ||= RecordHistory.new(sibling_versions, self.class).index(self)
|
@@ -282,6 +273,10 @@ module PaperTrail
|
|
282
273
|
|
283
274
|
# @api private
|
284
275
|
def load_changeset
|
276
|
+
if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset)
|
277
|
+
return PaperTrail.config.object_changes_adapter.load_changeset(self)
|
278
|
+
end
|
279
|
+
|
285
280
|
# First, deserialize the `object_changes` column.
|
286
281
|
changes = HashWithIndifferentAccess.new(object_changes_deserialized)
|
287
282
|
|
@@ -317,33 +312,43 @@ module PaperTrail
|
|
317
312
|
else
|
318
313
|
begin
|
319
314
|
PaperTrail.serializer.load(object_changes)
|
320
|
-
rescue # TODO: Rescue something specific
|
315
|
+
rescue StandardError # TODO: Rescue something more specific
|
321
316
|
{}
|
322
317
|
end
|
323
318
|
end
|
324
319
|
end
|
325
320
|
|
326
|
-
#
|
327
|
-
# IdentityMap, if enabled. This prevents insertion into the map.
|
328
|
-
# @api private
|
329
|
-
def without_identity_map(&block)
|
330
|
-
if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
|
331
|
-
::ActiveRecord::IdentityMap.without(&block)
|
332
|
-
else
|
333
|
-
yield
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
# Checks that a value has been set for the `version_limit` config
|
338
|
-
# option, and if so enforces it.
|
321
|
+
# Enforces the `version_limit`, if set. Default: no limit.
|
339
322
|
# @api private
|
340
323
|
def enforce_version_limit!
|
341
|
-
limit =
|
324
|
+
limit = version_limit
|
342
325
|
return unless limit.is_a? Numeric
|
343
|
-
previous_versions = sibling_versions.not_creates
|
326
|
+
previous_versions = sibling_versions.not_creates.
|
327
|
+
order(self.class.timestamp_sort_order("asc"))
|
344
328
|
return unless previous_versions.size > limit
|
345
329
|
excess_versions = previous_versions - previous_versions.last(limit)
|
346
330
|
excess_versions.map(&:destroy)
|
347
331
|
end
|
332
|
+
|
333
|
+
# @api private
|
334
|
+
def sibling_versions
|
335
|
+
@sibling_versions ||= self.class.with_item_keys(item_type, item_id)
|
336
|
+
end
|
337
|
+
|
338
|
+
# See docs section 2.e. Limiting the Number of Versions Created.
|
339
|
+
# The version limit can be global or per-model.
|
340
|
+
#
|
341
|
+
# @api private
|
342
|
+
#
|
343
|
+
# TODO: Duplication: similar `constantize` in Reifier#version_reification_class
|
344
|
+
def version_limit
|
345
|
+
if self.class.item_subtype_column_present?
|
346
|
+
klass = (item_subtype || item_type).constantize
|
347
|
+
if klass&.paper_trail_options&.key?(:limit)
|
348
|
+
return klass.paper_trail_options[:limit]
|
349
|
+
end
|
350
|
+
end
|
351
|
+
PaperTrail.config.version_limit
|
352
|
+
end
|
348
353
|
end
|
349
354
|
end
|