piko-smart-pkg 0.0.1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/paper_trail-17.0.0/LICENSE +20 -0
  3. data/paper_trail-17.0.0/lib/generators/paper_trail/install/USAGE +31 -0
  4. data/paper_trail-17.0.0/lib/generators/paper_trail/install/install_generator.rb +249 -0
  5. data/paper_trail-17.0.0/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  6. data/paper_trail-17.0.0/lib/generators/paper_trail/install/templates/create_versions.rb.erb +41 -0
  7. data/paper_trail-17.0.0/lib/generators/paper_trail/migration_generator.rb +65 -0
  8. data/paper_trail-17.0.0/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  9. data/paper_trail-17.0.0/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +86 -0
  10. data/paper_trail-17.0.0/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +40 -0
  11. data/paper_trail-17.0.0/lib/paper_trail/attribute_serializers/README.md +10 -0
  12. data/paper_trail-17.0.0/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
  13. data/paper_trail-17.0.0/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  14. data/paper_trail-17.0.0/lib/paper_trail/attribute_serializers/object_attribute.rb +48 -0
  15. data/paper_trail-17.0.0/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +51 -0
  16. data/paper_trail-17.0.0/lib/paper_trail/cleaner.rb +60 -0
  17. data/paper_trail-17.0.0/lib/paper_trail/compatibility.rb +51 -0
  18. data/paper_trail-17.0.0/lib/paper_trail/config.rb +41 -0
  19. data/paper_trail-17.0.0/lib/paper_trail/errors.rb +33 -0
  20. data/paper_trail-17.0.0/lib/paper_trail/events/base.rb +343 -0
  21. data/paper_trail-17.0.0/lib/paper_trail/events/create.rb +32 -0
  22. data/paper_trail-17.0.0/lib/paper_trail/events/destroy.rb +42 -0
  23. data/paper_trail-17.0.0/lib/paper_trail/events/update.rb +76 -0
  24. data/paper_trail-17.0.0/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +16 -0
  25. data/paper_trail-17.0.0/lib/paper_trail/frameworks/active_record.rb +12 -0
  26. data/paper_trail-17.0.0/lib/paper_trail/frameworks/cucumber.rb +33 -0
  27. data/paper_trail-17.0.0/lib/paper_trail/frameworks/rails/controller.rb +103 -0
  28. data/paper_trail-17.0.0/lib/paper_trail/frameworks/rails/railtie.rb +34 -0
  29. data/paper_trail-17.0.0/lib/paper_trail/frameworks/rails.rb +3 -0
  30. data/paper_trail-17.0.0/lib/paper_trail/frameworks/rspec/helpers.rb +29 -0
  31. data/paper_trail-17.0.0/lib/paper_trail/frameworks/rspec.rb +42 -0
  32. data/paper_trail-17.0.0/lib/paper_trail/has_paper_trail.rb +92 -0
  33. data/paper_trail-17.0.0/lib/paper_trail/model_config.rb +257 -0
  34. data/paper_trail-17.0.0/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  35. data/paper_trail-17.0.0/lib/paper_trail/queries/versions/where_object.rb +65 -0
  36. data/paper_trail-17.0.0/lib/paper_trail/queries/versions/where_object_changes.rb +70 -0
  37. data/paper_trail-17.0.0/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  38. data/paper_trail-17.0.0/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  39. data/paper_trail-17.0.0/lib/paper_trail/record_history.rb +51 -0
  40. data/paper_trail-17.0.0/lib/paper_trail/record_trail.rb +342 -0
  41. data/paper_trail-17.0.0/lib/paper_trail/reifier.rb +147 -0
  42. data/paper_trail-17.0.0/lib/paper_trail/request.rb +163 -0
  43. data/paper_trail-17.0.0/lib/paper_trail/serializers/json.rb +36 -0
  44. data/paper_trail-17.0.0/lib/paper_trail/serializers/yaml.rb +68 -0
  45. data/paper_trail-17.0.0/lib/paper_trail/type_serializers/postgres_array_serializer.rb +35 -0
  46. data/paper_trail-17.0.0/lib/paper_trail/version_concern.rb +406 -0
  47. data/paper_trail-17.0.0/lib/paper_trail/version_number.rb +23 -0
  48. data/paper_trail-17.0.0/lib/paper_trail.rb +138 -0
  49. data/piko-smart-pkg.gemspec +12 -0
  50. metadata +89 -0
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/events/create"
4
+ require "paper_trail/events/destroy"
5
+ require "paper_trail/events/update"
6
+
7
+ module PaperTrail
8
+ # Represents the "paper trail" for a single record.
9
+ class RecordTrail
10
+ def initialize(record)
11
+ @record = record
12
+ end
13
+
14
+ # Invoked after rollbacks to ensure versions records are not created for
15
+ # changes that never actually took place. Optimization: Use lazy `reset`
16
+ # instead of eager `reload` because, in many use cases, the association will
17
+ # not be used.
18
+ def clear_rolled_back_versions
19
+ versions.reset
20
+ end
21
+
22
+ # Invoked via`after_update` callback for when a previous version is
23
+ # reified and then saved.
24
+ def clear_version_instance
25
+ @record.send(:"#{@record.class.version_association_name}=", nil)
26
+ end
27
+
28
+ # Returns true if this instance is the current, live one;
29
+ # returns false if this instance came from a previous version.
30
+ def live?
31
+ source_version.nil?
32
+ end
33
+
34
+ # Returns the object (not a Version) as it became next.
35
+ # NOTE: if self (the item) was not reified from a version, i.e. it is the
36
+ # "live" item, we return nil. Perhaps we should return self instead?
37
+ def next_version
38
+ subsequent_version = source_version.next
39
+ subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
40
+ rescue StandardError # TODO: Rescue something more specific
41
+ nil
42
+ end
43
+
44
+ # Returns who put `@record` into its current state.
45
+ #
46
+ # @api public
47
+ def originator
48
+ (source_version || versions.last).try(:whodunnit)
49
+ end
50
+
51
+ # Returns the object (not a Version) as it was most recently.
52
+ #
53
+ # @api public
54
+ def previous_version
55
+ (source_version ? source_version.previous : versions.last).try(:reify)
56
+ end
57
+
58
+ def record_create
59
+ return unless enabled?
60
+
61
+ build_version_on_create(in_after_callback: true).tap do |version|
62
+ version.save!
63
+ # Because the version object was created using version_class.new instead
64
+ # of versions_assoc.build?, the association cache is unaware. So, we
65
+ # invalidate the `versions` association cache with `reset`.
66
+ versions.reset
67
+ rescue StandardError => e
68
+ handle_version_errors e, version, :create
69
+ end
70
+ end
71
+
72
+ # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
73
+ #
74
+ # @api private
75
+ # @return - The created version object, so that plugins can use it, e.g.
76
+ # paper_trail-association_tracking
77
+ def record_destroy(recording_order)
78
+ return unless enabled? && !@record.new_record?
79
+ in_after_callback = recording_order == "after"
80
+ event = Events::Destroy.new(@record, in_after_callback)
81
+
82
+ # Merge data from `Event` with data from PT-AT. We no longer use
83
+ # `data_for_destroy` but PT-AT still does.
84
+ data = event.data.merge(data_for_destroy)
85
+
86
+ version = @record.class.paper_trail.version_class.new(data)
87
+ begin
88
+ version.save!
89
+ assign_and_reset_version_association(version)
90
+ version
91
+ rescue StandardError => e
92
+ handle_version_errors e, version, :destroy
93
+ end
94
+ end
95
+
96
+ # @api private
97
+ # @param force [boolean] Insert a `Version` even if `@record` has not
98
+ # `changed_notably?`.
99
+ # @param in_after_callback [boolean] True when called from an `after_update`
100
+ # or `after_touch` callback.
101
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
102
+ # @return - The created version object, so that plugins can use it, e.g.
103
+ # paper_trail-association_tracking
104
+ def record_update(force:, in_after_callback:, is_touch:)
105
+ return unless enabled?
106
+
107
+ version = build_version_on_update(
108
+ force: force,
109
+ in_after_callback: in_after_callback,
110
+ is_touch: is_touch
111
+ )
112
+ return unless version
113
+
114
+ begin
115
+ version.save!
116
+ # Because the version object was created using version_class.new instead
117
+ # of versions_assoc.build?, the association cache is unaware. So, we
118
+ # invalidate the `versions` association cache with `reset`.
119
+ versions.reset
120
+ version
121
+ rescue StandardError => e
122
+ handle_version_errors e, version, :update
123
+ end
124
+ end
125
+
126
+ # Invoked via callback when a user attempts to persist a reified
127
+ # `Version`.
128
+ def reset_timestamp_attrs_for_update_if_needed
129
+ return if live?
130
+ @record.send(:timestamp_attributes_for_update_in_model).each do |column|
131
+ @record.send(:"restore_#{column}!")
132
+ end
133
+ end
134
+
135
+ # AR callback.
136
+ # @api private
137
+ def save_version?
138
+ if_condition = @record.paper_trail_options[:if]
139
+ unless_condition = @record.paper_trail_options[:unless]
140
+ (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
141
+ end
142
+
143
+ def source_version
144
+ version
145
+ end
146
+
147
+ # Save, and create a version record regardless of options such as `:on`,
148
+ # `:if`, or `:unless`.
149
+ #
150
+ # `in_after_callback`: Indicates if this method is being called within an
151
+ # `after` callback. Defaults to `false`.
152
+ # `options`: Optional arguments passed to `save`.
153
+ #
154
+ # This is an "update" event. That is, we record the same data we would in
155
+ # the case of a normal AR `update`.
156
+ def save_with_version(in_after_callback: false, **options)
157
+ ::PaperTrail.request(enabled: false) do
158
+ @record.save(**options)
159
+ end
160
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
161
+ end
162
+
163
+ # Like the `update_column` method from `ActiveRecord::Persistence`, but also
164
+ # creates a version to record those changes.
165
+ # @api public
166
+ def update_column(name, value)
167
+ update_columns(name => value)
168
+ end
169
+
170
+ # Like the `update_columns` method from `ActiveRecord::Persistence`, but also
171
+ # creates a version to record those changes.
172
+ # @api public
173
+ def update_columns(attributes)
174
+ # `@record.update_columns` skips dirty-tracking, so we can't just use
175
+ # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
176
+ # We need to build our own hash with the changes that will be made
177
+ # directly to the database.
178
+ changes = {}
179
+ attributes.each do |k, v|
180
+ changes[k] = [@record[k], v]
181
+ end
182
+ @record.update_columns(attributes)
183
+ record_update_columns(changes)
184
+ end
185
+
186
+ # Returns the object (not a Version) as it was at the given timestamp.
187
+ def version_at(timestamp, reify_options = {})
188
+ # Because a version stores how its object looked *before* the change,
189
+ # we need to look for the first version created *after* the timestamp.
190
+ v = versions.subsequent(timestamp, true).first
191
+ return v.reify(reify_options) if v
192
+ @record unless @record.destroyed?
193
+ end
194
+
195
+ # Returns the objects (not Versions) as they were between the given times.
196
+ def versions_between(start_time, end_time)
197
+ versions = send(@record.class.versions_association_name).between(start_time, end_time)
198
+ versions.collect { |version| version_at(version.created_at) }
199
+ end
200
+
201
+ private
202
+
203
+ # @api private
204
+ def assign_and_reset_version_association(version)
205
+ @record.send(:"#{@record.class.version_association_name}=", version)
206
+ @record.send(@record.class.versions_association_name).reset
207
+ end
208
+
209
+ # @api private
210
+ def build_version_on_create(in_after_callback:)
211
+ event = Events::Create.new(@record, in_after_callback)
212
+
213
+ # Merge data from `Event` with data from PT-AT. We no longer use
214
+ # `data_for_create` but PT-AT still does.
215
+ data = event.data.merge!(data_for_create)
216
+
217
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
218
+ @record.class.paper_trail.version_class.new(data)
219
+ end
220
+
221
+ # @api private
222
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
223
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
224
+ return unless force || event.changed_notably?
225
+ data = event.data
226
+
227
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
228
+ # of the `Version`. Without this feature, these two timestamps would
229
+ # differ by a few milliseconds. To some people, it seems a little
230
+ # unnatural to tamper with creation timestamps in this way. But, this
231
+ # feature has existed for a long time, almost a decade now, and some users
232
+ # may rely on it now.
233
+ if @record.respond_to?(:updated_at) &&
234
+ @record.paper_trail_options[:synchronize_version_creation_timestamp] != false
235
+ data[:created_at] = @record.updated_at
236
+ end
237
+
238
+ # Merge data from `Event` with data from PT-AT. We no longer use
239
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
240
+ # instead of `merge`.
241
+ data.merge!(data_for_update)
242
+
243
+ # Using `version_class.new` reduces memory usage compared to
244
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
245
+ # the association cache (see `versions.reset`) and that could cause an
246
+ # additional query in certain applications.
247
+ @record.class.paper_trail.version_class.new(data)
248
+ end
249
+
250
+ # PT-AT extends this method to add its transaction id.
251
+ #
252
+ # @api public
253
+ def data_for_create
254
+ {}
255
+ end
256
+
257
+ # PT-AT extends this method to add its transaction id.
258
+ #
259
+ # @api public
260
+ def data_for_destroy
261
+ {}
262
+ end
263
+
264
+ # PT-AT extends this method to add its transaction id.
265
+ #
266
+ # @api public
267
+ def data_for_update
268
+ {}
269
+ end
270
+
271
+ # PT-AT extends this method to add its transaction id.
272
+ #
273
+ # @api public
274
+ def data_for_update_columns
275
+ {}
276
+ end
277
+
278
+ # Is PT enabled for this particular record?
279
+ # @api private
280
+ def enabled?
281
+ PaperTrail.enabled? &&
282
+ PaperTrail.request.enabled? &&
283
+ PaperTrail.request.enabled_for_model?(@record.class)
284
+ end
285
+
286
+ def log_version_errors(version, action)
287
+ version.logger&.warn(
288
+ "Unable to create version for #{action} of #{@record.class.name}" \
289
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
290
+ )
291
+ end
292
+
293
+ # Centralized handler for version errors
294
+ # @api private
295
+ def handle_version_errors(e, version, action)
296
+ case PaperTrail.config.version_error_behavior
297
+ when :legacy
298
+ # legacy behavior was to raise on create and log on update/delete
299
+ if action == :create
300
+ raise e
301
+ else
302
+ log_version_errors(version, action)
303
+ end
304
+ when :log
305
+ log_version_errors(version, action)
306
+ when :exception
307
+ raise e
308
+ when :silent
309
+ # noop
310
+ end
311
+ end
312
+
313
+ # @api private
314
+ # @return - The created version object, so that plugins can use it, e.g.
315
+ # paper_trail-association_tracking
316
+ def record_update_columns(changes)
317
+ return unless enabled?
318
+ data = Events::Update.new(@record, false, false, changes).data
319
+
320
+ # Merge data from `Event` with data from PT-AT. We no longer use
321
+ # `data_for_update_columns` but PT-AT still does.
322
+ data.merge!(data_for_update_columns)
323
+
324
+ versions_assoc = @record.send(@record.class.versions_association_name)
325
+ version = versions_assoc.new(data)
326
+ begin
327
+ version.save!
328
+ version
329
+ rescue StandardError => e
330
+ handle_version_errors e, version, :update
331
+ end
332
+ end
333
+
334
+ def version
335
+ @record.public_send(@record.class.version_association_name)
336
+ end
337
+
338
+ def versions
339
+ @record.public_send(@record.class.versions_association_name)
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/attribute_serializers/object_attribute"
4
+
5
+ module PaperTrail
6
+ # Given a version record and some options, builds a new model object.
7
+ # @api private
8
+ module Reifier
9
+ class << self
10
+ # See `VersionConcern#reify` for documentation.
11
+ # @api private
12
+ def reify(version, options)
13
+ options = apply_defaults_to(options, version)
14
+ attrs = version.object_deserialized
15
+ model = init_model(attrs, options, version)
16
+ reify_attributes(model, version, attrs)
17
+ model.send :"#{model.class.version_association_name}=", version
18
+ model
19
+ end
20
+
21
+ private
22
+
23
+ # Given a hash of `options` for `.reify`, return a new hash with default
24
+ # values applied.
25
+ # @api private
26
+ def apply_defaults_to(options, version)
27
+ {
28
+ version_at: version.created_at,
29
+ mark_for_destruction: false,
30
+ has_one: false,
31
+ has_many: false,
32
+ belongs_to: false,
33
+ has_and_belongs_to_many: false,
34
+ unversioned_attributes: :nil
35
+ }.merge(options)
36
+ end
37
+
38
+ # Initialize a model object suitable for reifying `version` into. Does
39
+ # not perform reification, merely instantiates the appropriate model
40
+ # class and, if specified by `options[:unversioned_attributes]`, sets
41
+ # unversioned attributes to `nil`.
42
+ #
43
+ # Normally a polymorphic belongs_to relationship allows us to get the
44
+ # object we belong to by calling, in this case, `item`. However this
45
+ # returns nil if `item` has been destroyed, and we need to be able to
46
+ # retrieve destroyed objects.
47
+ #
48
+ # In this situation we constantize the `item_type` to get hold of the
49
+ # class...except when the stored object's attributes include a `type`
50
+ # key. If this is the case, the object we belong to is using single
51
+ # table inheritance (STI) and the `item_type` will be the base class,
52
+ # not the actual subclass. If `type` is present but empty, the class is
53
+ # the base class.
54
+ def init_model(attrs, options, version)
55
+ klass = version_reification_class(version, attrs)
56
+
57
+ # The `dup` option and destroyed version always returns a new object,
58
+ # otherwise we should attempt to load item or to look for the item
59
+ # outside of default scope(s).
60
+ model = if options[:dup] == true || version.event == "destroy"
61
+ klass.new
62
+ else
63
+ version.item || init_model_by_finding_item_id(klass, version) || klass.new
64
+ end
65
+
66
+ if options[:unversioned_attributes] == :nil && !model.new_record?
67
+ init_unversioned_attrs(attrs, model)
68
+ end
69
+
70
+ model
71
+ end
72
+
73
+ # @api private
74
+ def init_model_by_finding_item_id(klass, version)
75
+ klass.unscoped.where(klass.primary_key => version.item_id).first
76
+ end
77
+
78
+ # Look for attributes that exist in `model` and not in this version.
79
+ # These attributes should be set to nil. Modifies `attrs`.
80
+ # @api private
81
+ def init_unversioned_attrs(attrs, model)
82
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
83
+ end
84
+
85
+ # Reify onto `model` an attribute named `k` with value `v` from `version`.
86
+ #
87
+ # `ObjectAttribute#deserialize` will return the mapped enum value and in
88
+ # Rails < 5, the []= uses the integer type caster from the column
89
+ # definition (in general) and thus will turn a (usually) string to 0
90
+ # instead of the correct value.
91
+ #
92
+ # @api private
93
+ def reify_attribute(k, v, model, version)
94
+ if model.has_attribute?(k)
95
+ model[k.to_sym] = v
96
+ elsif model.respond_to?(:"#{k}=")
97
+ model.send(:"#{k}=", v)
98
+ elsif version.logger
99
+ version.logger.warn(
100
+ "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
101
+ )
102
+ end
103
+ end
104
+
105
+ # Reify onto `model` all the attributes of `version`.
106
+ # @api private
107
+ def reify_attributes(model, version, attrs)
108
+ AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
109
+ attrs.each do |k, v|
110
+ reify_attribute(k, v, model, version)
111
+ end
112
+ end
113
+
114
+ # Given a `version`, return the class to reify. This method supports
115
+ # Single Table Inheritance (STI) with custom inheritance columns and
116
+ # custom inheritance column values.
117
+ #
118
+ # For example, imagine a `version` whose `item_type` is "Animal". The
119
+ # `animals` table is an STI table (it has cats and dogs) and it has a
120
+ # custom inheritance column, `species`. If `attrs["species"]` is "Dog",
121
+ # this method returns the constant `Dog`. If `attrs["species"]` is blank,
122
+ # this method returns the constant `Animal`.
123
+ #
124
+ # The values contained in the inheritance columns may be non-camelized
125
+ # strings (e.g. 'dog' instead of 'Dog'). To reify classes in this case
126
+ # we need to call the parents class `sti_class_for` method to retrieve
127
+ # the correct record class.
128
+ #
129
+ # You can see these particular examples in action in
130
+ # `spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`
131
+ def version_reification_class(version, attrs)
132
+ clazz = version.item_type.constantize
133
+ inheritance_column_name = clazz.inheritance_column
134
+ inher_col_value = attrs[inheritance_column_name]
135
+ return clazz if inher_col_value.blank?
136
+
137
+ # Rails 6.1 adds a public method for clients to use to customize STI classes. If that
138
+ # method is not available, fall back to using the private one
139
+ if clazz.public_methods.include?(:sti_class_for)
140
+ return clazz.sti_class_for(inher_col_value)
141
+ end
142
+
143
+ clazz.send(:find_sti_class, inher_col_value)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,163 @@
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 << self
16
+ # Sets any data from the controller that you want PaperTrail to store.
17
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
18
+ #
19
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
20
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
21
+ #
22
+ # @api public
23
+ def controller_info=(value)
24
+ store[:controller_info] = value
25
+ end
26
+
27
+ # Returns the data from the controller that you want PaperTrail to store.
28
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
29
+ #
30
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
31
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
32
+ #
33
+ # @api public
34
+ def controller_info
35
+ store[:controller_info]
36
+ end
37
+
38
+ # Switches PaperTrail off for the given model.
39
+ # @api public
40
+ def disable_model(model_class)
41
+ enabled_for_model(model_class, false)
42
+ end
43
+
44
+ # Switches PaperTrail on for the given model.
45
+ # @api public
46
+ def enable_model(model_class)
47
+ enabled_for_model(model_class, true)
48
+ end
49
+
50
+ # Sets whether PaperTrail is enabled or disabled for the current request.
51
+ # @api public
52
+ def enabled=(value)
53
+ store[:enabled] = value
54
+ end
55
+
56
+ # Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
57
+ # See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
58
+ # @api public
59
+ def enabled?
60
+ !!store[:enabled]
61
+ end
62
+
63
+ # Sets whether PaperTrail is enabled or disabled for this model in the
64
+ # current request.
65
+ # @api public
66
+ def enabled_for_model(model, value)
67
+ store[:"enabled_for_#{model}"] = value
68
+ end
69
+
70
+ # Returns `true` if PaperTrail is enabled for this model in the current
71
+ # request, `false` otherwise.
72
+ # @api public
73
+ def enabled_for_model?(model)
74
+ model.include?(::PaperTrail::Model::InstanceMethods) &&
75
+ !!store.fetch(:"enabled_for_#{model}", true)
76
+ end
77
+
78
+ # Temporarily set `options` and execute a block.
79
+ # @api private
80
+ def with(options)
81
+ return unless block_given?
82
+ validate_public_options(options)
83
+ before = to_h
84
+ merge(options)
85
+ yield
86
+ ensure
87
+ set(before)
88
+ end
89
+
90
+ # Sets who is responsible for any changes that occur during request. You
91
+ # would normally use this in a migration or on the console, when working
92
+ # with models directly.
93
+ #
94
+ # `value` is usually a string, the name of a person, but you can set
95
+ # anything that responds to `to_s`. You can also set a Proc, which will
96
+ # not be evaluated until `whodunnit` is called later, usually right before
97
+ # inserting a `Version` record.
98
+ #
99
+ # @api public
100
+ def whodunnit=(value)
101
+ store[:whodunnit] = value
102
+ end
103
+
104
+ # Returns who is reponsible for any changes that occur during request.
105
+ #
106
+ # @api public
107
+ def whodunnit
108
+ who = store[:whodunnit]
109
+ who.respond_to?(:call) ? who.call : who
110
+ end
111
+
112
+ private
113
+
114
+ # @api private
115
+ def merge(options)
116
+ options.to_h.each do |k, v|
117
+ store[k] = v
118
+ end
119
+ end
120
+
121
+ # @api private
122
+ def set(options)
123
+ store.clear
124
+ merge(options)
125
+ end
126
+
127
+ # Returns a Hash, initializing with default values if necessary.
128
+ # @api private
129
+ def store
130
+ RequestStore.store[:paper_trail] ||= {
131
+ enabled: true
132
+ }
133
+ end
134
+
135
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
136
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
137
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
138
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
139
+ # @api private
140
+ def to_h
141
+ store.deep_dup
142
+ end
143
+
144
+ # Provide a helpful error message if someone has a typo in one of their
145
+ # option keys. We don't validate option values here. That's traditionally
146
+ # been handled with casting (`to_s`, `!!`) in the accessor method.
147
+ # @api private
148
+ def validate_public_options(options)
149
+ options.each do |k, _v|
150
+ case k
151
+ when :controller_info,
152
+ /enabled_for_/,
153
+ :enabled,
154
+ :whodunnit
155
+ next
156
+ else
157
+ raise InvalidOption, "Invalid option: #{k}"
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Serializers
5
+ # An alternate serializer for, e.g. `versions.object`.
6
+ module JSON
7
+ extend self # makes all instance methods become module methods as well
8
+
9
+ def load(string)
10
+ ActiveSupport::JSON.decode string
11
+ end
12
+
13
+ def dump(object)
14
+ ActiveSupport::JSON.encode object
15
+ end
16
+
17
+ # Returns a SQL LIKE condition to be used to match the given field and
18
+ # value in the serialized object.
19
+ def where_object_condition(arel_field, field, value)
20
+ # Convert to JSON to handle strings and nulls correctly.
21
+ json_value = value.to_json
22
+
23
+ # If the value is a number, we need to ensure that we find the next
24
+ # character too, which is either `,` or `}`, to ensure that searching
25
+ # for the value 12 doesn't yield false positives when the value is
26
+ # 123.
27
+ if value.is_a? Numeric
28
+ arel_field.matches("%\"#{field}\":#{json_value},%").
29
+ or(arel_field.matches("%\"#{field}\":#{json_value}}%"))
30
+ else
31
+ arel_field.matches("%\"#{field}\":#{json_value}%")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end