0

Synopsis

In Ruby on Rails, does the state machine gem support the use of a model instance that doesn't directly relate to the host model? If they do, how do I do it?

The conclusion I'm leaning toward is that authorization should be left to other parts of the framework, and the state machine should just be an interface defining the transition of states. That being said, I see some support for transition conditions and I was wondering if the data inside those conditions could be something NOT set on the host model, but instead passed in like a parameter.

Background

Say we have a Task that has the states in_progress and completed, and in order to transition from them respectively, the current_user (assigned in the session, access in the controller) needs to pass a check.

I understand through the documentation that in order to add a check to the transition I have to program it like this:

transition :in_progress => :completed, :if => :user_is_owner?

and define the function like:

def user_is_owner()
 true
end

but let's try to implement the restriction so that the task can only be edited if the user_id is the same as the id of the user that requested the task USING dynamic data.

def user_is_owner?(user)
 user.id == self.requester_id
end

Notice I don't have that user object, how would one pass the user object they need in?

Ruby Version: 1.9.3

Rails Version: 3.2.9

Thanks!

Thomas
  • 2,622
  • 1
  • 9
  • 16
  • 1
    Off the top of my head (and maybe there's a better solution), in `Task` you can have `user` as a transient attribute, `attr_accessor :user`. You would then assign this property in the controller where you build a `Task`. – Sergio Tulentsev Oct 05 '22 at 16:10
  • Well that would give me who created the task, which is already in `requester_id` . But it doesn't serve to prove that the session `user.id` is the same as the `task.requester_id`. The conclusion I'm leaning toward is that authorization should be left to other parts of the framework, and the state machine should just be an interface defining the transition of states. Though I see some support for transition conditions, I was wondering if the `data` inside those conditions could be something NOT set on the host model, but instead passed in like a parameter. – Thomas Oct 05 '22 at 16:17
  • 1
    "Well that would give me who created the task, which is already in requester_id" - no, you misunderstood. In your handler for `tasks/:id/complete` (or whatever) you would do `task = Task.find(params[:id]); task.user = current_user; task.complete!`. In this case, the task exists and its requester id is not necessarily the same as current user. – Sergio Tulentsev Oct 05 '22 at 18:57
  • 1
    extracting authorization to a pre-check is also a good idea. – Sergio Tulentsev Oct 05 '22 at 18:59
  • @SergioTulentsev Yeah this makes sense. The variable won't be saved to the database, but will be usable on the object. As the state machine knows about everything on the object, I suppose a solution would be to just throw a variable on there. A developer I work with said exactly what you said, but he called it a Plain Old Ruby Object. Which, so to speak means to not use the framework, and just utilize ruby's objects to do the work. – Thomas Oct 05 '22 at 19:08
  • 1
    @SergioTulentsev I would prefer to pull out the checks, and not use PORO's, because I feel like it's not consistent with other design patterns I've established. Though I also think there is a healthy balance between being "right" and wasting time. I think abstracting the two is a good idea, but considering the state machine isn't going to be that big, it's just a waste. Thanks! I'll post a more constructive answer to this once I'm done integrating it. – Thomas Oct 05 '22 at 19:13

2 Answers2

1

The thought process behind this post was that I wanted to use the framework the way it was meant to be used, MVC. Information specific to the connection doesn't belong on a model that represents something completely independent of the connection, it's just logical.

The solution I chose for my problem was what @SergioTulentsev mentioned, A transient attribute.

My Ruby on Rails solution included setting up a transient attribute on my model, by adding an attr_accessor

  attr_accessor :session_user

and a setter

  # @doc Setter function for transient variable @session_user
  def session_user
    @session_user
  end

and a function that uses the setter on my Task model

  def user_is_owner?
    requester == session_user
  end

then I utilized that function inside of my state_machine's transition

  transition :completed => :archived, :if => :user_is_owner?

The problems I see with this are that anytime you want to use the User to make authorization checks, you can't just pass it in as a parameter; it has to be on the object.

Thanks, I learned a lot. Hopefully this will be somewhat useful over the years...

Thomas
  • 2,622
  • 1
  • 9
  • 16
0

The original response is a valid approach, but I wound up going with this one. I think it's a much cleaner solution. Override the state machine events and extract the authorization.

  state_machine :status, :initial => :new do
    event :begin_work do
      transition :new => :in_progress
    end
  end
  def begin_work(user)
    if can_begin_work?(user)
      super # This calls the state transition, but only if we want.
    end
  end

Sources:

https://github.com/pluginaweek/state_machine/issues/193 https://www.rubydoc.info/github/pluginaweek/state_machine/StateMachine%2FMachine:before_transition Passing variables to Rails StateMachine gem transitions

Thomas
  • 2,622
  • 1
  • 9
  • 16