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.
Files changed (198) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install/USAGE +3 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  4. data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  10. data/lib/paper_trail.rb +82 -130
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  15. data/lib/paper_trail/cleaner.rb +3 -1
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +11 -49
  18. data/lib/paper_trail/events/base.rb +323 -0
  19. data/lib/paper_trail/events/create.rb +32 -0
  20. data/lib/paper_trail/events/destroy.rb +42 -0
  21. data/lib/paper_trail/events/update.rb +60 -0
  22. data/lib/paper_trail/frameworks/active_record.rb +2 -1
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
  24. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  25. data/lib/paper_trail/frameworks/rails.rb +2 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
  27. data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
  28. data/lib/paper_trail/frameworks/rspec.rb +17 -4
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  30. data/lib/paper_trail/has_paper_trail.rb +22 -310
  31. data/lib/paper_trail/model_config.rb +157 -109
  32. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  33. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  34. data/lib/paper_trail/record_history.rb +3 -9
  35. data/lib/paper_trail/record_trail.rb +169 -319
  36. data/lib/paper_trail/reifier.rb +53 -374
  37. data/lib/paper_trail/request.rb +166 -0
  38. data/lib/paper_trail/serializers/json.rb +9 -10
  39. data/lib/paper_trail/serializers/yaml.rb +15 -28
  40. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  41. data/lib/paper_trail/version_concern.rb +160 -155
  42. data/lib/paper_trail/version_number.rb +12 -4
  43. metadata +77 -372
  44. data/.github/CONTRIBUTING.md +0 -109
  45. data/.github/ISSUE_TEMPLATE.md +0 -13
  46. data/.gitignore +0 -23
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -99
  49. data/.rubocop_todo.yml +0 -22
  50. data/.travis.yml +0 -41
  51. data/Appraisals +0 -38
  52. data/CHANGELOG.md +0 -560
  53. data/Gemfile +0 -2
  54. data/MIT-LICENSE +0 -20
  55. data/README.md +0 -1654
  56. data/Rakefile +0 -30
  57. data/doc/bug_report_template.rb +0 -69
  58. data/doc/warning_about_not_setting_whodunnit.md +0 -32
  59. data/gemfiles/ar3.gemfile +0 -19
  60. data/gemfiles/ar4.gemfile +0 -8
  61. data/gemfiles/ar5.gemfile +0 -9
  62. data/lib/generators/paper_trail/USAGE +0 -2
  63. data/lib/generators/paper_trail/default_initializer.rb +0 -0
  64. data/lib/generators/paper_trail/install_generator.rb +0 -57
  65. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
  66. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
  67. data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
  68. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
  69. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
  70. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  71. data/lib/paper_trail/version_association_concern.rb +0 -17
  72. data/paper_trail.gemspec +0 -56
  73. data/spec/generators/install_generator_spec.rb +0 -66
  74. data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
  75. data/spec/models/animal_spec.rb +0 -36
  76. data/spec/models/boolit_spec.rb +0 -48
  77. data/spec/models/callback_modifier_spec.rb +0 -96
  78. data/spec/models/car_spec.rb +0 -13
  79. data/spec/models/custom_primary_key_record_spec.rb +0 -18
  80. data/spec/models/fluxor_spec.rb +0 -17
  81. data/spec/models/gadget_spec.rb +0 -68
  82. data/spec/models/joined_version_spec.rb +0 -47
  83. data/spec/models/json_version_spec.rb +0 -102
  84. data/spec/models/kitchen/banana_spec.rb +0 -14
  85. data/spec/models/not_on_update_spec.rb +0 -22
  86. data/spec/models/post_with_status_spec.rb +0 -50
  87. data/spec/models/skipper_spec.rb +0 -46
  88. data/spec/models/thing_spec.rb +0 -11
  89. data/spec/models/truck_spec.rb +0 -5
  90. data/spec/models/vehicle_spec.rb +0 -5
  91. data/spec/models/version_spec.rb +0 -272
  92. data/spec/models/widget_spec.rb +0 -343
  93. data/spec/modules/paper_trail_spec.rb +0 -27
  94. data/spec/modules/version_concern_spec.rb +0 -31
  95. data/spec/modules/version_number_spec.rb +0 -43
  96. data/spec/paper_trail/config_spec.rb +0 -33
  97. data/spec/paper_trail_spec.rb +0 -79
  98. data/spec/rails_helper.rb +0 -34
  99. data/spec/requests/articles_spec.rb +0 -34
  100. data/spec/spec_helper.rb +0 -114
  101. data/spec/support/alt_db_init.rb +0 -54
  102. data/test/custom_json_serializer.rb +0 -13
  103. data/test/dummy/Rakefile +0 -7
  104. data/test/dummy/app/controllers/application_controller.rb +0 -33
  105. data/test/dummy/app/controllers/articles_controller.rb +0 -20
  106. data/test/dummy/app/controllers/test_controller.rb +0 -5
  107. data/test/dummy/app/controllers/widgets_controller.rb +0 -32
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/models/animal.rb +0 -6
  110. data/test/dummy/app/models/article.rb +0 -24
  111. data/test/dummy/app/models/authorship.rb +0 -5
  112. data/test/dummy/app/models/bar_habtm.rb +0 -4
  113. data/test/dummy/app/models/book.rb +0 -9
  114. data/test/dummy/app/models/boolit.rb +0 -4
  115. data/test/dummy/app/models/callback_modifier.rb +0 -45
  116. data/test/dummy/app/models/car.rb +0 -3
  117. data/test/dummy/app/models/cat.rb +0 -2
  118. data/test/dummy/app/models/chapter.rb +0 -9
  119. data/test/dummy/app/models/citation.rb +0 -5
  120. data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
  121. data/test/dummy/app/models/customer.rb +0 -4
  122. data/test/dummy/app/models/document.rb +0 -4
  123. data/test/dummy/app/models/dog.rb +0 -2
  124. data/test/dummy/app/models/editor.rb +0 -4
  125. data/test/dummy/app/models/editorship.rb +0 -5
  126. data/test/dummy/app/models/elephant.rb +0 -3
  127. data/test/dummy/app/models/fluxor.rb +0 -3
  128. data/test/dummy/app/models/foo_habtm.rb +0 -5
  129. data/test/dummy/app/models/foo_widget.rb +0 -2
  130. data/test/dummy/app/models/fruit.rb +0 -5
  131. data/test/dummy/app/models/gadget.rb +0 -3
  132. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  133. data/test/dummy/app/models/legacy_widget.rb +0 -4
  134. data/test/dummy/app/models/line_item.rb +0 -4
  135. data/test/dummy/app/models/not_on_update.rb +0 -4
  136. data/test/dummy/app/models/order.rb +0 -5
  137. data/test/dummy/app/models/paragraph.rb +0 -5
  138. data/test/dummy/app/models/person.rb +0 -38
  139. data/test/dummy/app/models/post.rb +0 -3
  140. data/test/dummy/app/models/post_with_status.rb +0 -8
  141. data/test/dummy/app/models/protected_widget.rb +0 -3
  142. data/test/dummy/app/models/quotation.rb +0 -5
  143. data/test/dummy/app/models/section.rb +0 -6
  144. data/test/dummy/app/models/skipper.rb +0 -6
  145. data/test/dummy/app/models/song.rb +0 -41
  146. data/test/dummy/app/models/thing.rb +0 -3
  147. data/test/dummy/app/models/translation.rb +0 -4
  148. data/test/dummy/app/models/truck.rb +0 -4
  149. data/test/dummy/app/models/vehicle.rb +0 -4
  150. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  151. data/test/dummy/app/models/widget.rb +0 -16
  152. data/test/dummy/app/models/wotsit.rb +0 -8
  153. data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
  154. data/test/dummy/app/versions/joined_version.rb +0 -6
  155. data/test/dummy/app/versions/json_version.rb +0 -3
  156. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  157. data/test/dummy/app/versions/post_version.rb +0 -3
  158. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  159. data/test/dummy/config.ru +0 -4
  160. data/test/dummy/config/application.rb +0 -80
  161. data/test/dummy/config/boot.rb +0 -10
  162. data/test/dummy/config/database.mysql.yml +0 -19
  163. data/test/dummy/config/database.postgres.yml +0 -15
  164. data/test/dummy/config/database.sqlite.yml +0 -15
  165. data/test/dummy/config/environment.rb +0 -5
  166. data/test/dummy/config/environments/development.rb +0 -41
  167. data/test/dummy/config/environments/production.rb +0 -74
  168. data/test/dummy/config/environments/test.rb +0 -51
  169. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
  170. data/test/dummy/config/initializers/inflections.rb +0 -10
  171. data/test/dummy/config/initializers/mime_types.rb +0 -5
  172. data/test/dummy/config/initializers/paper_trail.rb +0 -9
  173. data/test/dummy/config/initializers/secret_token.rb +0 -9
  174. data/test/dummy/config/initializers/session_store.rb +0 -8
  175. data/test/dummy/config/locales/en.yml +0 -5
  176. data/test/dummy/config/routes.rb +0 -4
  177. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
  178. data/test/dummy/db/schema.rb +0 -288
  179. data/test/dummy/script/rails +0 -8
  180. data/test/functional/controller_test.rb +0 -90
  181. data/test/functional/enabled_for_controller_test.rb +0 -28
  182. data/test/functional/modular_sinatra_test.rb +0 -46
  183. data/test/functional/sinatra_test.rb +0 -51
  184. data/test/functional/thread_safety_test.rb +0 -46
  185. data/test/test_helper.rb +0 -127
  186. data/test/time_travel_helper.rb +0 -1
  187. data/test/unit/associations_test.rb +0 -1016
  188. data/test/unit/cleaner_test.rb +0 -188
  189. data/test/unit/inheritance_column_test.rb +0 -43
  190. data/test/unit/model_test.rb +0 -1489
  191. data/test/unit/protected_attrs_test.rb +0 -52
  192. data/test/unit/serializer_test.rb +0 -119
  193. data/test/unit/serializers/json_test.rb +0 -95
  194. data/test/unit/serializers/mixin_json_test.rb +0 -37
  195. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
  196. data/test/unit/serializers/yaml_test.rb +0 -54
  197. data/test/unit/timestamp_test.rb +0 -41
  198. 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
- require "active_support/json"
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
- # Returns a SQL LIKE condition to be used to match the given field and
36
- # value in the serialized `object_changes`.
37
- def where_object_changes_condition(arel_field, field, value)
38
- # Convert to JSON to handle strings and nulls correctly.
39
- json_value = value.to_json
40
-
41
- # Need to check first (before) and secondary (after) fields
42
- arel_field.matches("%\"#{field}\":[#{json_value},%").
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(arel_field, field, value)
26
- # Need to check first (before) and secondary (after) fields
27
- m1 = nil
28
- m2 = nil
29
- case yaml_engine_id
30
- when :psych
31
- m1 = "%\n#{field}:\n- #{value}\n%"
32
- m2 = "%\n#{field}:\n-%\n- #{value}\n%"
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
- require "active_support/concern"
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
- belongs_to :item, polymorphic: true
14
-
15
- # Since the test suite has test coverage for this, we want to declare
16
- # the association when the test suite is running. This makes it pass when
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[PaperTrail.timestamp_field].gt(start_time).
101
- and(arel_table[PaperTrail.timestamp_field].lt(end_time))
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[PaperTrail.timestamp_field].send(direction.downcase)].tap do |array|
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
- # Query the `versions.objects` column using the SQL LIKE operator.
114
- # Performs an attribute search on the serialized object by invoking the
115
- # identically-named method in the serializer being used.
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
- # Query the `versions.object_changes` column by attributes, using the
142
- # SQL LIKE operator.
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
- [:json, :jsonb].include?(columns_hash["object"].type)
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
- [:json, :jsonb].include?(columns_hash["object_changes"].try(:type))
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
- return nil if object.nil?
228
- without_identity_map do
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 (see `sibling_versions`). The "create" event,
275
- # for example, has an index of 0.
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
- # In Rails 3.1+, calling reify on a previous version confuses the
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 = PaperTrail.config.version_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