3

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.

freemanoid
  • 14,592
  • 6
  • 54
  • 77
Hector Villarreal
  • 822
  • 10
  • 20

3 Answers3

2

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 =]

Ryan Crews
  • 3,015
  • 1
  • 32
  • 28
1

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
Esse
  • 3,278
  • 2
  • 21
  • 25
  • interesting solution. I think it will work for my particular case, but there are some things to consider. 1) you're duplicating where the states are declared; once in the aasm declaration and the other in the array. 2) there might be multiple "branches" for a state machine, this just accounts for a single branch situation. – Hector Villarreal Nov 03 '14 at 14:37
  • 1) yes - I'm aware of that, however you need to declare order somehow. 2) when there are branches it may occur that you have some states that are not actually comparable. Consider this: http://faculty.kutztown.edu/rieksts/225/study/spring07/test3-ans_files/image006.jpg which element is bigger 8 or 6? Actually - we don't know. I believe assumption of linear ordering is required here to even consider such problem. – Esse Nov 03 '14 at 23:56
0

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.

wvengen
  • 383
  • 4
  • 10