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.
Files changed (5) hide show
  1. data/EXAMPLES.rdoc +342 -0
  2. data/README.rdoc +2 -0
  3. data/lib/aasm.rb +1 -1
  4. data/lib/event.rb +3 -3
  5. 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
@@ -56,6 +56,8 @@ Here's a quick example highlighting some of the features.
56
56
  transitions :to => :closed, :from => [:read, :unread]
57
57
  end
58
58
  end
59
+
60
+ See the more complex examples in EXAMPLES.rdoc file
59
61
 
60
62
  = Other Stuff
61
63
 
data/lib/aasm.rb CHANGED
@@ -5,7 +5,7 @@ require File.join(File.dirname(__FILE__), 'persistence')
5
5
 
6
6
  module AASM
7
7
  def self.Version
8
- '2.0.5'
8
+ '2.0.5.1'
9
9
  end
10
10
 
11
11
  class InvalidTransition < RuntimeError
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