Ok, stumbled upon this weirdness. I have this in my user model.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
self.save
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
(code is simplified for illustration purposes)
When I create my user and check it in the console I get role: 1, customer_id: nil. But!, if I remove saving from the first callback everything works fine.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
produces role: 1, customer_id: "hello". So seems like it only reads the first .save in the callbacks. I would like to understand what is the exact behaviour and why. I spent a lot of time trying to pinpoint this and wouldn't want to stumble on something similar again.
EDIT:
Maybe this is helpful. When I use self.save! in subscribe_to_basic_plan I get an error and the record is not saved at all. Putting self.save! in the assign_role doesn't change anything, so the problem is definitely with the second .save.
This answer is theoretical since I'd need to see the full model code to be sure.
Most likely your first save in assign_role is failing for some reason. When it fails and returns falls that causes rails to skip all callbacks after it. Then your second callback never runs at all.
Possible solutions in my preferred order:
Don't use callbacks. Have your controller set those values before you save the model.
Use before_create so you aren't doing 3 saves of the exact same model in a row.
Combine your two callbacks into one callback with only one save.
Save using save(validate: false) in case it is failing on validation.
Related
I've a method named update inside my DailyOrdersController:
def update
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
My DailyOrder model:
before_save :refresh_total
def refresh_total
# i do something here
end
What I'm trying to do now is, I want the refresh_total callback to be skipped if the update request is coming from current_admin.
I have 2 user model generated using Devise gem:
User (has current_user)
Admin (has current_admin)
I try to make it like this:
def update
if current_admin
DailyOrder.skip_callback :update, :before, :refresh_total
end
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
But it's not working and still keep calling the refresh_total callback if the update request is coming from current_admin (when the logged-in user is admin user).
What should I do now?
I think this is all what you need:
http://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks
If you skip callback, you should enable it later. Anyway, this does not look as the best solution. Perhaps you could avoid the callbacks otherwise.
One way would be to use update_all:
DailyOrder.where(id: #daily_order.id).update_all( daily_order_params.merge({default_order:false}) )
Or you could do something like this:
#in the model:
before_validation :refresh_total
#in the controller
#daily_order.assign_attributes( daily_order_params.merge({default_order:false}) )
#daily_order.save(validate: current_admin.nil?)
or maybe it would be the best to add a new column to the model: refresh_needed and then you would conditionally update that column on before_validation, and on before_save you would still call the same callback, but conditionally to the state of refresh_needed. In this callback you should reset that column. Please let me know if you would like me to illustrate this with some code.
This may come in handy:
http://www.davidverhasselt.com/set-attributes-in-activerecord/
UPDATE
Even better, you can call update_columns.
Here is what it says in the documentation of the method:
Updates the attributes directly in the database issuing an UPDATE SQL
statement and sets them in the receiver:
user.update_columns(last_request_at: Time.current)
This is the fastest way to update attributes because it goes straight to
the database, but take into account that in consequence the regular update
procedures are totally bypassed. In particular:
\Validations are skipped.
\Callbacks are skipped.
+updated_at+/+updated_on+ are not updated.
This method raises an ActiveRecord::ActiveRecordError when called on new
objects, or when at least one of the attributes is marked as readonly.
I have a TreatmentEvent model. Here are the relevant parts:
class TreatmentEvent < ActiveRecord::Base
attr_accessible :taken #boolean
attr_accessible :reported_taken_at #DateTime
end
When I set the taken column, I want to set reported_taken_at if taken is true. So I tried an after_save callback like so:
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
I think update_attribute calls save, so that's causing the stack level too deep error. But using the after_commit callback is causing this to happen, too.
Is there a better way to conditionally update one column when another changes? This answer seems to imply you should be able to call update_attributes in an after_save.
Edit
This also happens when using update_attributes:
def set_reported_taken_at
self.update_attributes(reported_taken_at: Time.now) if self.taken?
end
As a note, stack level too deep generally means an infinite loop
--
In your case, the issue will almost certainly be caused by:
after_commit :set_reported_token_at
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
--
The problem is after_commit is going to try and save the reported_taken_at even if you've just saved a record. So you're going to go over the record again and again and again and again...
Often known as a recursive loop - it's used a lot in native development, but for request (HTTP) based apps, it's bad as it leads to a never-ending processing of your request
Fix
Your fix should be like this:
#model
before_save :set_reported_token_at
def set_reported_taken_at
self.reported_taken_at = Time.now if taken? #-> assuming you have a "taken" method
end
Can't you use a before_save? You can see if the other field value has changed and if so update this field. That way you just have one DB call.
Thought it's an easy task however I have stuck a little bit with this issue:
Would like to update one of the attributes of the model whenever it's saved, thus having a callback in the model:
after_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
self.save
# => this doesn't work as well.... self.update_attribute :budget_contingency, (self.budget_accuracy * self.budget_estimate) / 1
end
And the webserver shoots back with the message ActiveRecord::StatementInvalid (SystemStackError: stack level too deep: INSERT INTO "versions"
Which basically tells me that there is an infite loop of save to the model, after_save and then we save the model again... which goes into another loop of saving the model
Just stuck at this point of time on this model attribute calculation. If anyone has encountered this issue, and has a nice nifty/rails solution, please shoot me a message below, thanks
Change your code to following
before_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
end
Reason for that is - if you run save in after_save you end up in infinite loop: a save calls after_save callback, which calls save which calls after_save, which...
In general it's wise you use after save only for changing associated models, etc.
Try before_save or before_validation, but don't include the .save
I have a model with a callback that runs after_update:
after_update :set_state
protected
def set_state
if self.valid?
self.state = 'complete'
else
self.state = 'in_progress'
end
end
But it doesn't actually save those values, why not? Regardless of if the model is valid or not it won't even write anything, even if i remove the if self.valid? condition, I can't seem to save the state.
Um, this might sound dumb, do I need to run save on it?
update
Actually, I can't run save there because it results in an infinite loop. [sighs]
after_update is run after update, so also after save. You can use update_attribute to save this value, or just call save (I'm not sure if there don't be any recurence). Eventualy you can assign it in before_update (list of availble options is here). On the other side invalid object will not be saved anyway, so why you want to assign here the state?
Judging by the fact that the examples in ActiveRecord documentation do things like this:
def before_save(record)
record.credit_card_number = encrypt(record.credit_card_number)
end
def after_save(record)
record.credit_card_number = decrypt(record.credit_card_number)
end
you do need to save the record yourself.
after_update works on the object in memory not on the record in the table. To update attributes in the DB do the following
after_update :set_state
protected
def set_state
if self.valid?
self.update_attribute('state', 'complete')
else
self.update_attribute('state', 'in_progress')
end
end
I have a Course class that has many WeightingScales and I am trying to get the WeightingScales validation to propagate through the system. The short of the problem is that the following code works except for the fact that the errors.add_to_base() call doesn't do anything (that I can see). The Course object saves just fine and the WeightingScale objects fail to save, but I don't ever see the error in the controller.
def weight_attributes=(weight_attributes)
weighting_scales.each do |scale|
scale.weight = weight_attributes.fetch(scale.id.to_s).fetch("weight")
unless scale.save
errors.add_to_base("The file is not in CSV format")
end
end
end
My question is similar to this 1: How can you add errors to a Model without being in a "validates" method?
link text
If you want the save to fail, you'll need to use a validate method. If not, you'll have to use callbacks like before_save or before_create to check that errors.invalid? is false before you allow the save to go through. Personally, i'll just use validate. Hope it helps =)
I had a similar problem, I wanted to validate a parameter that never needed to be saved to the model (just a confirmation flag).
If I did:
#user.errors.add_to_base('You need to confirm') unless params[:confirmed]
if #user.save
# yay
else
# show errors
end
it didn't work. I didn't delve into the source but from playing around in the console it looked like calling #user.save or #user.valid? cleared #user.errors before running the validations.
My workaround was to do something like:
if #user.valid? && params[:confirmed]
#user.save
# redirect to... yay!
elsif !params[:confirmed]
#user.errors.add_to_base('You need to confirm')
end
# show errors
Errors added to base after validations have been run stick around and display correctly in the view.
But this is a little different to your situation as it looks like you want to add errors in a setter not a controller action. You might want to look into before_validation or after_validation instead.