ruby-on-rails - rails - aasm state machine
Usando el state_machine de pluginaweek, ¿puedo hacer referencia al objeto activerecord durante un evento? (3)
Estoy tratando de implementar un evento de "suspensión" que transiciones del objeto al estado: suspendido. Pero necesito poder "volver a suspender" y volver al estado anterior. Agregué un campo previous_state al modelo, pero no puedo ver cómo acceder a él dentro de un bloque de eventos.
Esta es la lógica básica que estoy tratando de implementar:
event :suspend do
owner.previous_state = self.state
transition [:new, :old] => :suspended
end
event :unsuspend do
transition :suspended => owner.previous_state.to_sym
owner.previous_state = nil
end
Los documentos state_machine no han sido muy útiles y no puedo encontrar ejemplos en línea. A veces es difícil saber cómo describir algo a google :)
Esta no es una solución perfecta en mi opinión, pero sí descubrí cómo lograr mi tarea:
state_machine :initial => :new do
state :new
state :old
state :suspended
before_transition :to => :suspended, :do => :set_previous_state
state :unsuspended
after_transition :to => :unsuspended, :do => :restore_previous_state
event :suspend do
transition any - :suspended => :suspended
end
event :unsuspend do
transition :suspended => :unsuspended, :if => :previous_state_present?
end
end
private
def previous_state_present?
previous_state.present?
end
def set_previous_state
self.previous_state = state
end
def restore_previous_state
if previous_state
self.state = previous_state
self.previous_state = nil
end
end
Empecé añadiendo un estado "sin suspensión" a mi máquina. Aunque nunca quiero que algo permanezca en este estado, no puedo decirle dinámicamente a state_machine a qué estado quiero volver a ponerme.
Agregué una devolución de llamada before_transition al evento suspend, para guardar el estado antes de que se suspenda.
Agregué una devolución de llamada after_transition al evento unsuspende, por lo que el estado se actualiza inmediatamente al estado anterior, y ese estado anterior se borra para evitar problemas más adelante en la vida del objeto.
Esto no es perfecto Funciona, pero es mucho más complicado que simplemente crear los eventos de suspender y suspender el servicio como métodos independientes. No fui por esa ruta porque quiero que state_machine controle todos los cambios de estado, y salir de eso elimina las protecciones contra el movimiento hacia / desde estados inválidos, devoluciones de llamadas, etc.
El autor de state_machine también ha proporcionado una solución alternativa aquí: https://groups.google.com/d/msg/pluginaweek-talk/LL9VJdL_x9c/vP1qv6br734J
Esto es:
Otra posible solución es ser un poco creativo con la forma en que funciona la máquina de estado. Hay muchos ganchos en ORM como ActiveRecord que nos dan la capacidad de establecer el estado en cualquier etapa del proceso. Considera lo siguiente:
class Vehicle < ActiveRecord::Base
before_validation do |vehicle|
# Set the actual value based on the previous state if we''ve just restored
vehicle.state = vehicle.previous_state if vehicle.restored?
end
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
event :restore do
transition any => :restored
end
state :parked do
validates_presence_of :name
end
end
# Look up previous state here...
def previous_state
''parked''
end
end
En este ejemplo, se introduce un nuevo estado, restaurado, aunque en realidad nunca se conserve en la base de datos. En su lugar, proporcionamos un enlace before_validation que reescribe el estado en función del estado anterior. Puedes ver los resultados a continuación:
v = Vehicle.new(:name => ''test'') # => #<Vehicle id: nil, name: "test", state: "parked">
v.save # => true
v.name = nil # => nil
v.ignite # => true
v # => #<Vehicle id: 1, name: nil, state: "idling">
v.restore # => false
v.errors # => #<OrderedHash {:name=>["can''t be blank"]}>
v.state # => "idling"
v.name = ''test'' # => "test"
v.restore # => true
v # => #<Vehicle id: 1, name: "test", state: "parked">
v.parked? # => true
Esto debería requerir un hit de base de datos menos como ocurre antes de la validación. En mi caso, la imagen completa se ve así:
module Interpreting::Status
extend ActiveSupport::Concern
included do
before_validation :restore_previous_state, if: :interpreter_cancelled?
state_machine :state, :initial => :ordered do
before_transition :to => :interpreter_booked, :do => :set_previous_state
state :ordered
state :confirmed
state :interpreter_booked
state :interpreter_cancelled # Transient status
end
end
protected
def set_previous_state
self.previous_state = self.state
end
def restore_previous_state
self.state = self.previous_state
end
end
Solución simple
Proporciono una versión alternativa a la solución que usa el argumento de bloque owner
. Puede ser útil en algunos casos.
state_machine :initial => :new do
state :new
state :old
before_transition :on => :suspend do |owner|
owner.previous_state = owner.state
end
before_transition :on => :unsuspend do |owner|
owner.previous_state.present?
end
after_transition :on => :unsuspend do |owner|
owner.state = owner.previous_state
end
event :suspend do
transition any - :suspended => :suspended
end
event :unsuspend do
transition :suspended => :unsuspended
end
end
Uso de around_transition
También tenga en cuenta que puede reemplazar los dos bloques sin around_transition
con around_transition
:
around_transition :on => :unsuspend do |owner, transition_block|
if owner.previous_state.present?
transition_block.call
owner.state = owner.previous_state
end
end