I've some Active Record validations on my model:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
end
That seems fine. It validates that the field name is not nil, "" and that it must have exactly 10 characters. Now, if I want to add a custom validation, I'd add the validate call:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_must_start_with_abc
private
def name_must_start_with_abc
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
end
The problem is: when the name field is nil, the presence_of validation will catch it, but won't stop it from validating using the custom method, name_must_start_with_abc, raising a NoMethodError, as name is nil.
To overcome that, I'd have to add a nil-check on the name_must_start_with_abc method.
def name_must_start_with_abc
return if name.nil?
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
That's what I don't wan't to do, because if I add more "dependant" validations, I'd have to re-validate it on each custom validation method.
How to handle dependant validations on Rails? Is there a way to prevent a custom validation to be called if the other validations haven't passed?
I think there is no perfect solution unless you write all your validations as custom methods. Approach I use often:
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_custom_validator
private
def name_custom_validator
return if errors.include?(:name)
# validation code
end
end
This way you can add as many validations to :name and if any of them fails your custom validator won't execute. But the problem with this code is that your custom validation method must be last.
class Product < ApplicationRecord
validates :name, presence: true, length: { is: 10 }
validate :name_must_start_with_abc, unless: Proc.new { name.nil? }
private
def name_must_start_with_abc
unless name.start_with?('abc')
self.errors['name'] << 'must start with "abc"'
end
end
end
Please check allow_blank, :allow_nil and conditional validation as well for more options.
Related
I have a model "User" with attribute "Username". Can I use validations to prevent a User being created with the Username "home"?
class User < ActiveRecord::Base
validates :username, presence: true
end
You can use an exclusion validator:
class User < ActiveRecord::Base
USERNAME_BLACKLIST = ['home'].freeze
validates :username, presence: true, exclusion: { in: USERNAME_BLACKLIST }
end
Alternatively, you can always rely on a custom validation method, using validate instead of validates, for more complex types of validation that aren't easily expressed using built-in validators:
class User < ActiveRecord::Base
validates :username, presence: true
validate :username_not_on_restricted_list
protected
def username_not_on_restricted_list
errors.add(:username, :invalid) if username == 'home'
end
end
You could also write a custom validator if you intend to reuse this functionality across multiple models.
How can define custom validator that permits first name or last name to be null but not both
My Profile class:
class Profile < ActiveRecord::Base
belongs_to :user
validates :first_name, allow_nil: true
validates :last_name, allow_nil: true
validate :first_xor_last
def first_xor_last
if (first_name.nil? and last_name.nil?)
errors[:base] << ("Specify a first or a last.")
end
end
I tries create by self first_xor_last function but does not work.
I recieve this rspec test:
context "rq11" do
context "Validators:" do
it "does not allow a User without a username" do
expect(User.new(:username=> "")).to_not be_valid
end
it "does not allow a Profile with a null first and last name" do
profile = Profile.new(:first_name=>nil, :last_name=>nil, :gender=>"male")
expect(Profile.new(:first_name=>nil, :last_name=>nil, :gender=>"male")).to_not be_valid
end
it "does not allow a Profile with a gender other than male or female " do
expect(Profile.new(:first_name=>"first", :last_name=>"last", :gender=>"neutral")).to_not be_valid
end
it "does not allow a boy named Sue" do
expect(Profile.new(:first_name=>"Sue", :last_name=>"last", :gender=>"male")).to_not be_valid
end
end
end
I should pass it.
Thanks, Michael.
First of all, allow_nill is not a valid validator. You should be using presence or absence.
Unless you really need a custom message for both fields at the same time, there's no need to use a custom validator, simply do like this:
class Profile < ActiveRecord::Base
belongs_to :user
validates :first_name, :last_name, presence: true
end
If you want to allow either one, you can use conditional validation:
class Profile < ActiveRecord::Base
belongs_to :user
validates :first_name, presence: true, unless: "last_name.present?"
validates :last_name, presence: true, unless: "first_name.present?"
end
Instead of:
errors[:base] << ("Specify a first or a last.")
do
errors.add(:base, "Specify a first or a last.")
EDIT:
The error message you get is not caused by your custom validation, but by two other validations, which seems not needed, just get rid of those two lines:
validates :first_name, allow_nil: true
validates :last_name, allow_nil: true
class Profile < ActiveRecord::Base
belongs_to :user
validate :presence_of_first_or_last_name
def presence_of_first_or_last_name
if (first_name.blank? and last_name.blank?)
errors[:base] << ("Specify a first or a last.")
end
end
end
You can lose the two validates :first_name, allow_nil: true validations since they do absolutely nothing.
Instead of .nil? you might want to use the ActiveSupport method .blank? which checks not just for nil but also if the value is an empty string "" or consists of only whitespaces.
Also as David Newton pointed out XOR is eXclusive OR which would be first_name or last_name but not both. I try to name validators according to the general scheme of ActiveModel::Validations - a descriptive name which tells what kind of validation is performed.
Let's say I have a before_validation that checks to make sure the first letter of a name is "y":
before_validation :check_name_first_letter_is_y
But I want to make sure that the first name is also present:
validates :name, presence: true
But this will run the before_validation BEFORE validating if there's a first name present, right? How would I check if a name is present before running my before_validate?
I could try:
after_validation :check_name_first_letter_is_y
But will that stop the save method if I return false? Or is it too late because it's already been validated?
It might be easier to do this in one call as follows (if I didn't misread your question!):
validates :name, presence: ->(rec) { rec.name.initial == 'Y' }
Update:
Introduce a new ActiveModel::EachValidator that checks the first character in the name to be y and use the presence validator as you normally would, but ensure presence validator comes before the name validator so that presence check is done before checking first letter.
# app/validators/NameValidator.rb
class NameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.initial != 'y'
record.errors[attribute] << (options[:message] || "First letter in name must be `y`")
end
end
end
Then in your model, use the following two validations:
validates :name, presence: true
valdiates :name, name: true
Please refer: http://api.rubyonrails.org/classes/ActiveModel/Validator.html.
Also would suggest you to come up with a "Railsy" name for NameValidator class!
You can simply add a second format validation:
validates :name, presence: true, format: { with: /^y/, allow_blank: true }
I want to create a model called 'Persona' (which belongs_to model User). The user chooses a name in a form and then clicks the create button, so I need the attribute 'name' in the Persona model. I also want the Persona model to have a lower case version of the persona's name, so I need a second attribute 'downcase_name'. For example, the user may choose the persona's name to be Fooey Barman, so the downcase_name would be fooey barman.
My questions is, how do you initialise the downcase_name attribute? Do you put it in the Persona controller? In the new or create methods? Something like:
def create
#persona = Persona.new(persona_params)
#persona.downcase_name = #persona.name.downcase
if #persona.save
flash[:success] = "Welcome, " + #persona.name
redirect_to #persona
else
render 'new'
end
end
Or do you put it in the model?
class Persona < ActiveRecord::Base
before_create :make_downcase_name
validates :name, presence: true, length: { maximum: 50 }
private
def make_downcase_name
self.downcase_name = name.downcase
end
end
Or perhaps like this?
class Persona < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
validates :downcase_name, presence: true
end
EDIT:
So, am I right in thinking the way to do it is in the model, with a before_save and a validation, like this?:
class Persona < ActiveRecord::Base
before_save :make_downcase_name
validates :name, presence: true, length: { maximum: 50 }
validates :downcase_name, presence: true
private
def make_downcase_name
self.downcase_name = name.downcase
end
end
The use of the before_* callback seems pretty idiomatic. Alternatively, you can also have a service object created in the controller (say PersonaCreator) that would handle the logic and set the appropriate attributes on the model.
Note that before_create will be called only once, meaning that if the user changes the persona's name later, the callback will not be called. You might want to do something like before_save and check whether the name has been changed.
I'm building instances of a model in another model controller. All seems to work fine, child instances are well created with the parent id but as soon as I add validations for parent_id in this resource, the instance is no longer valid. Any idea what I'm missing ?
Mission model:
class Mission < ActiveRecord::Base
has_many :planned_times
validates :code, presence: true, uniqueness: { case_sensitive: false }
validates :days_sold, presence: true
end
PlannedTime model:
class PlannedTime < ActiveRecord::Base
belongs_to :mission
validates :date, presence: true
validates :mission_id, presence: true # this is the validation which causes problem
end
Mission controller:
class MissionsController < ApplicationController
def create
#mission = Mission.new(mission_params)
week_nums = params[:weeks].split(/[\s]*[,;\-:\/+][\s]*/).uniq
year = params[:year].to_i
week_nums.each do |week_num|
date = Date.commercial(params[:year].to_i,week_num.to_i)
#mission.planned_times.build(date: date)
end
if #mission.save
flash.now[:success] = "Mission added"
end
end
private
def mission_params
params.require(:mission).permit(:code, :days_sold)
end
end
So validating the presence of associations is a little tricky. In your case you're putting the mission_id validator on the child association but rails runs the validation on planned_time before it saves the mission so it will fail because mission_id is still nil. Also, by putting the validation on planned_time it'll mean that that validation won't run if you never mission.planned_items.build because the associated planned_time won't exist and therefore not run its validations.
With minimal changes to your code or validation logic you can get it to work like this:
class PlannedTime < ActiveRecord::Base
belongs_to :mission
validates :mission_id, presence: { if: ->(p) { p.mission.nil? } }
end
This part presence: { if: ->(p) { p.mission.nil? } } will check if there is a mission object present (albeit without an id yet) and if there is no mission object the validation will fail. So good, now we know we can't create a planned_time without its parent mission object present. But this says nothing about the mission requiring the planned_time to be created. If this is what you want then that's the solution. Although I'm left wondering if you really wanted it the other way around where you want to make sure a mission is always created along with its planned_time?