Using the state_machine gem, I want to have validations that run only on transitions. For example:
# Configured elsewhere:
# StateMachine::Callback.bind_to_object = true
class Walrus > ActiveRecord::Base
state_machine :life_cycle, :initial => :fetus do
state :fetus
state :child
state :adult
state :dead
event :coming_of_age do
transition :child => :adult
end
before_transition :child => :adult do
validate :coming_of_age_party_in_the_future
end
end
end
If I attach that validation to the adult state, it will fail once that date passes. But I only need it to be valid during the transition. I could add something like:
validate :coming_of_age_party_in_the_future, if: 'adult? && life_cycle_was == "child"'
but that seems to miss the point of transitions.
Also, since I am binding to the object in callbacks, how would it conditionally call 'validate', a class method? (Binding is necessary since many methods in callbacks are private.)
Have you considered using the transition if: :x feature?
It should work something like this:
event :coming_of_age do
transition :child => :adult, if: :coming_of_age_party_in_the_future
end
You can see the section 'Class definition' in 'Example' (near the top) or 'Transition context' in 'Syntax flexibility' (pretty far down) in the README
Related
I am using the state_machine gem in a model Event.
The initial state of an event is pending.
When I create an event I would like to run an after_create callback to see if I can make the first transition depending on the attributes of the event.
The event model also has a validation that checks if certain attributes did not change.
Now my Problem is, that when the state_machine event :verify gets called in the after_create callback all values are marked as changed from nil to "initial value" and the transition cannot be made due to the fact that the mentioned validation fails.
Now, I really do not understand how this is even possible.
How can event.changes return nil => "initial values" for all values if it is an after_create callback?
To me it seems that the after_create callback is called before the event was saved the first time.
I would expect it to be saved once then make the callback and then only the state attribute should have changed when I call changes before I try to save my event after calling the verifiy event.
Some example code:
class Event < ActiveRecord::Base
state_machine :initial => :pending do
...
state :pending
state :verified
...
event :verify do
transition :pending => :verified
end
end
...
validate :validate_some_attributes_did_not_change, :on => :update
after_create :initial_verification_check
...
private
def initial_verification_check
verify! if everything_fine?
end
...
end
This code shows what I'd like to do, but of course won't work because the Parent does not yet have an id:
class Parent < ActiveRecord::Base
has_many :children
after_initialize :find_children, :if => Proc.new {|parent| parent.new_record?}
private
def find_children
Child.where("blah blah blah").each do |child|
child.parent = self
#etc, etc, etc
end
end
end
It's almost as if my controller's "new" action needs to save the Parent before displaying the new form. This doesn't feel right. What is a good approach to this problem?
Update
The child objects in my specific case are BillTransactions (think fees and credits) and the parents are Bills. Throughout a billing period, these transactions are accrued on an Account. At the end of the billing period, the user creates a bill for a given period, hence the need for a bill to find its children when it's created.
I've been thinking about this some more after I posted the question. Since the Bill and BillTransactions can exist in many different states (pending, draft, active, emailed, etc) I'm going to use a state machine to manage the object's lifecycle. So far this is what I've come up with:
class Bill < ActiveRecord::Base
belongs_to :account
has_many :bill_transactions
attr_accessible :account, :bill_period_start, :bill_period_end
after_initialize :find_fees, :if => Proc.new {|bill| bill.new_record?}
validates_presence_of :account, :bill_period_start, :bill_period_end
state_machine :initial => :pending do
after_transition :pending => :canceled, :do => :destroy_self
before_transition :active => :emailed, :do => :email_bill
event :save_draft do
transition :pending => :draft
end
event :activate do
transition [:pending, :draft] => :active
end
event :email do
transition :active => :emailed
end
event :apply_payment do
transition [:active, :emailed] => :partial
transition [:active, :emailed, :partial] => :paid
end
event :cancel do
transition [:pending, :draft] => :canceled
end
end
private
def find_fees
self.save
unless [account, bill_period_start, bill_period_end].any? {|attr| attr.nil? }
BillTransaction.where(:account_id => account.id, :transaction_date => bill_period_start..bill_period_end, :transaction_type => BillTransaction::TRANS_TYPES['Fee']).each do |fee|
fee.stage self
end
end
end
def destroy_self
self.bill_transactions.each do |trans|
trans.unstage
end
self.destroy
end
end
So after a Bill is initialized for the first time, it basically saves itself, finds all relevant transactions, and "stages" them. This means BillTransaction's state is set to staged (which can transition back to unbilled if the new bill is destroyed) and its bill_id is set to the current Bill's id. You can see that if a Bill in the pending state is canceled, all of the transactions are unstaged (returned to the unbilled state).
The problem with this solution is that sending a GET request to BillsController#new is supposed to be idempotent. This solution isn't strictly idempotent and I'm having a hard time seeing how I can ensure that the server's state will be rolled back if the user navigates away from the new form.
Am I heading down a painful path here?
I would create a new "creator" method on Bill that returns a new bill with associated transactions attached. Something like:
def self.NewWithTransactions
bill = Bill.new
bill_transactions = find_candidate_transactions
bill
end
Then from your controller's new action, just do:
bill = Bill.NewWithTransactions
Throw that back to your view and you should be able to create the new bill with transactions attached when submitted. If that doesn't work, you probably have to do as one of the commentors suggested and send the unassociated transactions to your view and reassociate them in the create action.
I'm having some problems testing StateMachines with Factory Girl. it looks like it's down to the way Factory Girl initializes the objects.
Am I missing something, or is this not as easy as it should be?
class Car < ActiveRecord::Base
attr_accessor :stolen # This would be an ActiveRecord attribute
state_machine :initial => lambda { |object| object.stolen ? :moving : :parked } do
state :parked, :moving
end
end
Factory.define :car do |f|
end
So, the initial state depends on whether the stolen attribute is set during initialization. This seems to work fine, because ActiveRecord sets attributes as part of its initializer:
Car.new(:stolen => true)
## Broadly equivalent to
car = Car.new do |c|
c.attributes = {:stolen => true}
end
car.initialize_state # StateMachine calls this at the end of the main initializer
assert_equal car.state, 'moving'
However because Factory Girl initializes the object before individually setting its overrides (see factory_girl/proxy/build.rb), that means the flow is more like:
Factory(:car, :stolen => true)
## Broadly equivalent to
car = Car.new
car.initialize_state # StateMachine calls this at the end of the main initializer
car.stolen = true
assert_equal car.state, 'moving' # Fails, because the car wasn't 'stolen' when the state was initialized
You may be able to just add an after_build callback on your factory:
Factory.define :car do |c|
c.after_build { |car| car.initialize_state }
end
However, I don't think you should rely on setting your initial state in this way. It is very common to use ActiveRecord objects like FactoryGirl does (i.e. by calling c = Car.net; c.my_column = 123).
I suggest you allow your initial state to be nil. Then use an active record callback to set the state to to the desired value.
class Car < ActiveRecord::Base
attr_accessor :stolen # This would be an ActiveRecord attribute
state_machine do
state :parked, :moving
end
before_validation :set_initial_state, :on => :create
validates :state, :presence => true
private
def set_initial_state
self.state ||= stolen ? :moving : :parked
end
end
I think this will give you more predictable results.
One caveat is that working with unsaved Car objects will be difficult because the state won't be set yet.
Tried phylae's answer, found that new FactoryGirl does not accept this syntax, and after_build method does not exists on ActiveRecord object. This new syntax should work:
Factory.define
factory :car do
after(:build) do |car|
car.initialize_state
end
end
end
I would like to know if there's a way to use rails validations on a custom action.
For example I would like do something like this:
validates_presence_of :description, :on => :publish, :message => "can't be blank"
I do basic validations create and save, but there are a great many things I don't want to require up front. Ie, they should be able to save a barebones record without validating all the fields, however I have a custom "publish" action and state in my controller and model that when used should validate to make sure the record is 100%
The above example didn't work, any ideas?
UPDATE:
My state machine looks like this:
include ActiveRecord::Transitions
state_machine do
state :draft
state :active
state :offline
event :publish do
transitions :to => :active, :from => :draft, :on_transition => :do_submit_to_user, :guard => :validates_a_lot?
end
end
I found that I can add guards, but still I'd like to be able to use rails validations instead of doing it all on a custom method.
That looks more like business logic rather than model validation to me. I was in a project a few years ago in which we had to publish articles, and lots of the business rules were enforced just at that moment.
I would suggest you to do something like Model.publish() and that method should enforce all the business rules in order for the item to be published.
One option is to run a custom validation method, but you might need to add some fields to your model. Here's an example - I'll assume that you Model is called article
Class Article < ActiveRecord::Base
validate :ready_to_publish
def publish
self.published = true
//and anything else you need to do in order to mark an article as published
end
private
def ready_to_publish
if( published? )
//checks that all fields are set
errors.add(:description, "enter a description") if self.description.blank?
end
end
end
In this example, the client code should call an_article.publish and when article.save is invoked it will do the rest automatically. The other big benefit of this approach is that your model will always be consistent, rather than depending on which action was invoked.
If your 'publish' action sets some kind of status field to 'published' then you could do:
validates_presence_of :description, :if => Proc.new { |a| a.state == 'published' }
or, if each state has its own method
validates_presence_of :description, :if => Proc.new { |a| a.published? }
I have a model that relies on state_machine to manage its different states. One particular event requires a before_transition as it needs to build a join table fore doing the transition. Unfortunately it doesn't work.
class DocumentSet < ActiveRecord::Base
state_machine :state, :initial => :draft do
# Callbacks
before_transition :on=>:submit, :do=>:populate_join_table
# States
state :draft
state :submitted
# Events
event :submit do transition :draft=>:submitted, :if=>:can_submit? end
end
def populate_join_table
puts '::::::::: INSIDE POPULATE_JOIN_TABLE'
end
def can_submit?
raise "Document Set has no Document(s)" if self.document_versions.blank?
true
end
Now when I do DocumentSet.submit, it actually never goes into the populate_join_table as it evaluates the can_submit? as false.
What am I missing?
Think I found the solution. Basically what happens is that state_machine first evaluates the :if condition, and only then does the before_transition.
So the order is:
If (GuardCondition == true)
run before_transition
run transition
run before_transition
The guard condition(s) controls whether or not that event (and transition) is valid at that time. In this case, your guard returns false, so you won't transition. This can be extremely useful, but in your case, you might need to rework/rethink things to let that callback run.