views:

564

answers:

6

I can't seem to get the state_machine gem (http://github.com/pluginaweek/state_machine/) to work on existing records (it works correctly on new records).

Here's my model:

class Comment < ActiveRecord::Base
  state_machine :state, :initial => :pending do
    event :publish do
      transition all => :published
    end
  end
end

and here's an IRB session that demonstrates the issue (I did ActiveRecord::Base.logger = Logger.new(STDOUT) to make it easier to read):

>> c = Comment.new
=> #<Comment id: nil, song_id: nil, author: nil, body: nil, created_at: nil, updated_at: nil, state: "pending">
>> c.state
=> "pending"
>> c.publish
  Comment Create (0.6ms)   INSERT INTO "comments" ("updated_at", "body", "author", "song_id", "created_at", "state") VALUES('2009-11-02 02:44:37', NULL, NULL, NULL, '2009-11-02 02:44:37', 'published')
=> true
>> Comment.last.state
  Comment Load (0.4ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> "published"
>> c = Comment.create
  Comment Create (0.5ms)   INSERT INTO "comments" ("updated_at", "body", "author", "song_id", "created_at", "state") VALUES('2009-11-02 02:44:47', NULL, NULL, NULL, '2009-11-02 02:44:47', 'pending')
=> #<Comment id: 4, song_id: nil, author: nil, body: nil, created_at: "2009-11-02 02:44:47", updated_at: "2009-11-02 02:44:47", state: "pending">
>> c.publish
=> true
>> c.save
=> true
>> Comment.last.state
  Comment Load (0.4ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> "pending"

I.e., everything works fine when I publish an unsaved comment, but when I try to publish a comment that's already saved, nothing happens.

Another Edit: Perhaps the root of the problem?

=> true
>> a = Comment.last
  Comment Load (1.3ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> #<Comment id: 3, song_id: nil, author: nil, body: nil, created_at: "2009-11-03 03:03:54", updated_at: "2009-11-03 03:03:54", state: "pending">
>> a.state
=> "pending"
>> a.publish
=> true
>> a.state
=> "published"
>> a.state_changed?
=> false

I.e., even though the state has actually changed, state_changed? is returning false and therefore Rails won't update the corresponding database row when I call save.

It works when I turn off partial updates, but not when I try state_will_change!:

>> Comment.partial_updates = false
=> false
>> c = Comment.create
  Comment Create (0.5ms)   INSERT INTO "comments" ("updated_at", "body", "author", "song_id", "created_at", "state") VALUES('2009-11-07 05:06:49', NULL, NULL, NULL, '2009-11-07 05:06:49', 'pending')
=> #<Comment id: 7, song_id: nil, author: nil, body: nil, created_at: "2009-11-07 05:06:49", updated_at: "2009-11-07 05:06:49", state: "pending">
>> c.publish
  Comment Update (0.9ms)   UPDATE "comments" SET "created_at" = '2009-11-07 05:06:49', "author" = NULL, "state" = 'published', "body" = NULL, "song_id" = NULL, "updated_at" = '2009-11-07 05:06:53' WHERE "id" = 7
=> true
>> Comment.last.state
  Comment Load (0.5ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> "published"
>> Comment.partial_updates = true
=> true
>> c = Comment.create
  Comment Create (0.8ms)   INSERT INTO "comments" ("updated_at", "body", "author", "song_id", "created_at", "state") VALUES('2009-11-07 05:07:21', NULL, NULL, NULL, '2009-11-07 05:07:21', 'pending')
=> #<Comment id: 8, song_id: nil, author: nil, body: nil, created_at: "2009-11-07 05:07:21", updated_at: "2009-11-07 05:07:21", state: "pending">
>> c.state_will_change!
=> "pending"
>> c.publish
=> true
>> c.save
=> true
>> Comment.last.state
  Comment Load (0.5ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> "pending"

EDIT:

More weirdness:

>> a = Comment.last
  Comment Load (1.2ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> #<Comment id: 5, song_id: nil, author: nil, body: nil, created_at: "2009-11-02 06:33:19", updated_at: "2009-11-02 06:33:19", state: "pending">
>> a.state
=> "pending"
>> a.publish
=> true
>> a.state
=> "published"
>> a.save
=> true
>> a.id
=> 5
>> Comment.find(5).state
  Comment Load (0.3ms)   SELECT * FROM "comments" WHERE ("comments"."id" = 5) 
=> "pending"

Compare to:

>> a = Comment.last
  Comment Load (0.3ms)   SELECT * FROM "comments" ORDER BY comments.id DESC LIMIT 1
=> #<Comment id: 5, song_id: nil, author: nil, body: nil, created_at: "2009-11-02 06:33:19", updated_at: "2009-11-02 06:33:19", state: "pending">
>> a.state = "published"
=> "published"
>> a.save
  Comment Update (0.6ms)   UPDATE "comments" SET "state" = 'published', "updated_at" = '2009-11-02 08:29:34' WHERE "id" = 5
=> true
>> a.id
=> 5
>> Comment.find(5).state
  Comment Load (0.4ms)   SELECT * FROM "comments" WHERE ("comments"."id" = 5) 
=> "published"
+1  A: 

Can you please retry your state transitions with publish**!** instead of publish

ilan berci
`publish` and `publish!` have the same effect in the examples above (argh!)
Horace Loeb
A: 

Again, not a real answer to your question, but here I tried to simulate your session:

>> c = Comment.new
=> #<Comment id: nil, body: nil, created_at: nil, updated_at: nil, state: "pending">
>> c.state
=> "pending"
>> c.publish
=> true
>> Comment.last.state
=> "published"
>> c = Comment.create
=> #<Comment id: 4, body: nil, created_at: "2009-11-05 07:12:53", updated_at: "2009-11-05 07:12:53", state: "pending">
>> c.publish
=> true
>> c.save
=> true
>> Comment.last.state
=> "published"

As you can see, it works as expected for me. Checked it twice. (I created a model with body and state attributes and put your code in it.)

Milan Novota
+1  A: 

Not contributing anything useful, but I just wanted to say I'm struggling with this error as well, in multiple state_machines throughout my application. And I can't switch to AASM, because I need to have more than one state_machine in the same model... So frustrating!

Anyway, you're not alone, it definitely still needs a solution.

Josh
+1  A: 

Does this still happen with partial updates turned off? Comment.partial_updates = false

If so, then we know the issue is with identifying dirty objects. You should be able to call c.state_will_change! before you call c.publish

Michael Sepcot
This is what I was thinking is the likely culprit since your update isn't sending that column.
Luke Francl
Now we're talking! It works when I do `Comment.partial_updates = false`, but not when I do `c.state_will_change!` (see http://pastie.org/687584 for what I did). Unfortunately this model has some big text fields and therefore I'd prefer not to turn off partial updates as a workaround (though I'd implement state_will_change! as a workaround if that worked)
Horace Loeb
After `c.state_will_change!` run `c.changed` does the returned array have `"state"` in it?
Michael Sepcot
Yes, but the behavior is weird: http://pastie.org/689749
Horace Loeb
Does `update_attribute('state','published')` still work? You might just want to overwrite `publish!` to make the update call manually...
Michael Sepcot
`update_attribute('state','published')` works, but if I'd prefer not to break `state_machine`'s abstraction like that...
Horace Loeb
From http://pastie.org/689749 it definitely looks like dirty updates are broken. Do dirty updates work with any other ActiveRecord model that doesn't use the state machine? Are you using any other plugins/gems that may be trying to duplicate dirty behavior?
Michael Sepcot
A: 

Does the model call super when it's initialized?

The state_machine documentation says it's required for states to get initialized

def initialize
  @seatbelt_on = false
  super() # NOTE: This *must* be called, otherwise states won't get initialized
end
btelles
A: 

Try to remove :state from definition:

FROM: state_machine :state , :initial => :pending do

TO state_machine :initial => :pending do

Guillermo