control 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.md +87 -0
- data/Rakefile +18 -0
- data/control.gemspec +19 -0
- data/db/migrate/create_transitions.rb +18 -0
- data/db/schema.rb +14 -0
- data/lib/control/errors.rb +19 -0
- data/lib/control/state.rb +135 -0
- data/lib/control/transition.rb +53 -0
- data/lib/control/workflow.rb +52 -0
- data/lib/generators/control_install/USAGE +8 -0
- data/lib/generators/control_install/control_install_generator.rb +22 -0
- data/test/lib/control/state_test.rb +80 -0
- data/test/lib/control/transition_test.rb +84 -0
- data/test/lib/control/workflow_test.rb +107 -0
- data/test/rails_app/Gemfile +22 -0
- data/test/rails_app/Gemfile.lock +130 -0
- data/test/rails_app/README.rdoc +261 -0
- data/test/rails_app/Rakefile +7 -0
- data/test/rails_app/app/assets/images/rails.png +0 -0
- data/test/rails_app/app/assets/javascripts/application.js +15 -0
- data/test/rails_app/app/assets/javascripts/assemblies.js.coffee +3 -0
- data/test/rails_app/app/assets/javascripts/boxes.js.coffee +3 -0
- data/test/rails_app/app/assets/javascripts/products.js.coffee +3 -0
- data/test/rails_app/app/assets/javascripts/rejects.js.coffee +3 -0
- data/test/rails_app/app/assets/javascripts/validates.js.coffee +3 -0
- data/test/rails_app/app/assets/stylesheets/application.css +13 -0
- data/test/rails_app/app/assets/stylesheets/assemblies.css.scss +3 -0
- data/test/rails_app/app/assets/stylesheets/boxes.css.scss +3 -0
- data/test/rails_app/app/assets/stylesheets/products.css.scss +8 -0
- data/test/rails_app/app/assets/stylesheets/rejects.css.scss +3 -0
- data/test/rails_app/app/assets/stylesheets/scaffolds.css.scss +56 -0
- data/test/rails_app/app/assets/stylesheets/validates.css.scss +3 -0
- data/test/rails_app/app/controllers/application_controller.rb +3 -0
- data/test/rails_app/app/controllers/assemblies_controller.rb +88 -0
- data/test/rails_app/app/controllers/boxes_controller.rb +88 -0
- data/test/rails_app/app/controllers/products_controller.rb +83 -0
- data/test/rails_app/app/controllers/rejects_controller.rb +88 -0
- data/test/rails_app/app/controllers/validates_controller.rb +88 -0
- data/test/rails_app/app/helpers/application_helper.rb +2 -0
- data/test/rails_app/app/helpers/assemblies_helper.rb +2 -0
- data/test/rails_app/app/helpers/boxes_helper.rb +2 -0
- data/test/rails_app/app/helpers/products_helper.rb +14 -0
- data/test/rails_app/app/helpers/rejects_helper.rb +2 -0
- data/test/rails_app/app/helpers/validates_helper.rb +2 -0
- data/test/rails_app/app/models/assembly.rb +6 -0
- data/test/rails_app/app/models/box.rb +5 -0
- data/test/rails_app/app/models/product.rb +8 -0
- data/test/rails_app/app/models/reject.rb +6 -0
- data/test/rails_app/app/models/validate.rb +6 -0
- data/test/rails_app/app/views/assemblies/_form.html.erb +24 -0
- data/test/rails_app/app/views/assemblies/edit.html.erb +6 -0
- data/test/rails_app/app/views/assemblies/index.html.erb +25 -0
- data/test/rails_app/app/views/assemblies/new.html.erb +5 -0
- data/test/rails_app/app/views/assemblies/show.html.erb +14 -0
- data/test/rails_app/app/views/boxes/_form.html.erb +24 -0
- data/test/rails_app/app/views/boxes/edit.html.erb +6 -0
- data/test/rails_app/app/views/boxes/index.html.erb +25 -0
- data/test/rails_app/app/views/boxes/new.html.erb +5 -0
- data/test/rails_app/app/views/boxes/show.html.erb +14 -0
- data/test/rails_app/app/views/layouts/application.html.erb +14 -0
- data/test/rails_app/app/views/products/_form.html.erb +21 -0
- data/test/rails_app/app/views/products/edit.html.erb +6 -0
- data/test/rails_app/app/views/products/index.html.erb +23 -0
- data/test/rails_app/app/views/products/new.html.erb +5 -0
- data/test/rails_app/app/views/products/show.html.erb +34 -0
- data/test/rails_app/app/views/rejects/_form.html.erb +20 -0
- data/test/rails_app/app/views/rejects/edit.html.erb +6 -0
- data/test/rails_app/app/views/rejects/index.html.erb +23 -0
- data/test/rails_app/app/views/rejects/new.html.erb +5 -0
- data/test/rails_app/app/views/rejects/show.html.erb +9 -0
- data/test/rails_app/app/views/validates/_form.html.erb +40 -0
- data/test/rails_app/app/views/validates/edit.html.erb +6 -0
- data/test/rails_app/app/views/validates/index.html.erb +33 -0
- data/test/rails_app/app/views/validates/new.html.erb +5 -0
- data/test/rails_app/app/views/validates/show.html.erb +35 -0
- data/test/rails_app/config.ru +4 -0
- data/test/rails_app/config/application.rb +59 -0
- data/test/rails_app/config/boot.rb +6 -0
- data/test/rails_app/config/database.yml +25 -0
- data/test/rails_app/config/environment.rb +5 -0
- data/test/rails_app/config/environments/development.rb +37 -0
- data/test/rails_app/config/environments/production.rb +67 -0
- data/test/rails_app/config/environments/test.rb +37 -0
- data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails_app/config/initializers/inflections.rb +15 -0
- data/test/rails_app/config/initializers/mime_types.rb +5 -0
- data/test/rails_app/config/initializers/secret_token.rb +7 -0
- data/test/rails_app/config/initializers/session_store.rb +8 -0
- data/test/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/test/rails_app/config/locales/en.yml +5 -0
- data/test/rails_app/config/routes.rb +68 -0
- data/test/rails_app/db/development.sqlite3 +0 -0
- data/test/rails_app/db/migrate/20120126165218_create_products.rb +8 -0
- data/test/rails_app/db/migrate/20120126165821_add_codename_to_products.rb +6 -0
- data/test/rails_app/db/migrate/20120126173019_create_validates.rb +14 -0
- data/test/rails_app/db/migrate/20120126173232_create_assemblies.rb +10 -0
- data/test/rails_app/db/migrate/20120126173516_create_boxes.rb +10 -0
- data/test/rails_app/db/migrate/20120126173539_create_rejects.rb +9 -0
- data/test/rails_app/db/migrate/20120126174101_create_transitions.rb +18 -0
- data/test/rails_app/db/schema.rb +64 -0
- data/test/rails_app/db/seeds.rb +7 -0
- data/test/rails_app/db/test.sqlite3 +0 -0
- data/test/rails_app/doc/README_FOR_APP +2 -0
- data/test/rails_app/log/development.log +8963 -0
- data/test/rails_app/log/test.log +321 -0
- data/test/rails_app/public/404.html +26 -0
- data/test/rails_app/public/422.html +26 -0
- data/test/rails_app/public/500.html +25 -0
- data/test/rails_app/public/favicon.ico +0 -0
- data/test/rails_app/public/index.html.old +241 -0
- data/test/rails_app/public/robots.txt +5 -0
- data/test/rails_app/script/rails +6 -0
- data/test/rails_app/test/fixtures/assemblies.yml +9 -0
- data/test/rails_app/test/fixtures/boxes.yml +9 -0
- data/test/rails_app/test/fixtures/products.yml +11 -0
- data/test/rails_app/test/fixtures/rejects.yml +7 -0
- data/test/rails_app/test/fixtures/validates.yml +17 -0
- data/test/rails_app/test/functional/assemblies_controller_test.rb +49 -0
- data/test/rails_app/test/functional/boxes_controller_test.rb +49 -0
- data/test/rails_app/test/functional/products_controller_test.rb +49 -0
- data/test/rails_app/test/functional/rejects_controller_test.rb +49 -0
- data/test/rails_app/test/functional/validates_controller_test.rb +49 -0
- data/test/rails_app/test/performance/browsing_test.rb +12 -0
- data/test/rails_app/test/test_helper.rb +13 -0
- data/test/rails_app/test/unit/assembly_test.rb +7 -0
- data/test/rails_app/test/unit/box_test.rb +7 -0
- data/test/rails_app/test/unit/helpers/assemblies_helper_test.rb +4 -0
- data/test/rails_app/test/unit/helpers/boxes_helper_test.rb +4 -0
- data/test/rails_app/test/unit/helpers/products_helper_test.rb +4 -0
- data/test/rails_app/test/unit/helpers/rejects_helper_test.rb +4 -0
- data/test/rails_app/test/unit/helpers/validates_helper_test.rb +4 -0
- data/test/rails_app/test/unit/product_test.rb +7 -0
- data/test/rails_app/test/unit/reject_test.rb +7 -0
- data/test/rails_app/test/unit/validate_test.rb +7 -0
- data/test/rails_app/tmp/cache/assets/CD7/6F0/sprockets%2Fbd3936370d0f952ada5774e2230046ed +0 -0
- data/test/rails_app/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/rails_app/tmp/cache/assets/CDF/890/sprockets%2F1380c8a9a90c9434c33ea3a2b759546f +0 -0
- data/test/rails_app/tmp/cache/assets/CF0/DA0/sprockets%2Fd7d5b37686831d37c4dd75e645f5e016 +0 -0
- data/test/rails_app/tmp/cache/assets/CF1/E50/sprockets%2F627ffd39f3642a2a236775d77839fac3 +0 -0
- data/test/rails_app/tmp/cache/assets/CFC/170/sprockets%2F80efc018414af123b11d0c6485e87b1b +0 -0
- data/test/rails_app/tmp/cache/assets/D0B/B40/sprockets%2F2eace11b521905c77d38470c975fa28a +0 -0
- data/test/rails_app/tmp/cache/assets/D13/8E0/sprockets%2F4476d1e237763ebe413a93e0ae559e6b +0 -0
- data/test/rails_app/tmp/cache/assets/D13/E20/sprockets%2F382583b312ba3d6f09b596f6bb63a98b +0 -0
- data/test/rails_app/tmp/cache/assets/D15/750/sprockets%2F4cc25a6e69224dc83fed65a4242479c5 +0 -0
- data/test/rails_app/tmp/cache/assets/D1B/3B0/sprockets%2F0098fc768cb8d9c834fcb13163c85b54 +0 -0
- data/test/rails_app/tmp/cache/assets/D20/C40/sprockets%2F46d84fd895a96b157269bd6b7bae2623 +0 -0
- data/test/rails_app/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/rails_app/tmp/cache/assets/D3F/4A0/sprockets%2Fba62c891c908a4219fb4d2005f886aee +0 -0
- data/test/rails_app/tmp/cache/assets/D45/C10/sprockets%2F29eede3408740d29d159a8367d1abe4e +0 -0
- data/test/rails_app/tmp/cache/assets/D4B/EC0/sprockets%2Fb261fa987a3e88f07b352eba69495e3b +0 -0
- data/test/rails_app/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/test/rails_app/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/rails_app/tmp/cache/assets/D65/620/sprockets%2Fc821c64ad244b34cd1c92ce6e4844f2a +0 -0
- data/test/rails_app/tmp/cache/assets/D78/F10/sprockets%2Fc81a77308c5e0c2b8efc69bece538149 +0 -0
- data/test/rails_app/tmp/cache/assets/D7B/040/sprockets%2F2ea981ebd7b7a34bd9d75f626f81c526 +0 -0
- data/test/rails_app/tmp/cache/assets/D9C/4A0/sprockets%2Fc15750bf0161f2878cf9da9bb1a84ecb +0 -0
- data/test/rails_app/tmp/cache/assets/DA2/120/sprockets%2Fdfd12e4fb8ce07a2d3c4856974a2f4e3 +0 -0
- data/test/rails_app/tmp/cache/assets/DA9/180/sprockets%2F1eca18f8334b5ce5f73e18d778bc7e3b +0 -0
- data/test/rails_app/tmp/cache/assets/DB4/120/sprockets%2Fc4ad185fad2acc8f1133bb2a80f400d7 +0 -0
- data/test/rails_app/tmp/cache/assets/DD2/B40/sprockets%2Ff8f229b2c6e61975aca38da7a1cb7b8c +0 -0
- data/test/rails_app/tmp/cache/assets/DD5/CF0/sprockets%2Fac6d0c5baedfe70588f57e99b0c4c722 +0 -0
- data/test/rails_app/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/test/rails_app/tmp/cache/assets/DDD/AE0/sprockets%2F524b597ade6bc1d79c9a4ffd6e3682bd +0 -0
- data/test/rails_app/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/rails_app/tmp/cache/assets/E06/E60/sprockets%2F1afcf29a4e0c835cb47d9bf28cf8a86b +0 -0
- data/test/rails_app/tmp/cache/assets/E19/2A0/sprockets%2F10fcfbe6ebae11a40c8eac41939a1b9a +0 -0
- data/test/rails_app/tmp/cache/assets/E25/4C0/sprockets%2Fde2fd9fd11c04a582cdbbe3d84a35ae6 +0 -0
- data/test/rails_app/tmp/cache/assets/E35/980/sprockets%2Fa5beac77f747ffbc71d32edecf38904a +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/assemblies.css.scssc +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/boxes.css.scssc +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/products.css.scssc +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/rejects.css.scssc +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/scaffolds.css.scssc +0 -0
- data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/validates.css.scssc +0 -0
- data/test/test_helper.rb +67 -0
- data/test/test_schema.rb +32 -0
- metadata +210 -1
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
Workflow engine
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/zeto/control.png)](http://travis-ci.org/zeto/control)
|
4
|
+
|
5
|
+
Premises
|
6
|
+
<ul>
|
7
|
+
<li>ActiveRecord</li>
|
8
|
+
<li>Each state is a separate object.</li>
|
9
|
+
</ul>
|
10
|
+
|
11
|
+
Features
|
12
|
+
<ul>
|
13
|
+
<li>State can be as rich in functionality was wanted.</li>
|
14
|
+
<li>No data is ever lost. History is always saved. </li>
|
15
|
+
<li>It is possible to track every single transition done in the workflow.</li>
|
16
|
+
<li>Advancing a state is as easy as creating a state object and calling "save".</li>
|
17
|
+
<li>Can transition to same state.</li>
|
18
|
+
<li>Minimal code to define workflow and states.</li>
|
19
|
+
</ul>
|
20
|
+
|
21
|
+
Simple example for a Bulb with two states:
|
22
|
+
|
23
|
+
<pre><code>
|
24
|
+
class Bulb < ActiveRecord::Base
|
25
|
+
include Control::Workflow
|
26
|
+
|
27
|
+
has_many :ons
|
28
|
+
has_many :offs
|
29
|
+
end
|
30
|
+
|
31
|
+
class On < ActiveRecord::Base
|
32
|
+
include Control::State
|
33
|
+
|
34
|
+
belongs_to :bulb
|
35
|
+
end
|
36
|
+
|
37
|
+
class Off < ActiveRecord::Base
|
38
|
+
include Control::State
|
39
|
+
|
40
|
+
belongs_to :bulb
|
41
|
+
next_states :on # optional, to constrain possible next states, can also specify :none to make a state final
|
42
|
+
end
|
43
|
+
|
44
|
+
def example
|
45
|
+
my_bulb = Bulb.new
|
46
|
+
|
47
|
+
my_on = On.new do |o|
|
48
|
+
o.bulb = my_bulb
|
49
|
+
end
|
50
|
+
|
51
|
+
my_off = Off.new do |o|
|
52
|
+
o.bulb = my_bulb
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check possible states for my bulb. (On, Off)
|
56
|
+
my_bulb.states
|
57
|
+
|
58
|
+
# Transitions the bulb to the "On" state
|
59
|
+
my_on.save
|
60
|
+
|
61
|
+
# Transitions the bulb to the "Off" state
|
62
|
+
my_off.save
|
63
|
+
|
64
|
+
# Every transition is recorded. (On -> Off)
|
65
|
+
my_bulb.transitions
|
66
|
+
|
67
|
+
# Bulb knows which state is current. (Off)
|
68
|
+
my_bulb.current_state
|
69
|
+
end
|
70
|
+
</code></pre>
|
71
|
+
|
72
|
+
Installing:
|
73
|
+
|
74
|
+
1. Add control gem to gemfile.
|
75
|
+
<pre><code>gem 'control'</code></pre>
|
76
|
+
or
|
77
|
+
<pre><code>gem 'control', :git => 'git://github.com/zeto/control.git' # Edge</code></pre>
|
78
|
+
|
79
|
+
2. Generate tables (run rake db:migrate after)
|
80
|
+
<pre><code>$ rails generate control_install</pre></code>
|
81
|
+
|
82
|
+
Testing the gem:
|
83
|
+
<pre><code>
|
84
|
+
$ bundle
|
85
|
+
$ rake
|
86
|
+
</pre></code>
|
87
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => [:test_units]
|
5
|
+
|
6
|
+
desc "Run tests"
|
7
|
+
Rake::TestTask.new("test_units") do |t|
|
8
|
+
t.pattern = 'test/lib/control/*_test.rb'
|
9
|
+
t.verbose = true
|
10
|
+
t.warning = false
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
desc "Open an irb session"
|
15
|
+
task :console do
|
16
|
+
sh "irb -rubygems -I lib -r control.rb"
|
17
|
+
end
|
18
|
+
|
data/control.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'control'
|
3
|
+
s.version = '0.9.1'
|
4
|
+
s.date = '2012-08-07'
|
5
|
+
s.summary = "Workflow State Machine"
|
6
|
+
s.description = "State Machine integrated with ActiveRecord"
|
7
|
+
s.authors = ["Jose Goncalves"]
|
8
|
+
s.email = 'zetoeu@gmail.com'
|
9
|
+
s.homepage = 'http://rubygems.org/gems/control'
|
10
|
+
s.files = Dir["**/*"] - Dir["*.gem"] - ["Gemfile.lock"]
|
11
|
+
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.add_dependency('activerecord','>= 3.0.0')
|
14
|
+
s.add_dependency('activesupport','>= 3.0.0')
|
15
|
+
s.add_development_dependency "sqlite3-ruby"
|
16
|
+
s.add_development_dependency "turn"
|
17
|
+
s.add_development_dependency "debugger"
|
18
|
+
s.add_development_dependency "rake"
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateTransitions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table(:transitions) do |t|
|
4
|
+
t.string :workflow_class
|
5
|
+
t.integer :workflow_id
|
6
|
+
t.string :from_class
|
7
|
+
t.integer :from_id
|
8
|
+
t.string :to_class
|
9
|
+
t.integer :to_id
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :transitions
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
2
|
+
ActiveRecord::Schema.define(:version => 1) do
|
3
|
+
|
4
|
+
create_table :transitions do |t|
|
5
|
+
t.string :workflow
|
6
|
+
t.integer :workflow_id
|
7
|
+
t.string :from
|
8
|
+
t.integer :from_id
|
9
|
+
t.string :to
|
10
|
+
t.integer :to_id
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Control
|
2
|
+
class ControlError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
# Raised when the current state does not allow the transition, defined with next_states
|
6
|
+
class InvalidTransition < ControlError
|
7
|
+
end
|
8
|
+
|
9
|
+
class WorkflowDisabled < ControlError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raised when the state does not reference any workflow with 'belongs_to'
|
13
|
+
class NoAssociationToWorkflow < ControlError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Raised when the workflow is in a final state and is therefore blocked for any new transition
|
17
|
+
class FinalState < ControlError
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Control
|
2
|
+
module State
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.send :before_save, :validate_transition
|
7
|
+
base.send :after_save, :save_transition
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Getter/setter for possible next_states
|
12
|
+
#
|
13
|
+
# @param states state classes in underscore format. Can also use :none to mark state as final
|
14
|
+
# @return [Array<State>] called with no parameters, an array is returned with next possible states
|
15
|
+
def next_states(*states)
|
16
|
+
if states.any?
|
17
|
+
if states.include? :none
|
18
|
+
@final = true
|
19
|
+
else
|
20
|
+
@next_states = states.map{|s| s.to_s.classify}.compact
|
21
|
+
end
|
22
|
+
else
|
23
|
+
if @final # state is final, there are no possible next states
|
24
|
+
Array.new
|
25
|
+
else # state is not final, carry on
|
26
|
+
if @next_states # possible next states were previously declared in class
|
27
|
+
@next_states.map { |s| Kernel.qualified_const_get(s) }
|
28
|
+
else # all states connected to the workflow are valid, no constrains
|
29
|
+
workflow_class.states
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks if class is a state
|
36
|
+
# @return [Boolean]
|
37
|
+
def is_state?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Gets the workflow class associated with the state
|
42
|
+
# @return [Class] workflow class object
|
43
|
+
def workflow_class
|
44
|
+
unless @workflow_class
|
45
|
+
reflect_on_all_associations.each do |a|
|
46
|
+
klass = Kernel.qualified_const_get(a.name.to_s.classify)
|
47
|
+
@workflow_class = klass if klass.respond_to?(:is_workflow?) && klass.is_workflow?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@workflow_class
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks if a state is final. There can be no transitions.
|
54
|
+
# @return [Boolean]
|
55
|
+
def final?
|
56
|
+
!!@final
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Gets the workflow associated with state
|
61
|
+
# @return workflow object
|
62
|
+
def workflow
|
63
|
+
send self.class.workflow_class.to_s.underscore if self.class.workflow_class
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get previous state
|
67
|
+
# @return previous state object
|
68
|
+
def previous
|
69
|
+
transition = workflow.transitions.where(:to_class => self.class, :to_id => self.id).first
|
70
|
+
transition.from if transition
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get the next state
|
74
|
+
# @return next state object
|
75
|
+
def next
|
76
|
+
transition = workflow.transitions.where(:from_class => self.class, :from_id => self.id).first
|
77
|
+
transition.to if transition
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Validate requirements for transition. Will raise Exceptions if invalid
|
83
|
+
# - State must be associated with a workflow
|
84
|
+
# - Associated workflow must be enabled
|
85
|
+
# - Either the first state is being saved, or the state being saved is defined in next_states
|
86
|
+
# - No transition can occur if the state is declared as final
|
87
|
+
def validate_transition
|
88
|
+
raise Control::NoAssociationToWorkflow unless is_part_of_workflow?
|
89
|
+
raise Control::WorkflowDisabled unless workflow.enabled
|
90
|
+
raise Control::InvalidTransition unless workflow_initial_state_or_valid_next_state
|
91
|
+
raise Control::FinalState if current_state_is_final?
|
92
|
+
end
|
93
|
+
|
94
|
+
# Checks if current state is declared as final
|
95
|
+
# @return [Boolean]
|
96
|
+
def current_state_is_final?
|
97
|
+
workflow.current_state && workflow.current_state.class.final?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Checks if state is part of workflow. Defined with activerecord decorators.
|
101
|
+
# @return [Boolean]
|
102
|
+
def is_part_of_workflow?
|
103
|
+
!!workflow
|
104
|
+
end
|
105
|
+
|
106
|
+
# Checks if state being saved is defined in next_states of current workflow state.
|
107
|
+
# @return [Boolean]
|
108
|
+
def next_state_is_valid
|
109
|
+
workflow.current_state && ((workflow.current_state.class.next_states && (workflow.current_state.class.next_states.any? && (workflow.current_state.class.next_states.include? self.class))) || workflow.current_state.class.next_states.blank?)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Checks if state is the first being saved or complies with defined next states
|
113
|
+
# @return [Boolean]
|
114
|
+
def workflow_initial_state_or_valid_next_state
|
115
|
+
!workflow.current_state || next_state_is_valid
|
116
|
+
end
|
117
|
+
|
118
|
+
# Save a transition object
|
119
|
+
# Runs after any successfully saved state, connects the previous and now current state
|
120
|
+
def save_transition
|
121
|
+
transition = Control::Transition.new do |t|
|
122
|
+
t.workflow_class = workflow.class.name
|
123
|
+
t.workflow_id = workflow.id
|
124
|
+
if workflow.current_state
|
125
|
+
t.from_class = workflow.current_state.class.name
|
126
|
+
t.from_id = workflow.current_state.id
|
127
|
+
end
|
128
|
+
t.to_class = self.class.name
|
129
|
+
t.to_id = id
|
130
|
+
end
|
131
|
+
transition.save!
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Control
|
2
|
+
class Transition < ActiveRecord::Base
|
3
|
+
validates :workflow_class, :presence => true
|
4
|
+
validates :workflow_id, :presence => true
|
5
|
+
validates :to_class, :presence => true
|
6
|
+
validates :to_id, :presence => true
|
7
|
+
validates :from_id, :presence => true, :if => "!from_class.nil?"
|
8
|
+
validate :validate_classes
|
9
|
+
validate :validate_objects
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Validate if classes are declared
|
14
|
+
def validate_classes
|
15
|
+
Kernel.qualified_const_get(from_class) unless from_class.blank? rescue errors.add(:from_class,'invalid from')
|
16
|
+
Kernel.qualified_const_get(to_class) rescue errors.add(:to_class,'invalid to')
|
17
|
+
Kernel.qualified_const_get(workflow_class) rescue errors.add(:workflow_class,'invalid workflow')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Validate if state and workflow objects exist
|
21
|
+
def validate_objects
|
22
|
+
from rescue errors.add(:workflow,'invalid from')
|
23
|
+
to rescue errors.add(:to,'invalid to')
|
24
|
+
workflow rescue errors.add(:workflow,'invalid workflow')
|
25
|
+
end
|
26
|
+
|
27
|
+
public
|
28
|
+
|
29
|
+
# Pretty print a transition
|
30
|
+
# @return [String] transition description: Workflow, from and to states, date
|
31
|
+
def to_s
|
32
|
+
"Workflow: #{workflow_class} || #{created_at} #{from_class} -> #{to_class}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get workflow
|
36
|
+
# @return workflow object
|
37
|
+
def workflow
|
38
|
+
Kernel.qualified_const_get(workflow_class).find(workflow_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get destination state for transition
|
42
|
+
# @return state object
|
43
|
+
def to
|
44
|
+
Kernel.qualified_const_get(to_class).find(to_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get source state for transition
|
48
|
+
# @return state object
|
49
|
+
def from
|
50
|
+
Kernel.qualified_const_get(from_class).find(from_id) unless from_class.blank?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Control
|
2
|
+
module Workflow
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Checks if class is a workflow
|
10
|
+
def is_workflow?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get all state classes associated to this workflow
|
15
|
+
# @return [Array<Class>]
|
16
|
+
def states
|
17
|
+
reflect_on_all_associations.each.map do |a|
|
18
|
+
klass = Kernel.qualified_const_get(a.class_name.to_s.classify)
|
19
|
+
if klass.respond_to?(:is_state?) and klass.is_state?
|
20
|
+
klass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Checks if workflow is enabled
|
27
|
+
# @return [Boolean]
|
28
|
+
def enabled
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get all state classes associated to this workflow
|
33
|
+
# @return [Array<Class>]
|
34
|
+
def states
|
35
|
+
self.class.states
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get all transitions.
|
39
|
+
# @return [Array<Transition>]
|
40
|
+
def transitions
|
41
|
+
Control::Transition.where(:workflow_class => self.class.name, :workflow_id => self.id)
|
42
|
+
end
|
43
|
+
|
44
|
+
alias :history :transitions
|
45
|
+
|
46
|
+
# Get workflow current state
|
47
|
+
# @return the state object
|
48
|
+
def current_state
|
49
|
+
transitions.last.to if transitions.last
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class ControlInstallGenerator < Rails::Generators::Base
|
5
|
+
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
source_root File.expand_path('../../../../', __FILE__)
|
8
|
+
|
9
|
+
def self.next_migration_number(path)
|
10
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
11
|
+
end
|
12
|
+
|
13
|
+
def migration
|
14
|
+
unless transitions_table_exists? # SOURCE -> DESTINATION
|
15
|
+
migration_template "db/migrate/create_transitions.rb", "db/migrate/create_transitions.rb"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def transitions_table_exists?
|
20
|
+
ActiveRecord::Base.connection.table_exists?(:transitions)
|
21
|
+
end
|
22
|
+
end
|