20

Is it possible to send variables in the the transition? i.e.

@car.crash!(:crashed_by => current_user)

I have callbacks in my model but I need to send them the user who instigated the transition

after_crash do |car, transition|
  # Log the car crashers name
end

I can't access current_user because I'm in the Model and not the Controller/View.

And before you say it... I know I know.

Don't try to access session variables in the model

I get it.

However, whenever you wish to create a callback that logs or audits something then it's quite likely you're going to want to know who caused it? Ordinarily I'd have something in my controller that did something like...

@foo.some_method(current_user)

and my Foo model would be expecting some user to instigate some_method but how do I do this with a transition with the StateMachine gem?

Kevin Monk
  • 1,434
  • 1
  • 16
  • 23

5 Answers5

37

If you are referring to the state_machine gem - https://github.com/pluginaweek/state_machine - then it supports arguments to events

after_crash do |car, transition|
  Log.crash(car: car, crashed_by: transition.args.first)
end
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
Kevin Ansfield
  • 2,343
  • 1
  • 19
  • 20
  • 2
    By coincidence I ended up stumbling upon my own question again in a Google search and this answer was very useful. Thanks. – Kevin Monk Dec 07 '11 at 15:53
  • I think this is definitely better than User.current that I see others implementing when they run into this type of issue. User interaction belongs at the controller layer. – Eric Steen Jun 13 '12 at 00:50
  • 2
    This works fine, but you *do* have to spell transition correctly. Solution above has an extra 's'. – Brenda Nov 12 '12 at 14:47
  • 2
    To be clear, you can pass parameters just as the OP showed, e.g. `@car.crash!(:crashed_by => current_user)`. There are conversations about ways to get at the arguments in the block [here](https://github.com/pluginaweek/state_machine/issues/193) and [here](https://github.com/pluginaweek/state_machine/issues/239) – Tyler Collier Jun 12 '14 at 19:18
  • 1
    I know I am going to be downvoted for this, but this kind of complexity leads me to getting rid of state_machine and instead of a transition make one nicely written and readable method that updates the state. All I wanted was to access the currently logged in user in transition and if it takes a lot of effort, why bother with a gem that isn't supported anymore. – Anton Kuzmin Apr 15 '15 at 10:34
  • 1
    Just in case people are still reading this. A new maintained version of the gem seems to be available at https://github.com/state-machines/state_machines. Pretty much the same syntax etc. I am not sure if its a fork of the original. I just happened to come across this today and am using it for my latest project. – Gaurav Shetty Mar 21 '17 at 14:52
  • this issue deal with this question for the ones who use state-machine gem : https://github.com/state-machines/state_machines-activerecord/issues/74 – Raphaël Jul 12 '19 at 14:30
8

I was having trouble with all of the other answers, and then I found that you can simply override the event in the class.

class Car
  state_machine do
    ...
    event :crash do
      transition any => :crashed
    end
  end
  def crash(current_driver)
    logger.debug(current_driver)
    super
  end
end

Just make sure to call "super" in your custom method

deafgreatdane
  • 1,211
  • 11
  • 12
2

I don't think you can pass params to events with that gem, so maybe you could try storing the current_user on @car (temporarily) so that your audit callback can access it.

In controller

@car.driver = current_user

In callback

after_crash do |car, transition|
   create_audit_log car.driver, transition
end

Or something along those lines.. :)

keeran
  • 76
  • 2
  • Thanks Keeran. This is basically what I ended up doing in the end. As the attribute has no permanence - any thoughts on how you could do this without creating a new field in the DB? – Kevin Monk Aug 20 '10 at 10:34
  • You could try adding an attr_accessor for :driver, but I'm not sure if the callback will reload / use a 'fresh' instance of the model (and so wipe the temp var). – keeran Aug 21 '10 at 11:04
1

I used transactions, instead of updating the object and changing the state in one call. For example, in update action,

ActiveRecord::Base.transaction do
  if @car.update_attribute!(:crashed_by => current_user)
    if @car.crash!()
      format.html { redirect_to @car }
    else
      raise ActiveRecord::Rollback
  else
    raise ActiveRecord::Rollback
  end
end
Vish
  • 21
  • 1
  • 4
0

Another common pattern (see the state_machine docs) that saves you from having to pass variables between the controller and model is to dynamically define a state-checking method within the callback method. This wouldn't be very elegant in the example given above, but might be preferable in cases where the model needs to handle the same variable(s) in different states. For example, if you have 'crashed', 'stolen', and 'borrowed' states in your Car model, all of which can be associated with a responsible Person, you could have:

state :crashed, :stolen, :borrowed do
  def blameable?
    true
  end

state all - [:crashed, :stolen, :borrowed] do
  def blameable?
    false
  end

Then in the controller, you can do something like:

car.blame_person(person) if car.blameable?
Tal Yarkoni
  • 2,534
  • 1
  • 17
  • 12