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 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.7.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, :on_transition_proc
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
- return false
161
- else
162
- check_transition(event)
163
- run_on_transition(current_state, spec.states[event.transitions_to], name, *args)
164
- transition_value = transition(
165
- current_state, spec.states[event.transitions_to], name, *args
166
- )
167
- return_value.nil? ? transition_value : return_value
168
- end
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 transition(from, to, name, *args)
210
- run_on_exit(from, to, name, *args)
211
- val = persist_workflow_state to.to_s
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
7
+ - 8
8
8
  - 0
9
- version: 0.7.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-08 00:00:00 +02:00
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