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.
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