paper_trail 8.0.0 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install_generator.rb +3 -1
  3. data/lib/paper_trail.rb +151 -84
  4. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  5. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +7 -4
  6. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  7. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  8. data/lib/paper_trail/cleaner.rb +2 -0
  9. data/lib/paper_trail/config.rb +33 -8
  10. data/lib/paper_trail/frameworks/active_record.rb +2 -0
  11. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +2 -0
  12. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +2 -0
  13. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  14. data/lib/paper_trail/frameworks/rails.rb +2 -0
  15. data/lib/paper_trail/frameworks/rails/controller.rb +30 -15
  16. data/lib/paper_trail/frameworks/rails/engine.rb +2 -0
  17. data/lib/paper_trail/frameworks/rspec.rb +5 -3
  18. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  19. data/lib/paper_trail/has_paper_trail.rb +2 -2
  20. data/lib/paper_trail/model_config.rb +77 -22
  21. data/lib/paper_trail/queries/versions/where_object.rb +2 -0
  22. data/lib/paper_trail/queries/versions/where_object_changes.rb +3 -1
  23. data/lib/paper_trail/record_history.rb +2 -0
  24. data/lib/paper_trail/record_trail.rb +189 -55
  25. data/lib/paper_trail/reifier.rb +4 -2
  26. data/lib/paper_trail/reifiers/belongs_to.rb +3 -1
  27. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +3 -1
  28. data/lib/paper_trail/reifiers/has_many.rb +3 -1
  29. data/lib/paper_trail/reifiers/has_many_through.rb +3 -1
  30. data/lib/paper_trail/reifiers/has_one.rb +52 -4
  31. data/lib/paper_trail/request.rb +183 -0
  32. data/lib/paper_trail/serializers/json.rb +2 -2
  33. data/lib/paper_trail/serializers/yaml.rb +10 -5
  34. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +49 -0
  35. data/lib/paper_trail/version_association_concern.rb +1 -1
  36. data/lib/paper_trail/version_concern.rb +2 -6
  37. data/lib/paper_trail/version_number.rb +3 -1
  38. metadata +55 -81
  39. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/attribute_serializers/object_attribute"
2
4
  require "paper_trail/reifiers/belongs_to"
3
5
  require "paper_trail/reifiers/has_and_belongs_to_many"
@@ -54,7 +56,7 @@ module PaperTrail
54
56
  # @api private
55
57
  def each_enabled_association(associations)
56
58
  associations.each do |assoc|
57
- next unless assoc.klass.paper_trail.enabled?
59
+ next unless ::PaperTrail.request.enabled_for_model?(assoc.klass)
58
60
  yield assoc
59
61
  end
60
62
  end
@@ -192,7 +194,7 @@ module PaperTrail
192
194
  # @api private
193
195
  def reify_habtm_associations(transaction_id, model, options = {})
194
196
  model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
195
- pt_enabled = assoc.klass.paper_trail.enabled?
197
+ pt_enabled = ::PaperTrail.request.enabled_for_model?(assoc.klass)
196
198
  next unless model.class.paper_trail_save_join_tables.include?(assoc.name) || pt_enabled
197
199
  Reifiers::HasAndBelongsToMany.reify(pt_enabled, assoc, model, options, transaction_id)
198
200
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Reifiers
3
5
  # Reify a single `belongs_to` association of `model`.
@@ -37,7 +39,7 @@ module PaperTrail
37
39
  # @api private
38
40
  def load_version(assoc, id, transaction_id, version_at)
39
41
  assoc.klass.paper_trail.version_class.
40
- where("item_type = ?", assoc.klass.name).
42
+ where("item_type = ?", assoc.klass.base_class.name).
41
43
  where("item_id = ?", id).
42
44
  where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
43
45
  order("id").limit(1).first
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Reifiers
3
5
  # Reify a single HABTM association of `model`.
@@ -37,7 +39,7 @@ module PaperTrail
37
39
  # @api private
38
40
  def load_version(assoc, id, transaction_id, version_at)
39
41
  assoc.klass.paper_trail.version_class.
40
- where("item_type = ?", assoc.klass.name).
42
+ where("item_type = ?", assoc.klass.base_class.name).
41
43
  where("item_id = ?", id).
42
44
  where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
43
45
  order("id").
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Reifiers
3
5
  # Reify a single, direct (not `through`) `has_many` association of `model`.
@@ -98,7 +100,7 @@ module PaperTrail
98
100
  select("MIN(version_id)").
99
101
  where("foreign_key_name = ?", assoc.foreign_key).
100
102
  where("foreign_key_id = ?", model.id).
101
- where("#{version_table}.item_type = ?", assoc.klass.name).
103
+ where("#{version_table}.item_type = ?", assoc.klass.base_class.name).
102
104
  where("created_at >= ? OR transaction_id = ?", version_at, tx_id).
103
105
  group("item_id").
104
106
  to_sql
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Reifiers
3
5
  # Reify a single HMT association of `model`.
@@ -73,7 +75,7 @@ module PaperTrail
73
75
  def load_versions_for_hmt_association(assoc, ids, tx_id, version_at)
74
76
  version_id_subquery = assoc.klass.paper_trail.version_class.
75
77
  select("MIN(id)").
76
- where("item_type = ?", assoc.klass.name).
78
+ where("item_type = ?", assoc.klass.base_class.name).
77
79
  where("item_id IN (?)", ids).
78
80
  where(
79
81
  "created_at >= ? OR transaction_id = ?",
@@ -1,12 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Reifiers
3
5
  # Reify a single `has_one` association of `model`.
4
6
  # @api private
5
7
  module HasOne
8
+ # A more helpful error message, instead of the AssociationTypeMismatch
9
+ # you would get if, eg. we were to try to assign a Bicycle to the :car
10
+ # association (before, if there were multiple records we would just take
11
+ # the first and hope for the best).
12
+ # @api private
13
+ class FoundMoreThanOne < RuntimeError
14
+ MESSAGE_FMT = <<~STR
15
+ Unable to reify has_one association. Expected to find one %s,
16
+ but found %d.
17
+
18
+ This is a known issue, and a good example of why association tracking
19
+ is an experimental feature that should not be used in production.
20
+
21
+ That said, this is a rare error. In spec/models/person_spec.rb we
22
+ reproduce it by having two STI models with the same foreign_key (Car
23
+ and Bicycle are both Vehicles and the FK for both is owner_id)
24
+
25
+ If you'd like to help fix this error, please read
26
+ https://github.com/airblade/paper_trail/issues/594
27
+ and see spec/models/person_spec.rb
28
+ STR
29
+
30
+ def initialize(base_class_name, num_records_found)
31
+ @base_class_name = base_class_name.to_s
32
+ @num_records_found = num_records_found.to_i
33
+ end
34
+
35
+ def message
36
+ format(MESSAGE_FMT, @base_class_name, @num_records_found)
37
+ end
38
+ end
39
+
6
40
  class << self
7
41
  # @api private
8
42
  def reify(assoc, model, options, transaction_id)
9
- version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
43
+ version = load_version(assoc, model, transaction_id, options[:version_at])
10
44
  return unless version
11
45
  if version.event == "create"
12
46
  create_event(assoc, model, options)
@@ -31,15 +65,29 @@ module PaperTrail
31
65
  # Given a has-one association `assoc` on `model`, return the version
32
66
  # record from the point in time identified by `transaction_id` or `version_at`.
33
67
  # @api private
34
- def load_version_for_has_one(assoc, model, transaction_id, version_at)
68
+ def load_version(assoc, model, transaction_id, version_at)
69
+ base_class_name = assoc.klass.base_class.name
70
+ versions = load_versions(assoc, model, transaction_id, version_at, base_class_name)
71
+ case versions.length
72
+ when 0
73
+ nil
74
+ when 1
75
+ versions.first
76
+ else
77
+ raise FoundMoreThanOne.new(base_class_name, versions.length)
78
+ end
79
+ end
80
+
81
+ # @api private
82
+ def load_versions(assoc, model, transaction_id, version_at, base_class_name)
35
83
  version_table_name = model.class.paper_trail.version_class.table_name
36
84
  model.class.paper_trail.version_class.joins(:version_associations).
37
85
  where("version_associations.foreign_key_name = ?", assoc.foreign_key).
38
86
  where("version_associations.foreign_key_id = ?", model.id).
39
- where("#{version_table_name}.item_type = ?", assoc.klass.name).
87
+ where("#{version_table_name}.item_type = ?", base_class_name).
40
88
  where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
41
89
  order("#{version_table_name}.id ASC").
42
- first
90
+ load
43
91
  end
44
92
 
45
93
  # @api private
@@ -0,0 +1,183 @@
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
+ # @api private
20
+ def clear_transaction_id
21
+ self.transaction_id = nil
22
+ end
23
+
24
+ # Sets any data from the controller that you want PaperTrail to store.
25
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
26
+ #
27
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
28
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
29
+ #
30
+ # @api public
31
+ def controller_info=(value)
32
+ store[:controller_info] = value
33
+ end
34
+
35
+ # Returns the data from the controller that you want PaperTrail to store.
36
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
37
+ #
38
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
39
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
40
+ #
41
+ # @api public
42
+ def controller_info
43
+ store[:controller_info]
44
+ end
45
+
46
+ # Switches PaperTrail off for the given model.
47
+ # @api public
48
+ def disable_model(model_class)
49
+ enabled_for_model(model_class, false)
50
+ end
51
+
52
+ # Switches PaperTrail on for the given model.
53
+ # @api public
54
+ def enable_model(model_class)
55
+ enabled_for_model(model_class, true)
56
+ end
57
+
58
+ # Sets whether PaperTrail is enabled or disabled for the current request.
59
+ # @api public
60
+ def enabled=(value)
61
+ store[:enabled] = value
62
+ end
63
+
64
+ # Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
65
+ # See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
66
+ # @api public
67
+ def enabled?
68
+ !!store[:enabled]
69
+ end
70
+
71
+ # Sets whether PaperTrail is enabled or disabled for this model in the
72
+ # current request.
73
+ # @api public
74
+ def enabled_for_model(model, value)
75
+ store[:"enabled_for_#{model}"] = value
76
+ end
77
+
78
+ # Returns `true` if PaperTrail is enabled for this model in the current
79
+ # request, `false` otherwise.
80
+ # @api public
81
+ def enabled_for_model?(model)
82
+ model.include?(::PaperTrail::Model::InstanceMethods) &&
83
+ !!store.fetch(:"enabled_for_#{model}", true)
84
+ end
85
+
86
+ # @api private
87
+ def merge(options)
88
+ options.to_h.each do |k, v|
89
+ store[k] = v
90
+ end
91
+ end
92
+
93
+ # @api private
94
+ def set(options)
95
+ store.clear
96
+ merge(options)
97
+ end
98
+
99
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
100
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
101
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
102
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
103
+ # @api private
104
+ def to_h
105
+ store.deep_dup
106
+ end
107
+
108
+ # @api private
109
+ def transaction_id
110
+ store[:transaction_id]
111
+ end
112
+
113
+ # @api private
114
+ def transaction_id=(id)
115
+ store[:transaction_id] = id
116
+ end
117
+
118
+ # Temporarily set `options` and execute a block.
119
+ # @api private
120
+ def with(options)
121
+ return unless block_given?
122
+ validate_public_options(options)
123
+ before = to_h
124
+ merge(options)
125
+ yield
126
+ ensure
127
+ set(before)
128
+ end
129
+
130
+ # Sets who is responsible for any changes that occur during request. You
131
+ # would normally use this in a migration or on the console, when working
132
+ # with models directly.
133
+ #
134
+ # `value` is usually a string, the name of a person, but you can set
135
+ # anything that responds to `to_s`. You can also set a Proc, which will
136
+ # not be evaluated until `whodunnit` is called later, usually right before
137
+ # inserting a `Version` record.
138
+ #
139
+ # @api public
140
+ def whodunnit=(value)
141
+ store[:whodunnit] = value
142
+ end
143
+
144
+ # Returns who is reponsible for any changes that occur during request.
145
+ #
146
+ # @api public
147
+ def whodunnit
148
+ who = store[:whodunnit]
149
+ who.respond_to?(:call) ? who.call : who
150
+ end
151
+
152
+ private
153
+
154
+ # Returns a Hash, initializing with default values if necessary.
155
+ # @api private
156
+ def store
157
+ RequestStore.store[:paper_trail] ||= {
158
+ enabled: true
159
+ }
160
+ end
161
+
162
+ # Provide a helpful error message if someone has a typo in one of their
163
+ # option keys. We don't validate option values here. That's traditionally
164
+ # been handled with casting (`to_s`, `!!`) in the accessor method.
165
+ # @api private
166
+ def validate_public_options(options)
167
+ options.each do |k, _v|
168
+ case k
169
+ when :controller_info,
170
+ /enabled_for_/,
171
+ :enabled,
172
+ :whodunnit
173
+ next
174
+ when :transaction_id
175
+ raise InvalidOption, "Cannot set private option: #{k}"
176
+ else
177
+ raise InvalidOption, "Invalid option: #{k}"
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -1,4 +1,4 @@
1
- require "active_support/json"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module PaperTrail
4
4
  module Serializers
@@ -34,7 +34,7 @@ module PaperTrail
34
34
 
35
35
  def where_object_changes_condition(*)
36
36
  raise <<-STR.squish.freeze
37
- where_object_changes no longer supports reading json from a text
37
+ where_object_changes no longer supports reading JSON from a text
38
38
  column. The old implementation was inaccurate, returning more records
39
39
  than you wanted. This feature was deprecated in 7.1.0 and removed in
40
40
  8.0.0. The json and jsonb datatypes are still supported. See the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yaml"
2
4
 
3
5
  module PaperTrail
@@ -22,11 +24,14 @@ module PaperTrail
22
24
 
23
25
  # Returns a SQL LIKE condition to be used to match the given field and
24
26
  # value in the serialized `object_changes`.
25
- def where_object_changes_condition(arel_field, field, value)
26
- # Need to check first (before) and secondary (after) fields
27
- m1 = "%\n#{field}:\n- #{value}\n%"
28
- m2 = "%\n#{field}:\n-%\n- #{value}\n%"
29
- arel_field.matches(m1).or(arel_field.matches(m2))
27
+ def where_object_changes_condition(*)
28
+ raise <<-STR.squish.freeze
29
+ where_object_changes no longer supports reading YAML from a text
30
+ column. The old implementation was inaccurate, returning more records
31
+ than you wanted. This feature was deprecated in 8.1.0 and removed in
32
+ 9.0.0. The json and jsonb datatypes are still supported. See
33
+ discussion at https://github.com/airblade/paper_trail/pull/997
34
+ STR
30
35
  end
31
36
  end
32
37
  end
@@ -0,0 +1,49 @@
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::VERSION::MAJOR < 5 ||
33
+ (::ActiveRecord::VERSION::MINOR.zero? && ::ActiveRecord::VERSION::TINY < 2)
34
+ end
35
+
36
+ def serialize_with_ar(array)
37
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
38
+ new(@subtype, @delimiter).
39
+ serialize(array)
40
+ end
41
+
42
+ def deserialize_with_ar(array)
43
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
44
+ new(@subtype, @delimiter).
45
+ deserialize(array)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,4 +1,4 @@
1
- require "active_support/concern"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module PaperTrail
4
4
  # Functionality for `PaperTrail::VersionAssociation`. Exists in a module
@@ -1,4 +1,5 @@
1
- require "active_support/concern"
1
+ # frozen_string_literal: true
2
+
2
3
  require "paper_trail/attribute_serializers/object_changes_attribute"
3
4
  require "paper_trail/queries/versions/where_object"
4
5
  require "paper_trail/queries/versions/where_object_changes"
@@ -230,11 +231,6 @@ module PaperTrail
230
231
  @paper_trail_originator ||= previous.try(:whodunnit)
231
232
  end
232
233
 
233
- def originator
234
- ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
235
- paper_trail_originator
236
- end
237
-
238
234
  # Returns who changed the item from the state it had in this version. This
239
235
  # is an alias for `whodunnit`.
240
236
  def terminator