state_machines-audit_trail 1.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +167 -0
- data/Rakefile +9 -0
- data/lib/state_machines-audit_trail.rb +2 -0
- data/lib/state_machines/audit_trail.rb +16 -0
- data/lib/state_machines/audit_trail/backend.rb +56 -0
- data/lib/state_machines/audit_trail/backend/active_record.rb +23 -0
- data/lib/state_machines/audit_trail/backend/mongoid.rb +13 -0
- data/lib/state_machines/audit_trail/railtie.rb +5 -0
- data/lib/state_machines/audit_trail/transition_auditing.rb +59 -0
- data/lib/state_machines/audit_trail/version.rb +5 -0
- data/lib/state_machines/audit_trail_generator.rb +29 -0
- data/spec/helpers/active_record.rb +222 -0
- data/spec/helpers/mongoid.rb +79 -0
- data/spec/helpers/mongoid.yml +6 -0
- data/spec/lib/state_machines/audit_trail/backend/active_record_spec.rb +225 -0
- data/spec/lib/state_machines/audit_trail/backend/mongoid_spec.rb +98 -0
- data/spec/lib/state_machines/audit_trail_generator_spec.rb +54 -0
- data/spec/lib/state_machines/audit_trail_spec.rb +8 -0
- data/spec/spec_helper.rb +8 -0
- data/state_machines-audit_trail.gemspec +36 -0
- metadata +237 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b8a0bfd2dd9a83185bc24c1c55d3804b2fba5a8e
|
4
|
+
data.tar.gz: 03e55491819582dbf56a32b494756564d0da7ffd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1c764d8ed988ecb2a766939127e76ce902241320bd53fdce5641830bb3eb52c666179dae873d23fc2f989a5fd65013722af497716a59d65e06c5d5d5a659e6b
|
7
|
+
data.tar.gz: 60d5d11f419b337f253141fb36a17a51dcb2852cb549e89b64a59f4c8f06ed8a30fe7785038138a7a11884212ca1cd0b52cbac094cd9133699eb3165a5dd39f4
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Jesse Storimer & Willem van Bergen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
[](https://travis-ci.org/state-machines/state_machines-audit_trail)
|
2
|
+
[](https://codeclimate.com/github/state-machines/state_machines-audit_trail)
|
3
|
+
|
4
|
+
# state_machines-audit_trail
|
5
|
+
Log transitions on a [state_machines gem](https://github.com/state-machines/state_machines) to support auditing and business process analytics.
|
6
|
+
|
7
|
+
|
8
|
+
## Description
|
9
|
+
|
10
|
+
This plugin for the [state_machines gem](https://github.com/state-machines/state_machines) adds support for keeping an audit trail
|
11
|
+
for any state machine. Having an audit trail gives you a complete history of the state changes in your model. This history allows you
|
12
|
+
to investigate incidents or perform analytics, like: _"How long does it take on average to go from state a to state b?"_,
|
13
|
+
or _"What percentage of cases goes from state a to b via state c?"_
|
14
|
+
|
15
|
+
For more information read [Why developers should be force-fed state machines](http://www.shopify.com/technology/3383012-why-developers-should-be-force-fed-state-machines).
|
16
|
+
|
17
|
+
## ORM support
|
18
|
+
|
19
|
+
Note: while the state_machines gem integrates with multiple ORMs, this plugin is currently limited to the following ORM backends:
|
20
|
+
|
21
|
+
* ActiveRecord
|
22
|
+
* Mongoid
|
23
|
+
|
24
|
+
|
25
|
+
It should be easy to add new backends by looking at the implementation of the current backends. Pull requests are welcome!
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
First, make the gem available by adding it to your `Gemfile`, and run `bundle install`:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
|
32
|
+
# this gem
|
33
|
+
gem 'state_machines-audit_trail'
|
34
|
+
|
35
|
+
# required runtime dependency for your ORM; either
|
36
|
+
gem 'state_machines-activerecord'
|
37
|
+
|
38
|
+
# OR
|
39
|
+
gem 'state_machines-mongoid'
|
40
|
+
```
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
For the examples below, we will assume you have a pre-existing model called `Subscription` that has a `state_machine` configured to utilize the `state` attribute.
|
45
|
+
|
46
|
+
### Create/generate a model and migration that holds the audit trail specific to your target model
|
47
|
+
A Rails generator is provided to create a model and a migration, call it with:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
rails generate state_machines:audit_trail Subscription state
|
51
|
+
```
|
52
|
+
|
53
|
+
will generate the `SubscriptionStateTransition` model and an accompanying migration.
|
54
|
+
|
55
|
+
### Configure `audit_trail` in your `state_machine`:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Subscription < ActiveRecord::Base
|
59
|
+
state_machines :state, initial: :start do
|
60
|
+
audit_trail
|
61
|
+
...
|
62
|
+
```
|
63
|
+
|
64
|
+
### That's it!
|
65
|
+
`audit_trail` will register an `after_transition` callback that is used to log all transitions including the initial state if there is one.
|
66
|
+
|
67
|
+
## Configuration options
|
68
|
+
|
69
|
+
### `:initial` - turn off initial state logging
|
70
|
+
By default, upon instantiation, a `StateTransition` is saved for `null => initial` state. This is useful to understand the full history
|
71
|
+
of any model, but there are cases where this can pollute the `audit_trail`. For example, when a model has multiple `state_machine`s
|
72
|
+
that use a single `StateTransition` model for persistence (in conjunction with `context` below), there would be multiple initial state
|
73
|
+
transitions. By configuring `initial: false`, it will skip the initial state transition logging for this specific `state_machine`, while
|
74
|
+
leaving the others in the model unaffected.
|
75
|
+
```ruby
|
76
|
+
audit_trail initial: false
|
77
|
+
```
|
78
|
+
|
79
|
+
### `:class` - custom state transition class
|
80
|
+
If your `Transition` model does not use the default naming scheme, provide it using the `:class` option:
|
81
|
+
```ruby
|
82
|
+
audit_trail class: FooStateTransition
|
83
|
+
```
|
84
|
+
|
85
|
+
An example use of a custom `:class` and `:context` (below) would be for a model that has multiple `state_machine` definitions. The combination
|
86
|
+
of these options would allow the use of one transition class that logged state information from both state machines.
|
87
|
+
|
88
|
+
### `:context` - storing additional attribute or method values
|
89
|
+
Using the `:context` option, you can store method results (or attributes exposed as methods) in the state transition class.
|
90
|
+
|
91
|
+
In order to utilize this feature, you need to:
|
92
|
+
|
93
|
+
1. add a field/column to your state transition class (i.e. `SubscriptionStateTransitions`) and perhaps underlying database through a migration
|
94
|
+
2. expose the attribute as a method, or create a method to compute a dynamic value
|
95
|
+
3. configure `:context`
|
96
|
+
|
97
|
+
#### Example 1 - Store a single attribute value
|
98
|
+
Store `Subscription` `field1` in `Transition` field `field1`:
|
99
|
+
```ruby
|
100
|
+
audit_trail :context: :field1
|
101
|
+
```
|
102
|
+
|
103
|
+
#### Example 2 - Store multiple attribute values
|
104
|
+
Store `Subscription` `field1` and `field2` in `Transition` fields `field1` and `field2`:
|
105
|
+
```ruby
|
106
|
+
audit_trail context: [:field1, :field2]
|
107
|
+
```
|
108
|
+
|
109
|
+
#### Example 3 - Store simple method results
|
110
|
+
Store simple method results.
|
111
|
+
|
112
|
+
Sometimes it can be useful to store dynamically computed information, such as those from a `Subscription` method `#plan_time_remaining`
|
113
|
+
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class Subscription < ActiveRecord::Base
|
117
|
+
state_machines :state, initial: :start do
|
118
|
+
audit_trail :context: :plan_time_remaining
|
119
|
+
...
|
120
|
+
|
121
|
+
def plan_time_remaining
|
122
|
+
# Dynamically computed field e.g., based on other models
|
123
|
+
...
|
124
|
+
```
|
125
|
+
|
126
|
+
#### Example 4 - Store advanced method results
|
127
|
+
Store method results that interrogate the transition for information such as `event` arguments:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class Subscription < ActiveRecord::Base
|
131
|
+
state_machines :state, initial: :start do
|
132
|
+
audit_trail :context: :user_name
|
133
|
+
...
|
134
|
+
|
135
|
+
# method receives a state_machines transition object
|
136
|
+
def user_name(transition)
|
137
|
+
if transition.args.present?
|
138
|
+
user_id = transition.args.last.delete(:user_id)
|
139
|
+
User.find(user_id).name
|
140
|
+
else
|
141
|
+
'Undefined User'
|
142
|
+
end
|
143
|
+
...
|
144
|
+
|
145
|
+
model = Subscription.first
|
146
|
+
model.ignite!('arg1, 'arg2', 'arg3', user_id: 1)
|
147
|
+
```
|
148
|
+
|
149
|
+
## About
|
150
|
+
|
151
|
+
### Maintainers
|
152
|
+
Conversion from the original code to `state_machines` by [AlienFast](http://alienfast.com).
|
153
|
+
|
154
|
+
### Original Authors
|
155
|
+
[The original plugin](https://github.com/wvanbergen/state_machine-audit_trail) was written by Jesse Storimer and Willem van Bergen for Shopify.
|
156
|
+
Mongoid support was contributed by [Siddharth](https://github.com/svs).
|
157
|
+
|
158
|
+
### License
|
159
|
+
Released under the MIT license (see LICENSE).
|
160
|
+
|
161
|
+
## Contributing
|
162
|
+
|
163
|
+
1. Fork it ( https://github.com/state-machines/state_machines-audit_trail/fork )
|
164
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
165
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
166
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
167
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'state_machines'
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AuditTrail
|
5
|
+
def self.setup
|
6
|
+
StateMachines::Machine.send(:include, StateMachines::AuditTrail::TransitionAuditing)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'state_machines/audit_trail/version'
|
12
|
+
require 'state_machines/audit_trail/transition_auditing'
|
13
|
+
require 'state_machines/audit_trail/backend'
|
14
|
+
require 'state_machines/audit_trail/railtie' if defined?(::Rails)
|
15
|
+
|
16
|
+
StateMachines::AuditTrail.setup
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class StateMachines::AuditTrail::Backend < Struct.new(:transition_class, :owner_class, :context)
|
2
|
+
|
3
|
+
autoload :Mongoid, 'state_machines/audit_trail/backend/mongoid'
|
4
|
+
autoload :ActiveRecord, 'state_machines/audit_trail/backend/active_record'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Resolve field values and #persist
|
8
|
+
# - object: the object being watched by the state_machines observer
|
9
|
+
# - transition: state machine transition object that state machine passes to after/before transition callbacks
|
10
|
+
#
|
11
|
+
def log(object, transition)
|
12
|
+
fields = {event: transition.event ? transition.event.to_s : nil, from: transition.from, to: transition.to}
|
13
|
+
[context].flatten(1).each { |field|
|
14
|
+
fields[field] = resolve_context(object, field, transition)
|
15
|
+
} unless self.context.nil?
|
16
|
+
|
17
|
+
# begin
|
18
|
+
persist(object, fields)
|
19
|
+
# rescue => e
|
20
|
+
# puts "\nUncaught #{e.class} persisting audit_trail: #{e.message}"
|
21
|
+
# puts "\t" + e.backtrace.join($/ + "\t")
|
22
|
+
# raise e
|
23
|
+
# end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Creates an instance of the Backend class which does the actual persistence of transition state information.
|
28
|
+
# - transition_class: the Class which holds the audit trail
|
29
|
+
#
|
30
|
+
# To add a new ORM, implement something similar to lib/state_machines/audit_trail/backend/active_record.rb
|
31
|
+
# and return from here the appropriate object based on which ORM the transition_class is using
|
32
|
+
#
|
33
|
+
def self.create_for(transition_class, owner_class, context = nil)
|
34
|
+
if Object.const_defined?('ActiveRecord') && transition_class.ancestors.include?(::ActiveRecord::Base)
|
35
|
+
return StateMachines::AuditTrail::Backend::ActiveRecord.new(transition_class, owner_class, context)
|
36
|
+
elsif Object.const_defined?('Mongoid') && transition_class.ancestors.include?(::Mongoid::Document)
|
37
|
+
return StateMachines::AuditTrail::Backend::Mongoid.new(transition_class, owner_class, context)
|
38
|
+
else
|
39
|
+
raise 'Not implemented. Only support for ActiveRecord and Mongoid is implemented. Pull requests welcome.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def persist(object, fields)
|
46
|
+
raise 'Not implemented. Implement in a subclass.'
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve_context(object, context, transition)
|
50
|
+
if object.method(context).arity != 0
|
51
|
+
object.send(context, transition)
|
52
|
+
else
|
53
|
+
object.send(context)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'state_machines-activerecord'
|
2
|
+
|
3
|
+
class StateMachines::AuditTrail::Backend::ActiveRecord < StateMachines::AuditTrail::Backend
|
4
|
+
attr_accessor :context
|
5
|
+
|
6
|
+
def initialize(transition_class, owner_class, context = nil)
|
7
|
+
@association = transition_class.to_s.tableize.split('/').last.to_sym
|
8
|
+
super transition_class, owner_class
|
9
|
+
self.context = context # FIXME: actually not sure why we need to do this, but tests fail otherwise. Something with super's Struct?
|
10
|
+
owner_class.has_many(@association, class_name: transition_class.to_s) unless owner_class.reflect_on_association(@association)
|
11
|
+
end
|
12
|
+
|
13
|
+
def persist(object, fields)
|
14
|
+
# Let ActiveRecord manage the timestamp for us so it does the right thing with regards to timezones.
|
15
|
+
if object.new_record?
|
16
|
+
object.send(@association).build(fields)
|
17
|
+
else
|
18
|
+
object.send(@association).create(fields)
|
19
|
+
end
|
20
|
+
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'state_machines-mongoid'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Populate and persist the state transition to Mongoid
|
5
|
+
#
|
6
|
+
class StateMachines::AuditTrail::Backend::Mongoid < StateMachines::AuditTrail::Backend
|
7
|
+
|
8
|
+
def persist(object, fields)
|
9
|
+
foreign_key_field = transition_class.relations.keys.first
|
10
|
+
fields = fields.merge({foreign_key_field => object, created_at: Time.now})
|
11
|
+
transition_class.create(fields)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
#
|
3
|
+
# This module inserts hooks into the state machine.
|
4
|
+
# The transition_class is the class (optionally specified with the :class option) for the model which
|
5
|
+
# records audit information from one single transition i.e. SubscriptionStateTransition.
|
6
|
+
#
|
7
|
+
# Multiple `SubscriptionStateTransition`s compose the 'audit_trail'.
|
8
|
+
#
|
9
|
+
module StateMachines::AuditTrail::TransitionAuditing
|
10
|
+
|
11
|
+
# Hook for audit_trail inside a a state_machine declaration.
|
12
|
+
#
|
13
|
+
# options:
|
14
|
+
# - :class - custom state transition class
|
15
|
+
# - :context - methods to call/store in field of same name in the state transition class
|
16
|
+
# - :initial - if false, won't log null => initial state transition upon instantiation
|
17
|
+
#
|
18
|
+
def audit_trail(options = {})
|
19
|
+
state_machine = self
|
20
|
+
if options[:class].presence
|
21
|
+
raise ":class option[#{options[:class]}] must be a class (not a string)." unless options[:class].is_a? Class
|
22
|
+
end
|
23
|
+
transition_class = options[:class] || default_transition_class
|
24
|
+
|
25
|
+
# backend implements #log to store transition information
|
26
|
+
@backend = StateMachines::AuditTrail::Backend.create_for(transition_class, self.owner_class, options[:context])
|
27
|
+
|
28
|
+
# Initial state logging can be turned off. Very useful for a model with multiple state_machines using a single TransitionState object for logging
|
29
|
+
unless options[:initial] == false
|
30
|
+
unless state_machine.action == nil
|
31
|
+
# Log the initial transition from null => initial (upon object instantiation)
|
32
|
+
state_machine.owner_class.after_initialize do |object|
|
33
|
+
current_state = object.send(state_machine.attribute)
|
34
|
+
if !current_state.nil?
|
35
|
+
state_machine.backend.log(object, OpenStruct.new(to: current_state))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Log any transition (other than initial)
|
42
|
+
state_machine.after_transition do |object, transition|
|
43
|
+
state_machine.backend.log(object, transition)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public returns an instance of the class which does the actual audit trail logging
|
48
|
+
def backend
|
49
|
+
@backend
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def default_transition_class
|
55
|
+
owner_class_or_base_class = owner_class.respond_to?(:base_class) ? owner_class.base_class : owner_class
|
56
|
+
name = "#{owner_class_or_base_class.name}#{attribute.to_s.camelize}Transition"
|
57
|
+
name.constantize
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
class StateMachines::AuditTrailGenerator < ::Rails::Generators::Base
|
4
|
+
|
5
|
+
source_root File.join(File.dirname(__FILE__), 'templates')
|
6
|
+
|
7
|
+
argument :source_model
|
8
|
+
argument :state_attribute, default: 'state'
|
9
|
+
argument :transition_model, default: ''
|
10
|
+
|
11
|
+
|
12
|
+
def create_model
|
13
|
+
args = [transition_class_name,
|
14
|
+
"#{source_model.demodulize.tableize.singularize}:references",
|
15
|
+
'event:string',
|
16
|
+
'from:string',
|
17
|
+
'to:string',
|
18
|
+
'created_at:timestamp',
|
19
|
+
'--no-timestamps',
|
20
|
+
'--no-fixtures']
|
21
|
+
generate 'model', args.join(' ')
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def transition_class_name
|
27
|
+
transition_model.blank? ? "#{source_model.camelize}#{state_attribute.camelize}Transition" : transition_model
|
28
|
+
end
|
29
|
+
end
|