state_machine-audit_trail 0.1.0 → 0.1.1

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.
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  rvm:
2
2
  - 1.8.7
3
- - 1.9.1
4
3
  - 1.9.2
4
+ - 1.9.3
5
5
  - ruby-head
6
6
  - ree
7
7
  - rbx
8
- - jruby
data/README.rdoc CHANGED
@@ -2,18 +2,25 @@
2
2
 
3
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
4
 
5
- Note: while the state_machine gem integrates with multiple ORMs, this plugin currently only has an ActiveRecord backend. It should be easy to add support for other ActiveModel-based ORMs though.
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!
6
13
 
7
14
  == Usage
8
15
 
9
16
  First, make the gem available by adding it to your <tt>Gemfile</tt>, and run <tt>bundle install</tt>:
10
17
 
11
18
  gem 'state_machine-audit_trail'
12
-
13
- Create a model/table that holds the audit trail. The table needs to have a foreign key to the original object, am "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.
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.
14
21
 
15
22
  rails generate state_machine:audit_trail <model> <state_attribute>
16
-
23
+
17
24
  For a model called "Model", and a state attribute "state", this will generate the ModelStateTransition model and an accompanying migration.
18
25
 
19
26
  Next, tell your state machine you want to store an audit trail:
@@ -32,6 +39,9 @@ If your audit trail model does not use the default naming scheme, provide it usi
32
39
 
33
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.
34
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
+
35
45
  == About
36
46
 
37
- This plugin is written by Jesse Storimer and Willem van Bergen for Shopify. It is released under the MIT license (see LICENSE)
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).
@@ -1,9 +1,20 @@
1
1
  class StateMachine::AuditTrail::Backend::ActiveRecord < StateMachine::AuditTrail::Backend
2
+ attr_accessor :context_to_log
2
3
 
4
+ def initialize(transition_class, context_to_log)
5
+ self.context_to_log = context_to_log
6
+ super transition_class
7
+ end
3
8
  def log(object, event, from, to, timestamp = Time.now)
4
9
  # Let ActiveRecord manage the timestamp for us so it does the
5
10
  # right thing with regards to timezones.
6
- transition_class.create(foreign_key_field(object) => object.id, :event => event, :from => from, :to => to)
11
+ params = {foreign_key_field(object) => object.id, :event => event, :from => from, :to => to}
12
+
13
+ if context_to_log.is_a?(Array.class)
14
+ elsif !context_to_log.nil?
15
+ params[context_to_log] = object.send(context_to_log)
16
+ end
17
+ transition_class.create(params)
7
18
  end
8
19
 
9
20
  def foreign_key_field(object)
@@ -13,10 +13,13 @@ class StateMachine::AuditTrail::Backend < Struct.new(:transition_class)
13
13
  #
14
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
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)
16
+ def self.create_for_transition_class(transition_class, context_to_log = nil)
17
17
  if Object.const_defined?('ActiveRecord') && transition_class.ancestors.include?(::ActiveRecord::Base)
18
- return StateMachine::AuditTrail::Backend::ActiveRecord.new(transition_class)
18
+ return StateMachine::AuditTrail::Backend::ActiveRecord.new(transition_class, context_to_log)
19
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
+
20
23
  return StateMachine::AuditTrail::Backend::Mongoid.new(transition_class)
21
24
  else
22
25
  raise NotImplemented, "Only support for ActiveRecord and Mongoid is included at this time"
@@ -7,24 +7,25 @@ module StateMachine::AuditTrail::TransitionAuditing
7
7
 
8
8
  # Public tells the state machine to hook in the appropriate before / after behaviour
9
9
  #
10
- # options: a Hash of options. keys that are used are :to => CustomTransitionClass
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
11
12
  def store_audit_trail(options = {})
12
13
  state_machine = self
13
14
  state_machine.transition_class_name = (options[:to] || default_transition_class_name).to_s
14
15
  state_machine.after_transition do |object, transition|
15
- state_machine.audit_trail.log(object, transition.event, transition.from, transition.to)
16
+ state_machine.audit_trail(options[:context_to_log]).log(object, transition.event, transition.from, transition.to)
16
17
  end
17
18
 
18
19
  state_machine.owner_class.after_create do |object|
19
20
  if !object.send(state_machine.attribute).nil?
20
- state_machine.audit_trail.log(object, nil, nil, object.send(state_machine.attribute))
21
+ state_machine.audit_trail(options[:context_to_log]).log(object, nil, nil, object.send(state_machine.attribute))
21
22
  end
22
23
  end
23
24
  end
24
25
 
25
26
  # Public returns an instance of the class which does the actual audit trail logging
26
- def audit_trail
27
- @transition_auditor ||= StateMachine::AuditTrail::Backend.create_for_transition_class(transition_class)
27
+ def audit_trail(context_to_log = nil)
28
+ @transition_auditor ||= StateMachine::AuditTrail::Backend.create_for_transition_class(transition_class, context_to_log)
28
29
  end
29
30
 
30
31
  private
@@ -2,7 +2,7 @@ require 'state_machine'
2
2
 
3
3
  module StateMachine::AuditTrail
4
4
 
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
 
7
7
  def self.setup
8
8
  StateMachine::Machine.send(:include, StateMachine::AuditTrail::TransitionAuditing)
@@ -10,6 +10,12 @@ ActiveRecord::Base.connection.create_table(:active_record_test_models) do |t|
10
10
  t.timestamps
11
11
  end
12
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
+
13
19
  ActiveRecord::Base.connection.create_table(:active_record_test_model_with_multiple_state_machines) do |t|
14
20
  t.string :first
15
21
  t.string :second
@@ -21,6 +27,10 @@ class ActiveRecordTestModelStateTransition < ActiveRecord::Base
21
27
  belongs_to :test_model
22
28
  end
23
29
 
30
+ class ActiveRecordTestModelWithContextStateTransition < ActiveRecord::Base
31
+ belongs_to :test_model
32
+ end
33
+
24
34
  class ActiveRecordTestModelWithMultipleStateMachinesFirstTransition < ActiveRecord::Base
25
35
  belongs_to :test_model
26
36
  end
@@ -32,16 +42,34 @@ end
32
42
  class ActiveRecordTestModel < ActiveRecord::Base
33
43
 
34
44
  state_machine :state, :initial => :waiting do # log initial state?
35
- store_audit_trail
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
36
60
 
37
61
  event :start do
38
62
  transition [:waiting, :stopped] => :started
39
63
  end
40
-
64
+
41
65
  event :stop do
42
66
  transition :started => :stopped
43
67
  end
44
68
  end
69
+
70
+ def context
71
+ "Some context"
72
+ end
45
73
  end
46
74
 
47
75
  class ActiveRecordTestModelDescendant < ActiveRecordTestModel
@@ -50,15 +78,15 @@ end
50
78
  class ActiveRecordTestModelWithMultipleStateMachines < ActiveRecord::Base
51
79
 
52
80
  state_machine :first, :initial => :beginning do
53
- store_audit_trail
81
+ store_audit_trail
54
82
 
55
83
  event :begin_first do
56
84
  transition :beginning => :end
57
85
  end
58
86
  end
59
-
87
+
60
88
  state_machine :second do
61
- store_audit_trail
89
+ store_audit_trail
62
90
 
63
91
  event :begin_second do
64
92
  transition nil => :beginning_second
@@ -68,16 +96,18 @@ end
68
96
 
69
97
  def create_transition_table(owner_class, state)
70
98
  class_name = "#{owner_class.name}#{state.to_s.camelize}Transition"
71
-
72
99
  ActiveRecord::Base.connection.create_table(class_name.tableize) do |t|
100
+ add_context = owner_class.instance_methods.include? :context
73
101
  t.integer owner_class.name.foreign_key
74
102
  t.string :event
75
103
  t.string :from
76
104
  t.string :to
105
+ t.string :context if add_context
77
106
  t.datetime :created_at
78
107
  end
79
108
  end
80
109
 
81
- create_transition_table(ActiveRecordTestModel, :state)
82
- create_transition_table(ActiveRecordTestModelWithMultipleStateMachines, :first)
110
+ create_transition_table(ActiveRecordTestModel, :state)
111
+ create_transition_table(ActiveRecordTestModelWithContext, :state)
112
+ create_transition_table(ActiveRecordTestModelWithMultipleStateMachines, :first)
83
113
  create_transition_table(ActiveRecordTestModelWithMultipleStateMachines, :second)
@@ -2,15 +2,15 @@ require 'spec_helper'
2
2
  require 'helpers/active_record'
3
3
 
4
4
  describe StateMachine::AuditTrail::Backend::ActiveRecord do
5
-
5
+
6
6
  it "should create an ActiveRecord backend" do
7
7
  backend = StateMachine::AuditTrail::Backend.create_for_transition_class(ActiveRecordTestModelStateTransition)
8
8
  backend.should be_instance_of(StateMachine::AuditTrail::Backend::ActiveRecord)
9
- end
10
-
9
+ end
10
+
11
11
  context 'on an object with a single state machine' do
12
12
  let!(:state_machine) { ActiveRecordTestModel.create! }
13
-
13
+
14
14
  it "should log an event with all fields set correctly" do
15
15
  state_machine.start!
16
16
  last_transition = ActiveRecordTestModelStateTransition.where(:active_record_test_model_id => state_machine.id).last
@@ -18,21 +18,36 @@ describe StateMachine::AuditTrail::Backend::ActiveRecord do
18
18
  last_transition.event.to_s.should == 'start'
19
19
  last_transition.from.should == 'waiting'
20
20
  last_transition.to.should == 'started'
21
+ #last_transition.context.should_not be_nil
21
22
  last_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
22
23
  end
23
-
24
+
24
25
  it "should log multiple events" do
25
26
  lambda { state_machine.start && state_machine.stop && state_machine.start }.should change(ActiveRecordTestModelStateTransition, :count).by(3)
26
27
  end
27
-
28
- it "should do nothing when the transition is not exectuted successfully" do
28
+
29
+ it "should do nothing when the transition is not executed successfully" do
29
30
  lambda { state_machine.stop }.should_not change(ActiveRecordTestModelStateTransition, :count)
30
31
  end
31
32
  end
32
-
33
+
34
+ context 'on an object with a single state machine that wants to log a single context' do
35
+ before do
36
+ backend = StateMachine::AuditTrail::Backend.create_for_transition_class(ActiveRecordTestModelWithContextStateTransition, :context)
37
+ end
38
+
39
+ let!(:state_machine) { ActiveRecordTestModelWithContext.create! }
40
+
41
+ it "should log an event with all fields set correctly" do
42
+ state_machine.start!
43
+ last_transition = ActiveRecordTestModelWithContextStateTransition.where(:active_record_test_model_with_context_id => state_machine.id).last
44
+ last_transition.context.should == state_machine.context
45
+ end
46
+ end
47
+
33
48
  context 'on an object with multiple state machines' do
34
49
  let!(:state_machine) { ActiveRecordTestModelWithMultipleStateMachines.create! }
35
-
50
+
36
51
  it "should log a state transition for the affected state machine" do
37
52
  lambda { state_machine.begin_first! }.should change(ActiveRecordTestModelWithMultipleStateMachinesFirstTransition, :count).by(1)
38
53
  end
@@ -41,15 +56,15 @@ describe StateMachine::AuditTrail::Backend::ActiveRecord do
41
56
  lambda { state_machine.begin_first! }.should_not change(ActiveRecordTestModelWithMultipleStateMachinesSecondTransition, :count)
42
57
  end
43
58
  end
44
-
59
+
45
60
  context 'on an object with a state machine having an initial state' do
46
61
  let(:state_machine_class) { ActiveRecordTestModelWithMultipleStateMachines }
47
62
  let(:state_transition_class) { ActiveRecordTestModelWithMultipleStateMachinesFirstTransition }
48
-
63
+
49
64
  it "should log a state transition for the inital state" do
50
65
  lambda { state_machine_class.create! }.should change(state_transition_class, :count).by(1)
51
66
  end
52
-
67
+
53
68
  it "should only set the :to state for the initial transition" do
54
69
  state_machine_class.create!
55
70
  initial_transition = state_transition_class.last
@@ -59,11 +74,11 @@ describe StateMachine::AuditTrail::Backend::ActiveRecord do
59
74
  initial_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
60
75
  end
61
76
  end
62
-
77
+
63
78
  context 'on an object with a state machine not having an initial state' do
64
79
  let(:state_machine_class) { ActiveRecordTestModelWithMultipleStateMachines }
65
80
  let(:state_transition_class) { ActiveRecordTestModelWithMultipleStateMachinesSecondTransition }
66
-
81
+
67
82
  it "should not log a transition when the object is created" do
68
83
  lambda { state_machine_class.create! }.should_not change(state_transition_class, :count)
69
84
  end
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |s|
3
3
  s.name = "state_machine-audit_trail"
4
- s.version = "0.1.0"
4
+ s.version = "0.1.1"
5
5
  s.platform = Gem::Platform::RUBY
6
6
  s.authors = ["Willem van Bergen", "Jesse Storimer"]
7
7
  s.email = ["willem@shopify.com", "jesse@shopify.com"]
metadata CHANGED
@@ -1,118 +1,103 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: state_machine-audit_trail
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 1
8
- - 0
9
- version: 0.1.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Willem van Bergen
13
9
  - Jesse Storimer
14
10
  autorequire:
15
11
  bindir: bin
16
12
  cert_chain: []
17
-
18
- date: 2012-02-20 00:00:00 -05:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
13
+ date: 2012-07-28 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
22
16
  name: state_machine
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- segments:
29
- - 0
30
- version: "0"
17
+ requirement: &70159599977080 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
31
23
  type: :runtime
32
- version_requirements: *id001
33
- - !ruby/object:Gem::Dependency
34
- name: rake
35
24
  prerelease: false
36
- requirement: &id002 !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- segments:
41
- - 0
42
- version: "0"
25
+ version_requirements: *70159599977080
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: &70159599976060 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
43
34
  type: :development
44
- version_requirements: *id002
45
- - !ruby/object:Gem::Dependency
46
- name: rspec
47
35
  prerelease: false
48
- requirement: &id003 !ruby/object:Gem::Requirement
49
- requirements:
36
+ version_requirements: *70159599976060
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ requirement: &70159599992820 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
50
42
  - - ~>
51
- - !ruby/object:Gem::Version
52
- segments:
53
- - 2
54
- version: "2"
43
+ - !ruby/object:Gem::Version
44
+ version: '2'
55
45
  type: :development
56
- version_requirements: *id003
57
- - !ruby/object:Gem::Dependency
58
- name: activerecord
59
46
  prerelease: false
60
- requirement: &id004 !ruby/object:Gem::Requirement
61
- requirements:
47
+ version_requirements: *70159599992820
48
+ - !ruby/object:Gem::Dependency
49
+ name: activerecord
50
+ requirement: &70159599991160 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
62
53
  - - ~>
63
- - !ruby/object:Gem::Version
64
- segments:
65
- - 3
66
- version: "3"
54
+ - !ruby/object:Gem::Version
55
+ version: '3'
67
56
  type: :development
68
- version_requirements: *id004
69
- - !ruby/object:Gem::Dependency
70
- name: sqlite3
71
57
  prerelease: false
72
- requirement: &id005 !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- segments:
77
- - 0
78
- version: "0"
58
+ version_requirements: *70159599991160
59
+ - !ruby/object:Gem::Dependency
60
+ name: sqlite3
61
+ requirement: &70159599989960 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
79
67
  type: :development
80
- version_requirements: *id005
81
- - !ruby/object:Gem::Dependency
82
- name: mongoid
83
68
  prerelease: false
84
- requirement: &id006 !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- segments:
89
- - 0
90
- version: "0"
69
+ version_requirements: *70159599989960
70
+ - !ruby/object:Gem::Dependency
71
+ name: mongoid
72
+ requirement: &70159599988580 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
91
78
  type: :development
92
- version_requirements: *id006
93
- - !ruby/object:Gem::Dependency
94
- name: bson_ext
95
79
  prerelease: false
96
- requirement: &id007 !ruby/object:Gem::Requirement
97
- requirements:
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- segments:
101
- - 0
102
- version: "0"
80
+ version_requirements: *70159599988580
81
+ - !ruby/object:Gem::Dependency
82
+ name: bson_ext
83
+ requirement: &70159600002560 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
103
89
  type: :development
104
- version_requirements: *id007
105
- description: Log transitions on a state machine to support auditing and business process analytics.
106
- email:
90
+ prerelease: false
91
+ version_requirements: *70159600002560
92
+ description: Log transitions on a state machine to support auditing and business process
93
+ analytics.
94
+ email:
107
95
  - willem@shopify.com
108
96
  - jesse@shopify.com
109
97
  executables: []
110
-
111
98
  extensions: []
112
-
113
99
  extra_rdoc_files: []
114
-
115
- files:
100
+ files:
116
101
  - .gitignore
117
102
  - .travis.yml
118
103
  - Gemfile
@@ -135,37 +120,38 @@ files:
135
120
  - spec/state_machine/mongoid_spec.rb
136
121
  - state_machine-audit_trail.gemspec
137
122
  - tasks/github_gem.rb
138
- has_rdoc: true
139
123
  homepage: https://github.com/wvanbergen/state_machine-audit_trail
140
124
  licenses: []
141
-
142
125
  post_install_message:
143
126
  rdoc_options: []
144
-
145
- require_paths:
127
+ require_paths:
146
128
  - lib
147
- required_ruby_version: !ruby/object:Gem::Requirement
148
- requirements:
149
- - - ">="
150
- - !ruby/object:Gem::Version
151
- segments:
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ segments:
152
136
  - 0
153
- version: "0"
154
- required_rubygems_version: !ruby/object:Gem::Requirement
155
- requirements:
156
- - - ">="
157
- - !ruby/object:Gem::Version
158
- segments:
137
+ hash: 3519932254629100155
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ segments:
159
145
  - 0
160
- version: "0"
146
+ hash: 3519932254629100155
161
147
  requirements: []
162
-
163
148
  rubyforge_project: state_machine
164
- rubygems_version: 1.3.6
149
+ rubygems_version: 1.8.16
165
150
  signing_key:
166
151
  specification_version: 3
167
- summary: Log transitions on a state machine to support auditing and business process analytics.
168
- test_files:
152
+ summary: Log transitions on a state machine to support auditing and business process
153
+ analytics.
154
+ test_files:
169
155
  - spec/state_machine/active_record_spec.rb
170
156
  - spec/state_machine/audit_trail_spec.rb
171
157
  - spec/state_machine/mongoid_spec.rb