paper_trail 4.2.0 → 7.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/paper_trail/install_generator.rb +91 -17
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb +12 -0
- data/lib/generators/paper_trail/templates/{add_transaction_id_column_to_versions.rb → add_transaction_id_column_to_versions.rb.erb} +3 -1
- data/lib/generators/paper_trail/templates/create_version_associations.rb.erb +22 -0
- data/lib/generators/paper_trail/templates/{create_versions.rb → create_versions.rb.erb} +9 -7
- data/lib/paper_trail.rb +180 -148
- data/lib/paper_trail/attribute_serializers/README.md +10 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +80 -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 +16 -10
- data/lib/paper_trail/config.rb +28 -27
- 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/cucumber.rb +1 -0
- data/lib/paper_trail/frameworks/rails.rb +2 -7
- data/lib/paper_trail/frameworks/rails/controller.rb +20 -18
- data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
- data/lib/paper_trail/frameworks/rspec.rb +17 -6
- data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
- data/lib/paper_trail/has_paper_trail.rb +25 -503
- data/lib/paper_trail/model_config.rb +207 -0
- data/lib/paper_trail/queries/versions/where_object.rb +60 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +68 -0
- data/lib/paper_trail/record_history.rb +2 -12
- data/lib/paper_trail/record_trail.rb +573 -0
- data/lib/paper_trail/reifier.rb +164 -215
- data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
- data/lib/paper_trail/reifiers/has_many.rb +110 -0
- data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
- data/lib/paper_trail/reifiers/has_one.rb +76 -0
- data/lib/paper_trail/serializers/json.rb +16 -7
- data/lib/paper_trail/serializers/yaml.rb +9 -13
- data/lib/paper_trail/version_association_concern.rb +3 -5
- data/lib/paper_trail/version_concern.rb +138 -111
- data/lib/paper_trail/version_number.rb +10 -9
- metadata +95 -327
- data/.gitignore +0 -22
- data/.rspec +0 -2
- data/.travis.yml +0 -41
- data/CHANGELOG.md +0 -362
- data/CONTRIBUTING.md +0 -84
- data/Gemfile +0 -2
- data/MIT-LICENSE +0 -20
- data/README.md +0 -1535
- data/Rakefile +0 -30
- data/doc/bug_report_template.rb +0 -65
- data/gemfiles/ar3.gemfile +0 -61
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +0 -10
- data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -17
- data/lib/paper_trail/attributes_serialization.rb +0 -89
- data/lib/paper_trail/frameworks/sinatra.rb +0 -38
- data/paper_trail.gemspec +0 -59
- data/spec/generators/install_generator_spec.rb +0 -67
- 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/fluxor_spec.rb +0 -19
- data/spec/models/gadget_spec.rb +0 -70
- data/spec/models/joined_version_spec.rb +0 -47
- data/spec/models/json_version_spec.rb +0 -103
- data/spec/models/kitchen/banana_spec.rb +0 -14
- data/spec/models/not_on_update_spec.rb +0 -19
- data/spec/models/post_with_status_spec.rb +0 -17
- data/spec/models/skipper_spec.rb +0 -46
- data/spec/models/thing_spec.rb +0 -11
- data/spec/models/version_spec.rb +0 -239
- data/spec/models/widget_spec.rb +0 -298
- data/spec/modules/paper_trail_spec.rb +0 -27
- data/spec/modules/version_concern_spec.rb +0 -32
- data/spec/modules/version_number_spec.rb +0 -44
- data/spec/paper_trail/config_spec.rb +0 -52
- data/spec/paper_trail_spec.rb +0 -66
- data/spec/rails_helper.rb +0 -34
- data/spec/requests/articles_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -95
- data/spec/support/alt_db_init.rb +0 -59
- data/test/custom_json_serializer.rb +0 -13
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -20
- data/test/dummy/app/controllers/articles_controller.rb +0 -17
- data/test/dummy/app/controllers/test_controller.rb +0 -5
- data/test/dummy/app/controllers/widgets_controller.rb +0 -31
- 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 -16
- data/test/dummy/app/models/authorship.rb +0 -5
- 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/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/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_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 -32
- data/test/dummy/app/models/thing.rb +0 -3
- data/test/dummy/app/models/translation.rb +0 -4
- data/test/dummy/app/models/whatchamajigger.rb +0 -4
- data/test/dummy/app/models/widget.rb +0 -15
- data/test/dummy/app/models/wotsit.rb +0 -8
- data/test/dummy/app/versions/joined_version.rb +0 -5
- 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 -69
- 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 -40
- data/test/dummy/config/environments/production.rb +0 -73
- data/test/dummy/config/environments/test.rb +0 -41
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- 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 -10
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- 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 -287
- data/test/dummy/db/schema.rb +0 -246
- data/test/dummy/script/rails +0 -6
- data/test/functional/controller_test.rb +0 -91
- data/test/functional/enabled_for_controller_test.rb +0 -29
- data/test/functional/modular_sinatra_test.rb +0 -48
- data/test/functional/sinatra_test.rb +0 -49
- data/test/functional/thread_safety_test.rb +0 -48
- data/test/paper_trail_test.rb +0 -38
- data/test/test_helper.rb +0 -105
- data/test/time_travel_helper.rb +0 -15
- data/test/unit/associations_test.rb +0 -726
- data/test/unit/cleaner_test.rb +0 -182
- data/test/unit/inheritance_column_test.rb +0 -43
- data/test/unit/model_test.rb +0 -1373
- data/test/unit/protected_attrs_test.rb +0 -47
- data/test/unit/serializer_test.rb +0 -117
- data/test/unit/serializers/json_test.rb +0 -88
- data/test/unit/serializers/mixin_json_test.rb +0 -36
- data/test/unit/serializers/mixin_yaml_test.rb +0 -49
- data/test/unit/serializers/yaml_test.rb +0 -52
- data/test/unit/timestamp_test.rb +0 -43
- data/test/unit/version_test.rb +0 -119
@@ -0,0 +1,207 @@
|
|
1
|
+
require "active_support/core_ext"
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
# Configures an ActiveRecord model, mostly at application boot time, but also
|
5
|
+
# sometimes mid-request, with methods like enable/disable.
|
6
|
+
class ModelConfig
|
7
|
+
E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
|
8
|
+
paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
|
9
|
+
belongs_to_required_by_default and has no effect. Please use :before
|
10
|
+
or disable belongs_to_required_by_default.
|
11
|
+
STR
|
12
|
+
|
13
|
+
def initialize(model_class)
|
14
|
+
@model_class = model_class
|
15
|
+
end
|
16
|
+
|
17
|
+
# Switches PaperTrail off for this class.
|
18
|
+
def disable
|
19
|
+
::PaperTrail.enabled_for_model(@model_class, false)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Switches PaperTrail on for this class.
|
23
|
+
def enable
|
24
|
+
::PaperTrail.enabled_for_model(@model_class, true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def enabled?
|
28
|
+
return false unless @model_class.include?(::PaperTrail::Model::InstanceMethods)
|
29
|
+
::PaperTrail.enabled_for_model?(@model_class)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds a callback that records a version after a "create" event.
|
33
|
+
def on_create
|
34
|
+
@model_class.after_create { |r|
|
35
|
+
r.paper_trail.record_create if r.paper_trail.save_version?
|
36
|
+
}
|
37
|
+
return if @model_class.paper_trail_options[:on].include?(:create)
|
38
|
+
@model_class.paper_trail_options[:on] << :create
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adds a callback that records a version before or after a "destroy" event.
|
42
|
+
def on_destroy(recording_order = "before")
|
43
|
+
unless %w[after before].include?(recording_order.to_s)
|
44
|
+
raise ArgumentError, 'recording order can only be "after" or "before"'
|
45
|
+
end
|
46
|
+
|
47
|
+
if recording_order.to_s == "after" && cannot_record_after_destroy?
|
48
|
+
::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
|
49
|
+
end
|
50
|
+
|
51
|
+
@model_class.send(
|
52
|
+
"#{recording_order}_destroy",
|
53
|
+
->(r) { r.paper_trail.record_destroy if r.paper_trail.save_version? }
|
54
|
+
)
|
55
|
+
|
56
|
+
return if @model_class.paper_trail_options[:on].include?(:destroy)
|
57
|
+
@model_class.paper_trail_options[:on] << :destroy
|
58
|
+
end
|
59
|
+
|
60
|
+
# Adds a callback that records a version after an "update" event.
|
61
|
+
def on_update
|
62
|
+
@model_class.before_save(on: :update) { |r|
|
63
|
+
r.paper_trail.reset_timestamp_attrs_for_update_if_needed
|
64
|
+
}
|
65
|
+
@model_class.after_update { |r|
|
66
|
+
r.paper_trail.record_update(nil) if r.paper_trail.save_version?
|
67
|
+
}
|
68
|
+
@model_class.after_update { |r|
|
69
|
+
r.paper_trail.clear_version_instance
|
70
|
+
}
|
71
|
+
return if @model_class.paper_trail_options[:on].include?(:update)
|
72
|
+
@model_class.paper_trail_options[:on] << :update
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set up `@model_class` for PaperTrail. Installs callbacks, associations,
|
76
|
+
# "class attributes", instance methods, and more.
|
77
|
+
# @api private
|
78
|
+
def setup(options = {})
|
79
|
+
options[:on] ||= %i[create update destroy]
|
80
|
+
options[:on] = Array(options[:on]) # Support single symbol
|
81
|
+
@model_class.send :include, ::PaperTrail::Model::InstanceMethods
|
82
|
+
if ::ActiveRecord::VERSION::STRING < "4.2"
|
83
|
+
::ActiveSupport::Deprecation.warn(
|
84
|
+
"Your version of ActiveRecord (< 4.2) has reached EOL. PaperTrail " \
|
85
|
+
"will soon drop support. Please upgrade ActiveRecord ASAP."
|
86
|
+
)
|
87
|
+
@model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
|
88
|
+
end
|
89
|
+
setup_options(options)
|
90
|
+
setup_associations(options)
|
91
|
+
setup_transaction_callbacks
|
92
|
+
setup_callbacks_from_options options[:on]
|
93
|
+
setup_callbacks_for_habtm options[:join_tables]
|
94
|
+
end
|
95
|
+
|
96
|
+
def version_class
|
97
|
+
@_version_class ||= @model_class.version_class_name.constantize
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def active_record_gem_version
|
103
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING)
|
104
|
+
end
|
105
|
+
|
106
|
+
def cannot_record_after_destroy?
|
107
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
|
108
|
+
::ActiveRecord::Base.belongs_to_required_by_default
|
109
|
+
end
|
110
|
+
|
111
|
+
def habtm_assocs_not_skipped
|
112
|
+
@model_class.reflect_on_all_associations(:has_and_belongs_to_many).
|
113
|
+
reject { |a| @model_class.paper_trail_options[:skip].include?(a.name.to_s) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_associations(options)
|
117
|
+
@model_class.class_attribute :version_association_name
|
118
|
+
@model_class.version_association_name = options[:version] || :version
|
119
|
+
|
120
|
+
# The version this instance was reified from.
|
121
|
+
@model_class.send :attr_accessor, @model_class.version_association_name
|
122
|
+
|
123
|
+
@model_class.class_attribute :version_class_name
|
124
|
+
@model_class.version_class_name = options[:class_name] || "PaperTrail::Version"
|
125
|
+
|
126
|
+
@model_class.class_attribute :versions_association_name
|
127
|
+
@model_class.versions_association_name = options[:versions] || :versions
|
128
|
+
|
129
|
+
@model_class.send :attr_accessor, :paper_trail_event
|
130
|
+
|
131
|
+
@model_class.has_many(
|
132
|
+
@model_class.versions_association_name,
|
133
|
+
-> { order(model.timestamp_sort_order) },
|
134
|
+
class_name: @model_class.version_class_name,
|
135
|
+
as: :item
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Adds callbacks to record changes to habtm associations such that on save
|
140
|
+
# the previous version of the association (if changed) can be reconstructed.
|
141
|
+
def setup_callbacks_for_habtm(join_tables)
|
142
|
+
@model_class.send :attr_accessor, :paper_trail_habtm
|
143
|
+
@model_class.class_attribute :paper_trail_save_join_tables
|
144
|
+
@model_class.paper_trail_save_join_tables = Array.wrap(join_tables)
|
145
|
+
habtm_assocs_not_skipped.each(&method(:setup_habtm_change_callbacks))
|
146
|
+
end
|
147
|
+
|
148
|
+
def setup_callbacks_from_options(options_on = [])
|
149
|
+
options_on.each do |event|
|
150
|
+
public_send("on_#{event}")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def setup_habtm_change_callbacks(assoc)
|
155
|
+
assoc_name = assoc.name
|
156
|
+
%w[add remove].each do |verb|
|
157
|
+
@model_class.send(:"before_#{verb}_for_#{assoc_name}").send(
|
158
|
+
:<<,
|
159
|
+
lambda do |*args|
|
160
|
+
update_habtm_state(assoc_name, :"before_#{verb}", args[-2], args.last)
|
161
|
+
end
|
162
|
+
)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def setup_options(options)
|
167
|
+
@model_class.class_attribute :paper_trail_options
|
168
|
+
@model_class.paper_trail_options = options.dup
|
169
|
+
|
170
|
+
%i[ignore skip only].each do |k|
|
171
|
+
@model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
|
172
|
+
flatten.
|
173
|
+
compact.
|
174
|
+
map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
|
175
|
+
end
|
176
|
+
|
177
|
+
@model_class.paper_trail_options[:meta] ||= {}
|
178
|
+
if @model_class.paper_trail_options[:save_changes].nil?
|
179
|
+
@model_class.paper_trail_options[:save_changes] = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Reset the transaction id when the transaction is closed.
|
184
|
+
def setup_transaction_callbacks
|
185
|
+
@model_class.after_commit { PaperTrail.clear_transaction_id }
|
186
|
+
@model_class.after_rollback { PaperTrail.clear_transaction_id }
|
187
|
+
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
|
188
|
+
end
|
189
|
+
|
190
|
+
def update_habtm_state(name, callback, model, assoc)
|
191
|
+
model.paper_trail_habtm ||= {}
|
192
|
+
model.paper_trail_habtm[name] ||= { removed: [], added: [] }
|
193
|
+
state = model.paper_trail_habtm[name]
|
194
|
+
assoc_id = assoc.id
|
195
|
+
case callback
|
196
|
+
when :before_add
|
197
|
+
state[:added] |= [assoc_id]
|
198
|
+
state[:removed] -= [assoc_id]
|
199
|
+
when :before_remove
|
200
|
+
state[:removed] |= [assoc_id]
|
201
|
+
state[:added] -= [assoc_id]
|
202
|
+
else
|
203
|
+
raise "Invalid callback: #{callback}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module Queries
|
3
|
+
module Versions
|
4
|
+
# For public API documentation, see `where_object` in
|
5
|
+
# `paper_trail/version_concern.rb`.
|
6
|
+
# @api private
|
7
|
+
class WhereObject
|
8
|
+
# - version_model_class - The class that VersionConcern was mixed into.
|
9
|
+
# - attributes - A `Hash` of attributes and values. See the public API
|
10
|
+
# documentation for details.
|
11
|
+
# @api private
|
12
|
+
def initialize(version_model_class, attributes)
|
13
|
+
@version_model_class = version_model_class
|
14
|
+
@attributes = attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def execute
|
19
|
+
case @version_model_class.columns_hash["object"].type
|
20
|
+
when :jsonb
|
21
|
+
jsonb
|
22
|
+
when :json
|
23
|
+
json
|
24
|
+
else
|
25
|
+
text
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# @api private
|
32
|
+
def json
|
33
|
+
predicates = []
|
34
|
+
values = []
|
35
|
+
@attributes.each do |field, value|
|
36
|
+
predicates.push "object->>? = ?"
|
37
|
+
values.concat([field, value.to_s])
|
38
|
+
end
|
39
|
+
sql = predicates.join(" and ")
|
40
|
+
@version_model_class.where(sql, *values)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def jsonb
|
45
|
+
@version_model_class.where("object @> ?", @attributes.to_json)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @api private
|
49
|
+
def text
|
50
|
+
arel_field = @version_model_class.arel_table[:object]
|
51
|
+
where_conditions = @attributes.map { |field, value|
|
52
|
+
::PaperTrail.serializer.where_object_condition(arel_field, field, value)
|
53
|
+
}
|
54
|
+
where_conditions = where_conditions.reduce { |a, e| a.and(e) }
|
55
|
+
@version_model_class.where(where_conditions)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module Queries
|
3
|
+
module Versions
|
4
|
+
# For public API documentation, see `where_object` in
|
5
|
+
# `paper_trail/version_concern.rb`.
|
6
|
+
# @api private
|
7
|
+
class WhereObjectChanges
|
8
|
+
# - version_model_class - The class that VersionConcern was mixed into.
|
9
|
+
# - attributes - A `Hash` of attributes and values. See the public API
|
10
|
+
# documentation for details.
|
11
|
+
# @api private
|
12
|
+
def initialize(version_model_class, attributes)
|
13
|
+
@version_model_class = version_model_class
|
14
|
+
|
15
|
+
# Currently, this `deep_dup` is necessary because the `jsonb` branch
|
16
|
+
# modifies `@attributes`, and that would be a nasty suprise for
|
17
|
+
# consumers of this class.
|
18
|
+
# TODO: Stop modifying `@attributes`, then remove `deep_dup`.
|
19
|
+
@attributes = attributes.deep_dup
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def execute
|
24
|
+
case @version_model_class.columns_hash["object_changes"].type
|
25
|
+
when :jsonb
|
26
|
+
jsonb
|
27
|
+
when :json
|
28
|
+
json
|
29
|
+
else
|
30
|
+
text
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def json
|
38
|
+
predicates = []
|
39
|
+
values = []
|
40
|
+
@attributes.each do |field, value|
|
41
|
+
predicates.push(
|
42
|
+
"((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
|
43
|
+
)
|
44
|
+
values.concat([field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%"])
|
45
|
+
end
|
46
|
+
sql = predicates.join(" and ")
|
47
|
+
@version_model_class.where(sql, *values)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def jsonb
|
52
|
+
@attributes.each { |field, value| @attributes[field] = [value] }
|
53
|
+
@version_model_class.where("object_changes @> ?", @attributes.to_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def text
|
58
|
+
arel_field = @version_model_class.arel_table[:object_changes]
|
59
|
+
where_conditions = @attributes.map { |field, value|
|
60
|
+
::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
|
61
|
+
}
|
62
|
+
where_conditions = where_conditions.reduce { |a, e| a.and(e) }
|
63
|
+
@version_model_class.where(where_conditions)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
module PaperTrail
|
2
|
-
|
3
2
|
# Represents the history of a single record.
|
4
3
|
# @api private
|
5
4
|
class RecordHistory
|
6
|
-
|
7
5
|
# @param versions - ActiveRecord::Relation - All versions of the record.
|
8
6
|
# @param version_class - Class - Usually PaperTrail::Version,
|
9
7
|
# but it could also be a custom version class.
|
@@ -16,7 +14,7 @@ module PaperTrail
|
|
16
14
|
# Returns ordinal position of `version` in `sequence`.
|
17
15
|
# @api private
|
18
16
|
def index(version)
|
19
|
-
sequence.index(version)
|
17
|
+
sequence.to_a.index(version)
|
20
18
|
end
|
21
19
|
|
22
20
|
private
|
@@ -28,7 +26,7 @@ module PaperTrail
|
|
28
26
|
@versions.select(primary_key).order(primary_key.asc)
|
29
27
|
else
|
30
28
|
@versions.
|
31
|
-
select([
|
29
|
+
select([table[:created_at], primary_key]).
|
32
30
|
order(@version_class.timestamp_sort_order)
|
33
31
|
end
|
34
32
|
end
|
@@ -47,13 +45,5 @@ module PaperTrail
|
|
47
45
|
def table
|
48
46
|
@version_class.arel_table
|
49
47
|
end
|
50
|
-
|
51
|
-
# @return - Arel::Attribute - Attribute representing the timestamp column
|
52
|
-
# of the version table, usually named `created_at` (the rails convention)
|
53
|
-
# but not always.
|
54
|
-
# @api private
|
55
|
-
def timestamp
|
56
|
-
table[PaperTrail.timestamp_field]
|
57
|
-
end
|
58
48
|
end
|
59
49
|
end
|
@@ -0,0 +1,573 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
# Represents the "paper trail" for a single record.
|
3
|
+
class RecordTrail
|
4
|
+
# The respond_to? check here is specific to ActiveRecord 4.0 and can be
|
5
|
+
# removed when support for ActiveRecord < 4.2 is dropped.
|
6
|
+
RAILS_GTE_5_1 = ::ActiveRecord.respond_to?(:gem_version) &&
|
7
|
+
::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
|
8
|
+
|
9
|
+
def initialize(record)
|
10
|
+
@record = record
|
11
|
+
@in_after_callback = false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Utility method for reifying. Anything executed inside the block will
|
15
|
+
# appear like a new record.
|
16
|
+
#
|
17
|
+
# > .. as best as I can tell, the purpose of
|
18
|
+
# > appear_as_new_record was to attempt to prevent the callbacks in
|
19
|
+
# > AutosaveAssociation (which is the module responsible for persisting
|
20
|
+
# > foreign key changes earlier than most people want most of the time
|
21
|
+
# > because backwards compatibility or the maintainer hates himself or
|
22
|
+
# > something) from running. By also stubbing out persisted? we can
|
23
|
+
# > actually prevent those. A more stable option might be to use suppress
|
24
|
+
# > instead, similar to the other branch in reify_has_one.
|
25
|
+
# > -Sean Griffin (https://github.com/airblade/paper_trail/pull/899)
|
26
|
+
#
|
27
|
+
def appear_as_new_record
|
28
|
+
@record.instance_eval {
|
29
|
+
alias :old_new_record? :new_record?
|
30
|
+
alias :new_record? :present?
|
31
|
+
alias :old_persisted? :persisted?
|
32
|
+
alias :persisted? :nil?
|
33
|
+
}
|
34
|
+
yield
|
35
|
+
@record.instance_eval {
|
36
|
+
alias :new_record? :old_new_record?
|
37
|
+
alias :persisted? :old_persisted?
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def attributes_before_change
|
42
|
+
Hash[@record.attributes.map do |k, v|
|
43
|
+
if @record.class.column_names.include?(k)
|
44
|
+
[k, attribute_in_previous_version(k)]
|
45
|
+
else
|
46
|
+
[k, v]
|
47
|
+
end
|
48
|
+
end]
|
49
|
+
end
|
50
|
+
|
51
|
+
def changed_and_not_ignored
|
52
|
+
ignore = @record.paper_trail_options[:ignore].dup
|
53
|
+
# Remove Hash arguments and then evaluate whether the attributes (the
|
54
|
+
# keys of the hash) should also get pushed into the collection.
|
55
|
+
ignore.delete_if do |obj|
|
56
|
+
obj.is_a?(Hash) &&
|
57
|
+
obj.each { |attr, condition|
|
58
|
+
ignore << attr if condition.respond_to?(:call) && condition.call(@record)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
skip = @record.paper_trail_options[:skip]
|
62
|
+
changed_in_latest_version - ignore - skip
|
63
|
+
end
|
64
|
+
|
65
|
+
# Invoked after rollbacks to ensure versions records are not created for
|
66
|
+
# changes that never actually took place. Optimization: Use lazy `reset`
|
67
|
+
# instead of eager `reload` because, in many use cases, the association will
|
68
|
+
# not be used.
|
69
|
+
def clear_rolled_back_versions
|
70
|
+
versions.reset
|
71
|
+
end
|
72
|
+
|
73
|
+
# Invoked via`after_update` callback for when a previous version is
|
74
|
+
# reified and then saved.
|
75
|
+
def clear_version_instance
|
76
|
+
@record.send("#{@record.class.version_association_name}=", nil)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Determines whether it is appropriate to generate a new version
|
80
|
+
# instance. A timestamp-only update (e.g. only `updated_at` changed) is
|
81
|
+
# considered notable unless an ignored attribute was also changed.
|
82
|
+
def changed_notably?
|
83
|
+
if ignored_attr_has_changed?
|
84
|
+
timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
|
85
|
+
(notably_changed - timestamps).any?
|
86
|
+
else
|
87
|
+
notably_changed.any?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def changes
|
93
|
+
notable_changes = changes_in_latest_version.delete_if { |k, _v|
|
94
|
+
!notably_changed.include?(k)
|
95
|
+
}
|
96
|
+
AttributeSerializers::ObjectChangesAttribute.
|
97
|
+
new(@record.class).
|
98
|
+
serialize(notable_changes)
|
99
|
+
notable_changes.to_hash
|
100
|
+
end
|
101
|
+
|
102
|
+
def enabled?
|
103
|
+
PaperTrail.enabled? && PaperTrail.enabled_for_controller? && enabled_for_model?
|
104
|
+
end
|
105
|
+
|
106
|
+
def enabled_for_model?
|
107
|
+
@record.class.paper_trail.enabled?
|
108
|
+
end
|
109
|
+
|
110
|
+
# An attributed is "ignored" if it is listed in the `:ignore` option
|
111
|
+
# and/or the `:skip` option. Returns true if an ignored attribute has
|
112
|
+
# changed.
|
113
|
+
def ignored_attr_has_changed?
|
114
|
+
ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
|
115
|
+
ignored.any? && (changed_in_latest_version & ignored).any?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns true if this instance is the current, live one;
|
119
|
+
# returns false if this instance came from a previous version.
|
120
|
+
def live?
|
121
|
+
source_version.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
# Updates `data` from the model's `meta` option and from `controller_info`.
|
125
|
+
# @api private
|
126
|
+
def merge_metadata_into(data)
|
127
|
+
merge_metadata_from_model_into(data)
|
128
|
+
merge_metadata_from_controller_into(data)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Updates `data` from `controller_info`.
|
132
|
+
# @api private
|
133
|
+
def merge_metadata_from_controller_into(data)
|
134
|
+
data.merge(PaperTrail.controller_info || {})
|
135
|
+
end
|
136
|
+
|
137
|
+
# Updates `data` from the model's `meta` option.
|
138
|
+
# @api private
|
139
|
+
def merge_metadata_from_model_into(data)
|
140
|
+
@record.paper_trail_options[:meta].each do |k, v|
|
141
|
+
data[k] = model_metadatum(v, data[:event])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Given a `value` from the model's `meta` option, returns an object to be
|
146
|
+
# persisted. The `value` can be a simple scalar value, but it can also
|
147
|
+
# be a symbol that names a model method, or even a Proc.
|
148
|
+
# @api private
|
149
|
+
def model_metadatum(value, event)
|
150
|
+
if value.respond_to?(:call)
|
151
|
+
value.call(@record)
|
152
|
+
elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
|
153
|
+
# If it is an attribute that is changing in an existing object,
|
154
|
+
# be sure to grab the current version.
|
155
|
+
if event != "create" &&
|
156
|
+
@record.has_attribute?(value) &&
|
157
|
+
attribute_changed_in_latest_version?(value)
|
158
|
+
attribute_in_previous_version(value)
|
159
|
+
else
|
160
|
+
@record.send(value)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
value
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the object (not a Version) as it became next.
|
168
|
+
# NOTE: if self (the item) was not reified from a version, i.e. it is the
|
169
|
+
# "live" item, we return nil. Perhaps we should return self instead?
|
170
|
+
def next_version
|
171
|
+
subsequent_version = source_version.next
|
172
|
+
subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
|
173
|
+
rescue StandardError # TODO: Rescue something more specific
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
def notably_changed
|
178
|
+
only = @record.paper_trail_options[:only].dup
|
179
|
+
# Remove Hash arguments and then evaluate whether the attributes (the
|
180
|
+
# keys of the hash) should also get pushed into the collection.
|
181
|
+
only.delete_if do |obj|
|
182
|
+
obj.is_a?(Hash) &&
|
183
|
+
obj.each { |attr, condition|
|
184
|
+
only << attr if condition.respond_to?(:call) && condition.call(@record)
|
185
|
+
}
|
186
|
+
end
|
187
|
+
only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns hash of attributes (with appropriate attributes serialized),
|
191
|
+
# omitting attributes to be skipped.
|
192
|
+
def object_attrs_for_paper_trail
|
193
|
+
attrs = attributes_before_change.except(*@record.paper_trail_options[:skip])
|
194
|
+
AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
|
195
|
+
attrs
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns who put `@record` into its current state.
|
199
|
+
def originator
|
200
|
+
(source_version || versions.last).try(:whodunnit)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns the object (not a Version) as it was most recently.
|
204
|
+
def previous_version
|
205
|
+
(source_version ? source_version.previous : versions.last).try(:reify)
|
206
|
+
end
|
207
|
+
|
208
|
+
def record_create
|
209
|
+
@in_after_callback = true
|
210
|
+
return unless enabled?
|
211
|
+
versions_assoc = @record.send(@record.class.versions_association_name)
|
212
|
+
version = versions_assoc.create! data_for_create
|
213
|
+
update_transaction_id(version)
|
214
|
+
save_associations(version)
|
215
|
+
ensure
|
216
|
+
@in_after_callback = false
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns data for record create
|
220
|
+
# @api private
|
221
|
+
def data_for_create
|
222
|
+
data = {
|
223
|
+
event: @record.paper_trail_event || "create",
|
224
|
+
whodunnit: PaperTrail.whodunnit
|
225
|
+
}
|
226
|
+
if @record.respond_to?(:updated_at)
|
227
|
+
data[:created_at] = @record.updated_at
|
228
|
+
end
|
229
|
+
if record_object_changes? && changed_notably?
|
230
|
+
data[:object_changes] = recordable_object_changes
|
231
|
+
end
|
232
|
+
add_transaction_id_to(data)
|
233
|
+
merge_metadata_into(data)
|
234
|
+
end
|
235
|
+
|
236
|
+
def record_destroy
|
237
|
+
if enabled? && !@record.new_record?
|
238
|
+
version = @record.class.paper_trail.version_class.create(data_for_destroy)
|
239
|
+
if version.errors.any?
|
240
|
+
log_version_errors(version, :destroy)
|
241
|
+
else
|
242
|
+
@record.send("#{@record.class.version_association_name}=", version)
|
243
|
+
@record.send(@record.class.versions_association_name).reset
|
244
|
+
update_transaction_id(version)
|
245
|
+
save_associations(version)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns data for record destroy
|
251
|
+
# @api private
|
252
|
+
def data_for_destroy
|
253
|
+
data = {
|
254
|
+
item_id: @record.id,
|
255
|
+
item_type: @record.class.base_class.name,
|
256
|
+
event: @record.paper_trail_event || "destroy",
|
257
|
+
object: recordable_object,
|
258
|
+
whodunnit: PaperTrail.whodunnit
|
259
|
+
}
|
260
|
+
add_transaction_id_to(data)
|
261
|
+
merge_metadata_into(data)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns a boolean indicating whether to store serialized version diffs
|
265
|
+
# in the `object_changes` column of the version record.
|
266
|
+
# @api private
|
267
|
+
def record_object_changes?
|
268
|
+
@record.paper_trail_options[:save_changes] &&
|
269
|
+
@record.class.paper_trail.version_class.column_names.include?("object_changes")
|
270
|
+
end
|
271
|
+
|
272
|
+
def record_update(force)
|
273
|
+
@in_after_callback = true
|
274
|
+
if enabled? && (force || changed_notably?)
|
275
|
+
versions_assoc = @record.send(@record.class.versions_association_name)
|
276
|
+
version = versions_assoc.create(data_for_update)
|
277
|
+
if version.errors.any?
|
278
|
+
log_version_errors(version, :update)
|
279
|
+
else
|
280
|
+
update_transaction_id(version)
|
281
|
+
save_associations(version)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
ensure
|
285
|
+
@in_after_callback = false
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns data for record update
|
289
|
+
# @api private
|
290
|
+
def data_for_update
|
291
|
+
data = {
|
292
|
+
event: @record.paper_trail_event || "update",
|
293
|
+
object: recordable_object,
|
294
|
+
whodunnit: PaperTrail.whodunnit
|
295
|
+
}
|
296
|
+
if @record.respond_to?(:updated_at)
|
297
|
+
data[:created_at] = @record.updated_at
|
298
|
+
end
|
299
|
+
if record_object_changes?
|
300
|
+
data[:object_changes] = recordable_object_changes
|
301
|
+
end
|
302
|
+
add_transaction_id_to(data)
|
303
|
+
merge_metadata_into(data)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns an object which can be assigned to the `object` attribute of a
|
307
|
+
# nascent version record. If the `object` column is a postgres `json`
|
308
|
+
# column, then a hash can be used in the assignment, otherwise the column
|
309
|
+
# is a `text` column, and we must perform the serialization here, using
|
310
|
+
# `PaperTrail.serializer`.
|
311
|
+
# @api private
|
312
|
+
def recordable_object
|
313
|
+
if @record.class.paper_trail.version_class.object_col_is_json?
|
314
|
+
object_attrs_for_paper_trail
|
315
|
+
else
|
316
|
+
PaperTrail.serializer.dump(object_attrs_for_paper_trail)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns an object which can be assigned to the `object_changes`
|
321
|
+
# attribute of a nascent version record. If the `object_changes` column is
|
322
|
+
# a postgres `json` column, then a hash can be used in the assignment,
|
323
|
+
# otherwise the column is a `text` column, and we must perform the
|
324
|
+
# serialization here, using `PaperTrail.serializer`.
|
325
|
+
# @api private
|
326
|
+
def recordable_object_changes
|
327
|
+
if @record.class.paper_trail.version_class.object_changes_col_is_json?
|
328
|
+
changes
|
329
|
+
else
|
330
|
+
PaperTrail.serializer.dump(changes)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Invoked via callback when a user attempts to persist a reified
|
335
|
+
# `Version`.
|
336
|
+
def reset_timestamp_attrs_for_update_if_needed
|
337
|
+
return if live?
|
338
|
+
@record.send(:timestamp_attributes_for_update_in_model).each do |column|
|
339
|
+
# ActiveRecord 4.2 deprecated `reset_column!` in favor of
|
340
|
+
# `restore_column!`.
|
341
|
+
if @record.respond_to?("restore_#{column}!")
|
342
|
+
@record.send("restore_#{column}!")
|
343
|
+
else
|
344
|
+
@record.send("reset_#{column}!")
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Saves associations if the join table for `VersionAssociation` exists.
|
350
|
+
def save_associations(version)
|
351
|
+
return unless PaperTrail.config.track_associations?
|
352
|
+
save_bt_associations(version)
|
353
|
+
save_habtm_associations(version)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Save all `belongs_to` associations.
|
357
|
+
# @api private
|
358
|
+
def save_bt_associations(version)
|
359
|
+
@record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
360
|
+
save_bt_association(assoc, version)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# When a record is created, updated, or destroyed, we determine what the
|
365
|
+
# HABTM associations looked like before any changes were made, by using
|
366
|
+
# the `paper_trail_habtm` data structure. Then, we create
|
367
|
+
# `VersionAssociation` records for each of the associated records.
|
368
|
+
# @api private
|
369
|
+
def save_habtm_associations(version)
|
370
|
+
@record.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
|
371
|
+
next unless save_habtm_association?(a)
|
372
|
+
habtm_assoc_ids(a).each do |id|
|
373
|
+
PaperTrail::VersionAssociation.create(
|
374
|
+
version_id: version.transaction_id,
|
375
|
+
foreign_key_name: a.name,
|
376
|
+
foreign_key_id: id
|
377
|
+
)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# AR callback.
|
383
|
+
# @api private
|
384
|
+
def save_version?
|
385
|
+
if_condition = @record.paper_trail_options[:if]
|
386
|
+
unless_condition = @record.paper_trail_options[:unless]
|
387
|
+
(if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
|
388
|
+
end
|
389
|
+
|
390
|
+
def source_version
|
391
|
+
version
|
392
|
+
end
|
393
|
+
|
394
|
+
# Mimics the `touch` method from `ActiveRecord::Persistence`, but also
|
395
|
+
# creates a version. A version is created regardless of options such as
|
396
|
+
# `:on`, `:if`, or `:unless`.
|
397
|
+
#
|
398
|
+
# TODO: look into leveraging the `after_touch` callback from
|
399
|
+
# `ActiveRecord` to allow the regular `touch` method to generate a version
|
400
|
+
# as normal. May make sense to switch the `record_update` method to
|
401
|
+
# leverage an `after_update` callback anyways (likely for v4.0.0)
|
402
|
+
def touch_with_version(name = nil)
|
403
|
+
unless @record.persisted?
|
404
|
+
raise ::ActiveRecord::ActiveRecordError, "can not touch on a new record object"
|
405
|
+
end
|
406
|
+
attributes = @record.send :timestamp_attributes_for_update_in_model
|
407
|
+
attributes << name if name
|
408
|
+
current_time = @record.send :current_time_from_proper_timezone
|
409
|
+
attributes.each { |column|
|
410
|
+
@record.send(:write_attribute, column, current_time)
|
411
|
+
}
|
412
|
+
record_update(true) unless will_record_after_update?
|
413
|
+
@record.save!(validate: false)
|
414
|
+
end
|
415
|
+
|
416
|
+
# Returns the object (not a Version) as it was at the given timestamp.
|
417
|
+
def version_at(timestamp, reify_options = {})
|
418
|
+
# Because a version stores how its object looked *before* the change,
|
419
|
+
# we need to look for the first version created *after* the timestamp.
|
420
|
+
v = versions.subsequent(timestamp, true).first
|
421
|
+
return v.reify(reify_options) if v
|
422
|
+
@record unless @record.destroyed?
|
423
|
+
end
|
424
|
+
|
425
|
+
# Returns the objects (not Versions) as they were between the given times.
|
426
|
+
def versions_between(start_time, end_time)
|
427
|
+
versions = send(@record.class.versions_association_name).between(start_time, end_time)
|
428
|
+
versions.collect { |version| version_at(version.created_at) }
|
429
|
+
end
|
430
|
+
|
431
|
+
# Executes the given method or block without creating a new version.
|
432
|
+
def without_versioning(method = nil)
|
433
|
+
paper_trail_was_enabled = enabled_for_model?
|
434
|
+
@record.class.paper_trail.disable
|
435
|
+
if method
|
436
|
+
if respond_to?(method)
|
437
|
+
public_send(method)
|
438
|
+
else
|
439
|
+
@record.send(method)
|
440
|
+
end
|
441
|
+
else
|
442
|
+
yield @record
|
443
|
+
end
|
444
|
+
ensure
|
445
|
+
@record.class.paper_trail.enable if paper_trail_was_enabled
|
446
|
+
end
|
447
|
+
|
448
|
+
# Temporarily overwrites the value of whodunnit and then executes the
|
449
|
+
# provided block.
|
450
|
+
def whodunnit(value)
|
451
|
+
raise ArgumentError, "expected to receive a block" unless block_given?
|
452
|
+
current_whodunnit = PaperTrail.whodunnit
|
453
|
+
PaperTrail.whodunnit = value
|
454
|
+
yield @record
|
455
|
+
ensure
|
456
|
+
PaperTrail.whodunnit = current_whodunnit
|
457
|
+
end
|
458
|
+
|
459
|
+
private
|
460
|
+
|
461
|
+
def add_transaction_id_to(data)
|
462
|
+
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
463
|
+
data[:transaction_id] = PaperTrail.transaction_id
|
464
|
+
end
|
465
|
+
|
466
|
+
# @api private
|
467
|
+
def attribute_changed_in_latest_version?(attr_name)
|
468
|
+
if @in_after_callback && RAILS_GTE_5_1
|
469
|
+
@record.saved_change_to_attribute?(attr_name.to_s)
|
470
|
+
else
|
471
|
+
@record.attribute_changed?(attr_name.to_s)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# @api private
|
476
|
+
def attribute_in_previous_version(attr_name)
|
477
|
+
if @in_after_callback && RAILS_GTE_5_1
|
478
|
+
@record.attribute_before_last_save(attr_name.to_s)
|
479
|
+
else
|
480
|
+
# TODO: after dropping support for rails 4.0, remove send, because
|
481
|
+
# attribute_was is no longer private.
|
482
|
+
@record.send(:attribute_was, attr_name.to_s)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# @api private
|
487
|
+
def changed_in_latest_version
|
488
|
+
if @in_after_callback && RAILS_GTE_5_1
|
489
|
+
@record.saved_changes.keys
|
490
|
+
else
|
491
|
+
@record.changed
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# @api private
|
496
|
+
def changes_in_latest_version
|
497
|
+
if @in_after_callback && RAILS_GTE_5_1
|
498
|
+
@record.saved_changes
|
499
|
+
else
|
500
|
+
@record.changes
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# Given a HABTM association, returns an array of ids.
|
505
|
+
# @api private
|
506
|
+
def habtm_assoc_ids(habtm_assoc)
|
507
|
+
current = @record.send(habtm_assoc.name).to_a.map(&:id) # TODO: `pluck` would use less memory
|
508
|
+
removed = @record.paper_trail_habtm.try(:[], habtm_assoc.name).try(:[], :removed) || []
|
509
|
+
added = @record.paper_trail_habtm.try(:[], habtm_assoc.name).try(:[], :added) || []
|
510
|
+
current + removed - added
|
511
|
+
end
|
512
|
+
|
513
|
+
def log_version_errors(version, action)
|
514
|
+
version.logger && version.logger.warn(
|
515
|
+
"Unable to create version for #{action} of #{@record.class.name}" +
|
516
|
+
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
517
|
+
)
|
518
|
+
end
|
519
|
+
|
520
|
+
# Save a single `belongs_to` association.
|
521
|
+
# @api private
|
522
|
+
def save_bt_association(assoc, version)
|
523
|
+
assoc_version_args = {
|
524
|
+
version_id: version.id,
|
525
|
+
foreign_key_name: assoc.foreign_key
|
526
|
+
}
|
527
|
+
|
528
|
+
if assoc.options[:polymorphic]
|
529
|
+
associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
|
530
|
+
if associated_record && associated_record.class.paper_trail.enabled?
|
531
|
+
assoc_version_args[:foreign_key_id] = associated_record.id
|
532
|
+
end
|
533
|
+
elsif assoc.klass.paper_trail.enabled?
|
534
|
+
assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
|
535
|
+
end
|
536
|
+
|
537
|
+
if assoc_version_args.key?(:foreign_key_id)
|
538
|
+
PaperTrail::VersionAssociation.create(assoc_version_args)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# Returns true if the given HABTM association should be saved.
|
543
|
+
# @api private
|
544
|
+
def save_habtm_association?(assoc)
|
545
|
+
@record.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
546
|
+
assoc.klass.paper_trail.enabled?
|
547
|
+
end
|
548
|
+
|
549
|
+
# Returns true if `save` will cause `record_update`
|
550
|
+
# to be called via the `after_update` callback.
|
551
|
+
def will_record_after_update?
|
552
|
+
on = @record.paper_trail_options[:on]
|
553
|
+
on.nil? || on.include?(:update)
|
554
|
+
end
|
555
|
+
|
556
|
+
def update_transaction_id(version)
|
557
|
+
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
558
|
+
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
559
|
+
PaperTrail.transaction_id = version.id
|
560
|
+
version.transaction_id = version.id
|
561
|
+
version.save
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def version
|
566
|
+
@record.public_send(@record.class.version_association_name)
|
567
|
+
end
|
568
|
+
|
569
|
+
def versions
|
570
|
+
@record.public_send(@record.class.versions_association_name)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|