runcoderun-aasm 2.0.5 → 2.0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/EXAMPLES.rdoc +342 -0
- data/README.rdoc +2 -0
- data/lib/aasm.rb +1 -1
- data/lib/event.rb +3 -3
- metadata +3 -1
data/EXAMPLES.rdoc
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
= Examples
|
2
|
+
The examples below show how people have used some of the more or less advanced features of AASM.
|
3
|
+
|
4
|
+
== Transitions, events and guards
|
5
|
+
Imagine you have a person that is able to
|
6
|
+
* sleep
|
7
|
+
* work
|
8
|
+
* shower
|
9
|
+
* date
|
10
|
+
|
11
|
+
It is considered good manner to shower before dating and going to work. ASSM can easily make sure good manners are followed:
|
12
|
+
|
13
|
+
class Person < ActiveRecord::Base
|
14
|
+
include AASM
|
15
|
+
|
16
|
+
aasm_initial_state :sleeping
|
17
|
+
|
18
|
+
aasm_state :sleeping
|
19
|
+
aasm_state :showering
|
20
|
+
aasm_state :working
|
21
|
+
aasm_state :dating
|
22
|
+
|
23
|
+
event :shower do
|
24
|
+
transitions :from => :sleeping, :to => :showering
|
25
|
+
transitions :from => :working, :to => :showering
|
26
|
+
transitions :from => :dating, :to => :showering
|
27
|
+
end
|
28
|
+
|
29
|
+
event :work do
|
30
|
+
transitions :from => :showering, :to => :working
|
31
|
+
# Going to work before showering? Stinky.
|
32
|
+
transitions :from => :sleeping, :to => :working
|
33
|
+
end
|
34
|
+
|
35
|
+
event :date do
|
36
|
+
transitions :from => :showering, :to => :dating
|
37
|
+
end
|
38
|
+
|
39
|
+
event :sleep do
|
40
|
+
transitions :from => :showering, :to => :sleeping
|
41
|
+
transitions :from => :working, :to => :sleeping
|
42
|
+
transitions :from => :dating, :to => :sleeping
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
Note: The plugin makes an assumption that the state of your model is saved in field called state. This can be replaced by adding the additional option :aasm_column => :status (which will use the "status" column).
|
48
|
+
|
49
|
+
Warning: If you are using a model that stores addresses, be weary of a field called “state”. You can spend hours wondering why things aren’t working like they should be.
|
50
|
+
|
51
|
+
Usually a good idea is to describe the states in models as an adjective, and the event as a verb.
|
52
|
+
|
53
|
+
Notice how in line 2 we explicitly state the initial state of the model. In lines 3 to 6, we indicate the various states the Person may be in.
|
54
|
+
|
55
|
+
== State for ActiveRecord models require they are saved
|
56
|
+
There is a peculiar behavior when creating objects via new, in that the model’s state is not specified. It will only be specified when saving the new record. One solution is to specify the default state from within the migration. The other solution then is to call create.
|
57
|
+
|
58
|
+
person = Person.new
|
59
|
+
person.state # nil
|
60
|
+
person.save # true
|
61
|
+
person.state # "sleeping"
|
62
|
+
|
63
|
+
person = Person.create
|
64
|
+
person.state # "sleeping"
|
65
|
+
person.sleeping? # true
|
66
|
+
person.rotting? # false
|
67
|
+
|
68
|
+
person = Person.new
|
69
|
+
person.state = "rotting" # "rotting"
|
70
|
+
person.rotting? # true
|
71
|
+
person.sleeping? # false
|
72
|
+
|
73
|
+
If you didn’t notice the trend, the method to test if the model is in the state, is to append the state with a question mark: "state?"
|
74
|
+
|
75
|
+
The events you specified also creates instance methods, to transition the model from one state to another.
|
76
|
+
|
77
|
+
The following instance methods were created:
|
78
|
+
|
79
|
+
person.shower!
|
80
|
+
person.work!
|
81
|
+
person.date!
|
82
|
+
person.sleep!
|
83
|
+
|
84
|
+
|
85
|
+
Note: The instance methods created follow the pattern "event!"
|
86
|
+
=== Events
|
87
|
+
By calling any event, you also call ActiveRecord::Base.save. For when it fails, it only returns false. You can guard yourself by calling valid? and save!
|
88
|
+
|
89
|
+
Events help you to transition from one state to another. So suppose your person is sleeping, and you want him to shower, well we’ll just call shower!.
|
90
|
+
|
91
|
+
1. person.state # "sleeping"
|
92
|
+
2. person.shower!
|
93
|
+
3. person.state # "showering"
|
94
|
+
|
95
|
+
Events can help your organize the flow of your model. But they can get more powerful with callbacks.
|
96
|
+
|
97
|
+
=== Callbacks
|
98
|
+
|
99
|
+
The state also comes with a few callbacks that can be used.
|
100
|
+
|
101
|
+
state :sleeping,
|
102
|
+
:enter => :get_into_bed,
|
103
|
+
:after => Proc.new {|model| model.whack_alarm_clock },
|
104
|
+
:exit => :make_up_bed
|
105
|
+
|
106
|
+
Callbacks are called when the model is transitioning into the specified state.
|
107
|
+
|
108
|
+
Note:
|
109
|
+
|
110
|
+
* Callbacks can be either a symbol or a Proc. If used as a symbol, the instance method of the model will be called
|
111
|
+
* The callbacks act differently if the model is a new record and hasn’t been saved, versus an already saved model.
|
112
|
+
|
113
|
+
When put into consideration with ActiveRecord’s callbacks, a new record’s callback would look like this:
|
114
|
+
|
115
|
+
* ActiveRecord::Base.before_save
|
116
|
+
* ActiveRecord::Base.save
|
117
|
+
* acts_as_state_machine :enter sleeping
|
118
|
+
* acts_as_state_machine :after sleeping
|
119
|
+
* ActiveRecord::Base.after_save
|
120
|
+
|
121
|
+
When the model is no longer a new record, the callbacks execute as follows, if I had called the shower! method.
|
122
|
+
|
123
|
+
* acts_as_state_machine :enter showering
|
124
|
+
* ActiveRecord::Base.before_save
|
125
|
+
* ActiveRecord::Base.save
|
126
|
+
* ActiveRecord::Base.after_save
|
127
|
+
* acts_as_state_machine :after showering
|
128
|
+
* acts_as_state_machine :exit sleeping
|
129
|
+
|
130
|
+
== Guarding States
|
131
|
+
|
132
|
+
But how about if you want some sort of validation for a transition. You know, just to ensure data integrity.
|
133
|
+
|
134
|
+
event :work do
|
135
|
+
transitions :from => :showering, :to => :working
|
136
|
+
# Going to work before showering? Stinky.
|
137
|
+
transitions :from => :sleeping, :to => :working, :guard => Proc.new {|o| o.clean? }
|
138
|
+
end
|
139
|
+
|
140
|
+
The transition can be guarded by specifying a :guard option, with either a symbol or Proc (similar to the Callbacks). The method or Proc has to return true to proceed with the transition, else it will fail silently.
|
141
|
+
|
142
|
+
|
143
|
+
== Add a history log to all state changes
|
144
|
+
Example from Artem Vasiliev's blog (Note that the blog mentions :log_transitions which is changed to :on_transition
|
145
|
+
|
146
|
+
class ExpenseClaim < ActiveRecord::Base
|
147
|
+
has_many :action_history, :class_name => 'ActionHistoryItem', :as => :document,
|
148
|
+
:order => 'id desc'
|
149
|
+
|
150
|
+
acts_as_state_machine :initial => States::DRAFT, :on_transition => :log_transitions
|
151
|
+
|
152
|
+
state States::DRAFT, :owner => Roles::AUTHOR
|
153
|
+
state States::MANAGER_APPROVAL_REQUIRED, :owner => Roles::APPROVING_MANAGER
|
154
|
+
state States::REJECTED_BY_FINANCE, :owner => Roles::APPROVING_MANAGER
|
155
|
+
|
156
|
+
#...
|
157
|
+
event :send_for_manager_approval do
|
158
|
+
transitions :from => States::DRAFT, :to => States::MANAGER_APPROVAL_REQUIRED
|
159
|
+
transitions :from => States::REJECTED_BY_MANAGER,
|
160
|
+
:to => States::MANAGER_APPROVAL_REQUIRED
|
161
|
+
end
|
162
|
+
|
163
|
+
event :approve do
|
164
|
+
transitions :from => States::MANAGER_APPROVAL_REQUIRED,
|
165
|
+
:to => States::FINANCE_APPROVAL_REQUIRED
|
166
|
+
|
167
|
+
transitions :from => States::FINANCE_APPROVAL_REQUIRED,
|
168
|
+
:to => States::APPROVED
|
169
|
+
|
170
|
+
transitions :from => States::REJECTED_BY_FINANCE,
|
171
|
+
:to => States::FINANCE_APPROVAL_REQUIRED
|
172
|
+
end
|
173
|
+
|
174
|
+
#...
|
175
|
+
def log_transition(from, to, event, opts)
|
176
|
+
user = self.updated_by
|
177
|
+
raise "user is not set" if user.nil?
|
178
|
+
as_role = self.class.states_table[from].opts[:owner]
|
179
|
+
action_history << ActionHistoryItem.new({:from_state => from.to_s,
|
180
|
+
:to_state => to.to_s, :action => event.to_s, :user_id => user.id,
|
181
|
+
:as_role => as_role.to_s, :at => Time.now})
|
182
|
+
end
|
183
|
+
#...
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
= Booking example (complex model with many callbacks)
|
188
|
+
Here is a Booking model as an example.Requirements being a lifecycle of states that includes In Progress, Pending, In Review, Cancelled, Confirmed, Awaiting Payment and Service Rendered.
|
189
|
+
|
190
|
+
(NOTE that the example below uses outdated AASM syntax. Please submit a fix)
|
191
|
+
|
192
|
+
class Booking < ActiveRecord::Base
|
193
|
+
include Comparable
|
194
|
+
self.abstract_class = true
|
195
|
+
set_table_name 'bookings'
|
196
|
+
acts_as_recent 1.days
|
197
|
+
|
198
|
+
attr_accessor :payment_option
|
199
|
+
|
200
|
+
belongs_to :account
|
201
|
+
belongs_to :user
|
202
|
+
belongs_to :coupon
|
203
|
+
|
204
|
+
has_one :date, :class_name => 'BookingDate', :dependent => :destroy, :foreign_key => :booking_id
|
205
|
+
has_one :customer, :dependent => :destroy
|
206
|
+
has_one :payment, :as => :payable, :dependent => :destroy
|
207
|
+
has_one :progress, :class_name => 'BookingProgress', :dependent => :destroy
|
208
|
+
has_one :token, :class_name => 'PaymentToken', :dependent => :destroy
|
209
|
+
|
210
|
+
has_many :booking_products, :class_name => 'BookingProduct', :foreign_key => :booking_id, :dependent => :delete_all, :after_add => [Proc.new{|b,bp| bp.booking_extras.collect{|be| be.save! }}, Proc.new{|b,bp| bp.product.extras.compulsary_included.collect{|e| bp.extras << e }}]
|
211
|
+
has_many :booking_extras, :include => [:extra, :booking_product], :dependent => :delete_all
|
212
|
+
|
213
|
+
has_many :products, :through => :booking_products, :source => :product, :uniq => true
|
214
|
+
has_many :extras, :through => :booking_extras, :source => :extra, :uniq => true
|
215
|
+
|
216
|
+
has_many :events, :class_name => 'BookingEvent', :dependent => :delete_all, :order => 'booking_events.created_at DESC', :extend => BookingEventsExtension
|
217
|
+
has_many :extras, :through => :line_items_extras, :source => :extra
|
218
|
+
has_many :notes, :class_name => 'BookingNote', :dependent => :delete_all, :after_add => Proc.new {|b,bn| BookingMailer.deliver_note( bn )}, :extend => BookingNotesExtension
|
219
|
+
|
220
|
+
delegate :to_s, :to => :name
|
221
|
+
delegate :duration, :to => :date
|
222
|
+
delegate :date_from, :date_to, :duration, :to => :date
|
223
|
+
delegate :any_progress?, :to => :progress
|
224
|
+
|
225
|
+
validates_presence_of :reference, :account_id, :user_id
|
226
|
+
validates_uniqueness_of :reference, :scope => 'account_id', :on => :create
|
227
|
+
validates_length_of :reference, :is => 10
|
228
|
+
|
229
|
+
|
230
|
+
include AASM
|
231
|
+
aasm_initial_state :in_progress
|
232
|
+
aasm_column :status
|
233
|
+
|
234
|
+
state :in_progress
|
235
|
+
state :pending,
|
236
|
+
:enter => Proc.new{|b| b.commit!; BookingMailer.deliver_received( b ); }
|
237
|
+
|
238
|
+
state :awaiting_payment,
|
239
|
+
:enter => Proc.new{|b| ( b.create_token({ :account_id => b.account.id, :booking_id => b.id }) if b.token.nil? ); BookingMailer.deliver_payment( b ); },
|
240
|
+
:after => Proc.new{|b| b.log( 'Status set to %s.' / b.current_status_to_human ) }
|
241
|
+
|
242
|
+
state :in_review,
|
243
|
+
:enter => Proc.new{|b| BookingMailer.deliver_review( b ) },
|
244
|
+
:after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) }
|
245
|
+
|
246
|
+
state :cancelled,
|
247
|
+
:enter => Proc.new{|b| BookingMailer.deliver_cancelled( b ); },
|
248
|
+
:after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) }
|
249
|
+
|
250
|
+
state :confirmed,
|
251
|
+
:enter => Proc.new{|b| BookingMailer.deliver_confirmed( b ); },
|
252
|
+
:after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) }
|
253
|
+
|
254
|
+
state :service_rendered,
|
255
|
+
:enter => Proc.new{|b| BookingMailer.deliver_feedback( b ); },
|
256
|
+
:after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) }
|
257
|
+
|
258
|
+
event( :pending ){ transitions :to => :pending, :from => :in_progress }
|
259
|
+
|
260
|
+
event( :awaiting_payment ){ transitions :to => :awaiting_payment, :from => [:pending, :in_review] }
|
261
|
+
|
262
|
+
event( :in_review ){ transitions :to => :in_review, :from => [:pending, :awaiting_payment] }
|
263
|
+
|
264
|
+
event( :cancelled ){ transitions :to => :cancelled, :from => [:pending, :awaiting_payment, :in_review] }
|
265
|
+
|
266
|
+
event( :confirmed ){ transitions :to => :confirmed, :from => :awaiting_payment, :guard => Proc.new{|b| !b.payment.nil? && b.payment.authorized? } }
|
267
|
+
|
268
|
+
event( :service_rendered ){ transitions :to => :service_rendered, :from => :confirmed }
|
269
|
+
|
270
|
+
class << self
|
271
|
+
|
272
|
+
def status_to_human( status = :pending ) status.to_s.sub('_',' ').upcase end
|
273
|
+
|
274
|
+
def running_total
|
275
|
+
self.find(:all, :conditions => ['bookings.status = ?','confirmed']).inject( Globalize::Currency.free ) { |total, booking| booking.total + total }
|
276
|
+
end
|
277
|
+
|
278
|
+
def total_by_status(state = :in_progress)
|
279
|
+
self.find_in_state(:all, state).inject( Globalize::Currency.free ) { |total, booking| booking.total + total }
|
280
|
+
end
|
281
|
+
|
282
|
+
def count_by_status(state = :in_progress) self.count_in_state(state) end
|
283
|
+
|
284
|
+
def most_recent_by_status(state = :pending)
|
285
|
+
self.find_in_state(:first, state, :order => 'bookings.created_at DESC')
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
def current_status_to_human; self.class.status_to_human( self.current_state() ) end
|
291
|
+
|
292
|
+
def log( event_description )
|
293
|
+
self.events.create!({ :account_id => self.account.id, :event_description => event_description })
|
294
|
+
end
|
295
|
+
|
296
|
+
def total
|
297
|
+
Globalize::Currency.new( ( self.booking_products.sum(:cents).to_i + self.booking_extras.sum(:cents).to_i ) ) - discount
|
298
|
+
end
|
299
|
+
|
300
|
+
def discount; (!self.coupon.nil? ? self.coupon.price : Globalize::Currency.free) end
|
301
|
+
|
302
|
+
def name; "[#{self.reference}] #{self.customer.name}" end
|
303
|
+
|
304
|
+
def editable?() [:in_progress, :in_review].include?( self.current_state ) end
|
305
|
+
|
306
|
+
def committed?() self.current_state != :in_progress end
|
307
|
+
|
308
|
+
def <=>(other_booking)
|
309
|
+
self.date.date_from <=> other_booking.date.date_from
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
=== Event definitions
|
314
|
+
|
315
|
+
Declare the business logic required to manage the transitions between the required states.
|
316
|
+
|
317
|
+
A Booking may only be Confirmed once a payment method has been assigned AND authorized.
|
318
|
+
|
319
|
+
event( :confirmed ){ transitions
|
320
|
+
:to => :confirmed,
|
321
|
+
:from => :awaiting_payment,
|
322
|
+
:guard => Proc.new{|b| !b.payment.nil? && b.payment.authorized? }
|
323
|
+
}
|
324
|
+
|
325
|
+
An event accepts a symbol as the only argument ( :confirmed ).Express the transition within the block, with the following Hash keys allowed as arguments to the transitions singleton method:
|
326
|
+
|
327
|
+
{ :from => :the_intial_state,
|
328
|
+
:to => :the_target_state,
|
329
|
+
:guard => Proc.new{|record| record.condition_to_be_met_for_transition_to_occur? }
|
330
|
+
}
|
331
|
+
|
332
|
+
Do note that the resulting event is a destructive action (modifies the receiver) and should be invoked with
|
333
|
+
|
334
|
+
booking.confirmed! #trailing !
|
335
|
+
|
336
|
+
= Credits
|
337
|
+
Examples are gathered from existing sources. Especially thanks to
|
338
|
+
* Artem Vasiliev http://thirstydoh.wordpress.com/2007/12/05/aasm-improvements/
|
339
|
+
* Aizat Faiz http://rails.aizatto.com/2007/05/24/ruby-on-rails-finite-state-machine-plugin-acts_as_state_machine/
|
340
|
+
* Lourens Naude http://blog.methodmissing.com/2006/11/16/beyond-callbacks-for-complex-model-lifecycles/
|
341
|
+
* Jesper Rønn-Jensen ( http://justaddwater.dk/ ) for collecting examples
|
342
|
+
|
data/README.rdoc
CHANGED
data/lib/aasm.rb
CHANGED
data/lib/event.rb
CHANGED
@@ -34,11 +34,11 @@ module AASM
|
|
34
34
|
|
35
35
|
def execute_success_callback(obj)
|
36
36
|
case success
|
37
|
-
when String, Symbol
|
37
|
+
when String, Symbol
|
38
38
|
obj.send(success)
|
39
|
-
when Array
|
39
|
+
when Array
|
40
40
|
success.each { |meth| obj.send(meth) }
|
41
|
-
when Proc
|
41
|
+
when Proc
|
42
42
|
success.call(obj)
|
43
43
|
end
|
44
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: runcoderun-aasm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.5
|
4
|
+
version: 2.0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Barron
|
@@ -24,11 +24,13 @@ extra_rdoc_files:
|
|
24
24
|
- MIT-LICENSE
|
25
25
|
- TODO
|
26
26
|
- CHANGELOG
|
27
|
+
- EXAMPLES.rdoc
|
27
28
|
files:
|
28
29
|
- CHANGELOG
|
29
30
|
- MIT-LICENSE
|
30
31
|
- Rakefile
|
31
32
|
- README.rdoc
|
33
|
+
- EXAMPLES.rdoc
|
32
34
|
- TODO
|
33
35
|
- lib/aasm.rb
|
34
36
|
- lib/event.rb
|