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 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