branston 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (188) hide show
  1. data/README.rdoc +1 -1
  2. data/lib/branston/Gemfile +25 -0
  3. data/lib/branston/Gemfile.lock +76 -0
  4. data/lib/branston/app/controllers/application_controller.rb +1 -1
  5. data/lib/branston/app/controllers/outcomes_controller.rb +2 -0
  6. data/lib/branston/app/controllers/stories_controller.rb +82 -86
  7. data/lib/branston/app/controllers/users_controller.rb +69 -11
  8. data/lib/branston/app/helpers/iterations_helper.rb +13 -13
  9. data/lib/branston/app/models/iteration.rb +3 -1
  10. data/lib/branston/app/models/release.rb +0 -1
  11. data/lib/branston/app/models/story.rb +30 -28
  12. data/lib/branston/app/models/user.rb +46 -1
  13. data/lib/branston/app/views/layouts/_header.html.erb +8 -3
  14. data/lib/branston/app/views/layouts/user_roles.html.erb +5 -5
  15. data/lib/branston/app/views/sessions/new.html.erb +8 -14
  16. data/lib/branston/app/views/users/_admin_controls.html.erb +14 -0
  17. data/lib/branston/app/views/users/_form.html.erb +27 -0
  18. data/lib/branston/app/views/users/edit.html.erb +9 -0
  19. data/lib/branston/app/views/users/index.html.erb +14 -0
  20. data/lib/branston/app/views/users/new.html.erb +3 -22
  21. data/lib/branston/config/boot.rb +20 -0
  22. data/lib/branston/config/environment.rb +2 -7
  23. data/lib/branston/config/environments/test.rb +0 -8
  24. data/lib/branston/config/preinitializer.rb +21 -0
  25. data/lib/branston/config/routes.rb +15 -10
  26. data/lib/branston/db/development.sqlite3 +0 -0
  27. data/lib/branston/db/development_structure.sql +21 -8
  28. data/lib/branston/db/migrate/20100723161424_add_state_to_user.rb +12 -0
  29. data/lib/branston/db/migrate/20100726150322_add_activation_fields_to_user.rb +12 -0
  30. data/lib/branston/db/migrate/20100729125551_set_default_user_state_to_pending.rb +10 -0
  31. data/lib/branston/db/migrate/20100812133837_add_is_admin_property_to_user.rb +10 -0
  32. data/lib/branston/db/migrate/20100812140532_set_default_user_state_to_active.rb +10 -0
  33. data/lib/branston/db/migrate/20100812143455_add_default_admin_user.rb +17 -0
  34. data/lib/branston/db/migrate/20110408162438_remove_is_admin_property_and_add_role_instead.rb +12 -0
  35. data/lib/branston/db/pristine.sqlite3 +0 -0
  36. data/lib/branston/db/schema.rb +6 -8
  37. data/lib/branston/db/test.sqlite3 +0 -0
  38. data/lib/branston/log/development.log +1181 -433
  39. data/lib/branston/log/test.log +145306 -52026
  40. data/lib/branston/test/blueprints.rb +22 -28
  41. data/lib/branston/test/functional/iterations_controller_test.rb +149 -113
  42. data/lib/branston/test/functional/outcomes_controller_test.rb +94 -60
  43. data/lib/branston/test/functional/preconditions_controller_test.rb +101 -67
  44. data/lib/branston/test/functional/releases_controller_test.rb +85 -49
  45. data/lib/branston/test/functional/scenarios_controller_test.rb +104 -70
  46. data/lib/branston/test/functional/stories_controller_test.rb +41 -12
  47. data/lib/branston/test/functional/users_controller_test.rb +364 -43
  48. data/lib/branston/test/unit/iteration_test.rb +37 -6
  49. data/lib/branston/test/unit/outcome_test.rb +2 -2
  50. data/lib/branston/test/unit/participation_test.rb +2 -2
  51. data/lib/branston/test/unit/precondition_test.rb +3 -3
  52. data/lib/branston/test/unit/release_test.rb +4 -0
  53. data/lib/branston/test/unit/scenario_test.rb +4 -4
  54. data/lib/branston/test/unit/story_test.rb +62 -40
  55. data/lib/branston/test/unit/user_test.rb +195 -5
  56. metadata +136 -156
  57. data/lib/branston/app/controllers/user_roles_controller.rb +0 -105
  58. data/lib/branston/app/helpers/user_roles_helper.rb +0 -2
  59. data/lib/branston/app/models/user_role.rb +0 -21
  60. data/lib/branston/app/views/layouts/outcomes.html.erb +0 -17
  61. data/lib/branston/app/views/layouts/preconditions.html.erb +0 -17
  62. data/lib/branston/app/views/layouts/releases.html.erb +0 -17
  63. data/lib/branston/app/views/user_roles/edit.html.erb +0 -16
  64. data/lib/branston/app/views/user_roles/index.html.erb +0 -20
  65. data/lib/branston/app/views/user_roles/new.html.erb +0 -15
  66. data/lib/branston/app/views/user_roles/show.html.erb +0 -8
  67. data/lib/branston/coverage/app-controllers-application_controller_rb.html +0 -231
  68. data/lib/branston/coverage/app-controllers-iterations_controller_rb.html +0 -801
  69. data/lib/branston/coverage/app-controllers-outcomes_controller_rb.html +0 -759
  70. data/lib/branston/coverage/app-controllers-preconditions_controller_rb.html +0 -783
  71. data/lib/branston/coverage/app-controllers-releases_controller_rb.html +0 -705
  72. data/lib/branston/coverage/app-controllers-scenarios_controller_rb.html +0 -777
  73. data/lib/branston/coverage/app-controllers-sessions_controller_rb.html +0 -411
  74. data/lib/branston/coverage/app-controllers-stories_controller_rb.html +0 -1071
  75. data/lib/branston/coverage/app-controllers-user_roles_controller_rb.html +0 -693
  76. data/lib/branston/coverage/app-controllers-users_controller_rb.html +0 -315
  77. data/lib/branston/coverage/app-helpers-application_helper_rb.html +0 -327
  78. data/lib/branston/coverage/app-helpers-iterations_helper_rb.html +0 -363
  79. data/lib/branston/coverage/app-helpers-outcomes_helper_rb.html +0 -75
  80. data/lib/branston/coverage/app-helpers-preconditions_helper_rb.html +0 -75
  81. data/lib/branston/coverage/app-helpers-releases_helper_rb.html +0 -75
  82. data/lib/branston/coverage/app-helpers-sessions_helper_rb.html +0 -75
  83. data/lib/branston/coverage/app-helpers-stories_helper_rb.html +0 -75
  84. data/lib/branston/coverage/app-helpers-user_roles_helper_rb.html +0 -75
  85. data/lib/branston/coverage/app-models-iteration_rb.html +0 -321
  86. data/lib/branston/coverage/app-models-outcome_rb.html +0 -243
  87. data/lib/branston/coverage/app-models-participation_rb.html +0 -189
  88. data/lib/branston/coverage/app-models-precondition_rb.html +0 -243
  89. data/lib/branston/coverage/app-models-release_rb.html +0 -195
  90. data/lib/branston/coverage/app-models-scenario_rb.html +0 -231
  91. data/lib/branston/coverage/app-models-story_rb.html +0 -621
  92. data/lib/branston/coverage/app-models-user_rb.html +0 -513
  93. data/lib/branston/coverage/app-models-user_role_rb.html +0 -189
  94. data/lib/branston/coverage/index.html +0 -570
  95. data/lib/branston/coverage/jquery-1.3.2.min.js +0 -19
  96. data/lib/branston/coverage/jquery.tablesorter.min.js +0 -15
  97. data/lib/branston/coverage/lib-client_rb.html +0 -537
  98. data/lib/branston/coverage/lib-faker_extras_rb.html +0 -207
  99. data/lib/branston/coverage/lib-story_generator_rb.html +0 -873
  100. data/lib/branston/coverage/print.css +0 -12
  101. data/lib/branston/coverage/rcov.js +0 -42
  102. data/lib/branston/coverage/screen.css +0 -270
  103. data/lib/branston/db/migrate/20091127131037_create_user_roles.rb +0 -13
  104. data/lib/branston/db/migrate/20091127172950_add_story_id_to_user_role.rb +0 -10
  105. data/lib/branston/test/functional/user_roles_controller_test.rb +0 -71
  106. data/lib/branston/test/unit/helpers/user_roles_helper_test.rb +0 -4
  107. data/lib/branston/test/unit/user_role_test.rb +0 -9
  108. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_flat.txt +0 -8
  109. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_graph.html +0 -6718
  110. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_tree.txt +0 -9942
  111. data/lib/branston/vendor/plugins/state_machine/CHANGELOG.rdoc +0 -298
  112. data/lib/branston/vendor/plugins/state_machine/LICENSE +0 -20
  113. data/lib/branston/vendor/plugins/state_machine/README.rdoc +0 -466
  114. data/lib/branston/vendor/plugins/state_machine/Rakefile +0 -98
  115. data/lib/branston/vendor/plugins/state_machine/examples/AutoShop_state.png +0 -0
  116. data/lib/branston/vendor/plugins/state_machine/examples/Car_state.png +0 -0
  117. data/lib/branston/vendor/plugins/state_machine/examples/TrafficLight_state.png +0 -0
  118. data/lib/branston/vendor/plugins/state_machine/examples/Vehicle_state.png +0 -0
  119. data/lib/branston/vendor/plugins/state_machine/examples/auto_shop.rb +0 -11
  120. data/lib/branston/vendor/plugins/state_machine/examples/car.rb +0 -19
  121. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/controller.rb +0 -51
  122. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/model.rb +0 -28
  123. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_edit.html.erb +0 -24
  124. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_index.html.erb +0 -23
  125. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_new.html.erb +0 -13
  126. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_show.html.erb +0 -17
  127. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/controller.rb +0 -43
  128. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/migration.rb +0 -11
  129. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/model.rb +0 -23
  130. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_edit.html.erb +0 -25
  131. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_index.html.erb +0 -23
  132. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_new.html.erb +0 -14
  133. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_show.html.erb +0 -17
  134. data/lib/branston/vendor/plugins/state_machine/examples/traffic_light.rb +0 -7
  135. data/lib/branston/vendor/plugins/state_machine/examples/vehicle.rb +0 -31
  136. data/lib/branston/vendor/plugins/state_machine/init.rb +0 -1
  137. data/lib/branston/vendor/plugins/state_machine/lib/state_machine.rb +0 -388
  138. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/assertions.rb +0 -36
  139. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/callback.rb +0 -189
  140. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/condition_proxy.rb +0 -94
  141. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/eval_helpers.rb +0 -67
  142. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event.rb +0 -252
  143. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event_collection.rb +0 -122
  144. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/extensions.rb +0 -149
  145. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/guard.rb +0 -230
  146. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations.rb +0 -68
  147. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record.rb +0 -492
  148. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/locale.rb +0 -11
  149. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/observer.rb +0 -41
  150. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper.rb +0 -351
  151. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper/observer.rb +0 -139
  152. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/sequel.rb +0 -322
  153. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine.rb +0 -1467
  154. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine_collection.rb +0 -155
  155. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher.rb +0 -123
  156. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher_helpers.rb +0 -54
  157. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/node_collection.rb +0 -152
  158. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state.rb +0 -249
  159. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state_collection.rb +0 -112
  160. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/transition.rb +0 -394
  161. data/lib/branston/vendor/plugins/state_machine/state_machine.gemspec +0 -30
  162. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rake +0 -1
  163. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rb +0 -30
  164. data/lib/branston/vendor/plugins/state_machine/test/classes/switch.rb +0 -11
  165. data/lib/branston/vendor/plugins/state_machine/test/functional/state_machine_test.rb +0 -941
  166. data/lib/branston/vendor/plugins/state_machine/test/test_helper.rb +0 -4
  167. data/lib/branston/vendor/plugins/state_machine/test/unit/assertions_test.rb +0 -40
  168. data/lib/branston/vendor/plugins/state_machine/test/unit/callback_test.rb +0 -455
  169. data/lib/branston/vendor/plugins/state_machine/test/unit/condition_proxy_test.rb +0 -328
  170. data/lib/branston/vendor/plugins/state_machine/test/unit/eval_helpers_test.rb +0 -120
  171. data/lib/branston/vendor/plugins/state_machine/test/unit/event_collection_test.rb +0 -326
  172. data/lib/branston/vendor/plugins/state_machine/test/unit/event_test.rb +0 -743
  173. data/lib/branston/vendor/plugins/state_machine/test/unit/guard_test.rb +0 -908
  174. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/active_record_test.rb +0 -1367
  175. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/data_mapper_test.rb +0 -962
  176. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/sequel_test.rb +0 -859
  177. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations_test.rb +0 -42
  178. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_event_test.rb +0 -7
  179. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_transition_test.rb +0 -7
  180. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_collection_test.rb +0 -938
  181. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_test.rb +0 -2004
  182. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_helpers_test.rb +0 -37
  183. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_test.rb +0 -155
  184. data/lib/branston/vendor/plugins/state_machine/test/unit/node_collection_test.rb +0 -207
  185. data/lib/branston/vendor/plugins/state_machine/test/unit/state_collection_test.rb +0 -280
  186. data/lib/branston/vendor/plugins/state_machine/test/unit/state_machine_test.rb +0 -31
  187. data/lib/branston/vendor/plugins/state_machine/test/unit/state_test.rb +0 -795
  188. data/lib/branston/vendor/plugins/state_machine/test/unit/transition_test.rb +0 -1212
@@ -1,322 +0,0 @@
1
- module StateMachine
2
- module Integrations #:nodoc:
3
- # Adds support for integrating state machines with Sequel models.
4
- #
5
- # == Examples
6
- #
7
- # Below is an example of a simple state machine defined within a
8
- # Sequel model:
9
- #
10
- # class Vehicle < Sequel::Model
11
- # state_machine :initial => :parked do
12
- # event :ignite do
13
- # transition :parked => :idling
14
- # end
15
- # end
16
- # end
17
- #
18
- # The examples in the sections below will use the above class as a
19
- # reference.
20
- #
21
- # == Actions
22
- #
23
- # By default, the action that will be invoked when a state is transitioned
24
- # is the +save+ action. This will cause the resource to save the changes
25
- # made to the state machine's attribute. *Note* that if any other changes
26
- # were made to the resource prior to transition, then those changes will
27
- # be made as well.
28
- #
29
- # For example,
30
- #
31
- # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
32
- # vehicle.name = 'Ford Explorer'
33
- # vehicle.ignite # => true
34
- # vehicle.refresh # => #<Vehicle @values={:state=>"idling", :name=>"Ford Explorer", :id=>1}>
35
- #
36
- # == Events
37
- #
38
- # As described in StateMachine::InstanceMethods#state_machine, event
39
- # attributes are created for every machine that allow transitions to be
40
- # performed automatically when the object's action (in this case, :save)
41
- # is called.
42
- #
43
- # In Sequel, these automated events are run in the following order:
44
- # * before validation - Run before callbacks and persist new states, then validate
45
- # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
- # * after save - Run after callbacks
47
- #
48
- # For example,
49
- #
50
- # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
51
- # vehicle.state_event # => nil
52
- # vehicle.state_event = 'invalid'
53
- # vehicle.valid? # => false
54
- # vehicle.errors.full_messages # => ["state_event is invalid"]
55
- #
56
- # vehicle.state_event = 'ignite'
57
- # vehicle.valid? # => true
58
- # vehicle.save # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
59
- # vehicle.state # => "idling"
60
- # vehicle.state_event # => nil
61
- #
62
- # Note that this can also be done on a mass-assignment basis:
63
- #
64
- # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
65
- # vehicle.state # => "idling"
66
- #
67
- # === Security implications
68
- #
69
- # Beware that public event attributes mean that events can be fired
70
- # whenever mass-assignment is being used. If you want to prevent malicious
71
- # users from tampering with events through URLs / forms, the attribute
72
- # should be protected like so:
73
- #
74
- # class Vehicle < Sequel::Model
75
- # set_restricted_columns :state_event
76
- # # set_allowed_columns ... # Alternative technique
77
- #
78
- # state_machine do
79
- # ...
80
- # end
81
- # end
82
- #
83
- # If you want to only have *some* events be able to fire via mass-assignment,
84
- # you can build two state machines (one public and one protected) like so:
85
- #
86
- # class Vehicle < Sequel::Model
87
- # set_restricted_columns :state_event # Prevent access to events in the first machine
88
- #
89
- # state_machine do
90
- # # Define private events here
91
- # end
92
- #
93
- # # Allow both machines to share the same state
94
- # state_machine :public_state, :attribute => :state do
95
- # # Define public events here
96
- # end
97
- # end
98
- #
99
- # == Transactions
100
- #
101
- # In order to ensure that any changes made during transition callbacks
102
- # are rolled back during a failed attempt, every transition is wrapped
103
- # within a transaction.
104
- #
105
- # For example,
106
- #
107
- # class Message < Sequel::Model
108
- # end
109
- #
110
- # Vehicle.state_machine do
111
- # before_transition do |transition|
112
- # Message.create(:content => transition.inspect)
113
- # false
114
- # end
115
- # end
116
- #
117
- # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
118
- # vehicle.ignite # => false
119
- # Message.count # => 0
120
- #
121
- # *Note* that only before callbacks that halt the callback chain and
122
- # failed attempts to save the record will result in the transaction being
123
- # rolled back. If an after callback halts the chain, the previous result
124
- # still applies and the transaction is *not* rolled back.
125
- #
126
- # To turn off transactions:
127
- #
128
- # class Vehicle < Sequel::Model
129
- # state_machine :initial => :parked, :use_transactions => false do
130
- # ...
131
- # end
132
- # end
133
- #
134
- # == Validation errors
135
- #
136
- # If an event fails to successfully fire because there are no matching
137
- # transitions for the current record, a validation error is added to the
138
- # record's state attribute to help in determining why it failed and for
139
- # reporting via the UI.
140
- #
141
- # For example,
142
- #
143
- # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
144
- # vehicle.ignite # => false
145
- # vehicle.errors.full_messages # => ["state cannot transition via \"ignite\""]
146
- #
147
- # If an event fails to fire because of a validation error on the record and
148
- # *not* because a matching transition was not available, no error messages
149
- # will be added to the state attribute.
150
- #
151
- # == Scopes
152
- #
153
- # To assist in filtering models with specific states, a series of class
154
- # methods are defined on the model for finding records with or without a
155
- # particular set of states.
156
- #
157
- # These named scopes are the functional equivalent of the following
158
- # definitions:
159
- #
160
- # class Vehicle < Sequel::Model
161
- # class << self
162
- # def with_states(*states)
163
- # filter(:state => states)
164
- # end
165
- # alias_method :with_state, :with_states
166
- #
167
- # def without_states(*states)
168
- # filter(~{:state => states})
169
- # end
170
- # alias_method :without_state, :without_states
171
- # end
172
- # end
173
- #
174
- # *Note*, however, that the states are converted to their stored values
175
- # before being passed into the query.
176
- #
177
- # Because of the way scopes work in Sequel, they can be chained like so:
178
- #
179
- # Vehicle.with_state(:parked).order(:id.desc)
180
- #
181
- # == Callbacks
182
- #
183
- # All before/after transition callbacks defined for Sequel resources
184
- # behave in the same way that other Sequel hooks behave. Rather than
185
- # passing in the record as an argument to the callback, the callback is
186
- # instead bound to the object and evaluated within its context.
187
- #
188
- # For example,
189
- #
190
- # class Vehicle < Sequel::Model
191
- # state_machine :initial => :parked do
192
- # before_transition any => :idling do
193
- # put_on_seatbelt
194
- # end
195
- #
196
- # before_transition do |transition|
197
- # # log message
198
- # end
199
- #
200
- # event :ignite do
201
- # transition :parked => :idling
202
- # end
203
- # end
204
- #
205
- # def put_on_seatbelt
206
- # ...
207
- # end
208
- # end
209
- #
210
- # Note, also, that the transition can be accessed by simply defining
211
- # additional arguments in the callback block.
212
- module Sequel
213
- # The default options to use for state machines using this integration
214
- class << self; attr_reader :defaults; end
215
- @defaults = {:action => :save}
216
-
217
- # Should this integration be used for state machines in the given class?
218
- # Classes that include Sequel::Model will automatically use the Sequel
219
- # integration.
220
- def self.matches?(klass)
221
- defined?(::Sequel::Model) && klass <= ::Sequel::Model
222
- end
223
-
224
- # Loads additional files specific to Sequel
225
- def self.extended(base) #:nodoc:
226
- require 'sequel/extensions/inflector' if ::Sequel.const_defined?('VERSION') && ::Sequel::VERSION >= '2.12.0'
227
- end
228
-
229
- # Forces the change in state to be recognized regardless of whether the
230
- # state value actually changed
231
- def write(object, attribute, value)
232
- result = super
233
- column = self.attribute.to_sym
234
- object.changed_columns << column if attribute == :state && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
235
- result
236
- end
237
-
238
- # Adds a validation error to the given object
239
- def invalidate(object, attribute, message, values = [])
240
- object.errors.add(self.attribute(attribute), generate_message(message, values))
241
- end
242
-
243
- # Resets any errors previously added when invalidating the given object
244
- def reset(object)
245
- object.errors.clear
246
- end
247
-
248
- protected
249
- # Defines an initialization hook into the owner class for setting the
250
- # initial state of the machine *before* any attributes are set on the
251
- # object
252
- def define_state_initializer
253
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
254
- # Hooks in to attribute initialization to set the states *prior*
255
- # to the attributes being set
256
- def set(hash, *args)
257
- if new? && !@initialized_state_machines
258
- @initialized_state_machines = true
259
-
260
- ignore = setter_methods(nil, nil).map {|setter| setter.chop.to_sym} & (hash ? hash.keys.map {|attribute| attribute.to_sym} : [])
261
- initialize_state_machines(:dynamic => false, :ignore => ignore)
262
- result = super
263
- initialize_state_machines(:dynamic => true, :ignore => ignore)
264
- result
265
- else
266
- super
267
- end
268
- end
269
- end_eval
270
- end
271
-
272
- # Skips defining reader/writer methods since this is done automatically
273
- def define_state_accessor
274
- name = self.name
275
- owner_class.validates_each(attribute) do |record, attr, value|
276
- machine = record.class.state_machine(name)
277
- machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
278
- end
279
- end
280
-
281
- # Adds hooks into validation for automatically firing events
282
- def define_action_helpers
283
- if super && action == :save
284
- @instance_helper_module.class_eval do
285
- define_method(:valid?) do |*args|
286
- self.class.state_machines.fire_event_attributes(self, :save, false) { super(*args) }
287
- end
288
- end
289
- end
290
- end
291
-
292
- # Creates a scope for finding records *with* a particular state or
293
- # states for the attribute
294
- def create_with_scope(name)
295
- attribute = self.attribute
296
- lambda {|model, values| model.filter(attribute.to_sym => values)}
297
- end
298
-
299
- # Creates a scope for finding records *without* a particular state or
300
- # states for the attribute
301
- def create_without_scope(name)
302
- attribute = self.attribute
303
- lambda {|model, values| model.filter(~{attribute.to_sym => values})}
304
- end
305
-
306
- # Runs a new database transaction, rolling back any changes if the
307
- # yielded block fails (i.e. returns false).
308
- def transaction(object)
309
- object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
310
- end
311
-
312
- # Creates a new callback in the callback chain, always ensuring that
313
- # it's configured to bind to the object as this is the convention for
314
- # Sequel callbacks
315
- def add_callback(type, options, &block)
316
- options[:bind_to_object] = true
317
- options[:terminator] = @terminator ||= lambda {|result| result == false}
318
- super
319
- end
320
- end
321
- end
322
- end
@@ -1,1467 +0,0 @@
1
- require 'state_machine/extensions'
2
- require 'state_machine/assertions'
3
- require 'state_machine/integrations'
4
-
5
- require 'state_machine/state'
6
- require 'state_machine/event'
7
- require 'state_machine/callback'
8
- require 'state_machine/node_collection'
9
- require 'state_machine/state_collection'
10
- require 'state_machine/event_collection'
11
- require 'state_machine/matcher_helpers'
12
-
13
- module StateMachine
14
- # Represents a state machine for a particular attribute. State machines
15
- # consist of states, events and a set of transitions that define how the
16
- # state changes after a particular event is fired.
17
- #
18
- # A state machine will not know all of the possible states for an object
19
- # unless they are referenced *somewhere* in the state machine definition.
20
- # As a result, any unused states should be defined with the +other_states+
21
- # or +state+ helper.
22
- #
23
- # == Actions
24
- #
25
- # When an action is configured for a state machine, it is invoked when an
26
- # object transitions via an event. The success of the event becomes
27
- # dependent on the success of the action. If the action is successful, then
28
- # the transitioned state remains persisted. However, if the action fails
29
- # (by returning false), the transitioned state will be rolled back.
30
- #
31
- # For example,
32
- #
33
- # class Vehicle
34
- # attr_accessor :fail, :saving_state
35
- #
36
- # state_machine :initial => :parked, :action => :save do
37
- # event :ignite do
38
- # transition :parked => :idling
39
- # end
40
- #
41
- # event :park do
42
- # transition :idling => :parked
43
- # end
44
- # end
45
- #
46
- # def save
47
- # @saving_state = state
48
- # fail != true
49
- # end
50
- # end
51
- #
52
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
53
- # vehicle.save # => true
54
- # vehicle.saving_state # => "parked" # The state was "parked" was save was called
55
- #
56
- # # Successful event
57
- # vehicle.ignite # => true
58
- # vehicle.saving_state # => "idling" # The state was "idling" when save was called
59
- # vehicle.state # => "idling"
60
- #
61
- # # Failed event
62
- # vehicle.fail = true
63
- # vehicle.park # => false
64
- # vehicle.saving_state # => "parked"
65
- # vehicle.state # => "idling"
66
- #
67
- # As shown, even though the state is set prior to calling the +save+ action
68
- # on the object, it will be rolled back to the original state if the action
69
- # fails. *Note* that this will also be the case if an exception is raised
70
- # while calling the action.
71
- #
72
- # === Indirect transitions
73
- #
74
- # In addition to the action being run as the _result_ of an event, the action
75
- # can also be used to run events itself. For example, using the above as an
76
- # example:
77
- #
78
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
79
- #
80
- # vehicle.state_event = 'ignite'
81
- # vehicle.save # => true
82
- # vehicle.state # => "idling"
83
- # vehicle.state_event # => nil
84
- #
85
- # As can be seen, the +save+ action automatically invokes the event stored in
86
- # the +state_event+ attribute (<tt>:ignite</tt> in this case).
87
- #
88
- # One important note about using this technique for running transitions is
89
- # that if the class in which the state machine is defined *also* defines the
90
- # action being invoked (and not a superclass), then it must manually run the
91
- # StateMachine hook that checks for event attributes.
92
- #
93
- # For example, in ActiveRecord, DataMapper, and Sequel, the default action
94
- # (+save+) is already defined in a base class. As a result, when a state
95
- # machine is defined in a model / resource, StateMachine can automatically
96
- # hook into the +save+ action.
97
- #
98
- # On the other hand, the Vehicle class from above defined its own +save+
99
- # method (and there is no +save+ method in its superclass). As a result, it
100
- # must be modified like so:
101
- #
102
- # def save
103
- # self.class.state_machines.fire_event_attributes(self, :save) do
104
- # @saving_state = state
105
- # fail != true
106
- # end
107
- # end
108
- #
109
- # This will add in the functionality for firing the event stored in the
110
- # +state_event+ attribute.
111
- #
112
- # == Callbacks
113
- #
114
- # Callbacks are supported for hooking before and after every possible
115
- # transition in the machine. Each callback is invoked in the order in which
116
- # it was defined. See StateMachine::Machine#before_transition and
117
- # StateMachine::Machine#after_transition for documentation on how to define
118
- # new callbacks.
119
- #
120
- # *Note* that callbacks only get executed within the context of an event. As
121
- # a result, if a class has an initial state when it's created, any callbacks
122
- # that would normally get executed when the object enters that state will
123
- # *not* get triggered.
124
- #
125
- # For example,
126
- #
127
- # class Vehicle
128
- # state_machine :initial => :parked do
129
- # after_transition all => :parked do
130
- # raise ArgumentError
131
- # end
132
- # ...
133
- # end
134
- # end
135
- #
136
- # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
137
- # vehicle.save # => true (no exception raised)
138
- #
139
- # If you need callbacks to get triggered when an object is created, this
140
- # should be done by either:
141
- # * Use a <tt>before :save</tt> or equivalent hook, or
142
- # * Set an initial state of nil and use the correct event to create the
143
- # object with the proper state, resulting in callbacks being triggered and
144
- # the object getting persisted
145
- #
146
- # === Canceling callbacks
147
- #
148
- # Callbacks can be canceled by throwing :halt at any point during the
149
- # callback. For example,
150
- #
151
- # ...
152
- # throw :halt
153
- # ...
154
- #
155
- # If a +before+ callback halts the chain, the associated transition and all
156
- # later callbacks are canceled. If an +after+ callback halts the chain,
157
- # the later callbacks are canceled, but the transition is still successful.
158
- #
159
- # *Note* that if a +before+ callback fails and the bang version of an event
160
- # was invoked, an exception will be raised instead of returning false. For
161
- # example,
162
- #
163
- # class Vehicle
164
- # state_machine :initial => :parked do
165
- # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
166
- # ...
167
- # end
168
- # end
169
- #
170
- # vehicle = Vehicle.new
171
- # vehicle.park # => false
172
- # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
173
- #
174
- # == Observers
175
- #
176
- # Observers, in the sense of external classes and *not* Ruby's Observable
177
- # mechanism, can hook into state machines as well. Such observers use the
178
- # same callback api that's used internally.
179
- #
180
- # Below are examples of defining observers for the following state machine:
181
- #
182
- # class Vehicle
183
- # state_machine do
184
- # event :park do
185
- # transition :idling => :parked
186
- # end
187
- # ...
188
- # end
189
- # ...
190
- # end
191
- #
192
- # Event/Transition behaviors:
193
- #
194
- # class VehicleObserver
195
- # def self.before_park(vehicle, transition)
196
- # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
197
- # end
198
- #
199
- # def self.after_park(vehicle, transition, result)
200
- # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
201
- # end
202
- #
203
- # def self.before_transition(vehicle, transition)
204
- # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
205
- # end
206
- #
207
- # def self.after_transition(vehicle, transition)
208
- # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
209
- # end
210
- # end
211
- #
212
- # Vehicle.state_machine do
213
- # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
214
- # before_transition VehicleObserver.method(:before_transition)
215
- #
216
- # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
217
- # after_transition VehicleObserver.method(:after_transition)
218
- # end
219
- #
220
- # One common callback is to record transitions for all models in the system
221
- # for auditing/debugging purposes. Below is an example of an observer that
222
- # can easily automate this process for all models:
223
- #
224
- # class StateMachineObserver
225
- # def self.before_transition(object, transition)
226
- # Audit.log_transition(object.attributes)
227
- # end
228
- # end
229
- #
230
- # [Vehicle, Switch, Project].each do |klass|
231
- # klass.state_machines.each do |attribute, machine|
232
- # machine.before_transition StateMachineObserver.method(:before_transition)
233
- # end
234
- # end
235
- #
236
- # Additional observer-like behavior may be exposed by the various integrations
237
- # available. See below for more information on integrations.
238
- #
239
- # == Overriding instance / class methods
240
- #
241
- # Hooking in behavior to the generated instance / class methods from the
242
- # state machine, events, and states is very simple because of the way these
243
- # methods are generated on the class. Using the class's ancestors, the
244
- # original generated method can be referred to via +super+. For example,
245
- #
246
- # class Vehicle
247
- # state_machine do
248
- # event :park do
249
- # ...
250
- # end
251
- # end
252
- #
253
- # def park(*args)
254
- # logger.info "..."
255
- # super
256
- # end
257
- # end
258
- #
259
- # In the above example, the +park+ instance method that's generated on the
260
- # Vehicle class (by the associated event) is overridden with custom behavior.
261
- # Once this behavior is complete, the original method from the state machine
262
- # is invoked by simply calling +super+.
263
- #
264
- # The same technique can be used for +state+, +state_name+, and all other
265
- # instance *and* class methods on the Vehicle class.
266
- #
267
- # == Integrations
268
- #
269
- # By default, state machines are library-agnostic, meaning that they work
270
- # on any Ruby class and have no external dependencies. However, there are
271
- # certain libraries which expose additional behavior that can be taken
272
- # advantage of by state machines.
273
- #
274
- # This library is built to work out of the box with a few popular Ruby
275
- # libraries that allow for additional behavior to provide a cleaner and
276
- # smoother experience. This is especially the case for objects backed by a
277
- # database that may allow for transactions, persistent storage,
278
- # search/filters, callbacks, etc.
279
- #
280
- # When a state machine is defined for classes using any of the above libraries,
281
- # it will try to automatically determine the integration to use (Agnostic,
282
- # ActiveRecord, DataMapper, or Sequel) based on the class definition. To
283
- # see how each integration affects the machine's behavior, refer to all
284
- # constants defined under the StateMachine::Integrations namespace.
285
- class Machine
286
- include Assertions
287
- include MatcherHelpers
288
-
289
- class << self
290
- # Attempts to find or create a state machine for the given class. For
291
- # example,
292
- #
293
- # StateMachine::Machine.find_or_create(Vehicle)
294
- # StateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
295
- # StateMachine::Machine.find_or_create(Vehicle, :status)
296
- # StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
297
- #
298
- # If a machine of the given name already exists in one of the class's
299
- # superclasses, then a copy of that machine will be created and stored
300
- # in the new owner class (the original will remain unchanged).
301
- def find_or_create(owner_class, *args, &block)
302
- options = args.last.is_a?(Hash) ? args.pop : {}
303
- name = args.first || :state
304
-
305
- # Find an existing machine
306
- if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
307
- # Only create a new copy if changes are being made to the machine in
308
- # a subclass
309
- if machine.owner_class != owner_class && (options.any? || block_given?)
310
- machine = machine.clone
311
- machine.initial_state = options[:initial] if options.include?(:initial)
312
- machine.owner_class = owner_class
313
- end
314
-
315
- # Evaluate DSL
316
- machine.instance_eval(&block) if block_given?
317
- else
318
- # No existing machine: create a new one
319
- machine = new(owner_class, name, options, &block)
320
- end
321
-
322
- machine
323
- end
324
-
325
- # Draws the state machines defined in the given classes using GraphViz.
326
- # The given classes must be a comma-delimited string of class names.
327
- #
328
- # Configuration options:
329
- # * <tt>:file</tt> - A comma-delimited string of files to load that
330
- # contain the state machine definitions to draw
331
- # * <tt>:path</tt> - The path to write the graph file to
332
- # * <tt>:format</tt> - The image format to generate the graph in
333
- # * <tt>:font</tt> - The name of the font to draw state names in
334
- def draw(class_names, options = {})
335
- raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
336
-
337
- # Load any files
338
- if files = options.delete(:file)
339
- files.split(',').each {|file| require file}
340
- end
341
-
342
- class_names.split(',').each do |class_name|
343
- # Navigate through the namespace structure to get to the class
344
- klass = Object
345
- class_name.split('::').each do |name|
346
- klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
347
- end
348
-
349
- # Draw each of the class's state machines
350
- klass.state_machines.each_value do |machine|
351
- machine.draw(options)
352
- end
353
- end
354
- end
355
- end
356
-
357
- # Default messages to use for validation errors in ORM integrations
358
- class << self; attr_accessor :default_messages; end
359
- @default_messages = {
360
- :invalid => 'is invalid',
361
- :invalid_event => 'cannot transition when %s',
362
- :invalid_transition => 'cannot transition via "%s"'
363
- }
364
-
365
- # The class that the machine is defined in
366
- attr_accessor :owner_class
367
-
368
- # The name of the machine, used for scoping methods generated for the
369
- # machine as a whole (not states or events)
370
- attr_reader :name
371
-
372
- # The events that trigger transitions. These are sorted, by default, in
373
- # the order in which they were defined.
374
- attr_reader :events
375
-
376
- # A list of all of the states known to this state machine. This will pull
377
- # states from the following sources:
378
- # * Initial state
379
- # * State behaviors
380
- # * Event transitions (:to, :from, and :except_from options)
381
- # * Transition callbacks (:to, :from, :except_to, and :except_from options)
382
- # * Unreferenced states (using +other_states+ helper)
383
- #
384
- # These are sorted, by default, in the order in which they were referenced.
385
- attr_reader :states
386
-
387
- # The callbacks to invoke before/after a transition is performed
388
- #
389
- # Maps :before => callbacks and :after => callbacks
390
- attr_reader :callbacks
391
-
392
- # The action to invoke when an object transitions
393
- attr_reader :action
394
-
395
- # An identifier that forces all methods (including state predicates and
396
- # event methods) to be generated with the value prefixed or suffixed,
397
- # depending on the context.
398
- attr_reader :namespace
399
-
400
- # Whether the machine will use transactions when firing events
401
- attr_reader :use_transactions
402
-
403
- # Creates a new state machine for the given attribute
404
- def initialize(owner_class, *args, &block)
405
- options = args.last.is_a?(Hash) ? args.pop : {}
406
- assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
407
-
408
- # Find an integration that matches this machine's owner class
409
- if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
410
- extend integration
411
- options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
412
- end
413
-
414
- # Add machine-wide defaults
415
- options = {:use_transactions => true}.merge(options)
416
-
417
- # Set machine configuration
418
- @name = args.first || :state
419
- @attribute = options[:attribute] || @name
420
- @events = EventCollection.new(self)
421
- @states = StateCollection.new(self)
422
- @callbacks = {:before => [], :after => []}
423
- @namespace = options[:namespace]
424
- @messages = options[:messages] || {}
425
- @action = options[:action]
426
- @use_transactions = options[:use_transactions]
427
- self.owner_class = owner_class
428
- self.initial_state = options[:initial]
429
-
430
- # Define class integration
431
- define_helpers
432
- define_scopes(options[:plural])
433
- after_initialize
434
-
435
- # Evaluate DSL
436
- instance_eval(&block) if block_given?
437
- end
438
-
439
- # Creates a copy of this machine in addition to copies of each associated
440
- # event/states/callback, so that the modifications to those collections do
441
- # not affect the original machine.
442
- def initialize_copy(orig) #:nodoc:
443
- super
444
-
445
- @events = @events.dup
446
- @events.machine = self
447
- @states = @states.dup
448
- @states.machine = self
449
- @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
450
- end
451
-
452
- # Sets the class which is the owner of this state machine. Any methods
453
- # generated by states, events, or other parts of the machine will be defined
454
- # on the given owner class.
455
- def owner_class=(klass)
456
- @owner_class = klass
457
-
458
- # Create modules for extending the class with state/event-specific methods
459
- class_helper_module = @class_helper_module = Module.new
460
- instance_helper_module = @instance_helper_module = Module.new
461
- owner_class.class_eval do
462
- extend class_helper_module
463
- include instance_helper_module
464
- end
465
-
466
- # Add class-/instance-level methods to the owner class for state initialization
467
- unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
468
- owner_class.class_eval do
469
- extend StateMachine::ClassMethods
470
- include StateMachine::InstanceMethods
471
- end
472
-
473
- define_state_initializer
474
- end
475
-
476
- # Record this machine as matched to the name in the current owner class.
477
- # This will override any machines mapped to the same name in any superclasses.
478
- owner_class.state_machines[name] = self
479
- end
480
-
481
- # Sets the initial state of the machine. This can be either the static name
482
- # of a state or a lambda block which determines the initial state at
483
- # creation time.
484
- def initial_state=(new_initial_state)
485
- @initial_state = new_initial_state
486
- add_states([@initial_state]) unless dynamic_initial_state?
487
-
488
- # Update all states to reflect the new initial state
489
- states.each {|state| state.initial = (state.name == @initial_state)}
490
- end
491
-
492
- # Gets the actual name of the attribute on the machine's owner class that
493
- # stores data with the given name.
494
- def attribute(name = :state)
495
- name == :state ? @attribute : :"#{self.name}_#{name}"
496
- end
497
-
498
- # Defines a new instance method with the given name on the machine's owner
499
- # class. If the method is already defined in the class, then this will not
500
- # override it.
501
- #
502
- # Example:
503
- #
504
- # machine.define_instance_method(:state_name) do |machine, object|
505
- # machine.states.match(object)
506
- # end
507
- def define_instance_method(method, &block)
508
- name = self.name
509
-
510
- @instance_helper_module.class_eval do
511
- define_method(method) do |*args|
512
- block.call(self.class.state_machine(name), self, *args)
513
- end
514
- end
515
- end
516
- attr_reader :instance_helper_module
517
-
518
- # Defines a new class method with the given name on the machine's owner
519
- # class. If the method is already defined in the class, then this will not
520
- # override it.
521
- #
522
- # Example:
523
- #
524
- # machine.define_class_method(:states) do |machine, klass|
525
- # machine.states.keys
526
- # end
527
- def define_class_method(method, &block)
528
- name = self.name
529
-
530
- @class_helper_module.class_eval do
531
- define_method(method) do |*args|
532
- block.call(self.state_machine(name), self, *args)
533
- end
534
- end
535
- end
536
-
537
- # Gets the initial state of the machine for the given object. If a dynamic
538
- # initial state was configured for this machine, then the object will be
539
- # passed into the lambda block to help determine the actual state.
540
- #
541
- # == Examples
542
- #
543
- # With a static initial state:
544
- #
545
- # class Vehicle
546
- # state_machine :initial => :parked do
547
- # ...
548
- # end
549
- # end
550
- #
551
- # vehicle = Vehicle.new
552
- # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
553
- #
554
- # With a dynamic initial state:
555
- #
556
- # class Vehicle
557
- # attr_accessor :force_idle
558
- #
559
- # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
560
- # ...
561
- # end
562
- # end
563
- #
564
- # vehicle = Vehicle.new
565
- #
566
- # vehicle.force_idle = true
567
- # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
568
- #
569
- # vehicle.force_idle = false
570
- # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
571
- def initial_state(object)
572
- states.fetch(dynamic_initial_state? ? @initial_state.call(object) : @initial_state)
573
- end
574
-
575
- # Whether a dynamic initial state is being used in the machine
576
- def dynamic_initial_state?
577
- @initial_state.is_a?(Proc)
578
- end
579
-
580
- # Customizes the definition of one or more states in the machine.
581
- #
582
- # Configuration options:
583
- # * <tt>:value</tt> - The actual value to store when an object transitions
584
- # to the state. Default is the name (stringified).
585
- # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
586
- # then setting this to true will cache the evaluated result
587
- # * <tt>:if</tt> - Determines whether an object's value matches the state
588
- # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
589
- # By default, the configured value is matched.
590
- #
591
- # == Customizing the stored value
592
- #
593
- # Whenever a state is automatically discovered in the state machine, its
594
- # default value is assumed to be the stringified version of the name. For
595
- # example,
596
- #
597
- # class Vehicle
598
- # state_machine :initial => :parked do
599
- # event :ignite do
600
- # transition :parked => :idling
601
- # end
602
- # end
603
- # end
604
- #
605
- # In the above state machine, there are two states automatically discovered:
606
- # :parked and :idling. These states, by default, will store their stringified
607
- # equivalents when an object moves into that state (e.g. "parked" / "idling").
608
- #
609
- # For legacy systems or when tying state machines into existing frameworks,
610
- # it's oftentimes necessary to need to store a different value for a state
611
- # than the default. In order to continue taking advantage of an expressive
612
- # state machine and helper methods, every defined state can be re-configured
613
- # with a custom stored value. For example,
614
- #
615
- # class Vehicle
616
- # state_machine :initial => :parked do
617
- # event :ignite do
618
- # transition :parked => :idling
619
- # end
620
- #
621
- # state :idling, :value => 'IDLING'
622
- # state :parked, :value => 'PARKED
623
- # end
624
- # end
625
- #
626
- # This is also useful if being used in association with a database and,
627
- # instead of storing the state name in a column, you want to store the
628
- # state's foreign key:
629
- #
630
- # class VehicleState < ActiveRecord::Base
631
- # end
632
- #
633
- # class Vehicle < ActiveRecord::Base
634
- # state_machine :attribute => :state_id, :initial => :parked do
635
- # event :ignite do
636
- # transition :parked => :idling
637
- # end
638
- #
639
- # states.each do |state|
640
- # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
641
- # end
642
- # end
643
- # end
644
- #
645
- # In the above example, each known state is configured to store it's
646
- # associated database id in the +state_id+ attribute. Also, notice that a
647
- # lambda block is used to define the state's value. This is required in
648
- # situations (like testing) where the model is loaded without any existing
649
- # data (i.e. no VehicleState records available).
650
- #
651
- # One caveat to the above example is to keep performance in mind. To avoid
652
- # constant db hits for looking up the VehicleState ids, the value is cached
653
- # by specifying the <tt>:cache</tt> option. Alternatively, a custom
654
- # caching strategy can be used like so:
655
- #
656
- # class VehicleState < ActiveRecord::Base
657
- # cattr_accessor :cache_store
658
- # self.cache_store = ActiveSupport::Cache::MemoryStore.new
659
- #
660
- # def self.find_by_name(name)
661
- # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
662
- # end
663
- # end
664
- #
665
- # === Dynamic values
666
- #
667
- # In addition to customizing states with other value types, lambda blocks
668
- # can also be specified to allow for a state's value to be determined
669
- # dynamically at runtime. For example,
670
- #
671
- # class Vehicle
672
- # state_machine :purchased_at, :initial => :available do
673
- # event :purchase do
674
- # transition all => :purchased
675
- # end
676
- #
677
- # event :restock do
678
- # transition all => :available
679
- # end
680
- #
681
- # state :available, :value => nil
682
- # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
683
- # end
684
- # end
685
- #
686
- # In the above definition, the <tt>:purchased</tt> state is customized with
687
- # both a dynamic value *and* a value matcher.
688
- #
689
- # When an object transitions to the purchased state, the value's lambda
690
- # block will be called. This will get the current time and store it in the
691
- # object's +purchased_at+ attribute.
692
- #
693
- # *Note* that the custom matcher is very important here. Since there's no
694
- # way for the state machine to figure out an object's state when it's set to
695
- # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
696
- # were not configured for the state, then an ArgumentError exception would
697
- # be raised at runtime, indicating that the state machine could not figure
698
- # out what the current state of the object was.
699
- #
700
- # == Behaviors
701
- #
702
- # Behaviors define a series of methods to mixin with objects when the current
703
- # state matches the given one(s). This allows instance methods to behave
704
- # a specific way depending on what the value of the object's state is.
705
- #
706
- # For example,
707
- #
708
- # class Vehicle
709
- # attr_accessor :driver
710
- # attr_accessor :passenger
711
- #
712
- # state_machine :initial => :parked do
713
- # event :ignite do
714
- # transition :parked => :idling
715
- # end
716
- #
717
- # state :parked do
718
- # def speed
719
- # 0
720
- # end
721
- #
722
- # def rotate_driver
723
- # driver = self.driver
724
- # self.driver = passenger
725
- # self.passenger = driver
726
- # true
727
- # end
728
- # end
729
- #
730
- # state :idling, :first_gear do
731
- # def speed
732
- # 20
733
- # end
734
- #
735
- # def rotate_driver
736
- # self.state = 'parked'
737
- # rotate_driver
738
- # end
739
- # end
740
- #
741
- # other_states :backing_up
742
- # end
743
- # end
744
- #
745
- # In the above example, there are two dynamic behaviors defined for the
746
- # class:
747
- # * +speed+
748
- # * +rotate_driver+
749
- #
750
- # Each of these behaviors are instance methods on the Vehicle class. However,
751
- # which method actually gets invoked is based on the current state of the
752
- # object. Using the above class as the example:
753
- #
754
- # vehicle = Vehicle.new
755
- # vehicle.driver = 'John'
756
- # vehicle.passenger = 'Jane'
757
- #
758
- # # Behaviors in the "parked" state
759
- # vehicle.state # => "parked"
760
- # vehicle.speed # => 0
761
- # vehicle.rotate_driver # => true
762
- # vehicle.driver # => "Jane"
763
- # vehicle.passenger # => "John"
764
- #
765
- # vehicle.ignite # => true
766
- #
767
- # # Behaviors in the "idling" state
768
- # vehicle.state # => "idling"
769
- # vehicle.speed # => 20
770
- # vehicle.rotate_driver # => true
771
- # vehicle.driver # => "John"
772
- # vehicle.passenger # => "Jane"
773
- #
774
- # As can be seen, both the +speed+ and +rotate_driver+ instance method
775
- # implementations changed how they behave based on what the current state
776
- # of the vehicle was.
777
- #
778
- # === Invalid behaviors
779
- #
780
- # If a specific behavior has not been defined for a state, then a
781
- # NoMethodError exception will be raised, indicating that that method would
782
- # not normally exist for an object with that state.
783
- #
784
- # Using the example from before:
785
- #
786
- # vehicle = Vehicle.new
787
- # vehicle.state = 'backing_up'
788
- # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
789
- #
790
- # == State-aware class methods
791
- #
792
- # In addition to defining scopes for instance methods that are state-aware,
793
- # the same can be done for certain types of class methods.
794
- #
795
- # Some libraries have support for class-level methods that only run certain
796
- # behaviors based on a conditions hash passed in. For example:
797
- #
798
- # class Vehicle < ActiveRecord::Base
799
- # state_machine do
800
- # ...
801
- # state :first_gear, :second_gear, :third_gear do
802
- # validates_presence_of :speed
803
- # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
804
- # end
805
- # end
806
- # end
807
- #
808
- # In the above ActiveRecord model, two validations have been defined which
809
- # will *only* run when the Vehicle object is in one of the three states:
810
- # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
811
- # conditions can continue to be used.
812
- #
813
- # This functionality is not library-specific and can work for any class-level
814
- # method that is defined like so:
815
- #
816
- # def validates_presence_of(attribute, options = {})
817
- # ...
818
- # end
819
- #
820
- # The minimum requirement is that the last argument in the method be an
821
- # options hash which contains at least <tt>:if</tt> condition support.
822
- def state(*names, &block)
823
- options = names.last.is_a?(Hash) ? names.pop : {}
824
- assert_valid_keys(options, :value, :cache, :if)
825
-
826
- states = add_states(names)
827
- states.each do |state|
828
- if options.include?(:value)
829
- state.value = options[:value]
830
- self.states.update(state)
831
- end
832
-
833
- state.cache = options[:cache] if options.include?(:cache)
834
- state.matcher = options[:if] if options.include?(:if)
835
- state.context(&block) if block_given?
836
- end
837
-
838
- states.length == 1 ? states.first : states
839
- end
840
- alias_method :other_states, :state
841
-
842
- # Gets the current value stored in the given object's attribute.
843
- #
844
- # For example,
845
- #
846
- # class Vehicle
847
- # state_machine :initial => :parked do
848
- # ...
849
- # end
850
- # end
851
- #
852
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
853
- # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
854
- # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
855
- def read(object, attribute, ivar = false)
856
- attribute = self.attribute(attribute)
857
- ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
858
- end
859
-
860
- # Sets a new value in the given object's attribute.
861
- #
862
- # For example,
863
- #
864
- # class Vehicle
865
- # state_machine :initial => :parked do
866
- # ...
867
- # end
868
- # end
869
- #
870
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
871
- # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
872
- # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
873
- # vehicle.state # => "idling"
874
- # vehicle.event # => "park"
875
- def write(object, attribute, value)
876
- object.send("#{self.attribute(attribute)}=", value)
877
- end
878
-
879
- # Defines one or more events for the machine and the transitions that can
880
- # be performed when those events are run.
881
- #
882
- # This method is also aliased as +on+ for improved compatibility with
883
- # using a domain-specific language.
884
- #
885
- # == Instance methods
886
- #
887
- # The following instance methods are generated when a new event is defined
888
- # (the "park" event is used as an example):
889
- # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
890
- # the current state of the object. This will *not* run validations in
891
- # ORM integrations. To check whether an event can fire *and* passes
892
- # validations, use event attributes (e.g. state_event) as described in the
893
- # "Events" documentation of each ORM integration.
894
- # * <tt>park_transition</tt> - Gets the next transition that would be
895
- # performed if the "park" event were to be fired now on the object or nil
896
- # if no transitions can be performed.
897
- # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning
898
- # from the current state to the next valid state.
899
- # * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning
900
- # from the current state to the next valid state. If the transition fails,
901
- # then a StateMachine::InvalidTransition error will be raised.
902
- #
903
- # With a namespace of "car", the above names map to the following methods:
904
- # * <tt>can_park_car?</tt>
905
- # * <tt>park_car_transition</tt>
906
- # * <tt>park_car</tt>
907
- # * <tt>park_car!</tt>
908
- #
909
- # == Defining transitions
910
- #
911
- # +event+ requires a block which allows you to define the possible
912
- # transitions that can happen as a result of that event. For example,
913
- #
914
- # event :park, :stop do
915
- # transition :idling => :parked
916
- # end
917
- #
918
- # event :first_gear do
919
- # transition :parked => :first_gear, :if => :seatbelt_on?
920
- # end
921
- #
922
- # See StateMachine::Event#transition for more information on
923
- # the possible options that can be passed in.
924
- #
925
- # *Note* that this block is executed within the context of the actual event
926
- # object. As a result, you will not be able to reference any class methods
927
- # on the model without referencing the class itself. For example,
928
- #
929
- # class Vehicle
930
- # def self.safe_states
931
- # [:parked, :idling, :stalled]
932
- # end
933
- #
934
- # state_machine do
935
- # event :park do
936
- # transition Vehicle.safe_states => :parked
937
- # end
938
- # end
939
- # end
940
- #
941
- # == Defining additional arguments
942
- #
943
- # Additional arguments on event actions can be defined like so:
944
- #
945
- # class Vehicle
946
- # state_machine do
947
- # event :park do
948
- # ...
949
- # end
950
- # end
951
- #
952
- # def park(kind = :parallel, *args)
953
- # take_deep_breath if kind == :parallel
954
- # super
955
- # end
956
- #
957
- # def take_deep_breath
958
- # sleep 3
959
- # end
960
- # end
961
- #
962
- # Note that +super+ is called instead of <tt>super(*args)</tt>. This
963
- # allows the entire arguments list to be accessed by transition callbacks
964
- # through StateMachine::Transition#args like so:
965
- #
966
- # after_transition :on => :park do |vehicle, transition|
967
- # kind = *transition.args
968
- # ...
969
- # end
970
- #
971
- # == Example
972
- #
973
- # class Vehicle
974
- # state_machine do
975
- # # The park, stop, and halt events will all share the given transitions
976
- # event :park, :stop, :halt do
977
- # transition [:idling, :backing_up] => :parked
978
- # end
979
- #
980
- # event :stop do
981
- # transition :first_gear => :idling
982
- # end
983
- #
984
- # event :ignite do
985
- # transition :parked => :idling
986
- # end
987
- # end
988
- # end
989
- def event(*names, &block)
990
- events = names.collect do |name|
991
- unless event = self.events[name]
992
- self.events << event = Event.new(self, name)
993
- end
994
-
995
- if block_given?
996
- event.instance_eval(&block)
997
- add_states(event.known_states)
998
- end
999
-
1000
- event
1001
- end
1002
-
1003
- events.length == 1 ? events.first : events
1004
- end
1005
- alias_method :on, :event
1006
-
1007
- # Creates a callback that will be invoked *before* a transition is
1008
- # performed so long as the given requirements match the transition.
1009
- #
1010
- # == The callback
1011
- #
1012
- # Callbacks must be defined as either an argument, in the :do option, or
1013
- # as a block. For example,
1014
- #
1015
- # class Vehicle
1016
- # state_machine do
1017
- # before_transition :set_alarm
1018
- # before_transition :set_alarm, all => :parked
1019
- # before_transition all => :parked, :do => :set_alarm
1020
- # before_transition all => :parked do |vehicle, transition|
1021
- # vehicle.set_alarm
1022
- # end
1023
- # ...
1024
- # end
1025
- # end
1026
- #
1027
- # Notice that the first three callbacks are the same in terms of how the
1028
- # methods to invoke are defined. However, using the <tt>:do</tt> can
1029
- # provide for a more fluid DSL.
1030
- #
1031
- # In addition, multiple callbacks can be defined like so:
1032
- #
1033
- # class Vehicle
1034
- # state_machine do
1035
- # before_transition :set_alarm, :lock_doors, all => :parked
1036
- # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1037
- # before_transition :set_alarm do |vehicle, transition|
1038
- # vehicle.lock_doors
1039
- # end
1040
- # end
1041
- # end
1042
- #
1043
- # Notice that the different ways of configuring methods can be mixed.
1044
- #
1045
- # == State requirements
1046
- #
1047
- # Callbacks can require that the machine be transitioning from and to
1048
- # specific states. These requirements use a Hash syntax to map beginning
1049
- # states to ending states. For example,
1050
- #
1051
- # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1052
- #
1053
- # In this case, the +set_alarm+ callback will only be called if the machine
1054
- # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1055
- #
1056
- # To help define state requirements, a set of helpers are available for
1057
- # slightly more complex matching:
1058
- # * <tt>all</tt> - Matches every state/event in the machine
1059
- # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1060
- # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1061
- # * <tt>same</tt> - Matches the same state being transitioned from
1062
- #
1063
- # See StateMachine::MatcherHelpers for more information.
1064
- #
1065
- # Examples:
1066
- #
1067
- # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1068
- # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1069
- # before_transition all => :parked, :do => ... # Matches all states to parked
1070
- # before_transition any => same, :do => ... # Matches every loopback
1071
- #
1072
- # == Event requirements
1073
- #
1074
- # In addition to state requirements, an event requirement can be defined so
1075
- # that the callback is only invoked on specific events using the +on+
1076
- # option. This can also use the same matcher helpers as the state
1077
- # requirements.
1078
- #
1079
- # Examples:
1080
- #
1081
- # before_transition :on => :ignite, :do => ... # Matches only on ignite
1082
- # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1083
- # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1084
- #
1085
- # == Result requirements
1086
- #
1087
- # By default, after_transition callbacks will only be run if the transition
1088
- # was performed successfully. A transition is successful if the machine's
1089
- # action is not configured or does not return false when it is invoked.
1090
- # In order to include failed attempts when running an after_transition
1091
- # callback, the :include_failures option can be specified like so:
1092
- #
1093
- # after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
1094
- # after_transition :do => ... # Runs only on successful attempts to transition
1095
- #
1096
- # == Verbose Requirements
1097
- #
1098
- # Requirements can also be defined using verbose options rather than the
1099
- # implicit Hash syntax and helper methods described above.
1100
- #
1101
- # Configuration options:
1102
- # * <tt>:from</tt> - One or more states being transitioned from. If none
1103
- # are specified, then all states will match.
1104
- # * <tt>:to</tt> - One or more states being transitioned to. If none are
1105
- # specified, then all states will match.
1106
- # * <tt>:on</tt> - One or more events that fired the transition. If none
1107
- # are specified, then all events will match.
1108
- # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1109
- # * <tt>:except_to</tt> - One more states *not* being transitioned to
1110
- # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1111
- #
1112
- # Examples:
1113
- #
1114
- # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1115
- # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1116
- #
1117
- # == Conditions
1118
- #
1119
- # In addition to the state/event requirements, a condition can also be
1120
- # defined to help determine whether the callback should be invoked.
1121
- #
1122
- # Configuration options:
1123
- # * <tt>:if</tt> - A method, proc or string to call to determine if the
1124
- # callback should occur (e.g. :if => :allow_callbacks, or
1125
- # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1126
- # should return or evaluate to a true or false value.
1127
- # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1128
- # callback should not occur (e.g. :unless => :skip_callbacks, or
1129
- # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1130
- # string should return or evaluate to a true or false value.
1131
- #
1132
- # Examples:
1133
- #
1134
- # before_transition :parked => :idling, :if => :moving?, :do => ...
1135
- # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1136
- #
1137
- # === Accessing the transition
1138
- #
1139
- # In addition to passing the object being transitioned, the actual
1140
- # transition describing the context (e.g. event, from, to) can be accessed
1141
- # as well. This additional argument is only passed if the callback allows
1142
- # for it.
1143
- #
1144
- # For example,
1145
- #
1146
- # class Vehicle
1147
- # # Only specifies one parameter (the object being transitioned)
1148
- # before_transition all => :parked do |vehicle|
1149
- # vehicle.set_alarm
1150
- # end
1151
- #
1152
- # # Specifies 2 parameters (object being transitioned and actual transition)
1153
- # before_transition all => :parked do |vehicle, transition|
1154
- # vehicle.set_alarm(transition)
1155
- # end
1156
- # end
1157
- #
1158
- # *Note* that the object in the callback will only be passed in as an
1159
- # argument if callbacks are configured to *not* be bound to the object
1160
- # involved. This is the default and may change on a per-integration basis.
1161
- #
1162
- # See StateMachine::Transition for more information about the
1163
- # attributes available on the transition.
1164
- #
1165
- # == Examples
1166
- #
1167
- # Below is an example of a class with one state machine and various types
1168
- # of +before+ transitions defined for it:
1169
- #
1170
- # class Vehicle
1171
- # state_machine do
1172
- # # Before all transitions
1173
- # before_transition :update_dashboard
1174
- #
1175
- # # Before specific transition:
1176
- # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1177
- #
1178
- # # With conditional callback:
1179
- # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1180
- #
1181
- # # Using helpers:
1182
- # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1183
- # ...
1184
- # end
1185
- # end
1186
- #
1187
- # As can be seen, any number of transitions can be created using various
1188
- # combinations of configuration options.
1189
- def before_transition(*args, &block)
1190
- options = (args.last.is_a?(Hash) ? args.pop : {})
1191
- options[:do] = args if args.any?
1192
- add_callback(:before, options, &block)
1193
- end
1194
-
1195
- # Creates a callback that will be invoked *after* a transition is
1196
- # performed so long as the given requirements match the transition.
1197
- #
1198
- # See +before_transition+ for a description of the possible configurations
1199
- # for defining callbacks.
1200
- def after_transition(*args, &block)
1201
- options = (args.last.is_a?(Hash) ? args.pop : {})
1202
- options[:do] = args if args.any?
1203
- add_callback(:after, options, &block)
1204
- end
1205
-
1206
- # Marks the given object as invalid with the given message.
1207
- #
1208
- # By default, this is a no-op.
1209
- def invalidate(object, attribute, message, values = [])
1210
- end
1211
-
1212
- # Resets any errors previously added when invalidating the given object.
1213
- #
1214
- # By default, this is a no-op.
1215
- def reset(object)
1216
- end
1217
-
1218
- # Generates the message to use when invalidating the given object after
1219
- # failing to transition on a specific event
1220
- def generate_message(name, values = [])
1221
- (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1222
- end
1223
-
1224
- # Runs a transaction, rolling back any changes if the yielded block fails.
1225
- #
1226
- # This is only applicable to integrations that involve databases. By
1227
- # default, this will not run any transactions since the changes aren't
1228
- # taking place within the context of a database.
1229
- def within_transaction(object)
1230
- if use_transactions
1231
- transaction(object) { yield }
1232
- else
1233
- yield
1234
- end
1235
- end
1236
-
1237
- # Draws a directed graph of the machine for visualizing the various events,
1238
- # states, and their transitions.
1239
- #
1240
- # This requires both the Ruby graphviz gem and the graphviz library be
1241
- # installed on the system.
1242
- #
1243
- # Configuration options:
1244
- # * <tt>:name</tt> - The name of the file to write to (without the file extension).
1245
- # Default is "#{owner_class.name}_#{name}"
1246
- # * <tt>:path</tt> - The path to write the graph file to. Default is the
1247
- # current directory (".").
1248
- # * <tt>:format</tt> - The image format to generate the graph in.
1249
- # Default is "png'.
1250
- # * <tt>:font</tt> - The name of the font to draw state names in.
1251
- # Default is "Arial".
1252
- # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1253
- # "landscape"). Default is "portrait".
1254
- # * <tt>:output</tt> - Whether to generate the output of the graph
1255
- def draw(options = {})
1256
- options = {
1257
- :name => "#{owner_class.name}_#{name}",
1258
- :path => '.',
1259
- :format => 'png',
1260
- :font => 'Arial',
1261
- :orientation => 'portrait',
1262
- :output => true
1263
- }.merge(options)
1264
- assert_valid_keys(options, :name, :path, :format, :font, :orientation, :output)
1265
-
1266
- begin
1267
- # Load the graphviz library
1268
- require 'rubygems'
1269
- require 'graphviz'
1270
-
1271
- graph = GraphViz.new('G',
1272
- :output => options[:format],
1273
- :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"),
1274
- :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB'
1275
- )
1276
-
1277
- # Add nodes
1278
- states.by_priority.each do |state|
1279
- node = state.draw(graph)
1280
- node.fontname = options[:font]
1281
- end
1282
-
1283
- # Add edges
1284
- events.each do |event|
1285
- edges = event.draw(graph)
1286
- edges.each {|edge| edge.fontname = options[:font]}
1287
- end
1288
-
1289
- # Generate the graph
1290
- graph.output if options[:output]
1291
- graph
1292
- rescue LoadError
1293
- $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
1294
- false
1295
- end
1296
- end
1297
-
1298
- protected
1299
- # Runs additional initialization hooks. By default, this is a no-op.
1300
- def after_initialize
1301
- end
1302
-
1303
- # Adds helper methods for interacting with the state machine, including
1304
- # for states, events, and transitions
1305
- def define_helpers
1306
- define_state_accessor
1307
- define_state_predicate
1308
- define_event_helpers
1309
- define_action_helpers if action
1310
-
1311
- # Gets the state name for the current value
1312
- define_instance_method(attribute(:name)) do |machine, object|
1313
- machine.states.match!(object).name
1314
- end
1315
- end
1316
-
1317
- # Defines the initial values for state machine attributes. Static values
1318
- # are set prior to the original initialize method and dynamic values are
1319
- # set *after* the initialize method in case it is dependent on it.
1320
- def define_state_initializer
1321
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
1322
- def initialize(*args)
1323
- initialize_state_machines(:dynamic => false)
1324
- super
1325
- initialize_state_machines(:dynamic => true)
1326
- end
1327
- end_eval
1328
- end
1329
-
1330
- # Adds reader/writer methods for accessing the state attribute
1331
- def define_state_accessor
1332
- attribute = self.attribute
1333
-
1334
- @instance_helper_module.class_eval do
1335
- attr_accessor attribute
1336
- end
1337
- end
1338
-
1339
- # Adds predicate method to the owner class for determining the name of the
1340
- # current state
1341
- def define_state_predicate
1342
- define_instance_method("#{name}?") do |machine, object, state|
1343
- machine.states.matches?(object, state)
1344
- end
1345
- end
1346
-
1347
- # Adds helper methods for getting information about this state machine's
1348
- # events
1349
- def define_event_helpers
1350
- # Gets the events that are allowed to fire on the current object
1351
- define_instance_method(attribute(:events)) do |machine, object|
1352
- machine.events.valid_for(object).map {|event| event.name}
1353
- end
1354
-
1355
- # Gets the next possible transitions that can be run on the current
1356
- # object
1357
- define_instance_method(attribute(:transitions)) do |machine, object, *args|
1358
- machine.events.transitions_for(object, *args)
1359
- end
1360
-
1361
- # Add helpers for interacting with the action
1362
- if action
1363
- # Tracks the event / transition to invoke when the action is called
1364
- event_attribute = attribute(:event)
1365
- event_transition_attribute = attribute(:event_transition)
1366
- @instance_helper_module.class_eval do
1367
- attr_writer event_attribute
1368
-
1369
- protected
1370
- attr_accessor event_transition_attribute
1371
- end
1372
-
1373
- # Interpret non-blank events as present
1374
- define_instance_method(attribute(:event)) do |machine, object|
1375
- event = machine.read(object, :event, true)
1376
- event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1377
- end
1378
- end
1379
- end
1380
-
1381
- # Adds helper methods for automatically firing events when an action
1382
- # is invoked
1383
- def define_action_helpers(action_hook = self.action)
1384
- action = self.action
1385
- private_method = owner_class.private_method_defined?(action_hook)
1386
-
1387
- if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
1388
- # Action is defined and hasn't already been overridden by another machine
1389
- @instance_helper_module.class_eval do
1390
- # Override the default action to invoke the before / after hooks
1391
- define_method(action_hook) do |*args|
1392
- self.class.state_machines.fire_event_attributes(self, action) { super(*args) }
1393
- end
1394
-
1395
- private action_hook if private_method
1396
- end
1397
-
1398
- true
1399
- else
1400
- # Action already defined: don't add integration-specific hooks
1401
- false
1402
- end
1403
- end
1404
-
1405
- # Defines the with/without scope helpers for this attribute. Both the
1406
- # singular and plural versions of the attribute are defined for each
1407
- # scope helper. A custom plural can be specified if it cannot be
1408
- # automatically determined by either calling +pluralize+ on the attribute
1409
- # name or adding an "s" to the end of the name.
1410
- def define_scopes(custom_plural = nil)
1411
- plural = custom_plural || (name.to_s.respond_to?(:pluralize) ? name.to_s.pluralize : "#{name}s")
1412
-
1413
- [name, plural].uniq.each do |name|
1414
- [:with, :without].each do |kind|
1415
- method = "#{kind}_#{name}"
1416
-
1417
- if scope = send("create_#{kind}_scope", method)
1418
- # Converts state names to their corresponding values so that they
1419
- # can be looked up properly
1420
- define_class_method(method) do |machine, klass, *states|
1421
- values = states.flatten.map {|state| machine.states.fetch(state).value}
1422
- scope.call(klass, values)
1423
- end
1424
- end
1425
- end
1426
- end
1427
- end
1428
-
1429
- # Creates a scope for finding objects *with* a particular value or values
1430
- # for the attribute.
1431
- #
1432
- # By default, this is a no-op.
1433
- def create_with_scope(name)
1434
- end
1435
-
1436
- # Creates a scope for finding objects *without* a particular value or
1437
- # values for the attribute.
1438
- #
1439
- # By default, this is a no-op.
1440
- def create_without_scope(name)
1441
- end
1442
-
1443
- # Always yields
1444
- def transaction(object)
1445
- yield
1446
- end
1447
-
1448
- # Adds a new transition callback of the given type.
1449
- def add_callback(type, options, &block)
1450
- callbacks[type] << callback = Callback.new(options, &block)
1451
- add_states(callback.known_states)
1452
- callback
1453
- end
1454
-
1455
- # Tracks the given set of states in the list of all known states for
1456
- # this machine
1457
- def add_states(new_states)
1458
- new_states.map do |new_state|
1459
- unless state = states[new_state]
1460
- states << state = State.new(self, new_state)
1461
- end
1462
-
1463
- state
1464
- end
1465
- end
1466
- end
1467
- end