diff --git a/lib/state_machine/integrations/active_model.rb b/lib/state_machine/integrations/active_model.rb
index db5368c3..c73767f4 100644
--- a/lib/state_machine/integrations/active_model.rb
+++ b/lib/state_machine/integrations/active_model.rb
@@ -1,65 +1,65 @@
module StateMachine
module Integrations #:nodoc:
# Adds support for integrating state machines with ActiveModel classes.
- #
+ #
# == Examples
- #
+ #
# If using ActiveModel directly within your class, then any one of the
# following features need to be included in order for the integration to be
# detected:
# * ActiveModel::Observing
# * ActiveModel::Validations
- #
+ #
# Below is an example of a simple state machine defined within an
# ActiveModel class:
- #
+ #
# class Vehicle
# include ActiveModel::Observing
# include ActiveModel::Validations
- #
+ #
# attr_accessor :state
# define_attribute_methods [:state]
- #
+ #
# state_machine :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
# end
# end
- #
+ #
# The examples in the sections below will use the above class as a
# reference.
- #
+ #
# == Actions
- #
+ #
# By default, no action will be invoked when a state is transitioned. This
# means that if you want to save changes when transitioning, you must
# define the action yourself like so:
- #
+ #
# class Vehicle
# include ActiveModel::Validations
# attr_accessor :state
- #
+ #
# state_machine :action => :save do
# ...
# end
- #
+ #
# def save
# # Save changes
# end
# end
- #
+ #
# == Validations
- #
+ #
# As mentioned in StateMachine::Machine#state, you can define behaviors,
# like validations, that only execute for certain states. One *important*
# caveat here is that, due to a constraint in ActiveModel's validation
# framework, custom validators will not work as expected when defined to run
# in multiple states. For example:
- #
+ #
# class Vehicle
# include ActiveModel::Validations
- #
+ #
# state_machine do
# ...
# state :first_gear, :second_gear do
@@ -67,14 +67,14 @@ module Integrations #:nodoc:
# end
# end
# end
- #
+ #
# In this case, the :speed_is_legal validation will only get run
# for the :second_gear state. To avoid this, you can define your
# custom validation like so:
- #
+ #
# class Vehicle
# include ActiveModel::Validations
- #
+ #
# state_machine do
# ...
# state :first_gear, :second_gear do
@@ -82,103 +82,103 @@ module Integrations #:nodoc:
# end
# end
# end
- #
+ #
# == Validation errors
- #
+ #
# In order to hook in validation support for your model, the
# ActiveModel::Validations feature must be included. If this is included
# and an event fails to successfully fire because there are no matching
# transitions for the object, a validation error is added to the object's
# state attribute to help in determining why it failed.
- #
+ #
# For example,
- #
+ #
# vehicle = Vehicle.new
# vehicle.ignite # => false
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
- #
+ #
# In addition, if you're using the ignite! version of the event,
# then the failure reason (such as the current validation errors) will be
# included in the exception that gets raised when the event fails. For
# example, assuming there's a validation on a field called +name+ on the class:
- #
+ #
# vehicle = Vehicle.new
# vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
- #
+ #
# === Security implications
- #
+ #
# Beware that public event attributes mean that events can be fired
# whenever mass-assignment is being used. If you want to prevent malicious
# users from tampering with events through URLs / forms, the attribute
# should be protected like so:
- #
+ #
# class Vehicle
# include ActiveModel::MassAssignmentSecurity
# attr_accessor :state
- #
+ #
# attr_protected :state_event
# # attr_accessible ... # Alternative technique
- #
+ #
# state_machine do
# ...
# end
# end
- #
+ #
# If you want to only have *some* events be able to fire via mass-assignment,
# you can build two state machines (one public and one protected) like so:
- #
+ #
# class Vehicle
# include ActiveModel::MassAssignmentSecurity
# attr_accessor :state
- #
+ #
# attr_protected :state_event # Prevent access to events in the first machine
- #
+ #
# state_machine do
# # Define private events here
# end
- #
+ #
# # Public machine targets the same state as the private machine
# state_machine :public_state, :attribute => :state do
# # Define public events here
# end
# end
- #
+ #
# == Callbacks
- #
+ #
# All before/after transition callbacks defined for ActiveModel models
# behave in the same way that other ActiveSupport callbacks behave. The
# object involved in the transition is passed in as an argument.
- #
+ #
# For example,
- #
+ #
# class Vehicle
# include ActiveModel::Validations
# attr_accessor :state
- #
+ #
# state_machine :initial => :parked do
# before_transition any => :idling do |vehicle|
# vehicle.put_on_seatbelt
# end
- #
+ #
# before_transition do |vehicle, transition|
# # log message
# end
- #
+ #
# event :ignite do
# transition :parked => :idling
# end
# end
- #
+ #
# def put_on_seatbelt
# ...
# end
# end
- #
+ #
# Note, also, that the transition can be accessed by simply defining
# additional arguments in the callback block.
- #
+ #
# == Observers
- #
+ #
# In order to hook in observer support for your application, the
# ActiveModel::Observing feature must be included. Because of the way
# ActiveModel observers are designed, there is less flexibility around the
@@ -195,49 +195,49 @@ module Integrations #:nodoc:
# * before/after/after_failure_to-_transition_state_to_idling
# * before/after/after_failure_to-_transition_state
# * before/after/after_failure_to-_transition
- #
+ #
# The following class shows an example of some of these hooks:
- #
+ #
# class VehicleObserver < ActiveModel::Observer
# # Callback for :ignite event *before* the transition is performed
# def before_ignite(vehicle, transition)
# # log message
# end
- #
+ #
# # Callback for :ignite event *after* the transition has been performed
# def after_ignite(vehicle, transition)
# # put on seatbelt
# end
- #
+ #
# # Generic transition callback *before* the transition is performed
# def after_transition(vehicle, transition)
# Audit.log(vehicle, transition)
# end
- #
+ #
# def after_failure_to_transition(vehicle, transition)
# Audit.error(vehicle, transition)
# end
# end
- #
+ #
# More flexible transition callbacks can be defined directly within the
# model as described in StateMachine::Machine#before_transition
# and StateMachine::Machine#after_transition.
- #
+ #
# To define a single observer for multiple state machines:
- #
+ #
# class StateMachineObserver < ActiveModel::Observer
# observe Vehicle, Switch, Project
- #
+ #
# def after_transition(object, transition)
# Audit.log(object, transition)
# end
# end
- #
+ #
# == Internationalization
- #
+ #
# Any error message that is generated from performing invalid transitions
# can be localized. The following default translations are used:
- #
+ #
# en:
# activemodel:
# errors:
@@ -247,16 +247,16 @@ module Integrations #:nodoc:
# invalid_event: "cannot transition when %{state}"
# # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
# invalid_transition: "cannot transition via %{event}"
- #
+ #
# You can override these for a specific model like so:
- #
+ #
# en:
# activemodel:
# errors:
# models:
# user:
# invalid: "is not valid"
- #
+ #
# In addition to the above, you can also provide translations for the
# various states / events in each state machine. Using the Vehicle example,
# state translations will be looked for using the following keys, where
@@ -265,16 +265,16 @@ module Integrations #:nodoc:
# * activemodel.state_machines.#{model_name}.states.#{state_name}
# * activemodel.state_machines.#{machine_name}.states.#{state_name}
# * activemodel.state_machines.states.#{state_name}
- #
+ #
# Event translations will be looked for using the following keys, where
# +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
# * activemodel.state_machines.#{model_name}.#{machine_name}.events.#{event_name}
# * activemodel.state_machines.#{model_name}.events.#{event_name}
# * activemodel.state_machines.#{machine_name}.events.#{event_name}
# * activemodel.state_machines.events.#{event_name}
- #
+ #
# An example translation configuration might look like so:
- #
+ #
# es:
# activemodel:
# state_machines:
@@ -282,74 +282,74 @@ module Integrations #:nodoc:
# parked: 'estacionado'
# events:
# park: 'estacionarse'
- #
+ #
# == Dirty Attribute Tracking
- #
+ #
# When using the ActiveModel::Dirty extension, your model will keep track of
# any changes that are made to attributes. Depending on your ORM, an object
# will only be saved when there are attributes that have changed on the
# object. When integrating with state_machine, typically the +state+ field
# will be marked as dirty after a transition occurs. In some situations,
# however, this isn't the case.
- #
+ #
# If you define loopback transitions in your state machine, the value for
# the machine's attribute (e.g. state) will not change. Unless you explicitly
# indicate so, this means that your object won't persist anything on a
# loopback. For example:
- #
+ #
# class Vehicle
# include ActiveModel::Validations
# include ActiveModel::Dirty
# attr_accessor :state
- #
+ #
# state_machine :initial => :parked do
# event :park do
# transition :parked => :parked, ...
# end
# end
# end
- #
+ #
# If, instead, you'd like your object to always persist regardless of
# whether the value actually changed, you can do so by using the
# #{attribute}_will_change! helpers or defining a +before_transition+
# callback that actually changes an attribute on the model. For example:
- #
+ #
# class Vehicle
# ...
# state_machine :initial => :parked do
# before_transition all => same do |vehicle|
# vehicle.state_will_change!
- #
+ #
# # Alternative solution, updating timestamp
# # vehicle.updated_at = Time.curent
# end
# end
# end
- #
+ #
# == Creating new integrations
- #
+ #
# If you want to integrate state_machine with an ORM that implements parts
# or all of the ActiveModel API, only the machine defaults need to be
# specified. Otherwise, the implementation is similar to any other
# integration.
- #
+ #
# For example,
- #
+ #
# module StateMachine::Integrations::MyORM
# include StateMachine::Integrations::ActiveModel
- #
+ #
# @defaults = {:action = > :persist}
- #
+ #
# def self.matches?(klass)
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
# end
- #
+ #
# protected
# def runs_validations_on_action?
# action == :persist
# end
# end
- #
+ #
# If you wish to implement other features, such as attribute initialization
# with protected attributes, named scopes, or database transactions, you
# must add these independent of the ActiveModel integration. See the
@@ -358,21 +358,21 @@ module ActiveModel
def self.included(base) #:nodoc:
base.versions.unshift(*versions)
end
-
+
include Base
extend ClassMethods
-
+
require 'state_machine/integrations/active_model/versions'
-
+
@defaults = {}
-
+
# Classes that include ActiveModel::Observing or ActiveModel::Validations
# will automatically use the ActiveModel integration.
def self.matching_ancestors
%w(ActiveModel ActiveModel::Observing ActiveModel::Validations)
end
-
- # Adds a validation error to the given object
+
+ # Adds a validation error to the given object
def invalidate(object, attribute, message, values = [])
if supports_validations?
attribute = self.attribute(attribute)
@@ -380,85 +380,90 @@ def invalidate(object, attribute, message, values = [])
h[key] = value
h
end
-
+
default_options = default_error_message_options(object, attribute, message)
object.errors.add(attribute, message, options.merge(default_options))
end
end
-
+
# Describes the current validation errors on the given object. If none
# are specific, then the default error is interpeted as a "halt".
def errors_for(object)
object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
end
-
+
# Resets any errors previously added when invalidating the given object
def reset(object)
object.errors.clear if supports_validations?
end
-
+
+ # Runs state events around the object's validation process
+ def around_validation(object)
+ object.class.state_machines.transitions(object, action, after: false).perform { yield }
+ end
+
protected
# Whether observers are supported in the integration. Only true if
# ActiveModel::Observer is available.
def supports_observers?
defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
end
-
+
# Whether validations are supported in the integration. Only true if
# the ActiveModel feature is enabled on the owner class.
def supports_validations?
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
end
-
+
# Do validations run when the action configured this machine is
# invoked? This is used to determine whether to fire off attribute-based
# event transitions when the action is run.
def runs_validations_on_action?
false
end
-
+
# Gets the terminator to use for callbacks
def callback_terminator
@terminator ||= lambda {|result| result == false}
end
-
+
# Determines the base scope to use when looking up translations
def i18n_scope(klass)
klass.i18n_scope
end
-
+
# The default options to use when generating messages for validation
# errors
def default_error_message_options(object, attribute, message)
{:message => @messages[message]}
end
-
+
# Translates the given key / value combo. Translation keys are looked
# up in the following order:
# * #{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{model_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{plural_key}.#{value}
- #
+ #
# If no keys are found, then the humanized value will be the fallback.
def translate(klass, key, value)
ancestors = ancestors_for(klass)
group = key.to_s.pluralize
value = value ? value.to_s : 'nil'
-
+
# Generate all possible translation keys
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}"}
translations.concat(ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}"})
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
end
-
+
# Build a list of ancestors for the given class to use when
# determining which localization key to use for a particular string.
def ancestors_for(klass)
klass.lookup_ancestors
end
-
+
# Initializes class-level extensions and defaults for this machine
def after_initialize
super
@@ -466,18 +471,18 @@ def after_initialize
load_observer_extensions
add_default_callbacks
end
-
+
# Loads any locale files needed for translating validation errors
def load_locale
I18n.load_path.unshift(@integration.locale_path) unless I18n.load_path.include?(@integration.locale_path)
end
-
+
# Loads extensions to ActiveModel's Observers
def load_observer_extensions
require 'state_machine/integrations/active_model/observer'
require 'state_machine/integrations/active_model/observer_update'
end
-
+
# Adds a set of default callbacks that utilize the Observer extensions
def add_default_callbacks
if supports_observers?
@@ -486,40 +491,35 @@ def add_default_callbacks
callbacks[:failure] << Callback.new(:failure) {|object, transition| notify(:after_failure_to, object, transition)}
end
end
-
+
# Skips defining reader/writer methods since this is done automatically
def define_state_accessor
name = self.name
-
+
owner_class.validates_each(attribute) do |object, attr, value|
machine = object.class.state_machine(name)
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
end if supports_validations?
end
-
+
# Adds hooks into validation for automatically firing events
def define_action_helpers
super
define_validation_hook if runs_validations_on_action?
end
-
+
# Hooks into validations by defining around callbacks for the
# :validation event
def define_validation_hook
owner_class.set_callback(:validation, :around, self, :prepend => true)
end
-
- # Runs state events around the object's validation process
- def around_validation(object)
- object.class.state_machines.transitions(object, action, :after => false).perform { yield }
- end
-
+
# Creates a new callback in the callback chain, always inserting it
# before the default Observer callbacks that were created after
# initialization.
def add_callback(type, options, &block)
options[:terminator] = callback_terminator
-
+
if supports_observers?
@callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
add_states(callback.known_states)
@@ -528,21 +528,21 @@ def add_callback(type, options, &block)
super
end
end
-
+
# Configures new states with the built-in humanize scheme
def add_states(new_states)
super.each do |new_state|
new_state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
end
end
-
+
# Configures new event with the built-in humanize scheme
def add_events(new_events)
super.each do |new_event|
new_event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
end
end
-
+
# Notifies observers on the given object that a callback occurred
# involving the given transition. This will attempt to call the
# following methods on observers:
@@ -555,7 +555,7 @@ def add_events(new_events)
# * #{type}_transition_#{machine_name}_to_#{to}
# * #{type}_transition_#{machine_name}
# * #{type}_transition
- #
+ #
# This will always return true regardless of the results of the
# callbacks.
def notify(type, object, transition)
@@ -563,7 +563,7 @@ def notify(type, object, transition)
event = transition.qualified_event
from = transition.from_name || 'nil'
to = transition.to_name || 'nil'
-
+
# Machine-specific updates
["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
["_from_#{from}", nil].each do |from_segment|
@@ -573,11 +573,11 @@ def notify(type, object, transition)
end
end
end
-
+
# Generic updates
object.class.changed if object.class.respond_to?(:changed)
object.class.notify_observers('update_with_transition', ObserverUpdate.new("#{type}_transition", object, transition))
-
+
true
end
end