state_machine-audit_trail 0.1.0 → 0.1.1

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