control 0.9.0 → 0.9.1
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/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
|
+
[](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
|