How should my Rails before_validation callbacks handle bad data? - ruby-on-rails

I have several before_validation callbacks that operate on the attributes being set on my model. I run into trouble when I have a situation like this:
class Foo < ActiveRecord::Base
before_validation :capitalize_title
validates :title, :presence => true
def capitalize_title
title.upcase
end
end
I write a test to ensure that 'nil' title is not allowed, but the test gets an error because the nil.upcase is not defined. I'd like to handle this error, but I already have error handling that runs after before_validation callbacks.
I don't want to put checks around all of my before_validation callbacks to make sure the data exists, if I can avoid it.
Is there a clean or accepted way to deal with this type of situation?

Just check if you have a title. And don't forget to save the modified title.
def capitalize_title
title = title.upcase if title
end
If you need to patch things up with a before_validation hook then you're stuck with taking care of invalid data in two places. If your validation was complicated you could factor it into two pieces: one piece that has to be true before the before_validation can run and one piece that has to be true after the before_validation has run:
before_validation :mangle_data
validate :data_is_okay
#...
def mangle_data
return if(!data_is_mangleable)
#... mangle away
end
def date_is_okay
if(!data_is_mangleable)
# complain
end
if(!data_is_properly_mangled)
# complain some more
end
end
def data_is_mangleable
return false if(thing.nil?)
# etc.
end
def data_is_properly_mangled
# check that stuff that the before_validation hook doesn't
# have to care about.
end

Related

Rails distinct validations

How to distinct which validator failed ?
I have multiple validations on the same field:
class User < ActiveRecord::Base
validates :name, presence: true, length: {minimum: 1, maximum: 20 }
validates_uniqueness_of :name
end
When I save the user as user.save - I want to distinct what failed.
if user.__not_valid_name_length__?
# name length wrong
# do smth 1
end
if **user.__not_valid_name_unique__?
# name is not unique
# do smth 2
end
I can access user.errors[:name] and see all error messages for the field.
But I don't want to rely on message text which can change.
Is there any way to know which validator failed?
A feature to return machine-parseable symbols instead of strings was committed to Rails almost a year ago, but it's still not available in the 4-x-stable branch. You can use it if you use the edge version, and it will be available in Rails 5.
Example:
user = User.new
user.valid?
user.errors.details[:name] # returns: [{error: :blank}, {error: :too_short}]
More info:
https://github.com/rails/rails/pull/18322
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb
There are no built-in callbacks for rails validation fails, for rails validation available callbacks are:
before_validation
after_validation
To learn more about callback, please read this call_back
Checking validation fail base on the message is no good approach. It may change in different scenarios i.e internationalization. Do it by defining the method for validation i.e
# check presence_of validation
def is_name_present?
self.name.present?
end
# check uniqueness validation
def is_name_uniqe?
User.where(name: self.name).count == 0
end
Use one which suit you best, I suggest use after_validation
after_validation :post_validatiom
def post_validation
unless is_name_present?
# do_someting
end
unless is_name_uniqe?
# do_something
end
end

How can I programmatically copy ActiveModel validators from one model to another?

I'm writing a library that will require programmatically copying validations from one model to another, but I'm stumped on how to pull this off.
I have a model that is an ActiveModel::Model with some validation:
class User < ActiveRecord::Base
validates :name, presence: true
end
And another model that I'd like to have the same validations:
class UserForm
include ActiveModel::Model
attr_accessor :name
end
Now I'd like to give UserForm the same validations as User, and without modifying User. Copying the validators over doesn't work, because ActiveModel::Validations hooks into callbacks during the validation check:
UserForm._validators = User._validators
UserForm.new.valid?
# => true # We wanted to see `false` here, but no validations
# are actually running because the :validate callback
# is empty.
Unfortunately, there doesn't seem to be an easy way that I can see to programmatically give one model another's validation callbacks and still have it work. I think my best bet is if I can ask Rails to regenerate the validation callbacks based on the validators that are present at a given moment in time.
Is that possible? If not, is there a better way to do this?
Checking into the code of activerecord/lib/active_record/validations/presence.rb reveals how this can be achieved:
# File activerecord/lib/active_record/validations/presence.rb, line 60
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
So I guess I would try to hook into validates_with with an alias_method
alias_method :orig_validates_with :validates_with
Now you have a chance to get ahold of the values passed, so you can store them somewhere and retrieve them when you need to recreate the validation on UserForm
alias_method :orig_validates_with, :validates_with
def validates_with(*args)
# save the stuff you need, so you can recreate this method call on UserForm
orig_validates_with(*args)
end
Then you should be able to just call UserForm.validates_with(*saved_attrs). Sorry this is not something you can just copy/paste, but this should get you started. HTH

stack level too deep after_save callback in Rails4

My model is like this,
class Slot
include Mongoid::Document
after_save :calculate_period
field :slot, type: Array
def calculate_period
if condition
do something
end
self.slot = true
save
end
end
After submit button it will show this error,
SystemStackError in SlotsController#create
stack level too deep
and also consuming more time. If i remove the save from def calculate_period then the values are not storing after_save callback.
Any solution...!!!
You should change this to before_save, that way you can change the model's attributes, and then they will be saved to the database as normal.
class Slot
include Mongoid::Document
before_save :calculate_period
def calculate_period
if condition
#do something
end
end
end
You have infinite loop - calling save in calculate_period method invokes callbacks, including your calculate_period callback. The first solution that came into my mind is to add virtual attribute and check it before calling your callback method:
class Slot
include Mongoid::Document
after_save :calculate_period, unless: :period_calculated # I'm not sure if Mongoid allows this
attr_accessor :period_calculated
def calculate_period
if condition
# do something
end
self.period_calculated = true
save
end
end

Validate model for certain action

I need to validate a model only for a certain action (:create). I know this is not a good tactic, but i just need to do this in my case.
I tried using something like :
validate :check_gold, :if => :create
or
validate :check_gold, :on => :create
But i get errors. The problem is that i cannot have my custom check_gold validation execute on edit, but only on create (since checking gold has to be done, only when alliance is created, not edited).
Thanx for reading :)
I'm appending some actual code :
attr_accessor :required_gold, :has_alliance
validate :check_gold
validate :check_has_alliance
This is the Alliance model. :required_gold and :has_alliance are both set in the controller(they are virtual attributes, because i need info from the controller). Now, the actual validators are:
def check_gold
self.errors.add(:you_need, "100 gold to create your alliance!") if required_gold < GOLD_NEEDED_TO_CREATE_ALLIANCE
end
def check_has_alliance
self.errors.add(:you_already, "have an alliance and you cannot create another one !") if has_alliance == true
end
This works great for create, but i want to restrict it to create alone and not edit or the other actions of the scaffold.
All ActiveRecord validators have a :on option.
validates_numericality_of :value, :on => :create
Use the validate_on_create callback instead of validate:
validate_on_create :check_gold
validate_on_create :check_has_alliance
Edit:
If you use validates_each you can use the standard options available for a validator declaration.
validates_each :required_gold, :has_alliance, :on => :create do |r, attr, value|
r.check_gold if attr == :required_gold
r.check_has_alliance if attr == :has_alliance
end
Like Sam said, you need a before_create callback. Callbacks basically mean 'execute this method whenever this action is triggered'. (More about callbacks here : http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html).
This is what you want in your model:
before_create :check_gold
# other methods go here
private # validations don't need to be called outside the model
def check_gold
# do your validation magic here
end
The above method is the simplest to do what you want, but FYI there's also a way to use a before_save callback to execute additional actions on creation:
before_save :check_gold_levels
# other methods
private
def check_gold_levels
initialize_gold_level if new? # this will be done only on creation (i.e. if this model's instance hasn't been persisted in the database yet)
verify_gold_level # this happens on every save
end
For more info on 'new?' see http://api.rubyonrails.org/classes/ActiveResource/Base.html#method-i-new%3F
You need to look into callbacks. Someone once told me this and I didn't understand what they meant. Just do a search for rails callbacks and you will get the picture.
In your model you need to do a callback. The callback you need is before_create and then before a object is created you will be able to do some logic for check for errors.
model.rb
before_create :check_gold_validation
def check_gold_validation
validate :check_gold
end
def check_gold
errors.add_to_base "Some Error" if self.some_condition?
end

How to set some field in model as readonly when a condition is met?

I have models like this:
class Person
has_many :phones
...
end
class Phone
belongs_to :person
end
I want to forbid changing phones associated to person when some condition is met. Forbidden field is set to disabled in html form. When I added a custom validation to check it, it caused save error even when phone doesn't change. I think it is because a hash with attributes is passed to
#person.update_attributes(params[:person])
and there is some data with phone number (because form include fields for phone). How to update only attributes that changed? Or how to create validation that ignore saves when a field isn't changing? Or maybe I'm doing something wrong?
You might be able to use the
changed # => []
changed? # => true|false
changes # => {}
methods that are provided.
The changed method will return an array of changed attributes which you might be able to do an include?(...) against to build the functionality you are looking for.
Maybe something like
validate :check_for_changes
def check_for_changes
errors.add_to_base("Field is not changed") unless changed.include?("field")
end
def validate
errors.add :phone_number, "can't be updated" if phone_number_changed?
end
-- don't know if this works with associations though
Other way would be to override update_attributes, find values that haven't changed and remove them from params hash and finally call original update_attributes.
Why don't you use before_create, before_save callbacks in model to restrict create/update/save/delete or virtually any such operation. I think hooking up observers to decide whether you want to restrict the create or allow; would be a good approach. Following is a short example.
class Person < ActiveRecord::Base
#These callbacks are run every time a save/create is done.
before_save :ensure_my_condition_is_met
before_create :some_other_condition_check
protected
def some_other_condition_check
#checks here
end
def ensure_my_condition_is_met
# checks here
end
end
More information for callbacks can be obtained here:
http://guides.rubyonrails.org/activerecord_validations_callbacks.html#callbacks-overview
Hope it helps.

Resources