jsmestad-audit_trail 0.1.5

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: babb59af06913e7851b42c4dbaa32b06e770dc8a
4
+ data.tar.gz: ed51a452d6a2d8b5f0de32e84a361d14bd4e8d9f
5
+ SHA512:
6
+ metadata.gz: 0a5a764ce383d6d6b853c05278bf2abbbba400c61b0334704f36d65ea31e3665840c9eaab6639c616575a5dfa5baf973d39312322ee757e49cfb44c72bef1bdf
7
+ data.tar.gz: 71376d548bf8b496fdf762f68ebf4b79cf17704d31a3cd468d5a609c25922c3f53bb4fda62e59b8020e4d04082ce2bcd88dd9246d3ba3a9211c7bd059a7b02c8
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ /.bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ *.rbc
7
+ /doc
8
+ .yardoc
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ script: bundle exec rake
3
+ services: mongodb
4
+ rvm:
5
+ - 1.8.7
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - ree
9
+ - ruby-head
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jesse Storimer & Willem van Bergen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ = StateMachine audit trail
2
+
3
+ This plugin for the state machine gem (see https://github.com/pluginaweek/state_machine) adds support for keeping an audit trail for any state machine. Having an audit trail gives you a complete history of the state changes in your model. This history allows you to investigate incidents or perform analytics, like: "How long does it take on average to go from state a to state b?", or "What percentage of cases goes from state a to b via state c?"
4
+
5
+ == ORM support
6
+
7
+ Note: while the state_machine gem integrates with multiple ORMs, this plugin is currently limited to the following ORM backends:
8
+
9
+ * ActiveRecord
10
+ * Mongoid
11
+
12
+ It should be easy to add new backends by looking at the implementation of the current backends. Pull requests are welcome!
13
+
14
+ == Usage
15
+
16
+ First, make the gem available by adding it to your <tt>Gemfile</tt>, and run <tt>bundle install</tt>:
17
+
18
+ gem 'state_machine-audit_trail'
19
+
20
+ Create a model/table that holds the audit trail. The table needs to have a foreign key to the original object, an "event" field, a "from" state field, a "to" state field, and a "created_at" timestamp that stores the timestamp of the transition. This gem comes with a Rails 3 generator to create a model and a migration like that.
21
+
22
+ rails generate state_machine:audit_trail <model> <state_attribute>
23
+
24
+ For a model called "Model", and a state attribute "state", this will generate the ModelStateTransition model and an accompanying migration.
25
+
26
+ Next, tell your state machine you want to store an audit trail:
27
+
28
+ class Model < ActiveRecord::Base
29
+ state_machine :state, :initial => :start do
30
+ store_audit_trail
31
+ ...
32
+
33
+ If your audit trail model does not use the default naming scheme, provide it using the <tt>:to</tt> option:
34
+
35
+ class Model < ActiveRecord::Base
36
+ state_machine :state, :initial => :start do
37
+ store_audit_trail :to => 'ModelAuditTrail'
38
+ ...
39
+
40
+ That's it! The plugin will register an <tt>after_transition</tt> callback that is used to log all transitions. It will also log the initial state if there is one.
41
+
42
+ If you would like to store additional messages in the audit trail, you can do so with the following:
43
+ store_audit_trail :context_to_log => :state_message # Will grab the results of the state_message method on the model and store it in a field called state_message on the audit trail model
44
+
45
+ == About
46
+
47
+ This plugin is written by Jesse Storimer and Willem van Bergen for Shopify. Mongoid support was contributed by Siddharth (https://github.com/svs). It is released under the MIT license (see LICENSE).
@@ -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,29 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'state_machine/audit_trail/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "jsmestad-audit_trail"
9
+ s.version = StateMachine::AuditTrail::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["Willem van Bergen", "Jesse Storimer"]
12
+ s.email = ["willem@shopify.com", "jesse@shopify.com"]
13
+ s.homepage = "https://github.com/wvanbergen/state_machine-audit_trail"
14
+ s.summary = %q{Log transitions on a state machine to support auditing and business process analytics.}
15
+ s.description = %q{Log transitions on a state machine to support auditing and business process analytics.}
16
+ s.license = "MIT"
17
+
18
+ s.add_runtime_dependency('state_machine')
19
+
20
+ s.add_development_dependency('rake')
21
+ s.add_development_dependency('rspec', '~> 2')
22
+ s.add_development_dependency('activerecord', '~> 3')
23
+ s.add_development_dependency('sqlite3')
24
+ s.add_development_dependency('mongoid', '~> 2')
25
+ s.add_development_dependency('bson_ext')
26
+
27
+ s.files = `git ls-files`.split($/)
28
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
29
+ end
@@ -0,0 +1,2 @@
1
+ # To keep Rails happy
2
+ require 'state_machine/audit_trail'
@@ -0,0 +1,15 @@
1
+ require 'state_machine'
2
+
3
+ module StateMachine::AuditTrail
4
+
5
+ def self.setup
6
+ StateMachine::Machine.send(:include, StateMachine::AuditTrail::TransitionAuditing)
7
+ end
8
+ end
9
+
10
+ require 'state_machine/audit_trail/version'
11
+ require 'state_machine/audit_trail/transition_auditing'
12
+ require 'state_machine/audit_trail/backend'
13
+ require 'state_machine/audit_trail/railtie' if defined?(::Rails)
14
+
15
+ StateMachine::AuditTrail.setup
@@ -0,0 +1,28 @@
1
+ class StateMachine::AuditTrail::Backend < Struct.new(:transition_class, :owner_class)
2
+
3
+ autoload :Mongoid, 'state_machine/audit_trail/backend/mongoid'
4
+ autoload :ActiveRecord, 'state_machine/audit_trail/backend/active_record'
5
+
6
+ def log(object, event, from, to, timestamp = Time.now)
7
+ raise NotImplemented, "Implement in a subclass."
8
+ end
9
+
10
+ # Public creates an instance of the class which does the actual logging
11
+ #
12
+ # transition_class: the Class which holds the audit trail
13
+ #
14
+ # in order to adda new ORM here, copy audit_trail/mongoid.rb to whatever you want to call the new file and implement the #log function there
15
+ # then, return from here the appropriate object based on which ORM the transition_class is using
16
+ def self.create_for_transition_class(transition_class, owner_class, context_to_log = nil)
17
+ if Object.const_defined?('ActiveRecord') && transition_class.ancestors.include?(::ActiveRecord::Base)
18
+ return StateMachine::AuditTrail::Backend::ActiveRecord.new(transition_class, owner_class, context_to_log)
19
+ elsif Object.const_defined?('Mongoid') && transition_class.ancestors.include?(::Mongoid::Document)
20
+ # Mongoid implementation doesn't yet support additional context fields
21
+ raise NotImplemented, "Mongoid does not support additional context fields" if context_to_log.present?
22
+
23
+ return StateMachine::AuditTrail::Backend::Mongoid.new(transition_class, owner_class)
24
+ else
25
+ raise NotImplemented, "Only support for ActiveRecord and Mongoid is included at this time"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ class StateMachine::AuditTrail::Backend::ActiveRecord < StateMachine::AuditTrail::Backend
2
+ attr_accessor :context_to_log
3
+
4
+ def initialize(transition_class, owner_class, context_to_log = nil)
5
+ self.context_to_log = context_to_log
6
+ @association = transition_class.to_s.tableize.to_sym
7
+ super transition_class
8
+ owner_class.has_many @association
9
+ end
10
+
11
+ def log(object, event, from, to, timestamp = Time.now)
12
+ # Let ActiveRecord manage the timestamp for us so it does the
13
+ # right thing with regards to timezones.
14
+ params = {:event => event, :from_state => from, :to_state => to}
15
+ params[self.context_to_log] = object.send(self.context_to_log) unless self.context_to_log.nil?
16
+ object.send(@association).create(params)
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # This is the class that does the actual logging.
2
+ # We need one of these per ORM
3
+
4
+ class StateMachine::AuditTrail::Backend::Mongoid < StateMachine::AuditTrail::Backend
5
+
6
+ # Public writes the log to the database
7
+ #
8
+ # object: the object being watched by the state_machine observer
9
+ # event: the event being observed by the state machine
10
+ # from: the state of the object prior to the event
11
+ # to: the state of the object after the event
12
+ def log(object, event, from, to, timestamp = Time.now)
13
+ tc = transition_class
14
+ foreign_key_field = tc.relations.keys.first
15
+ transition_class.create(foreign_key_field => object, :event => event, :from => from, :to => to, :create_at => timestamp)
16
+ end
17
+
18
+
19
+ end
@@ -0,0 +1,5 @@
1
+ class StateMachine::AuditTrail::Railtie < ::Rails::Railtie
2
+ generators do
3
+ require 'state_machine/audit_trail_generator'
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ # this class inserts the appropriate hooks into the state machine.
2
+ # it also contains functions to instantiate an object of the "transition_class"
3
+ # the transition_class is the class for the model which holds the audit_trail
4
+
5
+ module StateMachine::AuditTrail::TransitionAuditing
6
+ attr_accessor :transition_class_name
7
+
8
+ # Public tells the state machine to hook in the appropriate before / after behaviour
9
+ #
10
+ # options: a Hash of options. keys that are used are :to => CustomTransitionClass,
11
+ # :context_to_log => method(s) to call on object and store in transitions
12
+ def store_audit_trail(options = {})
13
+ state_machine = self
14
+ state_machine.transition_class_name = (options[:to] || default_transition_class_name).to_s
15
+ state_machine.after_transition do |object, transition|
16
+ state_machine.audit_trail(options[:context_to_log]).log(object, transition.event, transition.from, transition.to)
17
+ end
18
+
19
+ state_machine.owner_class.after_create do |object|
20
+ if !object.send(state_machine.attribute).nil?
21
+ state_machine.audit_trail(options[:context_to_log]).log(object, nil, nil, object.send(state_machine.attribute))
22
+ end
23
+ end
24
+ end
25
+
26
+ # Public returns an instance of the class which does the actual audit trail logging
27
+ def audit_trail(context_to_log = nil)
28
+ @transition_auditor ||= StateMachine::AuditTrail::Backend.create_for_transition_class(transition_class, self.owner_class, context_to_log)
29
+ end
30
+
31
+ private
32
+
33
+ def transition_class
34
+ @transition_class ||= transition_class_name.constantize
35
+ end
36
+
37
+ def default_transition_class_name
38
+ owner_class_or_base_class = owner_class.respond_to?(:base_class) ? owner_class.base_class : owner_class
39
+ "#{owner_class_or_base_class.name}#{attribute.to_s.camelize}Transition"
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ module StateMachine
2
+ module AuditTrail
3
+ VERSION = "0.1.5"
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators'
2
+
3
+ class StateMachine::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
+ Rails::Generators.invoke('model', [transition_class_name, "#{source_model.tableize.singularize}:references", "event:string", "from_state:string", "to_state:string", "created_at:timestamp", '--no-timestamps', '--fixture=false'])
14
+ end
15
+
16
+ protected
17
+
18
+ def transition_class_name
19
+ transition_model.blank? ? "#{source_model.camelize}#{state_attribute.camelize}Transition" : transition_model
20
+ end
21
+ end
@@ -0,0 +1,122 @@
1
+ require 'active_record'
2
+
3
+ ### Setup test database
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
6
+
7
+ ActiveRecord::Base.connection.create_table(:active_record_test_models) do |t|
8
+ t.string :state
9
+ t.string :type
10
+ t.timestamps
11
+ end
12
+
13
+ ActiveRecord::Base.connection.create_table(:active_record_test_model_with_contexts) do |t|
14
+ t.string :state
15
+ t.string :type
16
+ t.timestamps
17
+ end
18
+
19
+ ActiveRecord::Base.connection.create_table(:active_record_test_model_with_multiple_state_machines) do |t|
20
+ t.string :first
21
+ t.string :second
22
+ t.timestamps
23
+ end
24
+
25
+ # We probably want to provide a generator for this model and the accompanying migration.
26
+ class ActiveRecordTestModelStateTransition < ActiveRecord::Base
27
+ belongs_to :test_model
28
+ end
29
+
30
+ class ActiveRecordTestModelWithContextStateTransition < ActiveRecord::Base
31
+ belongs_to :test_model
32
+ end
33
+
34
+ class ActiveRecordTestModelWithMultipleStateMachinesFirstTransition < ActiveRecord::Base
35
+ belongs_to :test_model
36
+ end
37
+
38
+ class ActiveRecordTestModelWithMultipleStateMachinesSecondTransition < ActiveRecord::Base
39
+ belongs_to :test_model
40
+ end
41
+
42
+ class ActiveRecordTestModel < ActiveRecord::Base
43
+
44
+ state_machine :state, :initial => :waiting do # log initial state?
45
+ store_audit_trail
46
+
47
+ event :start do
48
+ transition [:waiting, :stopped] => :started
49
+ end
50
+
51
+ event :stop do
52
+ transition :started => :stopped
53
+ end
54
+ end
55
+ end
56
+
57
+ class ActiveRecordTestModelWithContext < ActiveRecord::Base
58
+ state_machine :state, :initial => :waiting do # log initial state?
59
+ store_audit_trail :context_to_log => :context
60
+
61
+ event :start do
62
+ transition [:waiting, :stopped] => :started
63
+ end
64
+
65
+ event :stop do
66
+ transition :started => :stopped
67
+ end
68
+ end
69
+
70
+ def context
71
+ "Some context"
72
+ end
73
+ end
74
+
75
+ class ActiveRecordTestModelDescendant < ActiveRecordTestModel
76
+ end
77
+
78
+ class ActiveRecordTestModelDescendantWithOwnStateMachine < ActiveRecordTestModel
79
+ state_machine :state, :initial => :new do
80
+ store_audit_trail
81
+
82
+ event :complete do
83
+ transition [:new] => :completed
84
+ end
85
+ end
86
+ end
87
+
88
+ class ActiveRecordTestModelWithMultipleStateMachines < ActiveRecord::Base
89
+
90
+ state_machine :first, :initial => :beginning do
91
+ store_audit_trail
92
+
93
+ event :begin_first do
94
+ transition :beginning => :end
95
+ end
96
+ end
97
+
98
+ state_machine :second do
99
+ store_audit_trail
100
+
101
+ event :begin_second do
102
+ transition nil => :beginning_second
103
+ end
104
+ end
105
+ end
106
+
107
+ def create_transition_table(owner_class, state, add_context = false)
108
+ class_name = "#{owner_class.name}#{state.to_s.camelize}Transition"
109
+ ActiveRecord::Base.connection.create_table(class_name.tableize) do |t|
110
+ t.integer owner_class.name.foreign_key
111
+ t.string :event
112
+ t.string :from_state
113
+ t.string :to_state
114
+ t.string :context if add_context
115
+ t.datetime :created_at
116
+ end
117
+ end
118
+
119
+ create_transition_table(ActiveRecordTestModel, :state)
120
+ create_transition_table(ActiveRecordTestModelWithContext, :state, true)
121
+ create_transition_table(ActiveRecordTestModelWithMultipleStateMachines, :first)
122
+ create_transition_table(ActiveRecordTestModelWithMultipleStateMachines, :second)
@@ -0,0 +1,72 @@
1
+ require 'mongoid'
2
+
3
+ ### Setup test database
4
+
5
+ Mongoid.configure do |config|
6
+ config.master = Mongo::Connection.new.db("sm_audit_trail")
7
+ end
8
+
9
+
10
+
11
+ # We probably want to provide a generator for this model and the accompanying migration.
12
+ class MongoidTestModelStateTransition
13
+ include Mongoid::Document
14
+ include Mongoid::Timestamps
15
+ belongs_to :mongoid_test_model
16
+ end
17
+
18
+ class MongoidTestModelWithMultipleStateMachinesFirstTransition
19
+ include Mongoid::Document
20
+ include Mongoid::Timestamps
21
+ belongs_to :mongoid_test_model
22
+ end
23
+
24
+ class MongoidTestModelWithMultipleStateMachinesSecondTransition
25
+ include Mongoid::Document
26
+ include Mongoid::Timestamps
27
+ belongs_to :mongoid_test_model
28
+ end
29
+
30
+ class MongoidTestModel
31
+
32
+ include Mongoid::Document
33
+ include Mongoid::Timestamps
34
+
35
+ state_machine :state, :initial => :waiting do # log initial state?
36
+ store_audit_trail :orm => :mongoid
37
+
38
+ event :start do
39
+ transition [:waiting, :stopped] => :started
40
+ end
41
+
42
+ event :stop do
43
+ transition :started => :stopped
44
+ end
45
+ end
46
+ end
47
+
48
+ class MongoidTestModelDescendant < MongoidTestModel
49
+ include Mongoid::Timestamps
50
+ end
51
+
52
+ class MongoidTestModelWithMultipleStateMachines
53
+
54
+ include Mongoid::Document
55
+ include Mongoid::Timestamps
56
+
57
+ state_machine :first, :initial => :beginning do
58
+ store_audit_trail
59
+
60
+ event :begin_first do
61
+ transition :beginning => :end
62
+ end
63
+ end
64
+
65
+ state_machine :second do
66
+ store_audit_trail
67
+
68
+ event :begin_second do
69
+ transition nil => :beginning_second
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rspec'
5
+ require 'state_machine/audit_trail'
6
+
7
+ RSpec.configure do |config|
8
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+ require 'helpers/active_record'
3
+
4
+ describe StateMachine::AuditTrail::Backend::ActiveRecord do
5
+
6
+ it "should create an ActiveRecord backend" do
7
+ backend = StateMachine::AuditTrail::Backend.create_for_transition_class(ActiveRecordTestModelStateTransition, ActiveRecordTestModel)
8
+ backend.should be_instance_of(StateMachine::AuditTrail::Backend::ActiveRecord)
9
+ end
10
+
11
+ it "should create a has many association on the state machine owner" do
12
+ backend = StateMachine::AuditTrail::Backend.create_for_transition_class(ActiveRecordTestModelStateTransition, ActiveRecordTestModel)
13
+ ActiveRecordTestModel.reflect_on_association(:active_record_test_model_state_transitions).collection?.should be_true
14
+ end
15
+
16
+ context 'on an object with a single state machine' do
17
+ let!(:state_machine) { ActiveRecordTestModel.create! }
18
+
19
+ it "should log an event with all fields set correctly" do
20
+ state_machine.start!
21
+ last_transition = ActiveRecordTestModelStateTransition.where(:active_record_test_model_id => state_machine.id).last
22
+
23
+ last_transition.event.to_s.should == 'start'
24
+ last_transition.from_state.should == 'waiting'
25
+ last_transition.to_state.should == 'started'
26
+ #last_transition.context.should_not be_nil
27
+ last_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
28
+ end
29
+
30
+ it "should log multiple events" do
31
+ lambda { state_machine.start && state_machine.stop && state_machine.start }.should change(ActiveRecordTestModelStateTransition, :count).by(3)
32
+ end
33
+
34
+ it "should do nothing when the transition is not executed successfully" do
35
+ lambda { state_machine.stop }.should_not change(ActiveRecordTestModelStateTransition, :count)
36
+ end
37
+ end
38
+
39
+ context 'on an object with a single state machine that wants to log a single context' do
40
+ before do
41
+ backend = StateMachine::AuditTrail::Backend.create_for_transition_class(ActiveRecordTestModelWithContextStateTransition, ActiveRecordTestModelWithContext, :context)
42
+ end
43
+
44
+ let!(:state_machine) { ActiveRecordTestModelWithContext.create! }
45
+
46
+ it "should log an event with all fields set correctly" do
47
+ state_machine.start!
48
+ last_transition = ActiveRecordTestModelWithContextStateTransition.where(:active_record_test_model_with_context_id => state_machine.id).last
49
+ last_transition.context.should == state_machine.context
50
+ end
51
+ end
52
+
53
+ context 'on an object with multiple state machines' do
54
+ let!(:state_machine) { ActiveRecordTestModelWithMultipleStateMachines.create! }
55
+
56
+ it "should log a state transition for the affected state machine" do
57
+ lambda { state_machine.begin_first! }.should change(ActiveRecordTestModelWithMultipleStateMachinesFirstTransition, :count).by(1)
58
+ end
59
+
60
+ it "should not log a state transition for the unaffected state machine" do
61
+ lambda { state_machine.begin_first! }.should_not change(ActiveRecordTestModelWithMultipleStateMachinesSecondTransition, :count)
62
+ end
63
+ end
64
+
65
+ context 'on an object with a state machine having an initial state' do
66
+ let(:state_machine_class) { ActiveRecordTestModelWithMultipleStateMachines }
67
+ let(:state_transition_class) { ActiveRecordTestModelWithMultipleStateMachinesFirstTransition }
68
+
69
+ it "should log a state transition for the inital state" do
70
+ lambda { state_machine_class.create! }.should change(state_transition_class, :count).by(1)
71
+ end
72
+
73
+ it "should only set the :to state for the initial transition" do
74
+ state_machine_class.create!
75
+ initial_transition = state_transition_class.last
76
+ initial_transition.event.should be_nil
77
+ initial_transition.from_state.should be_nil
78
+ initial_transition.to_state.should == 'beginning'
79
+ initial_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
80
+ end
81
+ end
82
+
83
+ context 'on an object with a state machine not having an initial state' do
84
+ let(:state_machine_class) { ActiveRecordTestModelWithMultipleStateMachines }
85
+ let(:state_transition_class) { ActiveRecordTestModelWithMultipleStateMachinesSecondTransition }
86
+
87
+ it "should not log a transition when the object is created" do
88
+ lambda { state_machine_class.create! }.should_not change(state_transition_class, :count)
89
+ end
90
+
91
+ it "should log a transition for the first event" do
92
+ lambda { state_machine_class.create.begin_second! }.should change(state_transition_class, :count).by(1)
93
+ end
94
+
95
+ it "should not set a value for the :from state on the first transition" do
96
+ state_machine_class.create.begin_second!
97
+ first_transition = state_transition_class.last
98
+ first_transition.event.to_s.should == 'begin_second'
99
+ first_transition.from_state.should be_nil
100
+ first_transition.to_state.should == 'beginning_second'
101
+ first_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
102
+ end
103
+ end
104
+
105
+ context 'on a class using STI' do
106
+ it "should properly grab the class name from STI models" do
107
+ m = ActiveRecordTestModelDescendant.create!
108
+ lambda { m.start! }.should_not raise_error
109
+ end
110
+ end
111
+
112
+ context 'on a class using STI with own state machine' do
113
+ it "should properly grab the class name from STI models" do
114
+ m = ActiveRecordTestModelDescendantWithOwnStateMachine.create!
115
+ lambda { m.complete! }.should_not raise_error
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachine::AuditTrail do
4
+
5
+ it "should have a VERSION constant" do
6
+ StateMachine::AuditTrail.const_defined?('VERSION').should be_true
7
+ end
8
+
9
+ it "should include the auditing module into StateMachine::Machine" do
10
+ StateMachine::Machine.included_modules.should include(StateMachine::AuditTrail::TransitionAuditing)
11
+ end
12
+ end
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+ require 'helpers/mongoid'
3
+
4
+ describe StateMachine::AuditTrail::Backend::Mongoid do
5
+
6
+ it "should create a Mongoid backend" do
7
+ backend = StateMachine::AuditTrail::Backend.create_for_transition_class(MongoidTestModelStateTransition, MongoidTestModel)
8
+ backend.should be_instance_of(StateMachine::AuditTrail::Backend::Mongoid)
9
+ end
10
+
11
+ context 'on an object with a single state machine' do
12
+ let!(:state_machine) { MongoidTestModel.create! }
13
+
14
+ it "should log an event with all fields set correctly" do
15
+ state_machine.start!
16
+ last_transition = MongoidTestModelStateTransition.where(:mongoid_test_model_id => state_machine.id).last
17
+
18
+ last_transition.event.to_s.should == 'start'
19
+ last_transition.from.should == 'waiting'
20
+ last_transition.to.should == 'started'
21
+ last_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
22
+ end
23
+
24
+ it "should log multiple events" do
25
+ lambda { state_machine.start && state_machine.stop && state_machine.start }.should change(MongoidTestModelStateTransition, :count).by(3)
26
+ end
27
+
28
+ it "should do nothing when the transition is not exectuted successfully" do
29
+ lambda { state_machine.stop }.should_not change(MongoidTestModelStateTransition, :count)
30
+ end
31
+ end
32
+
33
+ context 'on an object with multiple state machines' do
34
+ let!(:state_machine) { MongoidTestModelWithMultipleStateMachines.create! }
35
+
36
+ it "should log a state transition for the affected state machine" do
37
+ lambda { state_machine.begin_first! }.should change(MongoidTestModelWithMultipleStateMachinesFirstTransition, :count).by(1)
38
+ end
39
+
40
+ it "should not log a state transition for the unaffected state machine" do
41
+ lambda { state_machine.begin_first! }.should_not change(MongoidTestModelWithMultipleStateMachinesSecondTransition, :count)
42
+ end
43
+ end
44
+
45
+ context 'on an object with a state machine having an initial state' do
46
+ let(:state_machine_class) { MongoidTestModelWithMultipleStateMachines }
47
+ let(:state_transition_class) { MongoidTestModelWithMultipleStateMachinesFirstTransition }
48
+
49
+ it "should log a state transition for the inital state" do
50
+ lambda { state_machine_class.create! }.should change(state_transition_class, :count).by(1)
51
+ end
52
+
53
+ it "should only set the :to state for the initial transition" do
54
+ state_machine_class.create!
55
+ initial_transition = state_transition_class.last
56
+ initial_transition.event.should be_nil
57
+ initial_transition.from.should be_nil
58
+ initial_transition.to.should == 'beginning'
59
+ initial_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
60
+ end
61
+ end
62
+
63
+ context 'on an object with a state machine not having an initial state' do
64
+ let(:state_machine_class) { MongoidTestModelWithMultipleStateMachines }
65
+ let(:state_transition_class) { MongoidTestModelWithMultipleStateMachinesSecondTransition }
66
+
67
+ it "should not log a transition when the object is created" do
68
+ lambda { state_machine_class.create! }.should_not change(state_transition_class, :count)
69
+ end
70
+
71
+ it "should log a transition for the first event" do
72
+ lambda { state_machine_class.create.begin_second! }.should change(state_transition_class, :count).by(1)
73
+ end
74
+
75
+ it "should not set a value for the :from state on the first transition" do
76
+ state_machine_class.create.begin_second!
77
+ first_transition = state_transition_class.last
78
+ first_transition.event.to_s.should == 'begin_second'
79
+ first_transition.from.should be_nil
80
+ first_transition.to.should == 'beginning_second'
81
+ first_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
82
+ end
83
+ end
84
+
85
+ context 'on a class using STI' do
86
+ it "should properly grab the class name from STI models" do
87
+ m = MongoidTestModelDescendant.create!
88
+ lambda { m.start! }.should_not raise_error
89
+ end
90
+ end
91
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsmestad-audit_trail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Willem van Bergen
8
+ - Jesse Storimer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: state_machine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '2'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: '2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: activerecord
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '3'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '3'
70
+ - !ruby/object:Gem::Dependency
71
+ name: sqlite3
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: mongoid
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ version: '2'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ~>
96
+ - !ruby/object:Gem::Version
97
+ version: '2'
98
+ - !ruby/object:Gem::Dependency
99
+ name: bson_ext
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ description: Log transitions on a state machine to support auditing and business process
113
+ analytics.
114
+ email:
115
+ - willem@shopify.com
116
+ - jesse@shopify.com
117
+ executables: []
118
+ extensions: []
119
+ extra_rdoc_files: []
120
+ files:
121
+ - .gitignore
122
+ - .travis.yml
123
+ - Gemfile
124
+ - LICENSE
125
+ - README.rdoc
126
+ - Rakefile
127
+ - jsmestad-audit_trail.gemspec
128
+ - lib/state_machine-audit_trail.rb
129
+ - lib/state_machine/audit_trail.rb
130
+ - lib/state_machine/audit_trail/backend.rb
131
+ - lib/state_machine/audit_trail/backend/active_record.rb
132
+ - lib/state_machine/audit_trail/backend/mongoid.rb
133
+ - lib/state_machine/audit_trail/railtie.rb
134
+ - lib/state_machine/audit_trail/transition_auditing.rb
135
+ - lib/state_machine/audit_trail/version.rb
136
+ - lib/state_machine/audit_trail_generator.rb
137
+ - spec/helpers/active_record.rb
138
+ - spec/helpers/mongoid.rb
139
+ - spec/spec_helper.rb
140
+ - spec/state_machine/active_record_spec.rb
141
+ - spec/state_machine/audit_trail_spec.rb
142
+ - spec/state_machine/mongoid_spec.rb
143
+ homepage: https://github.com/wvanbergen/state_machine-audit_trail
144
+ licenses:
145
+ - MIT
146
+ metadata: {}
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - '>='
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubyforge_project:
163
+ rubygems_version: 2.0.3
164
+ signing_key:
165
+ specification_version: 4
166
+ summary: Log transitions on a state machine to support auditing and business process
167
+ analytics.
168
+ test_files:
169
+ - spec/helpers/active_record.rb
170
+ - spec/helpers/mongoid.rb
171
+ - spec/spec_helper.rb
172
+ - spec/state_machine/active_record_spec.rb
173
+ - spec/state_machine/audit_trail_spec.rb
174
+ - spec/state_machine/mongoid_spec.rb
175
+ has_rdoc: