branston 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. data/lib/branston/app/controllers/scenarios_controller.rb +6 -5
  2. data/lib/branston/app/controllers/stories_controller.rb +101 -89
  3. data/lib/branston/app/models/story.rb +30 -1
  4. data/lib/branston/app/models/user.rb +4 -0
  5. data/lib/branston/app/views/iterations/index.html.erb +1 -1
  6. data/lib/branston/app/views/layouts/_header.html.erb +1 -2
  7. data/lib/branston/app/views/scenarios/_scenario.html.erb +6 -3
  8. data/lib/branston/app/views/scenarios/_scenarios.html.erb +4 -2
  9. data/lib/branston/app/views/stories/_form.html.erb +15 -4
  10. data/lib/branston/app/views/stories/_story.html.erb +26 -6
  11. data/lib/branston/app/views/stories/edit.html.erb +3 -3
  12. data/lib/branston/app/views/stories/index.html.erb +22 -3
  13. data/lib/branston/app/views/stories/new.html.erb +2 -2
  14. data/lib/branston/app/views/stories/show.html.erb +3 -3
  15. data/lib/branston/config/routes.rb +7 -4
  16. data/lib/branston/coverage/app-controllers-application_controller_rb.html +1 -1
  17. data/lib/branston/coverage/app-controllers-iterations_controller_rb.html +1 -1
  18. data/lib/branston/coverage/app-controllers-outcomes_controller_rb.html +1 -1
  19. data/lib/branston/coverage/app-controllers-preconditions_controller_rb.html +1 -1
  20. data/lib/branston/coverage/app-controllers-releases_controller_rb.html +1 -1
  21. data/lib/branston/coverage/app-controllers-scenarios_controller_rb.html +18 -12
  22. data/lib/branston/coverage/app-controllers-sessions_controller_rb.html +1 -1
  23. data/lib/branston/coverage/app-controllers-stories_controller_rb.html +193 -121
  24. data/lib/branston/coverage/app-controllers-user_roles_controller_rb.html +1 -1
  25. data/lib/branston/coverage/app-controllers-users_controller_rb.html +1 -1
  26. data/lib/branston/coverage/app-helpers-application_helper_rb.html +1 -1
  27. data/lib/branston/coverage/app-helpers-iterations_helper_rb.html +1 -1
  28. data/lib/branston/coverage/app-helpers-outcomes_helper_rb.html +1 -1
  29. data/lib/branston/coverage/app-helpers-preconditions_helper_rb.html +1 -1
  30. data/lib/branston/coverage/app-helpers-releases_helper_rb.html +1 -1
  31. data/lib/branston/coverage/app-helpers-sessions_helper_rb.html +1 -1
  32. data/lib/branston/coverage/app-helpers-stories_helper_rb.html +1 -1
  33. data/lib/branston/coverage/app-helpers-user_roles_helper_rb.html +1 -1
  34. data/lib/branston/coverage/app-models-iteration_rb.html +1 -1
  35. data/lib/branston/coverage/app-models-outcome_rb.html +1 -1
  36. data/lib/branston/coverage/app-models-participation_rb.html +1 -1
  37. data/lib/branston/coverage/app-models-precondition_rb.html +1 -1
  38. data/lib/branston/coverage/app-models-release_rb.html +1 -1
  39. data/lib/branston/coverage/app-models-scenario_rb.html +1 -1
  40. data/lib/branston/coverage/app-models-story_rb.html +192 -18
  41. data/lib/branston/coverage/app-models-user_rb.html +33 -9
  42. data/lib/branston/coverage/app-models-user_role_rb.html +1 -1
  43. data/lib/branston/coverage/index.html +13 -13
  44. data/lib/branston/coverage/lib-client_rb.html +1 -1
  45. data/lib/branston/coverage/lib-faker_extras_rb.html +1 -1
  46. data/lib/branston/coverage/lib-story_generator_rb.html +1 -1
  47. data/lib/branston/db/development.sqlite3 +0 -0
  48. data/lib/branston/db/migrate/20091223100903_add_status_to_story.rb +11 -0
  49. data/lib/branston/db/pristine.sqlite3 +0 -0
  50. data/lib/branston/db/schema.rb +5 -3
  51. data/lib/branston/db/test.sqlite3 +0 -0
  52. data/lib/branston/lib/branston.rb +4 -2
  53. data/lib/branston/log/development.log +4970 -0
  54. data/lib/branston/log/test.log +88225 -0
  55. data/lib/branston/test/blueprints.rb +10 -7
  56. data/lib/branston/test/functional/scenarios_controller_test.rb +22 -15
  57. data/lib/branston/test/functional/stories_controller_test.rb +51 -30
  58. data/lib/branston/test/unit/story_test.rb +47 -7
  59. data/lib/branston/test/unit/user_test.rb +4 -0
  60. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_flat.txt +3 -2
  61. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_graph.html +2041 -1307
  62. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_tree.txt +7922 -7922
  63. data/lib/branston/vendor/plugins/state_machine/CHANGELOG.rdoc +298 -0
  64. data/lib/branston/vendor/plugins/state_machine/LICENSE +20 -0
  65. data/lib/branston/vendor/plugins/state_machine/README.rdoc +466 -0
  66. data/lib/branston/vendor/plugins/state_machine/Rakefile +98 -0
  67. data/lib/branston/vendor/plugins/state_machine/examples/AutoShop_state.png +0 -0
  68. data/lib/branston/vendor/plugins/state_machine/examples/Car_state.png +0 -0
  69. data/lib/branston/vendor/plugins/state_machine/examples/TrafficLight_state.png +0 -0
  70. data/lib/branston/vendor/plugins/state_machine/examples/Vehicle_state.png +0 -0
  71. data/lib/branston/vendor/plugins/state_machine/examples/auto_shop.rb +11 -0
  72. data/lib/branston/vendor/plugins/state_machine/examples/car.rb +19 -0
  73. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/controller.rb +51 -0
  74. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/model.rb +28 -0
  75. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_edit.html.erb +24 -0
  76. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_index.html.erb +23 -0
  77. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_new.html.erb +13 -0
  78. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_show.html.erb +17 -0
  79. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/controller.rb +43 -0
  80. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/migration.rb +11 -0
  81. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/model.rb +23 -0
  82. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_edit.html.erb +25 -0
  83. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_index.html.erb +23 -0
  84. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_new.html.erb +14 -0
  85. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_show.html.erb +17 -0
  86. data/lib/branston/vendor/plugins/state_machine/examples/traffic_light.rb +7 -0
  87. data/lib/branston/vendor/plugins/state_machine/examples/vehicle.rb +31 -0
  88. data/lib/branston/vendor/plugins/state_machine/init.rb +1 -0
  89. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/assertions.rb +36 -0
  90. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/callback.rb +189 -0
  91. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/condition_proxy.rb +94 -0
  92. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/eval_helpers.rb +67 -0
  93. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event.rb +252 -0
  94. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event_collection.rb +122 -0
  95. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/extensions.rb +149 -0
  96. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/guard.rb +230 -0
  97. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/locale.rb +11 -0
  98. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/observer.rb +41 -0
  99. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record.rb +492 -0
  100. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  101. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper.rb +351 -0
  102. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/sequel.rb +322 -0
  103. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations.rb +68 -0
  104. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine.rb +1467 -0
  105. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine_collection.rb +155 -0
  106. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher.rb +123 -0
  107. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher_helpers.rb +54 -0
  108. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/node_collection.rb +152 -0
  109. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state.rb +249 -0
  110. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state_collection.rb +112 -0
  111. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/transition.rb +394 -0
  112. data/lib/branston/vendor/plugins/state_machine/lib/state_machine.rb +388 -0
  113. data/lib/branston/vendor/plugins/state_machine/state_machine.gemspec +30 -0
  114. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rake +1 -0
  115. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rb +30 -0
  116. data/lib/branston/vendor/plugins/state_machine/test/classes/switch.rb +11 -0
  117. data/lib/branston/vendor/plugins/state_machine/test/functional/state_machine_test.rb +941 -0
  118. data/lib/branston/vendor/plugins/state_machine/test/test_helper.rb +4 -0
  119. data/lib/branston/vendor/plugins/state_machine/test/unit/assertions_test.rb +40 -0
  120. data/lib/branston/vendor/plugins/state_machine/test/unit/callback_test.rb +455 -0
  121. data/lib/branston/vendor/plugins/state_machine/test/unit/condition_proxy_test.rb +328 -0
  122. data/lib/branston/vendor/plugins/state_machine/test/unit/eval_helpers_test.rb +120 -0
  123. data/lib/branston/vendor/plugins/state_machine/test/unit/event_collection_test.rb +326 -0
  124. data/lib/branston/vendor/plugins/state_machine/test/unit/event_test.rb +743 -0
  125. data/lib/branston/vendor/plugins/state_machine/test/unit/guard_test.rb +908 -0
  126. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/active_record_test.rb +1367 -0
  127. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/data_mapper_test.rb +962 -0
  128. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/sequel_test.rb +859 -0
  129. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations_test.rb +42 -0
  130. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_event_test.rb +7 -0
  131. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_transition_test.rb +7 -0
  132. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_collection_test.rb +938 -0
  133. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_test.rb +2004 -0
  134. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_helpers_test.rb +37 -0
  135. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_test.rb +155 -0
  136. data/lib/branston/vendor/plugins/state_machine/test/unit/node_collection_test.rb +207 -0
  137. data/lib/branston/vendor/plugins/state_machine/test/unit/state_collection_test.rb +280 -0
  138. data/lib/branston/vendor/plugins/state_machine/test/unit/state_machine_test.rb +31 -0
  139. data/lib/branston/vendor/plugins/state_machine/test/unit/state_test.rb +795 -0
  140. data/lib/branston/vendor/plugins/state_machine/test/unit/transition_test.rb +1212 -0
  141. metadata +81 -2
@@ -0,0 +1,351 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with DataMapper resources.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # DataMapper resource:
9
+ #
10
+ # class Vehicle
11
+ # include DataMapper::Resource
12
+ #
13
+ # property :id, Serial
14
+ # property :name, String
15
+ # property :state, String
16
+ #
17
+ # state_machine :initial => :parked do
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # The examples in the sections below will use the above class as a
25
+ # reference.
26
+ #
27
+ # == Actions
28
+ #
29
+ # By default, the action that will be invoked when a state is transitioned
30
+ # is the +save+ action. This will cause the resource to save the changes
31
+ # made to the state machine's attribute. *Note* that if any other changes
32
+ # were made to the resource prior to transition, then those changes will
33
+ # be saved as well.
34
+ #
35
+ # For example,
36
+ #
37
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
38
+ # vehicle.name = 'Ford Explorer'
39
+ # vehicle.ignite # => true
40
+ # vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
41
+ #
42
+ # == Events
43
+ #
44
+ # As described in StateMachine::InstanceMethods#state_machine, event
45
+ # attributes are created for every machine that allow transitions to be
46
+ # performed automatically when the object's action (in this case, :save)
47
+ # is called.
48
+ #
49
+ # In DataMapper, these automated events are run in the following order:
50
+ # * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
51
+ # * before save - If validation feature was skipped/not loaded, run before callbacks and persist new states, then save
52
+ # * after save - Run after callbacks
53
+ #
54
+ # For example,
55
+ #
56
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
57
+ # vehicle.state_event # => nil
58
+ # vehicle.state_event = 'invalid'
59
+ # vehicle.valid? # => false
60
+ # vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"state_event"=>["is invalid"]}>
61
+ #
62
+ # vehicle.state_event = 'ignite'
63
+ # vehicle.valid? # => true
64
+ # vehicle.save # => true
65
+ # vehicle.state # => "idling"
66
+ # vehicle.state_event # => nil
67
+ #
68
+ # Note that this can also be done on a mass-assignment basis:
69
+ #
70
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
71
+ # vehicle.state # => "idling"
72
+ #
73
+ # === Security implications
74
+ #
75
+ # Beware that public event attributes mean that events can be fired
76
+ # whenever mass-assignment is being used. If you want to prevent malicious
77
+ # users from tampering with events through URLs / forms, the attribute
78
+ # should be protected like so:
79
+ #
80
+ # class Vehicle
81
+ # include DataMapper::Resource
82
+ # ...
83
+ #
84
+ # state_machine do
85
+ # ...
86
+ # end
87
+ # protected :state_event
88
+ # end
89
+ #
90
+ # If you want to only have *some* events be able to fire via mass-assignment,
91
+ # you can build two state machines (one public and one protected) like so:
92
+ #
93
+ # class Vehicle
94
+ # include DataMapper::Resource
95
+ # ...
96
+ #
97
+ # state_machine do
98
+ # # Define private events here
99
+ # end
100
+ # protected :state_event= # Prevent access to events in the first machine
101
+ #
102
+ # # Allow both machines to share the same state
103
+ # state_machine :public_state, :attribute => :state do
104
+ # # Define public events here
105
+ # end
106
+ # end
107
+ #
108
+ # == Transactions
109
+ #
110
+ # By default, the use of transactions during an event transition is
111
+ # turned off to be consistent with DataMapper. This means that if
112
+ # changes are made to the database during a before callback, but the
113
+ # transition fails to complete, those changes will *not* be rolled back.
114
+ #
115
+ # For example,
116
+ #
117
+ # class Message
118
+ # include DataMapper::Resource
119
+ #
120
+ # property :id, Serial
121
+ # property :content, String
122
+ # end
123
+ #
124
+ # Vehicle.state_machine do
125
+ # before_transition do |transition|
126
+ # Message.create(:content => transition.inspect)
127
+ # throw :halt
128
+ # end
129
+ # end
130
+ #
131
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
132
+ # vehicle.ignite # => false
133
+ # Message.all.count # => 1
134
+ #
135
+ # To turn on transactions:
136
+ #
137
+ # class Vehicle < ActiveRecord::Base
138
+ # state_machine :initial => :parked, :use_transactions => true do
139
+ # ...
140
+ # end
141
+ # end
142
+ #
143
+ # == Validation errors
144
+ #
145
+ # If an event fails to successfully fire because there are no matching
146
+ # transitions for the current record, a validation error is added to the
147
+ # record's state attribute to help in determining why it failed and for
148
+ # reporting via the UI.
149
+ #
150
+ # For example,
151
+ #
152
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
153
+ # vehicle.ignite # => false
154
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
155
+ #
156
+ # If an event fails to fire because of a validation error on the record and
157
+ # *not* because a matching transition was not available, no error messages
158
+ # will be added to the state attribute.
159
+ #
160
+ # == Scopes
161
+ #
162
+ # To assist in filtering models with specific states, a series of class
163
+ # methods are defined on the model for finding records with or without a
164
+ # particular set of states.
165
+ #
166
+ # These named scopes are the functional equivalent of the following
167
+ # definitions:
168
+ #
169
+ # class Vehicle
170
+ # include DataMapper::Resource
171
+ #
172
+ # property :id, Serial
173
+ # property :state, String
174
+ #
175
+ # class << self
176
+ # def with_states(*states)
177
+ # all(:state => states.flatten)
178
+ # end
179
+ # alias_method :with_state, :with_states
180
+ #
181
+ # def without_states(*states)
182
+ # all(:state.not => states.flatten)
183
+ # end
184
+ # alias_method :without_state, :without_states
185
+ # end
186
+ # end
187
+ #
188
+ # *Note*, however, that the states are converted to their stored values
189
+ # before being passed into the query.
190
+ #
191
+ # Because of the way scopes work in DataMapper, they can be chained like
192
+ # so:
193
+ #
194
+ # Vehicle.with_state(:parked).all(:order => [:id.desc])
195
+ #
196
+ # == Callbacks / Observers
197
+ #
198
+ # All before/after transition callbacks defined for DataMapper resources
199
+ # behave in the same way that other DataMapper hooks behave. Rather than
200
+ # passing in the record as an argument to the callback, the callback is
201
+ # instead bound to the object and evaluated within its context.
202
+ #
203
+ # For example,
204
+ #
205
+ # class Vehicle
206
+ # include DataMapper::Resource
207
+ #
208
+ # property :id, Serial
209
+ # property :state, String
210
+ #
211
+ # state_machine :initial => :parked do
212
+ # before_transition any => :idling do
213
+ # put_on_seatbelt
214
+ # end
215
+ #
216
+ # before_transition do |transition|
217
+ # # log message
218
+ # end
219
+ #
220
+ # event :ignite do
221
+ # transition :parked => :idling
222
+ # end
223
+ # end
224
+ #
225
+ # def put_on_seatbelt
226
+ # ...
227
+ # end
228
+ # end
229
+ #
230
+ # Note, also, that the transition can be accessed by simply defining
231
+ # additional arguments in the callback block.
232
+ #
233
+ # In addition to support for DataMapper-like hooks, there is additional
234
+ # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
235
+ # for more information.
236
+ module DataMapper
237
+ # The default options to use for state machines using this integration
238
+ class << self; attr_reader :defaults; end
239
+ @defaults = {:action => :save, :use_transactions => false}
240
+
241
+ # Should this integration be used for state machines in the given class?
242
+ # Classes that include DataMapper::Resource will automatically use the
243
+ # DataMapper integration.
244
+ def self.matches?(klass)
245
+ defined?(::DataMapper::Resource) && klass <= ::DataMapper::Resource
246
+ end
247
+
248
+ # Loads additional files specific to DataMapper
249
+ def self.extended(base) #:nodoc:
250
+ require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
251
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
252
+ end
253
+
254
+ # Forces the change in state to be recognized regardless of whether the
255
+ # state value actually changed
256
+ def write(object, attribute, value)
257
+ result = super
258
+ if attribute == :state && owner_class.properties.detect {|property| property.name == self.attribute}
259
+ if ::DataMapper::VERSION =~ /^(0\.\d\.)/ # Match anything < 0.10
260
+ object.original_values[self.attribute] = "#{value}-ignored"
261
+ else
262
+ object.original_attributes[owner_class.properties[self.attribute]] = "#{value}-ignored"
263
+ end
264
+ end
265
+ result
266
+ end
267
+
268
+ # Adds a validation error to the given object
269
+ def invalidate(object, attribute, message, values = [])
270
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
271
+ end
272
+
273
+ # Resets any errors previously added when invalidating the given object
274
+ def reset(object)
275
+ object.errors.clear if supports_validations?
276
+ end
277
+
278
+ protected
279
+ # Is validation support currently loaded?
280
+ def supports_validations?
281
+ @supports_validations ||= ::DataMapper.const_defined?('Validate')
282
+ end
283
+
284
+ # Defines an initialization hook into the owner class for setting the
285
+ # initial state of the machine *before* any attributes are set on the
286
+ # object
287
+ def define_state_initializer
288
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
289
+ def initialize(attributes = {}, *args)
290
+ ignore = attributes ? attributes.keys : []
291
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
292
+ super
293
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
294
+ end
295
+ end_eval
296
+ end
297
+
298
+ # Skips defining reader/writer methods since this is done automatically
299
+ def define_state_accessor
300
+ owner_class.property(attribute, String) unless owner_class.properties.detect {|property| property.name == attribute}
301
+
302
+ if supports_validations?
303
+ name = self.name
304
+ owner_class.validates_with_block(attribute) do
305
+ machine = self.class.state_machine(name)
306
+ machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
307
+ end
308
+ end
309
+ end
310
+
311
+ # Adds hooks into validation for automatically firing events
312
+ def define_action_helpers
313
+ if super && action == :save && supports_validations?
314
+ @instance_helper_module.class_eval do
315
+ define_method(:valid?) do |*args|
316
+ self.class.state_machines.fire_event_attributes(self, :save, false) { super(*args) }
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ # Creates a scope for finding records *with* a particular state or
323
+ # states for the attribute
324
+ def create_with_scope(name)
325
+ attribute = self.attribute
326
+ lambda {|resource, values| resource.all(attribute => values)}
327
+ end
328
+
329
+ # Creates a scope for finding records *without* a particular state or
330
+ # states for the attribute
331
+ def create_without_scope(name)
332
+ attribute = self.attribute
333
+ lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
334
+ end
335
+
336
+ # Runs a new database transaction, rolling back any changes if the
337
+ # yielded block fails (i.e. returns false).
338
+ def transaction(object)
339
+ object.class.transaction {|t| t.rollback unless yield}
340
+ end
341
+
342
+ # Creates a new callback in the callback chain, always ensuring that
343
+ # it's configured to bind to the object as this is the convention for
344
+ # DataMapper/Extlib callbacks
345
+ def add_callback(type, options, &block)
346
+ options[:bind_to_object] = true
347
+ super
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,322 @@
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