workflow 0.7.0 → 0.8.0
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/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
|