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.
@@ -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
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ /.bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ *.rbc
7
+ /doc
8
+ .yardoc
9
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,17 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+
5
+ script: "bundle exec rake"
6
+
7
+ services: mongodb
8
+ rvm:
9
+ - 2.1
10
+ - 2.0.0
11
+ - 2.2
12
+ - jruby
13
+ - rbx-2
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: rbx-2
17
+ - rvm: jruby
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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.
@@ -0,0 +1,167 @@
1
+ [![Build Status](https://travis-ci.org/state-machines/state_machines-audit_trail.svg?branch=master)](https://travis-ci.org/state-machines/state_machines-audit_trail)
2
+ [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-audit_trail.png)](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
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |task|
5
+ task.pattern = "./spec/**/*_spec.rb"
6
+ task.rspec_opts = ['--color']
7
+ end
8
+
9
+ task :default => [:spec]
@@ -0,0 +1,2 @@
1
+ # To keep Rails happy
2
+ require 'state_machines/audit_trail'
@@ -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,5 @@
1
+ class StateMachines::AuditTrail::Railtie < ::Rails::Railtie
2
+ generators do
3
+ require 'state_machines/audit_trail_generator'
4
+ end
5
+ 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,5 @@
1
+ module StateMachines
2
+ module AuditTrail
3
+ VERSION = '1.0.0'
4
+ end
5
+ 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