I'm calling #foo.update and inside 1 of the attributes it's updating, I'm calling a the write method (def attribute=) in foo's model class and want it to fail the entire update conditionally. What can I put in there? I tried using errors[:base] but it doesn't fail the save. I can't use validates either because the attribute will be transformed into something else before it gets saved.
def attribute=(attr)
if bar
# code to fail entire db save
end
end
You can just check the condition on a before_save callback in model foo.rb and return false if you don't want to save it.
before_save :really_want_to_save?
private
def really_want_to_save?
conditional_says_yes ? true : false
end
If you want error message too, then
def really_want_to_save?
if conditional_says_yes
true
else
errors[:base] << "failed"
false
end
end
If you want to abort from within the setter, then raising an exception will suffice.
def attribute=(attr)
if bar
raise "Couldn't save because blah blah"
end
end
However, as mentioned in other posts, it is likely better to do this check before save. That's what validation is for.
validate :my_condition
def my_condition
if bar
errors.add(:base, "Couldn't save because blah blah")
end
end
Related
I have created a custom validation for a ruby model (using the luhn algorithm). Even when I explicitly just return false from the custom validation, the object still saves.
This is what I have in my CreditCard model:
before_save :check_card_number
private
def check_card_number
return false unless card_number_luhn
end
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
but even if I just return false:
before_save :check_card_number
private
def check_card_number
return false
end
#so this is never even called
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
the object still saves. This is true EVEN IF I use validate instead of before_save. What is going on?
From Rails 5, you need to explicitly call throw(:abort)
# ...
before_save :check_card_number
# ...
def card_number_luhn
valid = #luhn_algorithm_here_that_returns_true_or_false
throw(:abort) unless valid
end
another way:
ActiveSupport.halt_callback_chains_on_return_false = true
reference: https://www.bigbinary.com/blog/rails-5-does-not-halt-callback-chain-when-false-is-returned
I'm working on a refactor for some ruby on rails v6 application. I have made a custom validator to test if the string being passed could be parsed into a Date object, like so:
class DateFormatValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any? do |field|
if record.send(field).nil?
return true
end
unless valid_date?(record.send(field))
record.errors.add(field, :invalid)
return false
end
end
end
end
def valid_date?(date_string)
Date.parse(date_string)
true
rescue ArgumentError
false
end
end
It is being called, and it adds the error to the record with the correct key for the field i'm passing. It is being called like so from inside my model:
validates_with DateFormatValidator, fields: [:date_of_birth]
But, when I ran a test to failed that parser like this:
def test_it_raises_ArgumentError_due_to_invalid_date
customer = customers(:one)
assert_raises(ArgumentError) do
customer.update({date_of_birth: "18/0-8/1989"})
end
end
Now, the assert_raises is my problem. I would expect to be able to do customer.valid? and since an error has been added to the customer it would return false. But instead, it returns:
ArgumentError: invalid date
Any ideas of what am I doing wrong or how to make the valid? turn false when i add an error into the errors obj holding my record??
How can I go about rescuing an error that was generated in an after_save callback, and then ultimately display it to the user? The code in my model looks something like this:
Class MyModel
after_save :call_other_class_responsible_for_parsing
def call_other_class_responsible_for_parsing
# this method is used by multiple models
ModelTwo.parse_css
end
end
In my controller, I currently redirect the user elsewhere if the update was successful, however, I consider the update to be successful if it passed all of the existing validations and there were no errors in the callback (from the Less::Parser).
EDIT:
I mixed up my thoughts in my original question. MyModel gets saved from it's corresponding controller, which then runs the after_save callback from the model. Inside call_other_class_responsible_for_parsing, there is a call to another model, let's say ModelTwo, which does the Less parsing. I've tried using code like this:
def self.parse_css
#my_model = MyModel.find(1)
css_to_compile = Less::Parser.new.parse(css).to_css
rescue Less::Error => error
#my_model.errors[:base] << "Error message"
false
end
end
But the false does not prevent the transaction from succeeding, therefore a redirect happens.
You could use transaction :
def create
MyModel.transaction do
#my_model.save
#my_model.my_method
end
rescue ActiveRecord::RecordInvalid => exception
# rescue active record exception here
rescue Less::Error => exception
# rescue less error here
end
This remove the need for a callback.
I hope this helps!
Callback chains are implicitly wrapped in a transaction. When a callback returns false or raises an exception then the whole transaction is rolled back and saving fails.
In your case, you're parsing some CSS, so I'm not sure whether after_save is the right place for this. I recommend you give validations a try. Consider the following:
class MyModel < ActiveRecord::Base
validate :valid_css
private
def parsed_css
#parsed_css ||= Less::Parser.new.parse(css).to_css
end
def valid_css
parsed_css
rescue Less::Error => error
errors.add(:css, "Cannot parse CSS: #{error}")
end
end
This will parse the CSS before saving the object and add an error if it's invalid. Also, the result of #to_css will be stored so that you won't need to recompute it. This is the approach that I'd recommend.
If you'd like to stick with after_save then you should raise an exception to abort the transaction. In your case, it's simply about not rescuing Less::Error:
class MyModel < ActiveRecord::Base
after_save :compile_css
private
def compile_css
css_to_compile = Less::Parser.new.parse(css).to_css
end
end
If you're having trouble deciding which approach to use then leave a question below and I'll help.
I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
Is there a way to hook into the save! with a callback?
I am looking for something like:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
before_save :save_subservices! if bang_save?
private
def save_subservices
#subservices.each(&:save)
end
def save_subservices!
#subservices.each(&:save!)
end
end
Where a save! is cascaded and calls save! on the (faux) association subservices.
Technically you can do this, but I would advise not to use this approach in production because it can change in newer rails. And it is just wrong.
You can inspect call stack of your before callback and check if there is save! method.
class CompositeService < Service
before_save :some_callback
def some_callback
lines = caller.select { |line| line =~ /persistence.rb/ && line =~ /save!/ }
if lines.any?
#subservices.each(&:save!)
else
#subservices.each(&:save)
end
end
end
I wonder: is this extra logic even necessary?
If the save method on each of your #subservices obeys the ActiveRecord save semantics, then you probably will get the correct behavior for free.
In other words, make sure your save methods return true or false for success or failure. Then, the composite code becomes as simple as this:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
private
def save_subservices
#subservices.all?(&:save)
end
end
If any of your sub services fail to save, then the save_subservices callback will return false, which will abort the callback chain. This will cause the wrapping save to return false. And in the case of save!, it will raise an exception.
composite.save
# => false
composite.save!
# => ActiveRecord::RecordNotSaved
Look at ActiveRecord autosave attribute:
http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html