state_machines-audit_trail 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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