= Examples The examples below show how people have used some of the more or less advanced features of AASM. == Transitions, events and guards Imagine you have a person that is able to * sleep * work * shower * date It is considered good manner to shower before dating and going to work. ASSM can easily make sure good manners are followed: class Person < ActiveRecord::Base include AASM aasm_initial_state :sleeping aasm_state :sleeping aasm_state :showering aasm_state :working aasm_state :dating event :shower do transitions :from => :sleeping, :to => :showering transitions :from => :working, :to => :showering transitions :from => :dating, :to => :showering end event :work do transitions :from => :showering, :to => :working # Going to work before showering? Stinky. transitions :from => :sleeping, :to => :working end event :date do transitions :from => :showering, :to => :dating end event :sleep do transitions :from => :showering, :to => :sleeping transitions :from => :working, :to => :sleeping transitions :from => :dating, :to => :sleeping end end 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). 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. Usually a good idea is to describe the states in models as an adjective, and the event as a verb. 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. == State for ActiveRecord models require they are saved 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. person = Person.new person.state # nil person.save # true person.state # "sleeping" person = Person.create person.state # "sleeping" person.sleeping? # true person.rotting? # false person = Person.new person.state = "rotting" # "rotting" person.rotting? # true person.sleeping? # false 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?" The events you specified also creates instance methods, to transition the model from one state to another. The following instance methods were created: person.shower! person.work! person.date! person.sleep! Note: The instance methods created follow the pattern "event!" === Events 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! 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!. 1. person.state # "sleeping" 2. person.shower! 3. person.state # "showering" Events can help your organize the flow of your model. But they can get more powerful with callbacks. === Callbacks The state also comes with a few callbacks that can be used. state :sleeping, :enter => :get_into_bed, :after => Proc.new {|model| model.whack_alarm_clock }, :exit => :make_up_bed Callbacks are called when the model is transitioning into the specified state. Note: * Callbacks can be either a symbol or a Proc. If used as a symbol, the instance method of the model will be called * The callbacks act differently if the model is a new record and hasn’t been saved, versus an already saved model. When put into consideration with ActiveRecord’s callbacks, a new record’s callback would look like this: * ActiveRecord::Base.before_save * ActiveRecord::Base.save * acts_as_state_machine :enter sleeping * acts_as_state_machine :after sleeping * ActiveRecord::Base.after_save When the model is no longer a new record, the callbacks execute as follows, if I had called the shower! method. * acts_as_state_machine :enter showering * ActiveRecord::Base.before_save * ActiveRecord::Base.save * ActiveRecord::Base.after_save * acts_as_state_machine :after showering * acts_as_state_machine :exit sleeping == Guarding States But how about if you want some sort of validation for a transition. You know, just to ensure data integrity. event :work do transitions :from => :showering, :to => :working # Going to work before showering? Stinky. transitions :from => :sleeping, :to => :working, :guard => Proc.new {|o| o.clean? } end 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. == Add a history log to all state changes Example from Artem Vasiliev's blog (Note that the blog mentions :log_transitions which is changed to :on_transition class ExpenseClaim < ActiveRecord::Base has_many :action_history, :class_name => 'ActionHistoryItem', :as => :document, :order => 'id desc' acts_as_state_machine :initial => States::DRAFT, :on_transition => :log_transitions state States::DRAFT, :owner => Roles::AUTHOR state States::MANAGER_APPROVAL_REQUIRED, :owner => Roles::APPROVING_MANAGER state States::REJECTED_BY_FINANCE, :owner => Roles::APPROVING_MANAGER #... event :send_for_manager_approval do transitions :from => States::DRAFT, :to => States::MANAGER_APPROVAL_REQUIRED transitions :from => States::REJECTED_BY_MANAGER, :to => States::MANAGER_APPROVAL_REQUIRED end event :approve do transitions :from => States::MANAGER_APPROVAL_REQUIRED, :to => States::FINANCE_APPROVAL_REQUIRED transitions :from => States::FINANCE_APPROVAL_REQUIRED, :to => States::APPROVED transitions :from => States::REJECTED_BY_FINANCE, :to => States::FINANCE_APPROVAL_REQUIRED end #... def log_transition(from, to, event, opts) user = self.updated_by raise "user is not set" if user.nil? as_role = self.class.states_table[from].opts[:owner] action_history << ActionHistoryItem.new({:from_state => from.to_s, :to_state => to.to_s, :action => event.to_s, :user_id => user.id, :as_role => as_role.to_s, :at => Time.now}) end #... end = Booking example (complex model with many callbacks) 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. (NOTE that the example below uses outdated AASM syntax. Please submit a fix) class Booking < ActiveRecord::Base include Comparable self.abstract_class = true set_table_name 'bookings' acts_as_recent 1.days attr_accessor :payment_option belongs_to :account belongs_to :user belongs_to :coupon has_one :date, :class_name => 'BookingDate', :dependent => :destroy, :foreign_key => :booking_id has_one :customer, :dependent => :destroy has_one :payment, :as => :payable, :dependent => :destroy has_one :progress, :class_name => 'BookingProgress', :dependent => :destroy has_one :token, :class_name => 'PaymentToken', :dependent => :destroy 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 }}] has_many :booking_extras, :include => [:extra, :booking_product], :dependent => :delete_all has_many :products, :through => :booking_products, :source => :product, :uniq => true has_many :extras, :through => :booking_extras, :source => :extra, :uniq => true has_many :events, :class_name => 'BookingEvent', :dependent => :delete_all, :order => 'booking_events.created_at DESC', :extend => BookingEventsExtension has_many :extras, :through => :line_items_extras, :source => :extra has_many :notes, :class_name => 'BookingNote', :dependent => :delete_all, :after_add => Proc.new {|b,bn| BookingMailer.deliver_note( bn )}, :extend => BookingNotesExtension delegate :to_s, :to => :name delegate :duration, :to => :date delegate :date_from, :date_to, :duration, :to => :date delegate :any_progress?, :to => :progress validates_presence_of :reference, :account_id, :user_id validates_uniqueness_of :reference, :scope => 'account_id', :on => :create validates_length_of :reference, :is => 10 include AASM aasm_initial_state :in_progress aasm_column :status state :in_progress state :pending, :enter => Proc.new{|b| b.commit!; BookingMailer.deliver_received( b ); } state :awaiting_payment, :enter => Proc.new{|b| ( b.create_token({ :account_id => b.account.id, :booking_id => b.id }) if b.token.nil? ); BookingMailer.deliver_payment( b ); }, :after => Proc.new{|b| b.log( 'Status set to %s.' / b.current_status_to_human ) } state :in_review, :enter => Proc.new{|b| BookingMailer.deliver_review( b ) }, :after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) } state :cancelled, :enter => Proc.new{|b| BookingMailer.deliver_cancelled( b ); }, :after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) } state :confirmed, :enter => Proc.new{|b| BookingMailer.deliver_confirmed( b ); }, :after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) } state :service_rendered, :enter => Proc.new{|b| BookingMailer.deliver_feedback( b ); }, :after => Proc.new{|b| b.log( 'Booking status set to %s.' / b.current_status_to_human ) } event( :pending ){ transitions :to => :pending, :from => :in_progress } event( :awaiting_payment ){ transitions :to => :awaiting_payment, :from => [:pending, :in_review] } event( :in_review ){ transitions :to => :in_review, :from => [:pending, :awaiting_payment] } event( :cancelled ){ transitions :to => :cancelled, :from => [:pending, :awaiting_payment, :in_review] } event( :confirmed ){ transitions :to => :confirmed, :from => :awaiting_payment, :guard => Proc.new{|b| !b.payment.nil? && b.payment.authorized? } } event( :service_rendered ){ transitions :to => :service_rendered, :from => :confirmed } class << self def status_to_human( status = :pending ) status.to_s.sub('_',' ').upcase end def running_total self.find(:all, :conditions => ['bookings.status = ?','confirmed']).inject( Globalize::Currency.free ) { |total, booking| booking.total + total } end def total_by_status(state = :in_progress) self.find_in_state(:all, state).inject( Globalize::Currency.free ) { |total, booking| booking.total + total } end def count_by_status(state = :in_progress) self.count_in_state(state) end def most_recent_by_status(state = :pending) self.find_in_state(:first, state, :order => 'bookings.created_at DESC') end end def current_status_to_human; self.class.status_to_human( self.current_state() ) end def log( event_description ) self.events.create!({ :account_id => self.account.id, :event_description => event_description }) end def total Globalize::Currency.new( ( self.booking_products.sum(:cents).to_i + self.booking_extras.sum(:cents).to_i ) ) - discount end def discount; (!self.coupon.nil? ? self.coupon.price : Globalize::Currency.free) end def name; "[#{self.reference}] #{self.customer.name}" end def editable?() [:in_progress, :in_review].include?( self.current_state ) end def committed?() self.current_state != :in_progress end def <=>(other_booking) self.date.date_from <=> other_booking.date.date_from end end === Event definitions Declare the business logic required to manage the transitions between the required states. A Booking may only be Confirmed once a payment method has been assigned AND authorized. event( :confirmed ){ transitions :to => :confirmed, :from => :awaiting_payment, :guard => Proc.new{|b| !b.payment.nil? && b.payment.authorized? } } 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: { :from => :the_intial_state, :to => :the_target_state, :guard => Proc.new{|record| record.condition_to_be_met_for_transition_to_occur? } } Do note that the resulting event is a destructive action (modifies the receiver) and should be invoked with booking.confirmed! #trailing ! = Credits Examples are gathered from existing sources. Especially thanks to * Artem Vasiliev http://thirstydoh.wordpress.com/2007/12/05/aasm-improvements/ * Aizat Faiz http://rails.aizatto.com/2007/05/24/ruby-on-rails-finite-state-machine-plugin-acts_as_state_machine/ * Lourens Naude http://blog.methodmissing.com/2006/11/16/beyond-callbacks-for-complex-model-lifecycles/ * Jesper Rønn-Jensen ( http://justaddwater.dk/ ) for collecting examples