require_relative 'base'
module AASM
module Persistence
module ActiveRecordPersistence
# This method:
#
# * extends the model with ClassMethods
# * includes InstanceMethods
#
# Adds
#
# after_initialize :aasm_ensure_initial_state
#
# As a result, it doesn't matter when you define your methods - the following 2 are equivalent
#
# class Foo < ActiveRecord::Base
# def aasm_write_state(state)
# "bar"
# end
# include AASM
# end
#
# class Foo < ActiveRecord::Base
# include AASM
# def aasm_write_state(state)
# "bar"
# end
# end
#
def self.included(base)
base.send(:include, AASM::Persistence::Base)
base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
base.after_initialize do
aasm_ensure_initial_state
end
# ensure state is in the list of states
base.validate :aasm_validate_states
end
module InstanceMethods
# Writes state to the state column and persists it to the database
#
# foo = Foo.find(1)
# foo.aasm.current_state # => :opened
# foo.close!
# foo.aasm.current_state # => :closed
# Foo.find(1).aasm.current_state # => :closed
#
# NOTE: intended to be called from an event
def aasm_write_state(state, name=:default)
old_value = read_attribute(self.class.aasm(name).attribute_name)
aasm_write_attribute state, name
success = if aasm_skipping_validations(name)
value = aasm_raw_attribute_value(state, name)
aasm_update_column(name, value)
else
self.save
end
success ? true : aasm_rollback(name, old_value)
end
# Writes state to the state column, but does not persist it to the database
#
# foo = Foo.find(1)
# foo.aasm.current_state # => :opened
# foo.close
# foo.aasm.current_state # => :closed
# Foo.find(1).aasm.current_state # => :opened
# foo.save
# foo.aasm.current_state # => :closed
# Foo.find(1).aasm.current_state # => :closed
#
# NOTE: intended to be called from an event
def aasm_write_state_without_persistence(state, name=:default)
aasm_write_attribute(state, name)
end
private
def aasm_update_column(name, value)
self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm(name).attribute_name => value) == 1
end
def aasm_rollback(name, old_value)
write_attribute(self.class.aasm(name).attribute_name, old_value)
false
end
def aasm_enum(name=:default)
case AASM::StateMachine[self.class][name].config.enum
when false then nil
when true then aasm_guess_enum_method(name)
when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name)
else AASM::StateMachine[self.class][name].config.enum
end
end
def aasm_column_looks_like_enum(name=:default)
column_name = self.class.aasm(name).attribute_name.to_s
column = self.class.columns_hash[column_name]
raise NoMethodError.new("undefined method '#{column_name}' for #{self.class}") if column.nil?
column.type == :integer
end
def aasm_guess_enum_method(name=:default)
self.class.aasm(name).attribute_name.to_s.pluralize.to_sym
end
def aasm_skipping_validations(state_machine_name)
AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save
end
def aasm_write_attribute(state, name=:default)
write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name))
end
def aasm_raw_attribute_value(state, name=:default)
if aasm_enum(name)
self.class.send(aasm_enum(name))[state]
else
state.to_s
end
end
# Ensures that if the aasm_state column is nil and the record is new
# then the initial state gets populated after initialization
#
# foo = Foo.new
# foo.aasm_state # => "open" (where :open is the initial state)
#
#
# foo = Foo.find(:first)
# foo.aasm_state # => 1
# foo.aasm_state = nil
# foo.valid?
# foo.aasm_state # => nil
#
def aasm_ensure_initial_state
AASM::StateMachine[self.class].keys.each do |state_machine_name|
# checking via respond_to? does not work in Rails <= 3
# if respond_to?(self.class.aasm(state_machine_name).attribute_name) && send(self.class.aasm(state_machine_name).attribute_name).blank? # Rails 4
if aasm_column_is_blank?(state_machine_name)
aasm(state_machine_name).enter_initial_state
end
end
end
def aasm_column_is_blank?(state_machine_name)
attribute_name = self.class.aasm(state_machine_name).attribute_name
attribute_names.include?(attribute_name.to_s) && send(attribute_name).blank?
end
def aasm_fire_event(state_machine_name, name, options, *args, &block)
event = self.class.aasm(state_machine_name).state_machine.events[name]
if options[:persist]
event.fire_callbacks(:before_transaction, self, *args)
event.fire_global_callbacks(:before_all_transactions, self, *args)
end
begin
success = options[:persist] ? self.class.transaction(:requires_new => requires_new?(state_machine_name)) { super } : super
if options[:persist] && success
event.fire_callbacks(:after_commit, self, *args)
event.fire_global_callbacks(:after_all_commits, self, *args)
end
ensure
if options[:persist]
event.fire_callbacks(:after_transaction, self, *args)
event.fire_global_callbacks(:after_all_transactions, self, *args)
end
end
success
end
def requires_new?(state_machine_name)
AASM::StateMachine[self.class][state_machine_name].config.requires_new_transaction
end
def aasm_validate_states
AASM::StateMachine[self.class].keys.each do |state_machine_name|
unless aasm_skipping_validations(state_machine_name)
if aasm_invalid_state?(state_machine_name)
self.errors.add(AASM::StateMachine[self.class][state_machine_name].config.column , "is invalid")
end
end
end
end
def aasm_invalid_state?(state_machine_name)
aasm(state_machine_name).current_state && !aasm(state_machine_name).states.include?(aasm(state_machine_name).current_state)
end
end # InstanceMethods
end
end # Persistence
end # AASM