control 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. data/Gemfile +3 -0
  2. data/README.md +87 -0
  3. data/Rakefile +18 -0
  4. data/control.gemspec +19 -0
  5. data/db/migrate/create_transitions.rb +18 -0
  6. data/db/schema.rb +14 -0
  7. data/lib/control/errors.rb +19 -0
  8. data/lib/control/state.rb +135 -0
  9. data/lib/control/transition.rb +53 -0
  10. data/lib/control/workflow.rb +52 -0
  11. data/lib/generators/control_install/USAGE +8 -0
  12. data/lib/generators/control_install/control_install_generator.rb +22 -0
  13. data/test/lib/control/state_test.rb +80 -0
  14. data/test/lib/control/transition_test.rb +84 -0
  15. data/test/lib/control/workflow_test.rb +107 -0
  16. data/test/rails_app/Gemfile +22 -0
  17. data/test/rails_app/Gemfile.lock +130 -0
  18. data/test/rails_app/README.rdoc +261 -0
  19. data/test/rails_app/Rakefile +7 -0
  20. data/test/rails_app/app/assets/images/rails.png +0 -0
  21. data/test/rails_app/app/assets/javascripts/application.js +15 -0
  22. data/test/rails_app/app/assets/javascripts/assemblies.js.coffee +3 -0
  23. data/test/rails_app/app/assets/javascripts/boxes.js.coffee +3 -0
  24. data/test/rails_app/app/assets/javascripts/products.js.coffee +3 -0
  25. data/test/rails_app/app/assets/javascripts/rejects.js.coffee +3 -0
  26. data/test/rails_app/app/assets/javascripts/validates.js.coffee +3 -0
  27. data/test/rails_app/app/assets/stylesheets/application.css +13 -0
  28. data/test/rails_app/app/assets/stylesheets/assemblies.css.scss +3 -0
  29. data/test/rails_app/app/assets/stylesheets/boxes.css.scss +3 -0
  30. data/test/rails_app/app/assets/stylesheets/products.css.scss +8 -0
  31. data/test/rails_app/app/assets/stylesheets/rejects.css.scss +3 -0
  32. data/test/rails_app/app/assets/stylesheets/scaffolds.css.scss +56 -0
  33. data/test/rails_app/app/assets/stylesheets/validates.css.scss +3 -0
  34. data/test/rails_app/app/controllers/application_controller.rb +3 -0
  35. data/test/rails_app/app/controllers/assemblies_controller.rb +88 -0
  36. data/test/rails_app/app/controllers/boxes_controller.rb +88 -0
  37. data/test/rails_app/app/controllers/products_controller.rb +83 -0
  38. data/test/rails_app/app/controllers/rejects_controller.rb +88 -0
  39. data/test/rails_app/app/controllers/validates_controller.rb +88 -0
  40. data/test/rails_app/app/helpers/application_helper.rb +2 -0
  41. data/test/rails_app/app/helpers/assemblies_helper.rb +2 -0
  42. data/test/rails_app/app/helpers/boxes_helper.rb +2 -0
  43. data/test/rails_app/app/helpers/products_helper.rb +14 -0
  44. data/test/rails_app/app/helpers/rejects_helper.rb +2 -0
  45. data/test/rails_app/app/helpers/validates_helper.rb +2 -0
  46. data/test/rails_app/app/models/assembly.rb +6 -0
  47. data/test/rails_app/app/models/box.rb +5 -0
  48. data/test/rails_app/app/models/product.rb +8 -0
  49. data/test/rails_app/app/models/reject.rb +6 -0
  50. data/test/rails_app/app/models/validate.rb +6 -0
  51. data/test/rails_app/app/views/assemblies/_form.html.erb +24 -0
  52. data/test/rails_app/app/views/assemblies/edit.html.erb +6 -0
  53. data/test/rails_app/app/views/assemblies/index.html.erb +25 -0
  54. data/test/rails_app/app/views/assemblies/new.html.erb +5 -0
  55. data/test/rails_app/app/views/assemblies/show.html.erb +14 -0
  56. data/test/rails_app/app/views/boxes/_form.html.erb +24 -0
  57. data/test/rails_app/app/views/boxes/edit.html.erb +6 -0
  58. data/test/rails_app/app/views/boxes/index.html.erb +25 -0
  59. data/test/rails_app/app/views/boxes/new.html.erb +5 -0
  60. data/test/rails_app/app/views/boxes/show.html.erb +14 -0
  61. data/test/rails_app/app/views/layouts/application.html.erb +14 -0
  62. data/test/rails_app/app/views/products/_form.html.erb +21 -0
  63. data/test/rails_app/app/views/products/edit.html.erb +6 -0
  64. data/test/rails_app/app/views/products/index.html.erb +23 -0
  65. data/test/rails_app/app/views/products/new.html.erb +5 -0
  66. data/test/rails_app/app/views/products/show.html.erb +34 -0
  67. data/test/rails_app/app/views/rejects/_form.html.erb +20 -0
  68. data/test/rails_app/app/views/rejects/edit.html.erb +6 -0
  69. data/test/rails_app/app/views/rejects/index.html.erb +23 -0
  70. data/test/rails_app/app/views/rejects/new.html.erb +5 -0
  71. data/test/rails_app/app/views/rejects/show.html.erb +9 -0
  72. data/test/rails_app/app/views/validates/_form.html.erb +40 -0
  73. data/test/rails_app/app/views/validates/edit.html.erb +6 -0
  74. data/test/rails_app/app/views/validates/index.html.erb +33 -0
  75. data/test/rails_app/app/views/validates/new.html.erb +5 -0
  76. data/test/rails_app/app/views/validates/show.html.erb +35 -0
  77. data/test/rails_app/config.ru +4 -0
  78. data/test/rails_app/config/application.rb +59 -0
  79. data/test/rails_app/config/boot.rb +6 -0
  80. data/test/rails_app/config/database.yml +25 -0
  81. data/test/rails_app/config/environment.rb +5 -0
  82. data/test/rails_app/config/environments/development.rb +37 -0
  83. data/test/rails_app/config/environments/production.rb +67 -0
  84. data/test/rails_app/config/environments/test.rb +37 -0
  85. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  86. data/test/rails_app/config/initializers/inflections.rb +15 -0
  87. data/test/rails_app/config/initializers/mime_types.rb +5 -0
  88. data/test/rails_app/config/initializers/secret_token.rb +7 -0
  89. data/test/rails_app/config/initializers/session_store.rb +8 -0
  90. data/test/rails_app/config/initializers/wrap_parameters.rb +14 -0
  91. data/test/rails_app/config/locales/en.yml +5 -0
  92. data/test/rails_app/config/routes.rb +68 -0
  93. data/test/rails_app/db/development.sqlite3 +0 -0
  94. data/test/rails_app/db/migrate/20120126165218_create_products.rb +8 -0
  95. data/test/rails_app/db/migrate/20120126165821_add_codename_to_products.rb +6 -0
  96. data/test/rails_app/db/migrate/20120126173019_create_validates.rb +14 -0
  97. data/test/rails_app/db/migrate/20120126173232_create_assemblies.rb +10 -0
  98. data/test/rails_app/db/migrate/20120126173516_create_boxes.rb +10 -0
  99. data/test/rails_app/db/migrate/20120126173539_create_rejects.rb +9 -0
  100. data/test/rails_app/db/migrate/20120126174101_create_transitions.rb +18 -0
  101. data/test/rails_app/db/schema.rb +64 -0
  102. data/test/rails_app/db/seeds.rb +7 -0
  103. data/test/rails_app/db/test.sqlite3 +0 -0
  104. data/test/rails_app/doc/README_FOR_APP +2 -0
  105. data/test/rails_app/log/development.log +8963 -0
  106. data/test/rails_app/log/test.log +321 -0
  107. data/test/rails_app/public/404.html +26 -0
  108. data/test/rails_app/public/422.html +26 -0
  109. data/test/rails_app/public/500.html +25 -0
  110. data/test/rails_app/public/favicon.ico +0 -0
  111. data/test/rails_app/public/index.html.old +241 -0
  112. data/test/rails_app/public/robots.txt +5 -0
  113. data/test/rails_app/script/rails +6 -0
  114. data/test/rails_app/test/fixtures/assemblies.yml +9 -0
  115. data/test/rails_app/test/fixtures/boxes.yml +9 -0
  116. data/test/rails_app/test/fixtures/products.yml +11 -0
  117. data/test/rails_app/test/fixtures/rejects.yml +7 -0
  118. data/test/rails_app/test/fixtures/validates.yml +17 -0
  119. data/test/rails_app/test/functional/assemblies_controller_test.rb +49 -0
  120. data/test/rails_app/test/functional/boxes_controller_test.rb +49 -0
  121. data/test/rails_app/test/functional/products_controller_test.rb +49 -0
  122. data/test/rails_app/test/functional/rejects_controller_test.rb +49 -0
  123. data/test/rails_app/test/functional/validates_controller_test.rb +49 -0
  124. data/test/rails_app/test/performance/browsing_test.rb +12 -0
  125. data/test/rails_app/test/test_helper.rb +13 -0
  126. data/test/rails_app/test/unit/assembly_test.rb +7 -0
  127. data/test/rails_app/test/unit/box_test.rb +7 -0
  128. data/test/rails_app/test/unit/helpers/assemblies_helper_test.rb +4 -0
  129. data/test/rails_app/test/unit/helpers/boxes_helper_test.rb +4 -0
  130. data/test/rails_app/test/unit/helpers/products_helper_test.rb +4 -0
  131. data/test/rails_app/test/unit/helpers/rejects_helper_test.rb +4 -0
  132. data/test/rails_app/test/unit/helpers/validates_helper_test.rb +4 -0
  133. data/test/rails_app/test/unit/product_test.rb +7 -0
  134. data/test/rails_app/test/unit/reject_test.rb +7 -0
  135. data/test/rails_app/test/unit/validate_test.rb +7 -0
  136. data/test/rails_app/tmp/cache/assets/CD7/6F0/sprockets%2Fbd3936370d0f952ada5774e2230046ed +0 -0
  137. data/test/rails_app/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  138. data/test/rails_app/tmp/cache/assets/CDF/890/sprockets%2F1380c8a9a90c9434c33ea3a2b759546f +0 -0
  139. data/test/rails_app/tmp/cache/assets/CF0/DA0/sprockets%2Fd7d5b37686831d37c4dd75e645f5e016 +0 -0
  140. data/test/rails_app/tmp/cache/assets/CF1/E50/sprockets%2F627ffd39f3642a2a236775d77839fac3 +0 -0
  141. data/test/rails_app/tmp/cache/assets/CFC/170/sprockets%2F80efc018414af123b11d0c6485e87b1b +0 -0
  142. data/test/rails_app/tmp/cache/assets/D0B/B40/sprockets%2F2eace11b521905c77d38470c975fa28a +0 -0
  143. data/test/rails_app/tmp/cache/assets/D13/8E0/sprockets%2F4476d1e237763ebe413a93e0ae559e6b +0 -0
  144. data/test/rails_app/tmp/cache/assets/D13/E20/sprockets%2F382583b312ba3d6f09b596f6bb63a98b +0 -0
  145. data/test/rails_app/tmp/cache/assets/D15/750/sprockets%2F4cc25a6e69224dc83fed65a4242479c5 +0 -0
  146. data/test/rails_app/tmp/cache/assets/D1B/3B0/sprockets%2F0098fc768cb8d9c834fcb13163c85b54 +0 -0
  147. data/test/rails_app/tmp/cache/assets/D20/C40/sprockets%2F46d84fd895a96b157269bd6b7bae2623 +0 -0
  148. data/test/rails_app/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  149. data/test/rails_app/tmp/cache/assets/D3F/4A0/sprockets%2Fba62c891c908a4219fb4d2005f886aee +0 -0
  150. data/test/rails_app/tmp/cache/assets/D45/C10/sprockets%2F29eede3408740d29d159a8367d1abe4e +0 -0
  151. data/test/rails_app/tmp/cache/assets/D4B/EC0/sprockets%2Fb261fa987a3e88f07b352eba69495e3b +0 -0
  152. data/test/rails_app/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  153. data/test/rails_app/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  154. data/test/rails_app/tmp/cache/assets/D65/620/sprockets%2Fc821c64ad244b34cd1c92ce6e4844f2a +0 -0
  155. data/test/rails_app/tmp/cache/assets/D78/F10/sprockets%2Fc81a77308c5e0c2b8efc69bece538149 +0 -0
  156. data/test/rails_app/tmp/cache/assets/D7B/040/sprockets%2F2ea981ebd7b7a34bd9d75f626f81c526 +0 -0
  157. data/test/rails_app/tmp/cache/assets/D9C/4A0/sprockets%2Fc15750bf0161f2878cf9da9bb1a84ecb +0 -0
  158. data/test/rails_app/tmp/cache/assets/DA2/120/sprockets%2Fdfd12e4fb8ce07a2d3c4856974a2f4e3 +0 -0
  159. data/test/rails_app/tmp/cache/assets/DA9/180/sprockets%2F1eca18f8334b5ce5f73e18d778bc7e3b +0 -0
  160. data/test/rails_app/tmp/cache/assets/DB4/120/sprockets%2Fc4ad185fad2acc8f1133bb2a80f400d7 +0 -0
  161. data/test/rails_app/tmp/cache/assets/DD2/B40/sprockets%2Ff8f229b2c6e61975aca38da7a1cb7b8c +0 -0
  162. data/test/rails_app/tmp/cache/assets/DD5/CF0/sprockets%2Fac6d0c5baedfe70588f57e99b0c4c722 +0 -0
  163. data/test/rails_app/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  164. data/test/rails_app/tmp/cache/assets/DDD/AE0/sprockets%2F524b597ade6bc1d79c9a4ffd6e3682bd +0 -0
  165. data/test/rails_app/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  166. data/test/rails_app/tmp/cache/assets/E06/E60/sprockets%2F1afcf29a4e0c835cb47d9bf28cf8a86b +0 -0
  167. data/test/rails_app/tmp/cache/assets/E19/2A0/sprockets%2F10fcfbe6ebae11a40c8eac41939a1b9a +0 -0
  168. data/test/rails_app/tmp/cache/assets/E25/4C0/sprockets%2Fde2fd9fd11c04a582cdbbe3d84a35ae6 +0 -0
  169. data/test/rails_app/tmp/cache/assets/E35/980/sprockets%2Fa5beac77f747ffbc71d32edecf38904a +0 -0
  170. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/assemblies.css.scssc +0 -0
  171. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/boxes.css.scssc +0 -0
  172. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/products.css.scssc +0 -0
  173. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/rejects.css.scssc +0 -0
  174. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/scaffolds.css.scssc +0 -0
  175. data/test/rails_app/tmp/cache/sass/0caf4829b2adaabee514492a80140305d59a00b9/validates.css.scssc +0 -0
  176. data/test/test_helper.rb +67 -0
  177. data/test/test_schema.rb +32 -0
  178. metadata +210 -1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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 &lt; ActiveRecord::Base
25
+ include Control::Workflow
26
+
27
+ has_many :ons
28
+ has_many :offs
29
+ end
30
+
31
+ class On &lt; ActiveRecord::Base
32
+ include Control::State
33
+
34
+ belongs_to :bulb
35
+ end
36
+
37
+ class Off &lt; 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,8 @@
1
+ Description:
2
+ Installs control by coping and executing the Transition migration
3
+
4
+ Example:
5
+ rails generate control_install
6
+
7
+ This will create:
8
+ db/migrate/%Y%m%d%H%M%Screate_transitions.rb
@@ -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