Hector Villarreal
Hector Villarreal

Reputation: 822

Check if state is past another state in aasm?

Suppose there is an object with 4 states

:new
:in_process
:done
:verified

There is also a method that should only be executed when the object is in a state greater than :in_process

How do I go about doing this check? I thought it might be something

def some_action
  return unless my_object.state > :in_process
  #do some work
end

But that just compares the strings.

Am I missing something or is there an actual way of performing a check like this?

Thanks.

Upvotes: 3

Views: 1919

Answers (3)

wvengen
wvengen

Reputation: 422

If one takes care in defining the correct order in AASM and makes sure not to override any states (to specify extra options, for example), it is possible to use them.

The following mixin defines scopes like Model.done_or_before and Model.in_process_or_after, as well as methods like m.done_or_before?.

module AASMLinearity
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def aasm(*args, &block)
      r = super(*args, &block)
      if block
        states = r.state_machine.states.map(&:name)
        column = r.attribute_name
        states.each_with_index do |state, i|
          scope "#{state}_or_after", ->{ where(column => states[i..-1]) }
          scope "#{state}_or_before", ->{ where(column => states[0..i]) }

          define_method "#{state}_or_after?", ->{ states[i..-1].include? read_attribute(column).to_sym }
          define_method "#{state}_or_before?", ->{ states[0..i].include? read_attribute(column).to_sym }
        end
      end
      r
    end
  end
end

You can put this in something like app/models/concerns/aasm_linearity.rb, and include AASMLinearity after include AASM, but before the statemachine definition.

Upvotes: 0

Ryan Crews
Ryan Crews

Reputation: 3033

Ignoring the issue of non-linear state machines, I've found the following to work well for my needs on a few projects with simple state machines:

# Check if the stage is in or before the supplied stage (stage_to_check).
def in_or_before_stage?(stage_to_check)
  if stage_to_check.present? && self.stage.present?
    STAGES_IN_ORDER.reverse.lazy.drop_while { |stg| stg != stage_to_check }.include?(self.stage)
  else
    false
  end
end

and the other check is sometimes desired as well:

# Check if the stage is in or after the supplied stage (stage_to_check).
def in_or_after_stage?(stage_to_check)
  if stage_to_check.present? && self.stage.present?
    # Get all the stages that are in and after the stage we want to check (stage_to_check),
    # and then see if the stage is in that list (well, technically in a lazy enumerable).
    STAGES_IN_ORDER.lazy.drop_while { |stg| stg != stage_to_check }.include?(self.stage)
  else
    false
  end
end

Where "STAGES_IN_ORDER" is just an array with the stages in order from initial to final.

We're just removing items from the list and then checking if our object's current stage is in our resulting list. If we want to know if it's in or before some stage we remove the later stages until we reach our supplied test stage, if we want to know if it's after some given stage we remove items from the front of the list.

I realize you probably don't need this answer anymore, but hopefully it helps someone =]

Upvotes: 2

Esse
Esse

Reputation: 3298

The problem here is that you don't have order inside state machine. You need to provide and declare one.

I would stick to this solution:

  1. first declare constant in your model, containing states (in order!), so: STATES = [:new, :in_process, :done, :verified]

  2. And later, inside your model:

 

def current_state_index
  return state_index(self.state)
end

def state_index(state)
  return STATES.index(state)
end

def some_action
  return unless current_state_index > state_index(:in_process)
  #do some work end
end

Upvotes: 1

Related Questions