workflow 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +24 -0
- data/VERSION +1 -1
- data/lib/workflow.rb +44 -16
- data/test/advanced_hooks_and_validation_test.rb +118 -0
- data/test/before_transition_test.rb +36 -0
- data/test/main_test.rb +19 -0
- data/test/multiple_workflows_test.rb +9 -2
- metadata +7 -3
data/README.markdown
CHANGED
@@ -73,6 +73,10 @@ Events are actually instance methods on a workflow, and depending on the
|
|
73
73
|
state you're in, you'll have a different set of events used to
|
74
74
|
transition to other states.
|
75
75
|
|
76
|
+
It is also easy to check, if a certain transition is possible from the
|
77
|
+
current state . `article.can_submit?` checks if there is a `:submit`
|
78
|
+
event (transition) defined for the current state.
|
79
|
+
|
76
80
|
|
77
81
|
Installation
|
78
82
|
------------
|
@@ -246,6 +250,12 @@ couchrest library.
|
|
246
250
|
Please also have a look at
|
247
251
|
[the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
|
248
252
|
|
253
|
+
Integration with Mongoid
|
254
|
+
------------------------
|
255
|
+
|
256
|
+
You can integrate with Mongoid following the example above for CouchDB, but there is a gem that does that for you (and includes extensive tests):
|
257
|
+
[workflow_on_mongoid](http://github.com/bowsersenior/workflow_on_mongoid)
|
258
|
+
|
249
259
|
Accessing your workflow specification
|
250
260
|
-------------------------------------
|
251
261
|
|
@@ -308,6 +318,10 @@ logging then you can use the universal `on_transition` hook:
|
|
308
318
|
end
|
309
319
|
end
|
310
320
|
|
321
|
+
Please also have a look at the [advanced end to end
|
322
|
+
example][advanced_hooks_and_validation_test].
|
323
|
+
|
324
|
+
[advanced_hooks_and_validation_test]: http://github.com/geekq/workflow/blob/master/test/advanced_hooks_and_validation_test.rb
|
311
325
|
|
312
326
|
### Guards
|
313
327
|
|
@@ -332,11 +346,13 @@ You can check `halted?` and `halted_because` values later.
|
|
332
346
|
|
333
347
|
The whole event sequence is as follows:
|
334
348
|
|
349
|
+
* before_transition
|
335
350
|
* event specific action
|
336
351
|
* on_transition (if action did not halt)
|
337
352
|
* on_exit
|
338
353
|
* PERSIST WORKFLOW STATE, i.e. transition
|
339
354
|
* on_entry
|
355
|
+
* after_transition
|
340
356
|
|
341
357
|
|
342
358
|
Multiple Workflows
|
@@ -446,6 +462,14 @@ when using both a block and a callback method for an event, the block executes p
|
|
446
462
|
Changelog
|
447
463
|
---------
|
448
464
|
|
465
|
+
### New in the version 0.8.0
|
466
|
+
|
467
|
+
* check if a certain transition possible from the current state with
|
468
|
+
`can_....?`
|
469
|
+
* fix workflow_state persistence for multiple_workflows example
|
470
|
+
* add before_transition and after_transition hooks as suggested by
|
471
|
+
[kasperbn](https://github.com/kasperbn)
|
472
|
+
|
449
473
|
### New in the version 0.7.0
|
450
474
|
|
451
475
|
* fix issue#10 Workflow::create_workflow_diagram documentation and path
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.0
|
data/lib/workflow.rb
CHANGED
@@ -5,7 +5,8 @@ module Workflow
|
|
5
5
|
|
6
6
|
class Specification
|
7
7
|
|
8
|
-
attr_accessor :states, :initial_state, :meta,
|
8
|
+
attr_accessor :states, :initial_state, :meta,
|
9
|
+
:on_transition_proc, :before_transition_proc, :after_transition_proc
|
9
10
|
|
10
11
|
def initialize(meta = {}, &specification)
|
11
12
|
@states = Hash.new
|
@@ -45,6 +46,14 @@ module Workflow
|
|
45
46
|
@scoped_state.on_exit = proc
|
46
47
|
end
|
47
48
|
|
49
|
+
def after_transition(&proc)
|
50
|
+
@after_transition_proc = proc
|
51
|
+
end
|
52
|
+
|
53
|
+
def before_transition(&proc)
|
54
|
+
@before_transition_proc = proc
|
55
|
+
end
|
56
|
+
|
48
57
|
def on_transition(&proc)
|
49
58
|
@on_transition_proc = proc
|
50
59
|
end
|
@@ -123,6 +132,10 @@ module Workflow
|
|
123
132
|
define_method "#{event_name}!".to_sym do |*args|
|
124
133
|
process_event!(event_name, *args)
|
125
134
|
end
|
135
|
+
|
136
|
+
define_method "can_#{event_name}?" do
|
137
|
+
return self.current_state.events.include? event_name
|
138
|
+
end
|
126
139
|
end
|
127
140
|
end
|
128
141
|
end
|
@@ -155,17 +168,29 @@ module Workflow
|
|
155
168
|
if event.nil?
|
156
169
|
@halted_because = nil
|
157
170
|
@halted = false
|
171
|
+
|
172
|
+
check_transition(event)
|
173
|
+
|
174
|
+
from = current_state
|
175
|
+
to = spec.states[event.transitions_to]
|
176
|
+
|
177
|
+
run_before_transition(current_state, spec.states[event.transitions_to], name, *args)
|
178
|
+
return false if @halted
|
179
|
+
|
158
180
|
return_value = run_action(event.action, *args) || run_action_callback(event.name, *args)
|
159
|
-
if @halted
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
181
|
+
return false if @halted
|
182
|
+
|
183
|
+
run_on_transition(from, to, name, *args)
|
184
|
+
|
185
|
+
run_on_exit(from, to, name, *args)
|
186
|
+
|
187
|
+
transition_value = persist_workflow_state to.to_s
|
188
|
+
|
189
|
+
run_on_entry(to, from, name, *args)
|
190
|
+
|
191
|
+
run_after_transition(from, to, name, *args)
|
192
|
+
|
193
|
+
return_value.nil? ? transition_value : return_value
|
169
194
|
end
|
170
195
|
|
171
196
|
def halt(reason = nil)
|
@@ -206,17 +231,20 @@ module Workflow
|
|
206
231
|
end
|
207
232
|
end
|
208
233
|
|
209
|
-
def
|
210
|
-
|
211
|
-
|
212
|
-
run_on_entry(to, from, name, *args)
|
213
|
-
val
|
234
|
+
def run_before_transition(from, to, event, *args)
|
235
|
+
instance_exec(from.name, to.name, event, *args, &spec.before_transition_proc) if
|
236
|
+
spec.before_transition_proc
|
214
237
|
end
|
215
238
|
|
216
239
|
def run_on_transition(from, to, event, *args)
|
217
240
|
instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc
|
218
241
|
end
|
219
242
|
|
243
|
+
def run_after_transition(from, to, event, *args)
|
244
|
+
instance_exec(from.name, to.name, event, *args, &spec.after_transition_proc) if
|
245
|
+
spec.after_transition_proc
|
246
|
+
end
|
247
|
+
|
220
248
|
def run_action(action, *args)
|
221
249
|
instance_exec(*args, &action) if action
|
222
250
|
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
require 'active_record'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'workflow'
|
7
|
+
|
8
|
+
ActiveRecord::Migration.verbose = false
|
9
|
+
|
10
|
+
# Transition based validation
|
11
|
+
# ---------------------------
|
12
|
+
# If you are using ActiveRecord you might want to define different validations
|
13
|
+
# for different transitions. There is a `validates_presence_of` hook that let's
|
14
|
+
# you specify the attributes that need to be present for an successful transition.
|
15
|
+
# If the object is not valid at the end of the transition event the transition
|
16
|
+
# is halted and a TransitionHalted exception is thrown.
|
17
|
+
#
|
18
|
+
# Here is a sample that illustrates how to use the presence validation:
|
19
|
+
# (use case suggested by http://github.com/southdesign)
|
20
|
+
class Article < ActiveRecord::Base
|
21
|
+
include Workflow
|
22
|
+
workflow do
|
23
|
+
state :new do
|
24
|
+
event :accept, :transitions_to => :accepted, :meta => {:validates_presence_of => [:title, :body]}
|
25
|
+
event :reject, :transitions_to => :rejected
|
26
|
+
end
|
27
|
+
state :accepted do
|
28
|
+
event :blame, :transitions_to => :blamed, :meta => {:validates_presence_of => [:title, :body, :blame_reason]}
|
29
|
+
event :delete, :transitions_to => :deleted
|
30
|
+
end
|
31
|
+
state :rejected do
|
32
|
+
event :delete, :transitions_to => :deleted
|
33
|
+
end
|
34
|
+
state :blamed do
|
35
|
+
event :delete, :transitions_to => :deleted
|
36
|
+
end
|
37
|
+
state :deleted do
|
38
|
+
event :accept, :transitions_to => :accepted
|
39
|
+
end
|
40
|
+
|
41
|
+
on_transition do |from, to, triggering_event, *event_args|
|
42
|
+
if self.class.superclass.to_s.split("::").first == "ActiveRecord"
|
43
|
+
singleton = class << self; self end
|
44
|
+
validations = Proc.new {}
|
45
|
+
|
46
|
+
meta = Article.workflow_spec.states[from].events[triggering_event].meta
|
47
|
+
fields_to_validate = meta[:validates_presence_of]
|
48
|
+
if fields_to_validate
|
49
|
+
validations = Proc.new {
|
50
|
+
errors.add_on_blank(fields_to_validate) if fields_to_validate
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
singleton.send :define_method, :validate, &validations
|
55
|
+
halt! "Event[#{triggering_event}]'s transitions_to[#{to}] is not valid." if self.invalid?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class AdvancedHooksAndValidationTest < ActiveRecordTestCase
|
62
|
+
|
63
|
+
def setup
|
64
|
+
super
|
65
|
+
|
66
|
+
ActiveRecord::Schema.define do
|
67
|
+
create_table :articles do |t|
|
68
|
+
t.string :title
|
69
|
+
t.string :body
|
70
|
+
t.string :blame_reason
|
71
|
+
t.string :reject_reason
|
72
|
+
t.string :workflow_state
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new1', NULL, NULL, NULL, 'new')"
|
77
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new2', 'some content', NULL, NULL, 'new')"
|
78
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('accepted1', 'some content', NULL, NULL, 'accepted')"
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def assert_state(title, expected_state, klass = Order)
|
83
|
+
o = klass.find_by_title(title)
|
84
|
+
assert_equal expected_state, o.read_attribute(klass.workflow_column)
|
85
|
+
o
|
86
|
+
end
|
87
|
+
|
88
|
+
test 'deny transition from new to accepted because of the missing presence of the body' do
|
89
|
+
a = Article.find_by_title('new1');
|
90
|
+
assert_raise Workflow::TransitionHalted do
|
91
|
+
a.accept!
|
92
|
+
end
|
93
|
+
assert_state 'new1', 'new', Article
|
94
|
+
end
|
95
|
+
|
96
|
+
test 'allow transition from new to accepted because body is present this time' do
|
97
|
+
a = Article.find_by_title('new2');
|
98
|
+
assert a.accept!
|
99
|
+
assert_state 'new2', 'accepted', Article
|
100
|
+
end
|
101
|
+
|
102
|
+
test 'allow transition from accepted to blamed because of a blame_reason' do
|
103
|
+
a = Article.find_by_title('accepted1');
|
104
|
+
a.blame_reason = "Provocant thesis"
|
105
|
+
assert a.blame!
|
106
|
+
assert_state 'accepted1', 'blamed', Article
|
107
|
+
end
|
108
|
+
|
109
|
+
test 'deny transition from accepted to blamed because of no blame_reason' do
|
110
|
+
a = Article.find_by_title('accepted1');
|
111
|
+
assert_raise Workflow::TransitionHalted do
|
112
|
+
assert a.blame!
|
113
|
+
end
|
114
|
+
assert_state 'accepted1', 'accepted', Article
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'workflow'
|
3
|
+
|
4
|
+
class BeforeTransitionTest < Test::Unit::TestCase
|
5
|
+
class MyFlow
|
6
|
+
attr_reader :history
|
7
|
+
def initialize
|
8
|
+
@history = []
|
9
|
+
end
|
10
|
+
|
11
|
+
include Workflow
|
12
|
+
workflow do
|
13
|
+
state :first do
|
14
|
+
event :forward, :transitions_to => :second do
|
15
|
+
@history << 'forward'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
state :second do
|
19
|
+
event :back, :transitions_to => :first do
|
20
|
+
@history << 'back'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
before_transition { @history << 'before' }
|
25
|
+
after_transition { @history << 'after' }
|
26
|
+
on_transition { @history << 'on' }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'that before_transition is run before the action' do
|
31
|
+
flow = MyFlow.new
|
32
|
+
flow.forward!
|
33
|
+
flow.back!
|
34
|
+
assert flow.history == ['before', 'forward', 'on', 'after', 'before', 'back', 'on', 'after']
|
35
|
+
end
|
36
|
+
end
|
data/test/main_test.rb
CHANGED
@@ -437,6 +437,25 @@ class MainTest < ActiveRecordTestCase
|
|
437
437
|
assert article.rejected?, 'Transition should happen now'
|
438
438
|
end
|
439
439
|
|
440
|
+
test 'can fire event?' do
|
441
|
+
c = Class.new do
|
442
|
+
include Workflow
|
443
|
+
workflow do
|
444
|
+
state :newborn do
|
445
|
+
event :go_to_school, :transitions_to => :schoolboy
|
446
|
+
end
|
447
|
+
state :schoolboy do
|
448
|
+
event :go_to_college, :transitions_to => :student
|
449
|
+
end
|
450
|
+
state :student
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
human = c.new
|
455
|
+
assert human.can_go_to_school?
|
456
|
+
assert_equal false, human.can_go_to_college?
|
457
|
+
end
|
458
|
+
|
440
459
|
test 'workflow graph generation' do
|
441
460
|
Dir.chdir('tmp') do
|
442
461
|
capture_streams do
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'workflow'
|
2
3
|
class MultipleWorkflowsTest < ActiveRecordTestCase
|
3
4
|
|
4
5
|
test 'multiple workflows' do
|
@@ -15,12 +16,14 @@ class MultipleWorkflowsTest < ActiveRecordTestCase
|
|
15
16
|
exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking2', 'initial', 'workflow_2')"
|
16
17
|
|
17
18
|
class Booking < ActiveRecord::Base
|
19
|
+
|
20
|
+
include Workflow
|
21
|
+
|
18
22
|
def initialize_workflow
|
19
23
|
# define workflow per object instead of per class
|
20
24
|
case workflow_type
|
21
25
|
when 'workflow_1'
|
22
26
|
class << self
|
23
|
-
include Workflow
|
24
27
|
workflow do
|
25
28
|
state :initial do
|
26
29
|
event :progress, :transitions_to => :last
|
@@ -30,7 +33,6 @@ class MultipleWorkflowsTest < ActiveRecordTestCase
|
|
30
33
|
end
|
31
34
|
when 'workflow_2'
|
32
35
|
class << self
|
33
|
-
include Workflow
|
34
36
|
workflow do
|
35
37
|
state :initial do
|
36
38
|
event :progress, :transitions_to => :intermediate
|
@@ -67,6 +69,11 @@ class MultipleWorkflowsTest < ActiveRecordTestCase
|
|
67
69
|
assert booking1.workflow_spec, 'can access the individual workflow specification'
|
68
70
|
assert_equal 2, booking1.workflow_spec.states.length
|
69
71
|
assert_equal 3, booking2.workflow_spec.states.length
|
72
|
+
|
73
|
+
# check persistence
|
74
|
+
booking2reloaded = Booking.find_by_title('booking2')
|
75
|
+
booking2reloaded.initialize_workflow
|
76
|
+
assert booking2reloaded.intermediate?, 'persistence of workflow state does not work'
|
70
77
|
end
|
71
78
|
|
72
79
|
class Object
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 8
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.8.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Vladimir Dobriakov
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-09
|
17
|
+
date: 2010-12-09 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
@@ -33,6 +33,8 @@ files:
|
|
33
33
|
- Rakefile
|
34
34
|
- VERSION
|
35
35
|
- lib/workflow.rb
|
36
|
+
- test/advanced_hooks_and_validation_test.rb
|
37
|
+
- test/before_transition_test.rb
|
36
38
|
- test/couchtiny_example.rb
|
37
39
|
- test/main_test.rb
|
38
40
|
- test/multiple_workflows_test.rb
|
@@ -73,7 +75,9 @@ summary: A replacement for acts_as_state_machine.
|
|
73
75
|
test_files:
|
74
76
|
- test/couchtiny_example.rb
|
75
77
|
- test/main_test.rb
|
78
|
+
- test/before_transition_test.rb
|
76
79
|
- test/test_helper.rb
|
77
80
|
- test/without_active_record_test.rb
|
78
81
|
- test/multiple_workflows_test.rb
|
79
82
|
- test/readme_example.rb
|
83
|
+
- test/advanced_hooks_and_validation_test.rb
|