A new API for request variables

This commit is contained in:
Jared Beck 2018-02-01 12:04:50 -05:00
parent 0f3d8c8bcc
commit c659b1faf0
20 changed files with 583 additions and 334 deletions

View File

@ -22,8 +22,17 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
- Removed `warn_about_not_setting_whodunnit` controller method. Please remove
callbacks like `skip_after_action :warn_about_not_setting_whodunnit`.
### Deprecated
- [#1033](https://github.com/airblade/paper_trail/pull/1033) - Request variables
are now set using eg. `PaperTrail.request.whodunnit=` and the old way,
`PaperTrail.whodunnit=` is deprecated.
### Added
- [#1033](https://github.com/airblade/paper_trail/pull/1033) -
Set request variables temporarily using a block, eg.
`PaperTrail.request(whodunnit: 'Jared') do .. end`
- [#1037](https://github.com/airblade/paper_trail/pull/1037) Add `paper_trail.update_columns`
- [#961](https://github.com/airblade/paper_trail/issues/961) - Instead of
crashing when misconfigured Custom Version Classes are used, an error will be

118
README.md
View File

@ -126,13 +126,10 @@ Once you have a version, you can find out what happened:
```ruby
v = widget.versions.last
v.event # 'update', 'create', or 'destroy'
v.created_at # When the `event` occurred
v.whodunnit # If the update was via a controller and the
# controller has a current_user method, returns the
# id of the current user as a string.
widget = v.reify # The widget as it was before the update
# (nil for a create event)
v.event # 'update', 'create', 'destroy'. See also: Custom Event Names
v.created_at
v.whodunnit # ID of `current_user`. Requires `set_paper_trail_whodunnit` callback.
widget = v.reify # The widget as it was before the update (nil for a create event)
```
PaperTrail stores the pre-change version of the model, unlike some other
@ -204,17 +201,12 @@ widget.paper_trail.next_version
# version)
widget.paper_trail.touch_with_version
# Turn PaperTrail off for all widgets.
Widget.paper_trail.disable
# Turn PaperTrail on for all widgets.
Widget.paper_trail.enable
# Enable/disable PaperTrail, for Widget, for the current request (not all threads)
PaperTrail.request.disable_model(Widget)
PaperTrail.request.enable_model(Widget)
# Is PaperTrail enabled for Widget, the class?
Widget.paper_trail.enabled?
# Is PaperTrail enabled for widget, the instance?
widget.paper_trail.enabled_for_model?
PaperTrail.request.enabled_for_model?(Widget)
```
And a `PaperTrail::Version` instance (which is just an ordinary ActiveRecord
@ -298,11 +290,12 @@ end
other callbacks in your model, their order relative to those installed by
PaperTrail may matter, so be aware of any potential interactions.
#### Custom Event Name
You may also have the `PaperTrail::Version` model save a custom string in its
`event` field instead of the typical `create`, `update`, `destroy`. PaperTrail
supplies a custom accessor method called `paper_trail_event`, which it will
attempt to use to fill the `event` field before falling back on one of the
default events.
adds an `attr_accessor` to your model named `paper_trail_event`, and will insert
it, if present, in the `event` column.
```ruby
a = Article.create
@ -480,7 +473,7 @@ Add a `paper_trail_enabled_for_controller` method to your controller.
```ruby
class ApplicationController < ActionController::Base
def paper_trail_enabled_for_controller
request.user_agent != 'Disable User-Agent'
super && request.user_agent != 'Disable User-Agent'
end
end
```
@ -488,10 +481,13 @@ end
#### Per Class
```ruby
Widget.paper_trail.disable
Widget.paper_trail.enable
PaperTrail.request.enable_model(Widget)
PaperTrail.request.disable_model(Widget)
```
This setting, as with all `PaperTrail.request` settings, affects only the
current request, not all threads.
#### Per Method
You can call a method without creating a new version using `without_versioning`.
@ -509,7 +505,7 @@ Or a block:
end
```
PaperTrail is disabled for the whole model
During `without_versioning`, PaperTrail is disabled for the whole model
(e.g. `Widget`), not just for the instance (e.g. `@widget`).
### 2.e. Limiting the Number of Versions Created
@ -703,34 +699,44 @@ PaperTrail::Version.delete_all ['created_at < ?', 1.week.ago]
### 4.a. Finding Out Who Was Responsible For A Change
Set `PaperTrail.whodunnit=`, and that value will be stored in the version's
`whodunnit` column.
Set `PaperTrail.request.whodunnit=`, and that value will be stored in the
version's `whodunnit` column.
```ruby
PaperTrail.whodunnit = 'Andy Stewart'
PaperTrail.request.whodunnit = 'Andy Stewart'
widget.update_attributes name: 'Wibble'
widget.versions.last.whodunnit # Andy Stewart
widget.versions.last.whodunnit # Andy Stewart
```
`whodunnit` also accepts a block, a convenient way to temporarily set the value.
#### Setting `whodunnit` to a `Proc`
`whodunnit=` also accepts a `Proc`, in the rare case that lazy evaluation is
required.
```ruby
PaperTrail.whodunnit('Dorian Marié') do
widget.update_attributes name: 'Wibble'
end
```
`whodunnit` also accepts a `Proc`.
```ruby
PaperTrail.whodunnit = proc do
PaperTrail.request.whodunnit = proc do
caller.first{ |c| c.starts_with? Rails.root.to_s }
end
```
Because lazy evaluation can be hard to troubleshoot, this is not
recommended for common use.
#### Setting `whodunnit` Temporarily
To set whodunnit temporarily, for the duration of a block, use
`PaperTrail.request`:
```ruby
PaperTrail.request(whodunnit: 'Dorian Marié') do
widget.update_attributes name: 'Wibble'
end
```
#### Setting `whodunnit` with a controller callback
If your controller has a `current_user` method, PaperTrail provides a
`before_action` that will assign `current_user.id` to `PaperTrail.whodunnit`.
You can add this `before_action` to your `ApplicationController`.
callback that will assign `current_user.id` to `whodunnit`.
```ruby
class ApplicationController
@ -752,25 +758,13 @@ end
See also: [Setting whodunnit in the rails console][33]
Sometimes you want to define who is responsible for a change in a small scope
without overwriting value of `PaperTrail.whodunnit`. It is possible to define
the `whodunnit` value for an operation inside a block like this:
#### Terminator and Originator
```ruby
PaperTrail.whodunnit = 'Andy Stewart'
widget.paper_trail.whodunnit('Lucas Souza') do
widget.update_attributes name: 'Wibble'
end
widget.versions.last.whodunnit # Lucas Souza
widget.update_attributes name: 'Clair'
widget.versions.last.whodunnit # Andy Stewart
```
A version's `whodunnit` records who changed the object causing the `version` to
be stored. Because a version stores the object as it looked before the change
(see the table above), `whodunnit` returns who stopped the object looking like
this -- not who made it look like this. Hence `whodunnit` is aliased as
`terminator`.
A version's `whodunnit` column tells us who changed the object, causing the
`version` to be stored. Because a version stores the object as it looked before
the change (see the table above), `whodunnit` tells us who *stopped* the object
looking like this -- not who made it look like this. Hence `whodunnit` is
aliased as `terminator`.
To find out who made a version's object look that way, use
`version.paper_trail_originator`. And to find out who made a "live" object look
@ -778,10 +772,10 @@ like it does, call `paper_trail_originator` on the object.
```ruby
widget = Widget.find 153 # assume widget has 0 versions
PaperTrail.whodunnit = 'Alice'
PaperTrail.request.whodunnit = 'Alice'
widget.update_attributes name: 'Yankee'
widget.paper_trail.originator # 'Alice'
PaperTrail.whodunnit = 'Bob'
PaperTrail.request.whodunnit = 'Bob'
widget.update_attributes name: 'Zulu'
widget.paper_trail.originator # 'Bob'
first_version, last_version = widget.versions.first, widget.versions.last
@ -1117,7 +1111,7 @@ module PaperTrail
end
```
This unsupported workaround has been tested with protected_attributes 1.0.9 /
This *unsupported workaround* has been tested with protected_attributes 1.0.9 /
rails 4.2.8 / paper_trail 7.0.3.
## 6. Extensibility
@ -1392,7 +1386,7 @@ describe 'RSpec test group' do
end
```
The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each
The helper will also reset `whodunnit` to `nil` before each
test to help prevent data spillover between tests. If you are using PaperTrail
with Rails, the helper will automatically set the `PaperTrail.controller_info`
value to `{}` as well, again, to help prevent data spillover between tests.
@ -1489,7 +1483,7 @@ Given /I want versioning on my model/ do
end
```
The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each
The helper will also reset the `whodunnit` value to `nil` before each
test to help prevent data spillover between tests. If you are using PaperTrail
with Rails, the helper will automatically set the `PaperTrail.controller_info`
value to `{}` as well, again, to help prevent data spillover between tests.

View File

@ -20,6 +20,7 @@ require "paper_trail/config"
require "paper_trail/has_paper_trail"
require "paper_trail/record_history"
require "paper_trail/reifier"
require "paper_trail/request"
require "paper_trail/version_association_concern"
require "paper_trail/version_concern"
require "paper_trail/version_number"
@ -46,124 +47,158 @@ module PaperTrail
class << self
# @api private
def clear_transaction_id
self.transaction_id = nil
::ActiveSupport::Deprecation.warn(
"PaperTrail.clear_transaction_id is deprecated, " \
"use PaperTrail.request.clear_transaction_id",
caller(1)
)
request.clear_transaction_id
end
# Switches PaperTrail on or off.
# Switches PaperTrail on or off, for all threads.
# @api public
def enabled=(value)
PaperTrail.config.enabled = value
end
# Returns `true` if PaperTrail is on, `false` otherwise.
# PaperTrail is enabled by default.
# Returns `true` if PaperTrail is on, `false` otherwise. This is the
# on/off switch that affects all threads. Enabled by default.
# @api public
def enabled?
!!PaperTrail.config.enabled
end
# Sets whether PaperTrail is enabled or disabled for the current request.
# @api public
# @deprecated
def enabled_for_controller=(value)
paper_trail_store[:request_enabled_for_controller] = value
::ActiveSupport::Deprecation.warn(
"PaperTrail.enabled_for_controller= is deprecated, " \
"use PaperTrail.request.enabled_for_controller=",
caller(1)
)
request.enabled_for_controller = value
end
# Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
#
# See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
# @api public
# @deprecated
def enabled_for_controller?
!!paper_trail_store[:request_enabled_for_controller]
::ActiveSupport::Deprecation.warn(
"PaperTrail.enabled_for_controller? is deprecated, " \
"use PaperTrail.request.enabled_for_controller?",
caller(1)
)
request.enabled_for_controller?
end
# Sets whether PaperTrail is enabled or disabled for this model in the
# current request.
# @api public
# @deprecated
def enabled_for_model(model, value)
paper_trail_store[:"enabled_for_#{model}"] = value
::ActiveSupport::Deprecation.warn(
"PaperTrail.enabled_for_model is deprecated, " \
"use PaperTrail.request.enabled_for_model",
caller(1)
)
request.enabled_for_model(model, value)
end
# Returns `true` if PaperTrail is enabled for this model in the current
# request, `false` otherwise.
# @api public
# @deprecated
def enabled_for_model?(model)
!!paper_trail_store.fetch(:"enabled_for_#{model}", true)
::ActiveSupport::Deprecation.warn(
"PaperTrail.enabled_for_model? is deprecated, " \
"use PaperTrail.request.enabled_for_model?",
caller(1)
)
request.enabled_for_model?(model)
end
# Returns a `::Gem::Version`, convenient for comparisons. This is
# Returns PaperTrail's `::Gem::Version`, convenient for comparisons. This is
# recommended over `::PaperTrail::VERSION::STRING`.
# @api public
def gem_version
::Gem::Version.new(VERSION::STRING)
end
# Set variables for the current request, eg. whodunnit.
#
# All request-level variables are now managed here, as of PT 9. Having the
# word "request" right there in your application code will remind you that
# these variables only affect the current request, not all threads.
#
# Given a block, temporarily sets the given `options` and execute the block.
#
# Without a block, this currently just returns `PaperTrail::Request`.
# However, please do not use `PaperTrail::Request` directly. Currently,
# `Request` is a `Module`, but in the future it is quite possible we may
# make it a `Class`. If we make such a choice, we will not provide any
# warning and will not treat it as a breaking change. You've been warned :)
#
# @api public
def request(options = nil, &block)
if options.nil? && !block_given?
Request
else
Request.with(options, &block)
nil
end
end
# Set the field which records when a version was created.
# @api public
def timestamp_field=(_field_name)
raise(E_TIMESTAMP_FIELD_CONFIG)
end
# Sets who is responsible for any changes that occur. You would normally use
# this in a migration or on the console, when working with models directly.
# In a controller it is set automatically to the `current_user`.
# @api public
# @deprecated
def whodunnit=(value)
paper_trail_store[:whodunnit] = value
::ActiveSupport::Deprecation.warn(
"PaperTrail.whodunnit= is deprecated, use PaperTrail.request.whodunnit=",
caller(1)
)
request.whodunnit = value
end
# If nothing passed, returns who is reponsible for any changes that occur.
#
# PaperTrail.whodunnit = "someone"
# PaperTrail.whodunnit # => "someone"
#
# If value and block passed, set this value as whodunnit for the duration of the block
#
# PaperTrail.whodunnit("me") do
# puts PaperTrail.whodunnit # => "me"
# end
#
# @api public
def whodunnit(value = nil)
if value
raise ArgumentError, "no block given" unless block_given?
previous_whodunnit = paper_trail_store[:whodunnit]
paper_trail_store[:whodunnit] = value
begin
yield
ensure
paper_trail_store[:whodunnit] = previous_whodunnit
end
elsif paper_trail_store[:whodunnit].respond_to?(:call)
paper_trail_store[:whodunnit].call
# @deprecated
def whodunnit(value = nil, &block)
if value.nil?
::ActiveSupport::Deprecation.warn(
"PaperTrail.whodunnit is deprecated, use PaperTrail.request.whodunnit",
caller(1)
)
request.whodunnit
elsif block_given?
::ActiveSupport::Deprecation.warn(
"Passing a block to PaperTrail.whodunnit is deprecated, " \
'use PaperTrail.request(whodunnit: "John") do .. end',
caller(1)
)
request(whodunnit: value, &block)
else
paper_trail_store[:whodunnit]
raise ArgumentError, "Invalid arguments"
end
end
# Sets any information from the controller that you want PaperTrail to
# store. By default this is set automatically by a before filter.
# @api public
# @deprecated
def controller_info=(value)
paper_trail_store[:controller_info] = value
::ActiveSupport::Deprecation.warn(
"PaperTrail.controller_info= is deprecated, use PaperTrail.request.controller_info=",
caller(1)
)
request.controller_info = value
end
# Returns any information from the controller that you want
# PaperTrail to store.
#
# See `PaperTrail::Rails::Controller#info_for_paper_trail`.
# @api public
# @deprecated
def controller_info
paper_trail_store[:controller_info]
::ActiveSupport::Deprecation.warn(
"PaperTrail.controller_info is deprecated, use PaperTrail.request.controller_info",
caller(1)
)
request.controller_info
end
# Getter and Setter for PaperTrail Serializer
# Set the PaperTrail serializer. This setting affects all threads.
# @api public
def serializer=(value)
PaperTrail.config.serializer = value
end
# Get the PaperTrail serializer used by all threads.
# @api public
def serializer
PaperTrail.config.serializer
@ -174,24 +209,26 @@ module PaperTrail
::ActiveRecord::Base.connection.open_transactions.positive?
end
# @api public
# @deprecated
def transaction_id
paper_trail_store[:transaction_id]
::ActiveSupport::Deprecation.warn(
"PaperTrail.transaction_id is deprecated without replacement.",
caller(1)
)
request.transaction_id
end
# @api public
# @deprecated
def transaction_id=(id)
paper_trail_store[:transaction_id] = id
::ActiveSupport::Deprecation.warn(
"PaperTrail.transaction_id= is deprecated without replacement.",
caller(1)
)
request.transaction_id = id
end
# Thread-safe hash to hold PaperTrail's data. Initializing with needed
# default values.
# @api private
def paper_trail_store
RequestStore.store[:paper_trail] ||= { request_enabled_for_controller: true }
end
# Returns PaperTrail's configuration object.
# Returns PaperTrail's global configuration object, a singleton. These
# settings affect all threads.
# @api private
def config
@config ||= PaperTrail::Config.instance

View File

@ -3,9 +3,9 @@
# before hook for Cucumber
Before do
PaperTrail.enabled = false
PaperTrail.enabled_for_controller = true
PaperTrail.whodunnit = nil
PaperTrail.controller_info = {} if defined? Rails
PaperTrail.request.enabled_for_controller = true
PaperTrail.request.whodunnit = nil
PaperTrail.request.controller_info = {} if defined?(::Rails)
end
module PaperTrail

View File

@ -63,21 +63,21 @@ module PaperTrail
# Tells PaperTrail whether versions should be saved in the current
# request.
def set_paper_trail_enabled_for_controller
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
::PaperTrail.request.enabled_for_controller = paper_trail_enabled_for_controller
end
# Tells PaperTrail who is responsible for any changes that occur.
def set_paper_trail_whodunnit
if ::PaperTrail.enabled_for_controller?
::PaperTrail.whodunnit = user_for_paper_trail
if ::PaperTrail.request.enabled_for_controller?
::PaperTrail.request.whodunnit = user_for_paper_trail
end
end
# Tells PaperTrail any information from the controller you want to store
# alongside any changes that occur.
def set_paper_trail_controller_info
if ::PaperTrail.enabled_for_controller?
::PaperTrail.controller_info = info_for_paper_trail
if ::PaperTrail.request.enabled_for_controller?
::PaperTrail.request.controller_info = info_for_paper_trail
end
end
end

View File

@ -10,9 +10,9 @@ RSpec.configure do |config|
config.before(:each) do
::PaperTrail.enabled = false
::PaperTrail.enabled_for_controller = true
::PaperTrail.whodunnit = nil
::PaperTrail.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
::PaperTrail.request.enabled_for_controller = true
::PaperTrail.request.whodunnit = nil
::PaperTrail.request.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
end
config.before(:each, versioning: true) do

View File

@ -4,12 +4,32 @@ module PaperTrail
# Configures an ActiveRecord model, mostly at application boot time, but also
# sometimes mid-request, with methods like enable/disable.
class ModelConfig
DPR_DISABLE = <<-STR.squish.freeze
MyModel.paper_trail.disable is deprecated, use
PaperTrail.request.disable_model(MyModel). This new API makes it clear
that only the current request is affected, not all threads. Also, all
other request-variables now go through the same `request` method, so this
new API is more consistent.
STR
DPR_ENABLE = <<-STR.squish.freeze
MyModel.paper_trail.enable is deprecated, use
PaperTrail.request.enable_model(MyModel). This new API makes it clear
that only the current request is affected, not all threads. Also, all
other request-variables now go through the same `request` method, so this
new API is more consistent.
STR
DPR_ENABLED = <<-STR.squish.freeze
MyModel.paper_trail.enabled? is deprecated, use
PaperTrail.request.enabled_for_model?(MyModel). This new API makes it clear
that this is a setting specific to the current request, not all threads.
Also, all other request-variables now go through the same `request`
method, so this new API is more consistent.
STR
E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
belongs_to_required_by_default. Use on_destroy(:before)
or disable belongs_to_required_by_default.
STR
E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze
An application model (%s) has been configured to use PaperTrail (via
`has_paper_trail`), but the version model it has been told to use (%s) is
@ -24,19 +44,22 @@ module PaperTrail
@model_class = model_class
end
# Switches PaperTrail off for this class.
# @deprecated
def disable
::PaperTrail.enabled_for_model(@model_class, false)
::ActiveSupport::Deprecation.warn(DPR_DISABLE, caller(1))
::PaperTrail.request.disable_model(@model_class)
end
# Switches PaperTrail on for this class.
# @deprecated
def enable
::PaperTrail.enabled_for_model(@model_class, true)
::ActiveSupport::Deprecation.warn(DPR_ENABLE, caller(1))
::PaperTrail.request.enable_model(@model_class)
end
# @deprecated
def enabled?
return false unless @model_class.include?(::PaperTrail::Model::InstanceMethods)
::PaperTrail.enabled_for_model?(@model_class)
::ActiveSupport::Deprecation.warn(DPR_ENABLED, caller(1))
::PaperTrail.request.enabled_for_model?(@model_class)
end
# Adds a callback that records a version after a "create" event.
@ -195,8 +218,8 @@ module PaperTrail
# Reset the transaction id when the transaction is closed.
def setup_transaction_callbacks
@model_class.after_commit { PaperTrail.clear_transaction_id }
@model_class.after_rollback { PaperTrail.clear_transaction_id }
@model_class.after_commit { PaperTrail.request.clear_transaction_id }
@model_class.after_rollback { PaperTrail.request.clear_transaction_id }
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
end

View File

@ -3,6 +3,10 @@
module PaperTrail
# Represents the "paper trail" for a single record.
class RecordTrail
DPR_WHODUNNIT = <<-STR.squish.freeze
my_model_instance.paper_trail.whodunnit('John') is deprecated,
please use PaperTrail.request(whodunnit: 'John')
STR
RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
def initialize(record)
@ -98,12 +102,24 @@ module PaperTrail
notable_changes.to_hash
end
# Is PT enabled for this particular record?
# @api private
def enabled?
PaperTrail.enabled? && PaperTrail.enabled_for_controller? && enabled_for_model?
PaperTrail.enabled? &&
PaperTrail.request.enabled_for_controller? &&
PaperTrail.request.enabled_for_model?(@record.class)
end
# Not sure why, but this method was mentioned in the README in the past,
# so we need to deprecate it properly.
# @deprecated
def enabled_for_model?
@record.class.paper_trail.enabled?
::ActiveSupport::Deprecation.warn(
"MyModel#paper_trail.enabled_for_model? is deprecated, use " \
"PaperTrail.request.enabled_for_model?(MyModel) instead.",
caller(1)
)
PaperTrail.request.enabled_for_model?(@record.class)
end
# An attributed is "ignored" if it is listed in the `:ignore` option
@ -130,7 +146,7 @@ module PaperTrail
# Updates `data` from `controller_info`.
# @api private
def merge_metadata_from_controller_into(data)
data.merge(PaperTrail.controller_info || {})
data.merge(PaperTrail.request.controller_info || {})
end
# Updates `data` from the model's `meta` option.
@ -220,7 +236,7 @@ module PaperTrail
def data_for_create
data = {
event: @record.paper_trail_event || "create",
whodunnit: PaperTrail.whodunnit
whodunnit: PaperTrail.request.whodunnit
}
if @record.respond_to?(:updated_at)
data[:created_at] = @record.updated_at
@ -254,7 +270,7 @@ module PaperTrail
item_type: @record.class.base_class.name,
event: @record.paper_trail_event || "destroy",
object: recordable_object,
whodunnit: PaperTrail.whodunnit
whodunnit: PaperTrail.request.whodunnit
}
add_transaction_id_to(data)
merge_metadata_into(data)
@ -290,7 +306,7 @@ module PaperTrail
data = {
event: @record.paper_trail_event || "update",
object: recordable_object,
whodunnit: PaperTrail.whodunnit
whodunnit: PaperTrail.request.whodunnit
}
if @record.respond_to?(:updated_at)
data[:created_at] = @record.updated_at
@ -321,7 +337,7 @@ module PaperTrail
data = {
event: @record.paper_trail_event || "update",
object: recordable_object,
whodunnit: PaperTrail.whodunnit
whodunnit: PaperTrail.request.whodunnit
}
if record_object_changes?
data[:object_changes] = recordable_object_changes(changes)
@ -473,8 +489,8 @@ module PaperTrail
# Executes the given method or block without creating a new version.
def without_versioning(method = nil)
paper_trail_was_enabled = enabled_for_model?
@record.class.paper_trail.disable
paper_trail_was_enabled = PaperTrail.request.enabled_for_model?(@record.class)
PaperTrail.request.disable_model(@record.class)
if method
if respond_to?(method)
public_send(method)
@ -485,25 +501,23 @@ module PaperTrail
yield @record
end
ensure
@record.class.paper_trail.enable if paper_trail_was_enabled
PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
end
# Temporarily overwrites the value of whodunnit and then executes the
# provided block.
# @deprecated
def whodunnit(value)
raise ArgumentError, "expected to receive a block" unless block_given?
current_whodunnit = PaperTrail.whodunnit
PaperTrail.whodunnit = value
yield @record
ensure
PaperTrail.whodunnit = current_whodunnit
::ActiveSupport::Deprecation.warn(DPR_WHODUNNIT, caller(1))
::PaperTrail.request(whodunnit: value) do
yield @record
end
end
private
def add_transaction_id_to(data)
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
data[:transaction_id] = PaperTrail.transaction_id
data[:transaction_id] = PaperTrail.request.transaction_id
end
# @api private
@ -568,10 +582,10 @@ module PaperTrail
if assoc.options[:polymorphic]
associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
if associated_record && associated_record.class.paper_trail.enabled?
if associated_record && PaperTrail.request.enabled_for_model?(associated_record.class)
assoc_version_args[:foreign_key_id] = associated_record.id
end
elsif assoc.klass.paper_trail.enabled?
elsif PaperTrail.request.enabled_for_model?(assoc.klass)
assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
end
@ -584,7 +598,7 @@ module PaperTrail
# @api private
def save_habtm_association?(assoc)
@record.class.paper_trail_save_join_tables.include?(assoc.name) ||
assoc.klass.paper_trail.enabled?
PaperTrail.request.enabled_for_model?(assoc.klass)
end
# Returns true if `save` will cause `record_update`
@ -596,8 +610,8 @@ module PaperTrail
def update_transaction_id(version)
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
PaperTrail.transaction_id = version.id
if PaperTrail.transaction? && PaperTrail.request.transaction_id.nil?
PaperTrail.request.transaction_id = version.id
version.transaction_id = version.id
version.save
end

View File

@ -56,7 +56,7 @@ module PaperTrail
# @api private
def each_enabled_association(associations)
associations.each do |assoc|
next unless assoc.klass.paper_trail.enabled?
next unless ::PaperTrail.request.enabled_for_model?(assoc.klass)
yield assoc
end
end
@ -194,7 +194,7 @@ module PaperTrail
# @api private
def reify_habtm_associations(transaction_id, model, options = {})
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
pt_enabled = assoc.klass.paper_trail.enabled?
pt_enabled = ::PaperTrail.request.enabled_for_model?(assoc.klass)
next unless model.class.paper_trail_save_join_tables.include?(assoc.name) || pt_enabled
Reifiers::HasAndBelongsToMany.reify(pt_enabled, assoc, model, options, transaction_id)
end

184
lib/paper_trail/request.rb Normal file
View File

@ -0,0 +1,184 @@
# frozen_string_literal: true
require "request_store"
module PaperTrail
# Manages variables that affect the current HTTP request, such as `whodunnit`.
#
# Please do not use `PaperTrail::Request` directly, use `PaperTrail.request`.
# Currently, `Request` is a `Module`, but in the future it is quite possible
# we may make it a `Class`. If we make such a choice, we will not provide any
# warning and will not treat it as a breaking change. You've been warned :)
#
# @api private
module Request
class InvalidOption < RuntimeError
end
class << self
# @api private
def clear_transaction_id
self.transaction_id = nil
end
# Sets any data from the controller that you want PaperTrail to store.
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
#
# PaperTrail.request.controller_info = { ip: request_user_ip }
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
#
# @api public
def controller_info=(value)
store[:controller_info] = value
end
# Returns the data from the controller that you want PaperTrail to store.
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
#
# PaperTrail.request.controller_info = { ip: request_user_ip }
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
#
# @api public
def controller_info
store[:controller_info]
end
# Switches PaperTrail off for the given model.
# @api public
def disable_model(model_class)
enabled_for_model(model_class, false)
end
# Switches PaperTrail on for the given model.
# @api public
def enable_model(model_class)
enabled_for_model(model_class, true)
end
# Sets whether PaperTrail is enabled or disabled for the current request.
# @api public
def enabled_for_controller=(value)
store[:request_enabled_for_controller] = value
end
# Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
#
# See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
# @api public
def enabled_for_controller?
!!store[:request_enabled_for_controller]
end
# Sets whether PaperTrail is enabled or disabled for this model in the
# current request.
# @api public
def enabled_for_model(model, value)
store[:"enabled_for_#{model}"] = value
end
# Returns `true` if PaperTrail is enabled for this model in the current
# request, `false` otherwise.
# @api public
def enabled_for_model?(model)
model.include?(::PaperTrail::Model::InstanceMethods) &&
!!store.fetch(:"enabled_for_#{model}", true)
end
# @api private
def merge(options)
options.to_h.each do |k, v|
store[k] = v
end
end
# @api private
def set(options)
store.clear
merge(options)
end
# Returns a deep copy of the internal hash from our RequestStore. Keys are
# all symbols. Values are mostly primitives, but whodunnit can be a Proc.
# We cannot use Marshal.dump here because it doesn't support Proc. It is
# unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
# @api private
def to_h
store.deep_dup
end
# @api private
def transaction_id
store[:transaction_id]
end
# @api private
def transaction_id=(id)
store[:transaction_id] = id
end
# Temporarily set `options` and execute a block.
# @api private
def with(options)
return unless block_given?
validate_public_options(options)
before = to_h
merge(options)
yield
ensure
set(before)
end
# Sets who is responsible for any changes that occur during request. You
# would normally use this in a migration or on the console, when working
# with models directly.
#
# `value` is usually a string, the name of a person, but you can set
# anything that responds to `to_s`. You can also set a Proc, which will
# not be evaluated until `whodunnit` is called later, usually right before
# inserting a `Version` record.
#
# @api public
def whodunnit=(value)
store[:whodunnit] = value
end
# Returns who is reponsible for any changes that occur during request.
#
# @api public
def whodunnit
who = store[:whodunnit]
who.respond_to?(:call) ? who.call : who
end
private
# Returns a Hash, initializing with default values if necessary.
# @api private
def store
RequestStore.store[:paper_trail] ||= {
request_enabled_for_controller: true
}
end
# Provide a helpful error message if someone has a typo in one of their
# option keys. We don't validate option values here. That's traditionally
# been handled with casting (`to_s`, `!!`) in the accessor method.
# @api private
def validate_public_options(options)
options.each do |k, _v|
case k
when :controller_info,
/enabled_for_/,
:request_enabled_for_controller,
:whodunnit
next
when :transaction_id
raise InvalidOption, "Cannot set private option: #{k}"
else
raise InvalidOption, "Invalid option: #{k}"
end
end
end
end
end
end

View File

@ -9,7 +9,7 @@ module PaperTrail
module VERSION
MAJOR = 8
MINOR = 1
TINY = 1
TINY = 2
PRE = nil
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".").freeze

View File

@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe ArticlesController, type: :controller do
describe "PaperTrail.enabled_for_controller?" do
describe "PaperTrail.request.enabled_for_controller?" do
context "PaperTrail.enabled? == true" do
before { PaperTrail.enabled = true }
@ -11,7 +11,7 @@ RSpec.describe ArticlesController, type: :controller do
assert PaperTrail.enabled?
post :create, params_wrapper(article: { title: "Doh", content: FFaker::Lorem.sentence })
expect(assigns(:article)).not_to be_nil
assert PaperTrail.enabled_for_controller?
assert PaperTrail.request.enabled_for_controller?
assert_equal 1, assigns(:article).versions.length
end
@ -22,7 +22,7 @@ RSpec.describe ArticlesController, type: :controller do
it "returns false" do
assert !PaperTrail.enabled?
post :create, params_wrapper(article: { title: "Doh", content: FFaker::Lorem.sentence })
assert !PaperTrail.enabled_for_controller?
assert !PaperTrail.request.enabled_for_controller?
assert_equal 0, assigns(:article).versions.length
end
end

View File

@ -20,11 +20,11 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
it "controller metadata methods should get evaluated" do
request.env["HTTP_USER_AGENT"] = "User-Agent"
post :create, params_wrapper(widget: { name: "Flugel" })
expect(PaperTrail.enabled_for_controller?).to(eq(true))
expect(PaperTrail.whodunnit).to(eq(153))
expect(PaperTrail.controller_info.present?).to(eq(true))
expect(PaperTrail.controller_info.keys.include?(:ip)).to(eq(true))
expect(PaperTrail.controller_info.keys.include?(:user_agent)).to(eq(true))
expect(PaperTrail.request.enabled_for_controller?).to(eq(true))
expect(PaperTrail.request.whodunnit).to(eq(153))
expect(PaperTrail.request.controller_info.present?).to(eq(true))
expect(PaperTrail.request.controller_info.keys.include?(:ip)).to(eq(true))
expect(PaperTrail.request.controller_info.keys.include?(:user_agent)).to(eq(true))
end
end
@ -33,9 +33,9 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
request.env["HTTP_USER_AGENT"] = "Disable User-Agent"
post :create, params_wrapper(widget: { name: "Flugel" })
expect(assigns(:widget).versions.length).to(eq(0))
expect(PaperTrail).not_to be_enabled_for_controller
expect(PaperTrail.whodunnit).to be_nil
expect(PaperTrail.controller_info).to eq({})
expect(PaperTrail.request).not_to be_enabled_for_controller
expect(PaperTrail.request.whodunnit).to be_nil
expect(PaperTrail.request.controller_info).to eq({})
end
end
end

View File

@ -1,5 +1,17 @@
# frozen_string_literal: true
class Elephant < Animal
paper_trail.disable
end
# Nice! We used to have `paper_trail.disable` inside the class, which was really
# misleading because it looked like a permanent, global setting. It's so much
# more obvious now that we are disabling the model for this request only. Of
# course, we run the PT unit tests in a single thread, and I think this setting
# will affect multiple unit tests, but in a normal application, this new API is
# a huge improvement.
#
# TODO: If this call to `disable_model` were moved to the unit tests, this file
# would be more like normal application code. It'd be pretty strange for someone
# to do this in app code, especially now that it is obvious that it only affects
# the current request.
PaperTrail.request.disable_model(Elephant)

View File

@ -178,21 +178,21 @@ RSpec.describe Widget, type: :model do
let(:new_name) { FFaker::Name.name }
before do
PaperTrail.whodunnit = orig_name
PaperTrail.request.whodunnit = orig_name
end
it "returns the originator for the model at a given state" do
expect(widget.paper_trail).to be_live
expect(widget.paper_trail.originator).to eq(orig_name)
widget.paper_trail.whodunnit(new_name) { |w|
w.update_attributes(name: "Elizabeth")
::PaperTrail.request(whodunnit: new_name) {
widget.update_attributes(name: "Elizabeth")
}
expect(widget.paper_trail.originator).to eq(new_name)
end
it "returns the appropriate originator" do
widget.update_attributes(name: "Andy")
PaperTrail.whodunnit = new_name
PaperTrail.request.whodunnit = new_name
widget.update_attributes(name: "Elizabeth")
reified_widget = widget.versions[1].reify
expect(reified_widget.paper_trail.originator).to eq(orig_name)
@ -201,7 +201,7 @@ RSpec.describe Widget, type: :model do
it "can create a new instance with options[:dup]" do
widget.update_attributes(name: "Andy")
PaperTrail.whodunnit = new_name
PaperTrail.request.whodunnit = new_name
widget.update_attributes(name: "Elizabeth")
reified_widget = widget.versions[1].reify(dup: true)
expect(reified_widget.paper_trail.originator).to eq(orig_name)
@ -221,47 +221,12 @@ RSpec.describe Widget, type: :model do
end
describe "#whodunnit", versioning: true do
context "no block given" do
it "raises an error" do
expect {
widget.paper_trail.whodunnit("Ben")
}.to raise_error(ArgumentError, "expected to receive a block")
end
end
context "block given" do
let(:orig_name) { FFaker::Name.name }
let(:new_name) { FFaker::Name.name }
before do
PaperTrail.whodunnit = orig_name
widget # persist `widget` (call the `let`)
end
it "modifies value of `PaperTrail.whodunnit` while executing the block" do
expect(widget.versions.last.whodunnit).to eq(orig_name)
widget.paper_trail.whodunnit(new_name) do
expect(PaperTrail.whodunnit).to eq(new_name)
widget.update_attributes(name: "Elizabeth")
end
expect(widget.versions.last.whodunnit).to eq(new_name)
end
it "reverts value of whodunnit to previous value after executing the block" do
expect(widget.versions.last.whodunnit).to eq(orig_name)
widget.paper_trail.whodunnit(new_name) { |w|
w.update_attributes(name: "Elizabeth")
}
expect(PaperTrail.whodunnit).to eq(orig_name)
end
it "reverts to previous value, even if error within block" do
expect(widget.versions.last.whodunnit).to eq(orig_name)
expect {
widget.paper_trail.whodunnit(new_name) { raise }
}.to raise_error(RuntimeError)
expect(PaperTrail.whodunnit).to eq(orig_name)
end
it "is deprecated, delegates to Request.whodunnit" do
allow(::ActiveSupport::Deprecation).to receive(:warn)
allow(::PaperTrail::Request).to receive(:with)
widget.paper_trail.whodunnit("Alex") {}
expect(::ActiveSupport::Deprecation).to have_received(:warn).once
expect(::PaperTrail::Request).to have_received(:with).with(whodunnit: "Alex")
end
end
@ -307,35 +272,4 @@ RSpec.describe Widget, type: :model do
assert_equal 2, widget.versions.length
end
end
describe ".paper_trail.enabled?" do
it "returns true" do
expect(Widget.paper_trail.enabled?).to eq(true)
end
end
describe ".disable" do
it "sets the `paper_trail.enabled?` to `false`" do
expect(Widget.paper_trail.enabled?).to eq(true)
Widget.paper_trail.disable
expect(Widget.paper_trail.enabled?).to eq(false)
end
after do
Widget.paper_trail.enable
end
end
describe ".enable" do
it "sets the `paper_trail.enabled?` to `true`" do
Widget.paper_trail.disable
expect(Widget.paper_trail.enabled?).to eq(false)
Widget.paper_trail.enable
expect(Widget.paper_trail.enabled?).to eq(true)
end
after do
Widget.paper_trail.enable
end
end
end

View File

@ -324,11 +324,13 @@ RSpec.describe(::PaperTrail, versioning: true) do
context "with its paper trail turned off" do
before do
Widget.paper_trail.disable
PaperTrail.request.disable_model(Widget)
@count = @widget.versions.length
end
after { Widget.paper_trail.enable }
after do
PaperTrail.request.enable_model(Widget)
end
context "when updated" do
before { @widget.update_attributes(name: "Beeblebrox") }
@ -341,12 +343,14 @@ RSpec.describe(::PaperTrail, versioning: true) do
context "when destroyed \"without versioning\"" do
it "leave paper trail off after call" do
@widget.paper_trail.without_versioning(:destroy)
expect(Widget.paper_trail.enabled?).to(eq(false))
expect(::PaperTrail.request.enabled_for_model?(Widget)).to eq(false)
end
end
context "and then its paper trail turned on" do
before { Widget.paper_trail.enable }
before do
PaperTrail.request.enable_model(Widget)
end
context "when updated" do
before { @widget.update_attributes(name: "Ford") }
@ -371,19 +375,15 @@ RSpec.describe(::PaperTrail, versioning: true) do
end
it "enable paper trail after call" do
expect(Widget.paper_trail.enabled?).to(eq(true))
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(true)
end
end
context "when receiving a method name as an argument" do
before { @widget.paper_trail.without_versioning(:touch_with_version) }
it "not create new version" do
context "given a symbol, specifying a method name" do
it "does not create a new version" do
@widget.paper_trail.without_versioning(:touch_with_version)
expect(@widget.versions.length).to(eq(@count))
end
it "enable paper trail after call" do
expect(Widget.paper_trail.enabled?).to(eq(true))
expect(::PaperTrail.request.enabled_for_model?(Widget)).to eq(true)
end
end
end
@ -395,7 +395,7 @@ RSpec.describe(::PaperTrail, versioning: true) do
context "when a record is created" do
before do
PaperTrail.whodunnit = "Alice"
PaperTrail.request.whodunnit = "Alice"
@widget.save
@version = @widget.versions.last
end
@ -409,7 +409,7 @@ RSpec.describe(::PaperTrail, versioning: true) do
context "when a record is updated" do
before do
PaperTrail.whodunnit = "Bob"
PaperTrail.request.whodunnit = "Bob"
@widget.update_attributes(name: "Rivet")
@version = @widget.versions.last
end
@ -423,7 +423,7 @@ RSpec.describe(::PaperTrail, versioning: true) do
context "when a record is destroyed" do
before do
PaperTrail.whodunnit = "Charlie"
PaperTrail.request.whodunnit = "Charlie"
@widget.destroy
@version = PaperTrail::Version.last
end
@ -467,9 +467,9 @@ RSpec.describe(::PaperTrail, versioning: true) do
end
it "returns the correct originator" do
PaperTrail.whodunnit = "Ben"
PaperTrail.request.whodunnit = "Ben"
@foo.update_attribute(:name, "Geoffrey")
expect(@foo.paper_trail.originator).to(eq(PaperTrail.whodunnit))
expect(@foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
end
context "when destroyed" do

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require "spec_helper"
module PaperTrail
::RSpec.describe(Request, versioning: true) do
describe ".enabled_for_model?" do
it "returns true" do
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(true)
end
end
describe ".disable_model" do
it "sets enabled_for_model? to false" do
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(true)
PaperTrail.request.disable_model(Widget)
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(false)
end
after do
PaperTrail.request.enable_model(Widget)
end
end
describe ".enable_model" do
it "sets enabled_for_model? to true" do
PaperTrail.request.disable_model(Widget)
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(false)
PaperTrail.request.enable_model(Widget)
expect(PaperTrail.request.enabled_for_model?(Widget)).to eq(true)
end
after do
PaperTrail.request.enable_model(Widget)
end
end
describe ".whodunnit" do
context "when set to a proc" do
it "evaluates the proc each time a version is made" do
call_count = 0
described_class.whodunnit = proc { call_count += 1 }
expect(described_class.whodunnit).to eq(1)
expect(described_class.whodunnit).to eq(2)
end
end
end
describe ".with" do
context "block given" do
it "sets whodunnit only for the block passed" do
described_class.with(whodunnit: "foo") do
expect(described_class.whodunnit).to eq("foo")
end
expect(described_class.whodunnit).to be_nil
end
it "sets whodunnit only for the current thread" do
described_class.with(whodunnit: "foo") do
expect(described_class.whodunnit).to eq("foo")
Thread.new { expect(described_class.whodunnit).to be_nil }.join
end
expect(described_class.whodunnit).to be_nil
end
end
end
end
end

View File

@ -10,12 +10,12 @@ RSpec.describe PaperTrail do
controller = TestController.new
controller.send(:set_paper_trail_whodunnit)
sleep(0.001) while blocked
described_class.whodunnit
described_class.request.whodunnit
end
fast_thread = Thread.new do
controller = TestController.new
controller.send(:set_paper_trail_whodunnit)
who = described_class.whodunnit
who = described_class.request.whodunnit
blocked = false
who
end
@ -26,21 +26,23 @@ RSpec.describe PaperTrail do
describe "#without_versioning" do
it "is thread-safe" do
enabled = nil
slow_thread = Thread.new do
t1 = Thread.new do
Widget.new.paper_trail.without_versioning do
sleep(0.01)
enabled = Widget.paper_trail.enabled?
enabled = described_class.request.enabled_for_model?(Widget)
sleep(0.01)
end
enabled
end
fast_thread = Thread.new do
# A second thread is timed so that it runs during the first thread's
# `without_versioning` block.
t2 = Thread.new do
sleep(0.005)
Widget.paper_trail.enabled?
described_class.request.enabled_for_model?(Widget)
end
expect(fast_thread.value).not_to(eq(slow_thread.value))
expect(Widget.paper_trail.enabled?).to(eq(true))
expect(described_class.enabled_for_model?(Widget)).to(eq(true))
expect(t1.value).to eq(false)
expect(t2.value).to eq(true) # see? unaffected by t1
expect(described_class.request.enabled_for_model?(Widget)).to eq(true)
end
end
end

View File

@ -94,37 +94,8 @@ RSpec.describe PaperTrail do
end
describe ".version" do
it { expect(described_class).to respond_to(:version) }
it { expect(described_class.version).to eq(described_class::VERSION::STRING) }
end
describe ".whodunnit" do
context "with block passed" do
it "sets whodunnit only for the block passed" do
described_class.whodunnit("foo") do
expect(described_class.whodunnit).to eq("foo")
end
expect(described_class.whodunnit).to be_nil
end
it "sets whodunnit only for the current thread" do
described_class.whodunnit("foo") do
expect(described_class.whodunnit).to eq("foo")
Thread.new { expect(described_class.whodunnit).to be_nil }.join
end
expect(described_class.whodunnit).to be_nil
end
end
context "when set to a proc" do
it "evaluates the proc each time a version is made" do
call_count = 0
described_class.whodunnit = proc { call_count += 1 }
expect(described_class.whodunnit).to eq(1)
expect(described_class.whodunnit).to eq(2)
end
it "returns the expected String" do
expect(described_class.version).to eq(described_class::VERSION::STRING)
end
end
end

View File

@ -9,14 +9,15 @@ RSpec.describe "Articles management", type: :request, order: :defined do
specify { expect(PaperTrail).not_to be_enabled }
it "does not create a version" do
expect(PaperTrail).to be_enabled_for_controller
expect(PaperTrail.request).to be_enabled_for_controller
expect {
post articles_path, params_wrapper(valid_params)
}.not_to change(PaperTrail::Version, :count)
end
it "does not leak the state of the `PaperTrail.enabled_for_controller?` into the next test" do
expect(PaperTrail).to be_enabled_for_controller
it "does not leak the state of the `PaperTrail.request.enabled_for_controller?` \
into the next test" do
expect(PaperTrail.request).to be_enabled_for_controller
end
end