Acts_as_state_machine: bug?! obj.save doesn't work


#1

Hi all

I’m trying to understand, what acts_as_state_machine really does (I use
it because restful_authentication uses it).

restful_authentication defines the following stuff:

acts_as_state_machine :initial => :pending
state :passive
state :pending, :enter => :make_activation_code
state :active, :enter => :do_activate
state :suspended
state :deleted, :enter => :do_delete

event :register do
transitions :from => :passive, :to => :pending, :guard => Proc.new
{|u| !(u.crypted_password.blank? && u.password.blank?) }
end

event :activate do
transitions :from => :pending, :to => :active
end

event :suspend do
transitions :from => [:passive, :pending, :active], :to =>
:suspended
end

event :delete do
transitions :from => [:passive, :pending, :active, :suspended], :to
=> :deleted
end

event :unsuspend do
transitions :from => :suspended, :to => :active, :guard => Proc.new
{|u| !u.activated_at.blank? }
transitions :from => :suspended, :to => :pending, :guard => Proc.new
{|u| !u.activation_code.blank? }
transitions :from => :suspended, :to => :passive
end

I played a bit with the console…

josh$ script/console
Loading development environment (Rails 2.1.0)

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘quire’, :password_confirmation => ‘quire’ })
=> #<User id: nil, first_name: nil, last_name: nil, login: “quire”,
email: “removed_email_address@domain.invalid”, remember_token: nil, crypted_password: nil,
password_reset_code: nil, salt: nil, activation_code: nil,
remember_token_expires_at: nil, activated_at: nil, deleted_at: nil,
state: “passive”, created_at: nil, updated_at: nil>

Why is the state “passive”? Is the state of an unsaved object always
“passive”?

record.save
=> true

record
=> #<User id: 4, first_name: nil, last_name: nil, login: “quire”, email:
“removed_email_address@domain.invalid”, remember_token: nil, crypted_password:
“5670fb5c84b89d64ef405b315e4337304f88dc2b”, password_reset_code: nil,
salt: “a6ff544223bf2a7653651ea7f29888a195155c8d”, activation_code:
“43745d2e79d7aab4dfe2b4a3bd64d8ac577aeafe”, remember_token_expires_at:
nil, activated_at: nil, deleted_at: nil, state: “pending”, created_at:
“2009-04-20 20:19:15”, updated_at: “2009-04-20 20:19:15”>

Looks good so far… But when looking at the database entry, the
activation_code actually is NULL! Let’s prove this:

record.reload
=> #<User id: 4, first_name: nil, last_name: nil, login: “quire”, email:
“removed_email_address@domain.invalid”, remember_token: nil, crypted_password:
“5670fb5c84b89d64ef405b315e4337304f88dc2b”, password_reset_code: nil,
salt: “a6ff544223bf2a7653651ea7f29888a195155c8d”, activation_code: nil,
remember_token_expires_at: nil, activated_at: nil, deleted_at: nil,
state: “pending”, created_at: “2009-04-20 20:19:15”, updated_at:
“2009-04-20 20:19:15”>

Tadaah! This is a serious bug, isn’t it? The User object only works
correct when using “register!” instead of save:

josh$ script/console
Loading development environment (Rails 2.1.0)

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘quire’, :password_confirmation => ‘quire’ })
=> #<User id: nil, first_name: nil, last_name: nil, login: “quire”,
email: “removed_email_address@domain.invalid”, remember_token: nil, crypted_password: nil,
password_reset_code: nil, salt: nil, activation_code: nil,
remember_token_expires_at: nil, activated_at: nil, deleted_at: nil,
state: “passive”, created_at: nil, updated_at: nil>

record.register!
=> true

record.reload
=> #<User id: 5, first_name: nil, last_name: nil, login: “quire”, email:
“removed_email_address@domain.invalid”, remember_token: nil, crypted_password:
“465f2d6572f47e9adec58022d938b134e570077b”, password_reset_code: nil,
salt: “293aaa4b1a391f472803767c93c07bf7966e9141”, activation_code:
“1129116e89f989fee4dccb7eb38946c8e16af93a”, remember_token_expires_at:
nil, activated_at: nil, deleted_at: nil, state: “pending”, created_at:
“2009-04-20 20:22:03”, updated_at: “2009-04-20 20:22:03”>

Can anyone approve this? In my oppinion, save should have exactly the
same effect like “register!”… but it definitely doesn’t.

Thanks for your opinion.
Josh


#2

Not sure I understand your problem…

event :register do
transitions :from => :passive,
:to => :pending,
:guard => Proc.new {|u|
!(u.crypted_password.blank? &&
u.password.blank?) }
end

sort of tells me that UsersController#register causes a state
transition from :passive to :pending

and

event :activate do
transitions :from => :pending, :to => :active
end

UsersController#activate causes a state transition from :pending
to :active

I don’t see any mention of UsersController#save anywhere in either
restful_authentication/lib/… or in the controllers and views created
using script/generate authenticated User Sessions --options…


#3

Rick Lloyd wrote:

Not sure I understand your problem…

I don’t see any mention of UsersController#save anywhere in either
restful_authentication/lib/… or in the controllers and views created
using script/generate authenticated User Sessions --options…

The save method is the original ActiveRecord save method. It doesn’t
have to do anything with acts_as_state_machine! :wink:

Just look at the following to see what I mean:

josh$ script/console
Loading development environment (Rails 2.1.0)

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘quire’, :password_confirmation => ‘quire’ })
=> #<User … activation_code: nil, state: “passive” …> # No
activation code, state is passive

Why is the state passive? In my opinion it should be pending because of
this line:

acts_as_state_machine :initial => :pending

And because in my opinion it should be pending, it should also have an
activation code:

state :pending, :enter => :make_activation_code

However, it doesn’t so far.

record.save
=> true

record
=> #<User … activation_code:
“996b495dcbb15a61cdda19ef5da07a78b27dff87”, state: “pending” … >

Well, after calling ActiveRecord’s save method it seems to have changed
to the pending state, and the activation_code is available. Looks quite
good, so far.

In my opinion, the activation_code should now also be saved to the DB.
So let’s check this:

record.reload
=> #<User … activation_code: nil, state: “pending” … >

So here we have the problem! The activation_code is nil again! No idea
why, but it’s nil!

The only way to get this stuff to work is by calling the “register!”
method:

josh$ script/console
Loading development environment (Rails 2.1.0)

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘quire’, :password_confirmation => ‘quire’ })
=> #<User id: nil, first_name: nil, last_name: nil, login: “quire”,
email: “removed_email_address@domain.invalid”, remember_token: nil, crypted_password: nil,
password_reset_code: nil, salt: nil, activation_code: nil,
remember_token_expires_at: nil, activated_at: nil, deleted_at: nil,
state: “passive”, created_at: nil, updated_at: nil>

record.register!
=> true

record
=> #<User … activation_code:
“f8395f836ec66c9db966da457ba19cdf5f29dd0a”, state: “pending” … >

record.reload
=> #<User … activation_code:
“d5402915503a0c3f1a9eb984c116bc0ae4c07877”, state: “pending” … > # The
activation_code stays now

This doesn’t make any sense to me. The only thing that “register!” does
is changing the state from “passive” to “pending”:

event :register do
transitions :from => :passive, :to => :pending, :guard => Proc.new
{|u| !(u.crypted_password.blank? && u.password.blank?) }
end

OK, this sounds reasonable, but then I don’t understand, why and where
the state is changed when calling save (it’s exactly the same transition
from “passive” to “pending” as when calling register!), and why the
activation_code gets lost in this case…?

Looks everything really strange to me… You see my problem now?


#4

Perhaps the AASM library is buggy. I haven’t used it in ages, but I
seem to recall some issue of it not calling the enter transition unless
the event was manually triggered. That’s what the behavior above
suggests.

If the library is giving you hassles, then generate the authentication
code without the aasm flag. This code was submitted to me by someone
else, and I don’t have much experience with it.


#5

Rick O. wrote:

Perhaps the AASM library is buggy. I haven’t used it in ages, but I
seem to recall some issue of it not calling the enter transition unless
the event was manually triggered. That’s what the behavior above
suggests.

If the library is giving you hassles, then generate the authentication
code without the aasm flag. This code was submitted to me by someone
else, and I don’t have much experience with it.

First of all thanks a lot for taking this serious and answering me upon
my request, Rick. :slight_smile:

What do you suggest? Should I try to find the bug in AASM and submit a
fix? I don’t like to use workarounds for evidently buggy software… :wink:


#6

You can now follow the sequence:

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘enquire’, :password_confirmation => ‘enquire’ })
=> #<User id: nil, login: “quire”, name: “”, email:
“removed_email_address@domain.invalid”, crypted_password: nil, salt: nil, created_at:
nil, updated_at: nil, remember_token: nil, remember_token_expires_at:
nil, activation_code: nil, activated_at: nil, state: “passive”,
deleted_at: nil>

record.save
=> true

record
=> #<User id: nil, login: “quire”, name: “”, email:
“removed_email_address@domain.invalid”, crypted_password: nil, salt: nil, created_at:
nil, updated_at: nil, remember_token: nil, remember_token_expires_at:
nil, activation_code: “8508423584b5c775a60939f9d1966653cd8ea493”,
activated_at: nil, state: “pending”, deleted_at: nil>

Actually, you forgot to reload the model, because THAT caused the
problem…

And it seems that we don’t use the same version of the
restful_authentication plugin, because mine doesn’t contain a file
called aasm_roles.rb; take a look at the attached screen…


#7

You might want to look at this a little closer before you submit a bug
report. For instance, if you make two changes:

  1. FILE: app/controllers/users_controller.rb
    IN: def create
    FROM: @user.register! if @user && @user.valid?
    TO: @user.save! if @user && @user.valid?

  2. FILE: vendor/plugins/restful-authentication/lib/authorization/
    aasm_roles.rb
    FROM: aasm_event :register do
    TO: aasm_event :save do

You can now follow the sequence:

record = User.new({ :login => ‘quire’, :email => ‘removed_email_address@domain.invalid’, :password => ‘enquire’, :password_confirmation => ‘enquire’ })
=> #<User id: nil, login: “quire”, name: “”, email:
“removed_email_address@domain.invalid”, crypted_password: nil, salt: nil, created_at:
nil, updated_at: nil, remember_token: nil, remember_token_expires_at:
nil, activation_code: nil, activated_at: nil, state: “passive”,
deleted_at: nil>

record.save
=> true

record
=> #<User id: nil, login: “quire”, name: “”, email:
“removed_email_address@domain.invalid”, crypted_password: nil, salt: nil, created_at:
nil, updated_at: nil, remember_token: nil, remember_token_expires_at:
nil, activation_code: “8508423584b5c775a60939f9d1966653cd8ea493”,
activated_at: nil, state: “pending”, deleted_at: nil>

Of course, you can not actually save record to the database anymore
since you’ve masked ActiveRecord’s save method. There’s probably a
way to clear that up as well but that’s not your problem here.

As far as the “passive” - “pending” issue, I suspect you’re not
catching your record early enough in it’s aasm life.

These are all good questions to ask the rubyist-aasm folks - I just
wouldn’t open the conversation with a !!!BUG REPORT!!! salvo
iynwim :wink:
On Apr 21, 6:28 am, Joshua M. removed_email_address@domain.invalid


#8

Actually, I didn’t forget. The model wasn’t available for reload
because, by masking ActiveRecord#save with UsersController#save, there
was nothing to reload.

But yes, version is probably going to make any more discussion
difficult.

Here’s where I go:
http://github.com/technoweenie/restful-authentication/tree/master.

This site is actively supported, maybe you should move there.