workflow 1.0.0 → 1.1.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.
- 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
|
[](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
|