Define custom validator by ActiveRecord - ruby-on-rails

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.

Related

How to handle dependant validations on Rails?

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.

How can I prevent an attribute from being a certain value?

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.

before_validation to prevent a duplicate category name

I have a categories model. I want to ensure that a user doesn't add a duplicate category name to his/her list of categories.
Here's my categories model:
class Category < ActiveRecord::Base
belongs_to :user
validates :name, presence: true
validates :user_id, presence: true
before_validation :validate
private
def validate
errors.add(:name, "is already taken") if Category.where("name = '?' AND user_id = ?", self.name, self.user_id).any?
end
end
Here is my RSpec test:
it "is invalid with duplicate name for same user" do
existing_category = Category.first
new_category = Category.new(:name => existing_category.name, :user_id => existing_category.user_id)
expect(new_category).to have(1).errors_on(:name)
end
Should I use before_save or before_validate? Also, I'm unsure how to write this. I guess if a duplicate is detected, I want to add an error for :name. Above is my attempt but doesn't seem to make it pass, is there anything obviously wrong? Also, is this good practise for adding custom validation?
Here is a much simpler way to achieve your goal - you can use scope option of validates_uniqueness_of validator:
validates_uniqueness_of :name, scope: :user_id
Your spec fails because it has an error. It expect new_category has error, but it doesn't run validations on this object. To do that, you just need to add:
new_category.valid?
before expect#... line.

Rails: Looking up foreign key id using virtual attribute failing

My app allows users to add words from a master words table into their own custom list. So, a word list contains multiple custom words each of which link to a master word.
In my view, I have a field called word_text (virtual attribute) where I let users enter a word, and in my model I am trying to look up the master_word_id and set it on the custom word table. I am unable to access the #word_text value in the model. I always seem to get an error that the master word is a required field (because the look up is failing).
class CustomWord < ActiveRecord::Base
attr_accessible :master_word_id, :word_list_id, :word_text
attr_accessor :word_text
belongs_to :word_list
belongs_to :master_word
validates :word_list, presence: true
validates :master_word, presence: true
before_validation :set_master_word
private
def set_master_word
logger.debug "Received word text #{#word_text}"
_mw_id = nil
if !#word_text.nil?
master_word = MasterWord.find_word(#word_text)
if master_word.nil?
errors.add("#{#word_text} is not a valid word")
else
_mw_id = master_word.id
end
end
self.master_word_id = _mw_id
end
end
I sincerely appreciate any suggestions as to how I can set the value of the master_word_id correctly.
There are several things to fix:
class CustomWord < ActiveRecord::Base
attr_accessible :master_word_id, :word_list_id, :word_text
attr_accessor :word_text
belongs_to :word_list
belongs_to :master_word
validates :word_list, presence: true
#validates :master_word, presence: true <= This will always throw error
validates :word_text, presence: true
validates :master_word_id, presence: true
before_validation :set_master_word
private
def set_master_word
logger.debug "Received word text #{self.word_text}"
self.master_word_id = MasterWord.find_by_word_text(self.word_text).id
end
end
Not sure if it will work because I don't know the rest of your app but I hope it points you in the right direction.

Validate whether foreign key exists if not nil

I have a many-to-one relationship defined in Rails 4:
class Event < ActiveRecord::Base
belongs_to :user
end
How do I check, that the :user key exists if it is set?
The following correctly checks, that the key exists but does not allow for `nil? values:
validates :user, presence: true
The following allows any value, even non-existing IDs:
validates :user, presence: true, allow_nil: true
How do you do such kind of validation.
You can use the before_save callback to check that the user is valid if it is supplied:
class Event < ActiveRecord::Base
belongs_to :user
before_save: :validate_user unless: Proc.new { |event| event.user.nil? }
private
def validate_user
if User.where(id: user_id).empty?
errors[:user] << "must be valid"
end
end
end
You must keep the presence validation in Event and add in User model:
has_many :events, inverse_of: :user
Just faced this problem and here are the 2 solutions
1.
validates :user, presence: true, if: 'user_id.present?'
2.
. https://github.com/perfectline/validates_existence gem
can be used like this
validates : user, existence: { allow_nil: true, both: false }
both option is for the error messages: false will show 1 error(association only), true will show 2 errors (association and foreign key)

Resources