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
Related
I have a simple aasm model like the one below. When calling post.fill!(data), it needs to create a new record on a different table with the data value. This works in the below code by calling generate_new_log_record on before_enter.
class Post < ApplicationRecord
belongs_to :parent_model
include AASM
aasm column: :status do
state :posted, initial: true
state :filled, before_enter: Proc.new { |data| generate_new_log_record(data) }
event :fill do
transitions from: [:posted], to: :filled
end
end
def generate_new_log_record(data)
# how to make this inside the event transaction????
parent_model.log_records.create(start_date: expected_start_time, end_date: expected_end_time, data: data)
end
end
The issue is that even when for some reason generate_new_log_record is failed to create a new record, the state is still changing to filled. How can we make sure it happens inside a single DB transaction so that when the generate_new_log_record fails it undo every change that happened?
Thank you
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
Is there a way to track changes to model on after_commit when a record is created? I have tried using dirty module and was able to track changes when the record was updated, but when record is created changes are not recorded.
You can't use the rails changed? method, as it will always return false. To track changes after the transaction is committed, use the previous_changes method. It will return a hash with attribute name as key. You can can then check if your attribute_name is in the hash:
after_commit :foo
def foo
if previous_changes[attribute_name]
#do your task
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 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.