workflow 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +8 -1
- data/Gemfile +0 -1
- data/README.markdown +73 -27
- data/Rakefile +3 -9
- data/gemfiles/Gemfile.rails-2.3.x +1 -0
- data/gemfiles/Gemfile.rails-3.x +11 -0
- data/gemfiles/Gemfile.rails-edge +12 -0
- data/lib/workflow.rb +52 -227
- data/lib/workflow/adapters/active_record.rb +66 -0
- data/lib/workflow/adapters/remodel.rb +15 -0
- data/lib/workflow/draw.rb +79 -0
- data/lib/workflow/errors.rb +18 -0
- data/lib/workflow/event.rb +18 -0
- data/lib/workflow/specification.rb +64 -0
- data/lib/workflow/state.rb +44 -0
- data/lib/workflow/version.rb +1 -1
- data/orders_workflow.png +0 -0
- data/test/active_record_scopes_test.rb +49 -0
- data/test/advanced_examples_test.rb +21 -0
- data/test/attr_protected_test.rb +3 -0
- data/test/inheritance_test.rb +60 -0
- data/test/main_test.rb +7 -6
- data/test/new_versions/compare_states_test.rb +2 -1
- data/workflow.gemspec +1 -0
- metadata +86 -46
- data/workflow.rb +0 -1
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6c18ece78f869cf0939fd801a99b63a93b99eafd
|
4
|
+
data.tar.gz: 949a3b5bff0e79ced88e7ba5a3050820f58efd40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 801d65a4147f19643d429d38e794f9529ff760147cf63185ab19762ddf618deb8f8ff19deb0ec590b8a3cd7166c99bfa129fdf716dc8b261c7cb98478685a494
|
7
|
+
data.tar.gz: 5cc6722b3029ab9a96a2247215706315561735f6f7fa5950727f22b969040c53bba907532e3fada31d485c9c9ac6a39a66b130ed956f61b8ef68a2f566018332
|
data/.travis.yml
CHANGED
@@ -2,8 +2,9 @@ before_install:
|
|
2
2
|
- sudo apt-get install -qq graphviz
|
3
3
|
|
4
4
|
rvm:
|
5
|
-
- 1.9.3
|
6
5
|
- 2.0.0
|
6
|
+
gemfile:
|
7
|
+
- gemfiles/Gemfile.rails-edge
|
7
8
|
|
8
9
|
matrix:
|
9
10
|
include:
|
@@ -12,3 +13,9 @@ matrix:
|
|
12
13
|
gemfile: gemfiles/Gemfile.rails-2.3.x
|
13
14
|
# running a smaller test set for old Rails and Ruby
|
14
15
|
script: rake test_without_new_versions
|
16
|
+
|
17
|
+
- rvm: 1.9.3
|
18
|
+
gemfile: gemfiles/Gemfile.rails-3.x
|
19
|
+
|
20
|
+
- rvm: 2.0.0
|
21
|
+
gemfile: gemfiles/Gemfile.rails-3.x
|
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
[![Build Status](https://travis-ci.org/geekq/workflow.png?branch=master)](https://travis-ci.org/geekq/workflow)
|
2
2
|
|
3
|
+
|
3
4
|
What is workflow?
|
4
5
|
-----------------
|
5
6
|
|
@@ -63,10 +64,10 @@ Let's create an article instance and check in which state it is:
|
|
63
64
|
You can also access the whole `current_state` object including the list
|
64
65
|
of possible events and other meta information:
|
65
66
|
|
66
|
-
article.current_state
|
67
|
+
article.current_state
|
67
68
|
=> #<Workflow::State:0x7f1e3d6731f0 @events={
|
68
|
-
:submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
|
69
|
-
@transitions_to=:awaiting_review, @name=:submit, @meta={}>},
|
69
|
+
:submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
|
70
|
+
@transitions_to=:awaiting_review, @name=:submit, @meta={}>},
|
70
71
|
name:new, meta{}
|
71
72
|
|
72
73
|
On Ruby 1.9 and above, you can check whether a state comes before or
|
@@ -86,7 +87,7 @@ Now we can call the submit event, which transitions to the
|
|
86
87
|
|
87
88
|
article.submit!
|
88
89
|
article.awaiting_review? # => true
|
89
|
-
|
90
|
+
|
90
91
|
Events are actually instance methods on a workflow, and depending on the
|
91
92
|
state you're in, you'll have a different set of events used to
|
92
93
|
transition to other states.
|
@@ -101,18 +102,20 @@ Installation
|
|
101
102
|
|
102
103
|
gem install workflow
|
103
104
|
|
104
|
-
|
105
|
-
the
|
105
|
+
**Important**: If you're interested in graphing your workflow state machine, you will also need to
|
106
|
+
install the `active_support` and `ruby-graphviz` gems.
|
106
107
|
|
108
|
+
Versions up to and including 1.0.0 are also available as a single file download -
|
109
|
+
[lib/workflow.rb file](https://github.com/geekq/workflow/blob/v1.0.0/lib/workflow.rb).
|
107
110
|
|
108
111
|
Ruby 1.9
|
109
112
|
--------
|
110
113
|
|
111
|
-
Workflow gem does not work with some
|
112
|
-
builds due to a known bug in Ruby 1.9. Either
|
114
|
+
Workflow gem does not work with some Ruby 1.9
|
115
|
+
builds due to a known bug in Ruby 1.9. Either
|
113
116
|
|
114
117
|
* use newer ruby build, 1.9.2-p136 and -p180 tested to work
|
115
|
-
* or compile your Ruby 1.9 from source
|
118
|
+
* or compile your Ruby 1.9 from source
|
116
119
|
* or [comment out some lines in workflow](http://github.com/geekq/workflow/issues#issue/6)
|
117
120
|
(reduces functionality).
|
118
121
|
|
@@ -148,6 +151,13 @@ be:
|
|
148
151
|
(if integrated with ActiveRecord) and invoke this user defined reject
|
149
152
|
method.
|
150
153
|
|
154
|
+
Note: on successful transition from one state to another the workflow
|
155
|
+
gem immediately persists the new workflow state with `update_column()`,
|
156
|
+
bypassing any ActiveRecord callbacks including `updated_at` update.
|
157
|
+
This way it is possible to deal with the validation and to save the
|
158
|
+
pending changes to a record at some later point instead of the moment
|
159
|
+
when transition occurs.
|
160
|
+
|
151
161
|
You can also define event handler accepting/requiring additional
|
152
162
|
arguments:
|
153
163
|
|
@@ -175,7 +185,7 @@ invoked for particular transitions leads to a bumpy and poorly readable code
|
|
175
185
|
due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
|
176
186
|
we decided to invoke an optional user defined callback method with the same
|
177
187
|
name as the event (convention over configuration) as explained before.
|
178
|
-
|
188
|
+
|
179
189
|
|
180
190
|
Integration with ActiveRecord
|
181
191
|
-----------------------------
|
@@ -193,7 +203,7 @@ and include the workflow mixin in your model class as usual:
|
|
193
203
|
|
194
204
|
On a database record loading all the state check methods e.g.
|
195
205
|
`article.state`, `article.awaiting_review?` are immediately available.
|
196
|
-
For new records or if the workflow_state field is not set the state
|
206
|
+
For new records or if the `workflow_state` field is not set the state
|
197
207
|
defaults to the first state declared in the workflow specification. In
|
198
208
|
our example it is `:new`, so `Article.new.new?` returns true and
|
199
209
|
`Article.new.approved?` returns false.
|
@@ -204,6 +214,25 @@ new state is immediately saved in the database.
|
|
204
214
|
You can change this behaviour by overriding `persist_workflow_state`
|
205
215
|
method.
|
206
216
|
|
217
|
+
### Scopes
|
218
|
+
|
219
|
+
Workflow library also adds automatically generated scopes with names based on
|
220
|
+
states names:
|
221
|
+
|
222
|
+
class Order < ActiveRecord::Base
|
223
|
+
include Workflow
|
224
|
+
workflow do
|
225
|
+
state :approved
|
226
|
+
state :pending
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# returns all orders with `approved` state
|
231
|
+
Order.with_approved_state
|
232
|
+
|
233
|
+
# returns all orders with `pending` state
|
234
|
+
Order.with_pending_state
|
235
|
+
|
207
236
|
|
208
237
|
### Custom workflow database column
|
209
238
|
|
@@ -212,7 +241,7 @@ custom persistence column easily, e.g. for a legacy database schema:
|
|
212
241
|
|
213
242
|
class LegacyOrder < ActiveRecord::Base
|
214
243
|
include Workflow
|
215
|
-
|
244
|
+
|
216
245
|
workflow_column :foo_bar # use this legacy database column for
|
217
246
|
# persistence
|
218
247
|
end
|
@@ -234,8 +263,8 @@ need to override `load_workflow_state` and
|
|
234
263
|
`persist_workflow_state(new_value)` methods. Next section contains an example for
|
235
264
|
using CouchDB, a document oriented database.
|
236
265
|
|
237
|
-
[Tim Lossen](http://tim.lossen.de/) implemented support
|
238
|
-
for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
|
266
|
+
[Tim Lossen](http://tim.lossen.de/) implemented support
|
267
|
+
for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
|
239
268
|
key-value store.
|
240
269
|
|
241
270
|
Integration with CouchDB
|
@@ -268,7 +297,7 @@ couchrest library.
|
|
268
297
|
end
|
269
298
|
end
|
270
299
|
|
271
|
-
Please also have a look at
|
300
|
+
Please also have a look at
|
272
301
|
[the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
|
273
302
|
|
274
303
|
Integration with Mongoid
|
@@ -310,7 +339,7 @@ state and every event:
|
|
310
339
|
|
311
340
|
The workflow library itself uses this feature to tweak the graphical
|
312
341
|
representation of the workflow. See below.
|
313
|
-
|
342
|
+
|
314
343
|
|
315
344
|
Advanced transition hooks
|
316
345
|
-------------------------
|
@@ -346,7 +375,7 @@ example][advanced_hooks_and_validation_test].
|
|
346
375
|
|
347
376
|
### on_error
|
348
377
|
|
349
|
-
If you want to do custom exception handling internal to workflow, you can define an `on_error` hook in your workflow.
|
378
|
+
If you want to do custom exception handling internal to workflow, you can define an `on_error` hook in your workflow.
|
350
379
|
For example:
|
351
380
|
|
352
381
|
workflow do
|
@@ -356,12 +385,12 @@ For example:
|
|
356
385
|
state :second
|
357
386
|
|
358
387
|
on_error do |error, from, to, event, *args|
|
359
|
-
Log.info "Exception(#error.class) on #{from} -> #{to}"
|
388
|
+
Log.info "Exception(#error.class) on #{from} -> #{to}"
|
360
389
|
end
|
361
390
|
end
|
362
391
|
|
363
|
-
If forward! results in an exception, `on_error` is invoked and the workflow stays in a 'first' state. This capability
|
364
|
-
is particularly useful if your errors are transient and you want to queue up a job to retry in the future without
|
392
|
+
If forward! results in an exception, `on_error` is invoked and the workflow stays in a 'first' state. This capability
|
393
|
+
is particularly useful if your errors are transient and you want to queue up a job to retry in the future without
|
365
394
|
affecting the existing workflow state.
|
366
395
|
|
367
396
|
### Guards
|
@@ -400,7 +429,7 @@ Multiple Workflows
|
|
400
429
|
------------------
|
401
430
|
|
402
431
|
I am frequently asked if it's possible to represent multiple "workflows"
|
403
|
-
in an ActiveRecord class.
|
432
|
+
in an ActiveRecord class.
|
404
433
|
|
405
434
|
The solution depends on your business logic and how you want to
|
406
435
|
structure your implementation.
|
@@ -462,8 +491,17 @@ example][multiple_workflow_test]!
|
|
462
491
|
Documenting with diagrams
|
463
492
|
-------------------------
|
464
493
|
|
465
|
-
You can generate a graphical representation of
|
466
|
-
documentation purposes.
|
494
|
+
You can generate a graphical representation of the workflow for
|
495
|
+
a particular class for documentation purposes.
|
496
|
+
Use `Workflow::create_workflow_diagram(class)` in your rake task like:
|
497
|
+
|
498
|
+
namespace :doc do
|
499
|
+
desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
|
500
|
+
task :workflow => :environment do
|
501
|
+
require 'workflow/draw'
|
502
|
+
Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
|
503
|
+
end
|
504
|
+
end
|
467
505
|
|
468
506
|
|
469
507
|
Earlier versions
|
@@ -472,7 +510,7 @@ Earlier versions
|
|
472
510
|
The `workflow` library was originally written by Ryan Allen.
|
473
511
|
|
474
512
|
The version 0.3 was almost completely (including ActiveRecord
|
475
|
-
integration, API for accessing workflow specification,
|
513
|
+
integration, API for accessing workflow specification,
|
476
514
|
method_missing free implementation) rewritten by Vladimir Dobriakov
|
477
515
|
keeping the original workflow DSL spirit.
|
478
516
|
|
@@ -503,6 +541,14 @@ when using both a block and a callback method for an event, the block executes p
|
|
503
541
|
Changelog
|
504
542
|
---------
|
505
543
|
|
544
|
+
### New in the version 1.1.0
|
545
|
+
|
546
|
+
* Tested with ActiveRecord 4.0 (Rails 4.0)
|
547
|
+
* Tested with Ruby 2.0
|
548
|
+
* automatically generated scopes with names based on state names
|
549
|
+
* clean workflow definition override for class inheritance - undefining
|
550
|
+
the old convinience methods, s. <http://git.io/FZO02A>
|
551
|
+
|
506
552
|
### New in the version 1.0.0
|
507
553
|
|
508
554
|
* **Support to private/protected callback methods.**
|
@@ -568,7 +614,7 @@ Intermixing of transition graph definition (states, transitions)
|
|
568
614
|
on the one side and implementation of the actions on the other side
|
569
615
|
for a bigger state machine can introduce clutter.
|
570
616
|
|
571
|
-
To reduce this clutter it is now possible to use state entry- and
|
617
|
+
To reduce this clutter it is now possible to use state entry- and
|
572
618
|
exit- hooks defined through a naming convention. For example, if there
|
573
619
|
is a state :pending, then instead of using a
|
574
620
|
block:
|
@@ -579,7 +625,7 @@ block:
|
|
579
625
|
end
|
580
626
|
end
|
581
627
|
|
582
|
-
you can hook in by defining method
|
628
|
+
you can hook in by defining method
|
583
629
|
|
584
630
|
def on_pending_exit(new_state, event, *args)
|
585
631
|
# your implementation here
|
@@ -590,7 +636,7 @@ like `def on_pending_exit(*args)` if your are not interested in
|
|
590
636
|
arguments. Please note: `def on_pending_exit()` with an empty list
|
591
637
|
would not work.
|
592
638
|
|
593
|
-
If both a function with a name according to naming convention and the
|
639
|
+
If both a function with a name according to naming convention and the
|
594
640
|
on_entry/on_exit block are given, then only on_entry/on_exit block is used.
|
595
641
|
|
596
642
|
|
data/Rakefile
CHANGED
@@ -1,17 +1,11 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require "bundler/gem_tasks"
|
3
2
|
require 'rake/testtask'
|
4
3
|
require 'rdoc/task'
|
5
4
|
|
6
|
-
|
5
|
+
require 'bundler'
|
6
|
+
Bundler.setup
|
7
7
|
|
8
|
-
|
9
|
-
Bundler.setup(:default, :development)
|
10
|
-
rescue Bundler::BundlerError => e
|
11
|
-
$stderr.puts e.message
|
12
|
-
$stderr.puts "Run `bundle install` to install missing gems"
|
13
|
-
exit e.status_code
|
14
|
-
end
|
8
|
+
task :default => [:test]
|
15
9
|
|
16
10
|
require 'rake'
|
17
11
|
Rake::TestTask.new do |t|
|
data/lib/workflow.rb
CHANGED
@@ -1,130 +1,12 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
|
3
|
+
require 'workflow/specification'
|
4
|
+
require 'workflow/adapters/active_record'
|
5
|
+
require 'workflow/adapters/remodel'
|
6
|
+
|
3
7
|
# See also README.markdown for documentation
|
4
8
|
module Workflow
|
5
|
-
|
6
|
-
class Specification
|
7
|
-
|
8
|
-
attr_accessor :states, :initial_state, :meta,
|
9
|
-
:on_transition_proc, :before_transition_proc, :after_transition_proc, :on_error_proc
|
10
|
-
|
11
|
-
def initialize(meta = {}, &specification)
|
12
|
-
@states = Hash.new
|
13
|
-
@meta = meta
|
14
|
-
instance_eval(&specification)
|
15
|
-
end
|
16
|
-
|
17
|
-
def state_names
|
18
|
-
states.keys
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def state(name, meta = {:meta => {}}, &events_and_etc)
|
24
|
-
# meta[:meta] to keep the API consistent..., gah
|
25
|
-
new_state = Workflow::State.new(name, self, meta[:meta])
|
26
|
-
@initial_state = new_state if @states.empty?
|
27
|
-
@states[name.to_sym] = new_state
|
28
|
-
@scoped_state = new_state
|
29
|
-
instance_eval(&events_and_etc) if events_and_etc
|
30
|
-
end
|
31
|
-
|
32
|
-
def event(name, args = {}, &action)
|
33
|
-
target = args[:transitions_to] || args[:transition_to]
|
34
|
-
raise WorkflowDefinitionError.new(
|
35
|
-
"missing ':transitions_to' in workflow event definition for '#{name}'") \
|
36
|
-
if target.nil?
|
37
|
-
@scoped_state.events[name.to_sym] =
|
38
|
-
Workflow::Event.new(name, target, (args[:meta] or {}), &action)
|
39
|
-
end
|
40
|
-
|
41
|
-
def on_entry(&proc)
|
42
|
-
@scoped_state.on_entry = proc
|
43
|
-
end
|
44
|
-
|
45
|
-
def on_exit(&proc)
|
46
|
-
@scoped_state.on_exit = proc
|
47
|
-
end
|
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
|
-
|
57
|
-
def on_transition(&proc)
|
58
|
-
@on_transition_proc = proc
|
59
|
-
end
|
60
|
-
|
61
|
-
def on_error(&proc)
|
62
|
-
@on_error_proc = proc
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
class TransitionHalted < Exception
|
67
|
-
|
68
|
-
attr_reader :halted_because
|
69
|
-
|
70
|
-
def initialize(msg = nil)
|
71
|
-
@halted_because = msg
|
72
|
-
super msg
|
73
|
-
end
|
74
|
-
|
75
|
-
end
|
76
|
-
|
77
|
-
class NoTransitionAllowed < Exception; end
|
78
|
-
|
79
|
-
class WorkflowError < Exception; end
|
80
|
-
|
81
|
-
class WorkflowDefinitionError < Exception; end
|
82
|
-
|
83
|
-
class State
|
84
|
-
|
85
|
-
attr_accessor :name, :events, :meta, :on_entry, :on_exit
|
86
|
-
attr_reader :spec
|
87
|
-
|
88
|
-
def initialize(name, spec, meta = {})
|
89
|
-
@name, @spec, @events, @meta = name, spec, Hash.new, meta
|
90
|
-
end
|
91
|
-
|
92
|
-
unless RUBY_VERSION < '1.9'
|
93
|
-
include Comparable
|
94
|
-
|
95
|
-
def <=>(other_state)
|
96
|
-
states = spec.states.keys
|
97
|
-
raise ArgumentError, "state `#{other_state}' does not exist" unless other_state.in? states
|
98
|
-
if states.index(self.to_sym) < states.index(other_state.to_sym)
|
99
|
-
-1
|
100
|
-
elsif states.index(self.to_sym) > states.index(other_state.to_sym)
|
101
|
-
1
|
102
|
-
else
|
103
|
-
0
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def to_s
|
109
|
-
"#{name}"
|
110
|
-
end
|
111
|
-
|
112
|
-
def to_sym
|
113
|
-
name.to_sym
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
class Event
|
118
|
-
|
119
|
-
attr_accessor :name, :transitions_to, :meta, :action
|
120
|
-
|
121
|
-
def initialize(name, transitions_to, meta = {}, &action)
|
122
|
-
@name, @transitions_to, @meta, @action = name, transitions_to.to_sym, meta, action
|
123
|
-
end
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
module WorkflowClassMethods
|
9
|
+
module ClassMethods
|
128
10
|
attr_reader :workflow_spec
|
129
11
|
|
130
12
|
def workflow_column(column_name=nil)
|
@@ -138,7 +20,35 @@ module Workflow
|
|
138
20
|
end
|
139
21
|
|
140
22
|
def workflow(&specification)
|
141
|
-
|
23
|
+
assign_workflow Specification.new(Hash.new, &specification)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Creates the convinience methods like `my_transition!`
|
29
|
+
def assign_workflow(specification_object)
|
30
|
+
|
31
|
+
# Merging two workflow specifications can **not** be done automically, so
|
32
|
+
# just make the latest specification win. Same for inheritance -
|
33
|
+
# definition in the subclass wins.
|
34
|
+
if respond_to? :inherited_workflow_spec # undefine methods defined by the old workflow_spec
|
35
|
+
inherited_workflow_spec.states.values.each do |state|
|
36
|
+
state_name = state.name
|
37
|
+
module_eval do
|
38
|
+
undef_method "#{state_name}?"
|
39
|
+
end
|
40
|
+
|
41
|
+
state.events.values.each do |event|
|
42
|
+
event_name = event.name
|
43
|
+
module_eval do
|
44
|
+
undef_method "#{event_name}!".to_sym
|
45
|
+
undef_method "can_#{event_name}?"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@workflow_spec = specification_object
|
142
52
|
@workflow_spec.states.values.each do |state|
|
143
53
|
state_name = state.name
|
144
54
|
module_eval do
|
@@ -163,7 +73,7 @@ module Workflow
|
|
163
73
|
end
|
164
74
|
end
|
165
75
|
|
166
|
-
module
|
76
|
+
module InstanceMethods
|
167
77
|
|
168
78
|
def current_state
|
169
79
|
loaded_state = load_workflow_state
|
@@ -335,118 +245,33 @@ module Workflow
|
|
335
245
|
end
|
336
246
|
end
|
337
247
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
# older Rails; beware of side effect: other (pending) attribute changes will be persisted too
|
351
|
-
update_attribute self.class.workflow_column, new_value
|
248
|
+
def self.included(klass)
|
249
|
+
klass.send :include, InstanceMethods
|
250
|
+
|
251
|
+
# backup the parent workflow spec, making accessible through #inherited_workflow_spec
|
252
|
+
if klass.superclass.respond_to?(:workflow_spec, true)
|
253
|
+
klass.module_eval do
|
254
|
+
# see http://stackoverflow.com/a/2495650/111995 for implementation explanation
|
255
|
+
pro = Proc.new { klass.superclass.workflow_spec }
|
256
|
+
singleton_class = class << self; self; end
|
257
|
+
singleton_class.send(:define_method, :inherited_workflow_spec) do
|
258
|
+
pro.call
|
259
|
+
end
|
352
260
|
end
|
353
261
|
end
|
354
262
|
|
355
|
-
|
356
|
-
|
357
|
-
# Motivation: even if NULL is stored in the workflow_state database column,
|
358
|
-
# the current_state is correctly recognized in the Ruby code. The problem
|
359
|
-
# arises when you want to SELECT records filtering by the value of initial
|
360
|
-
# state. That's why it is important to save the string with the name of the
|
361
|
-
# initial state in all the new records.
|
362
|
-
def write_initial_state
|
363
|
-
write_attribute self.class.workflow_column, current_state.to_s
|
364
|
-
end
|
365
|
-
end
|
366
|
-
|
367
|
-
module RemodelInstanceMethods
|
368
|
-
def load_workflow_state
|
369
|
-
send(self.class.workflow_column)
|
370
|
-
end
|
371
|
-
|
372
|
-
def persist_workflow_state(new_value)
|
373
|
-
update(self.class.workflow_column => new_value)
|
374
|
-
end
|
375
|
-
end
|
263
|
+
klass.extend ClassMethods
|
376
264
|
|
377
|
-
def self.included(klass)
|
378
|
-
klass.send :include, WorkflowInstanceMethods
|
379
|
-
klass.extend WorkflowClassMethods
|
380
265
|
if Object.const_defined?(:ActiveRecord)
|
381
266
|
if klass < ActiveRecord::Base
|
382
|
-
klass.send :include,
|
267
|
+
klass.send :include, Adapter::ActiveRecord::InstanceMethods
|
268
|
+
klass.send :extend, Adapter::ActiveRecord::Scopes
|
383
269
|
klass.before_validation :write_initial_state
|
384
270
|
end
|
385
271
|
elsif Object.const_defined?(:Remodel)
|
386
|
-
if klass < Remodel::Entity
|
387
|
-
klass.send :include,
|
388
|
-
end
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
# Generates a `dot` graph of the workflow.
|
393
|
-
# Prerequisite: the `dot` binary. (Download from http://www.graphviz.org/)
|
394
|
-
# You can use this method in your own Rakefile like this:
|
395
|
-
#
|
396
|
-
# namespace :doc do
|
397
|
-
# desc "Generate a graph of the workflow."
|
398
|
-
# task :workflow => :environment do # needs access to the Rails environment
|
399
|
-
# Workflow::create_workflow_diagram(Order)
|
400
|
-
# end
|
401
|
-
# end
|
402
|
-
#
|
403
|
-
# You can influence the placement of nodes by specifying
|
404
|
-
# additional meta information in your states and transition descriptions.
|
405
|
-
# You can assign higher `doc_weight` value to the typical transitions
|
406
|
-
# in your workflow. All other states and transitions will be arranged
|
407
|
-
# around that main line. See also `weight` in the graphviz documentation.
|
408
|
-
# Example:
|
409
|
-
#
|
410
|
-
# state :new do
|
411
|
-
# event :approve, :transitions_to => :approved, :meta => {:doc_weight => 8}
|
412
|
-
# end
|
413
|
-
#
|
414
|
-
#
|
415
|
-
# @param klass A class with the Workflow mixin, for which you wish the graphical workflow representation
|
416
|
-
# @param [String] target_dir Directory, where to save the dot and the pdf files
|
417
|
-
# @param [String] graph_options You can change graph orientation, size etc. See graphviz documentation
|
418
|
-
def self.create_workflow_diagram(klass, target_dir='.', graph_options='rankdir="LR", size="7,11.6", ratio="fill"')
|
419
|
-
workflow_name = "#{klass.name.tableize}_workflow".gsub('/', '_')
|
420
|
-
fname = File.join(target_dir, "generated_#{workflow_name}")
|
421
|
-
File.open("#{fname}.dot", 'w') do |file|
|
422
|
-
file.puts %Q|
|
423
|
-
digraph #{workflow_name} {
|
424
|
-
graph [#{graph_options}];
|
425
|
-
node [shape=box];
|
426
|
-
edge [len=1];
|
427
|
-
|
|
428
|
-
|
429
|
-
klass.workflow_spec.states.each do |state_name, state|
|
430
|
-
file.puts %Q{ #{state.name} [label="#{state.name}"];}
|
431
|
-
state.events.each do |event_name, event|
|
432
|
-
meta_info = event.meta
|
433
|
-
if meta_info[:doc_weight]
|
434
|
-
weight_prop = ", weight=#{meta_info[:doc_weight]}"
|
435
|
-
else
|
436
|
-
weight_prop = ''
|
437
|
-
end
|
438
|
-
file.puts %Q{ #{state.name} -> #{event.transitions_to} [label="#{event_name.to_s.humanize}" #{weight_prop}];}
|
439
|
-
end
|
272
|
+
if klass < Adapter::Remodel::Entity
|
273
|
+
klass.send :include, Remodel::InstanceMethods
|
440
274
|
end
|
441
|
-
file.puts "}"
|
442
|
-
file.puts
|
443
275
|
end
|
444
|
-
`dot -Tpdf -o'#{fname}.pdf' '#{fname}.dot'`
|
445
|
-
puts "
|
446
|
-
Please run the following to open the generated file:
|
447
|
-
|
448
|
-
open '#{fname}.pdf'
|
449
|
-
|
450
|
-
"
|
451
276
|
end
|
452
277
|
end
|