Refactoring custom validation for multiple fields in model - ruby-on-rails

I have a model with a custom validation function which verifies that the date is not in the past. Currently, the validation is hard-coded to check a single field, selected_date, in the model. How do I go about refactoring the validation so that I can either pass a parameter to the custom validation so I can test 2 fields?
class Appointment < ActiveRecord::Base
attr_accessible :selected_date, :alternate_date
validates_presence_of :selected_date
validate :validate_date
def validate_date
if selected_date < Date.today
errors.add(:selected_date, 'Date has passed')
end
end
end

create file lib/future_validator.rb:
class FutureValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
if value < Date.today
object.errors[attribute] << "has passed"
end
end
end
In your models:
validates :selected_date, presence: true, future: true
validates :other_date, presence: true, future: true
See this RailsCast: Validations in Rails 3
NOTE: Make sure lib files are auto loaded in config/application.rb and restart the server after adding that file.

Related

Rails model common validations inherited from abstract base class but unique field validations occur on subclass

I'm trying to understand if it's possible, given two models that share some methods and fields, to put the validations that are common between the two of them in an abstract base class. Below code represents a simplified version of my situation.
There are two classes of invoice line items: sales and collections. These line items share a common field invoice_amount I want to validate the presence of the invoice_amount from an abstract base class but fields that are not common to both models get validated by the subclass.
class Collection < InvoiceLineItem
belongs_to :invoice
validates :c_number, :invoice_number, :invoice_date, presence: true
.
.
.
end
class Sale < InvoiceLineItem
belongs_to :invoice
.
.
.
end
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
def invoice_amount
self[:invoice_amount] || '0.00'
end
def export_date
invoice_date
end
end
I've tried several things to get this to work with no success. Some of my attempts included adding the following code to my InvoiceLineItem base class
def self.inherited(base)
base.class_eval do
validates :invoice_amount, presence: true
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and
def self.inherited(base)
base.class_eval do
base.send(:validates, :invoice_amount, presence: true)
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and this as described here (https://medium.com/#jeremy_96642/deep-rails-how-to-use-abstract-classes-6aee9b686e75) which seemed promising because it described exactly what I want to do however it does not work for me.
with_options presence:true do
validates :invoice_amount
end
In all these cases the code executes without error however if I write a test like below it fails because validation succeeds!
RSpec.describe Collection, type: :model do
it "Requires an invoice amount" do
result = Collection.create(invoice_amount: nil, c_number: 'CUST012', invoice_number: 'INV001', invoice_date: Date.new(1999, 1,1))
expect(result.valid?).to be false
expect(result.errors[:invoice_amount]).to include("can't be blank")
end
end
I'm not really interested in hearing answers about how it should be done using composition instead of inheritance I won't go into the details but just assume that it has to be done using inheritance. It seems like it should be possible but I'm not sure and I can't find any source on the internet that has a solution that actually works.
Any help would be very much appreciated!
I figured out my issue, it turns out this code was working:
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
with_options presence: true do
validates :invoice_amount
end
def invoice_amount
self[:invoice_amount] || 0.00
end
def export_date
invoice_date
end
def debtor_number_up_to_space
debtor_number.split[0]
end
end
the validator was using the method invoice_amount which was shadowing the field on the model so invoice_amount wasn't being seen as nil but as 0.00 thus my test validation was passing correctly.
Once I removed the || 0.00 I could get the test to fail.

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.

Custom validator with validation helpers in it

I've defined a class which is getting fat because of many validations defined in it. So, I created a custom validator which includes all validations specific to a given context, and it's working fine.
But the issue is that, while validaing any attribute, the options which are passed while defining a validation aren't getting considered.
Consider this Post class,
class Post
include Mongoid::Document
field :state
field :description
validates_with PublishableValidator, on: :publish
end
Now, while publishing a post, its description is mandatory. So I am validating it with publish context.
#post.valid?(:publish)
Custom validator for all publishable validations is defined as,
class PublishableValidator < ActiveModel::Validator
include ActiveModel::Validations
validates :description, presence: true, unless: :admin?
def validate(post)
self.class.validators.each do |validator|
validator.validate(post)
end
end
end
Now, there is constraint in description validation that, for admin, don't run this validation( not a good idea but admin can do whatever they want :] ).
But when I validate it with blank description and admin privilege, it still gives error, without considering provided constraint.
Suggestions ??
I managed to solve it by using SimpleDelegator class.
First, I inherited PublishableValidator from SimpleDelegator.
Delegated PublishableValidator object to #post.
Ran validations on publishable object.
And last, merged publishable errors to post object.
Updated PublishableValidator
class PublishableValidator < SimpleDelegator
include ActiveModel::Validations
validates :description, presence: true, unless: :admin?
def validate(post)
self.__setobj__(post)
super
post.errors.messages.merge!(self.errors.messages)
end
end
Thanks to this blog

Adding custom validations to ActiveRecord module via extend?

I'm trying to move my validations to a module. I want to extend an existing object an aribtrary set of validators, but I'm struggling to figure out how to get them to execute. Any ideas?
Active Record Object
class Test < ActiveRecord::Base
has_many :resources
end
Validator
module TestValidator
extend ActiveSupport::Concern
included do
validates_associated :resources
end
end
Console
t = Test.new
t.extend TestValidator
t.valid?
# true ... should be false
I hope this can help
6.1 Custom Validators
Custom validators are classes that extend ActiveModel::Validator. These classes must implement a validate method which takes a record as an argument and performs the validation on it. The custom validator is called using the validates_with method.
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
The easiest way to add custom validators for validating individual attributes is with the convenient ActiveModel::EachValidator. In this case, the custom validator class must implement a validate_each method which takes three arguments: record, attribute and value which correspond to the instance, the attribute to be validated and the value of the attribute in the passed instance.
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ActiveRecord::Base
validates :email, :presence => true, :email => true
end
As shown in the example, you can also combine standard validations with your own custom validators.
https://guides.rubyonrails.org/active_record_validations.html#custom-validators

Rails ActiveRecord: validate single attribute

Is there any way I can validate a single attribute in ActiveRecord?
Something like:
ac_object.valid?(attribute_name)
You can implement your own method in your model. Something like this
def valid_attribute?(attribute_name)
self.valid?
self.errors[attribute_name].blank?
end
Or add it to ActiveRecord::Base
Sometimes there are validations that are quite expensive (e.g. validations that need to perform database queries). In that case you need to avoid using valid? because it simply does a lot more than you need.
There is an alternative solution. You can use the validators_on method of ActiveModel::Validations.
validators_on(*attributes) public
List all validators that are being used to validate a specific
attribute.
according to which you can manually validate for the attributes you want
e.g. we only want to validate the title of Post:
class Post < ActiveRecord::Base
validates :body, caps_off: true
validates :body, no_swearing: true
validates :body, spell_check_ok: true
validates presence_of: :title
validates length_of: :title, minimum: 30
end
Where no_swearing and spell_check_ok are complex methods that are extremely expensive.
We can do the following:
def validate_title(a_title)
Post.validators_on(:title).each do |validator|
validator.validate_each(self, :title, a_title)
end
end
which will validate only the title attribute without invoking any other validations.
p = Post.new
p.validate_title("")
p.errors.messages
#=> {:title => ["title can not be empty"]
note
I am not completely confident that we are supposed to use validators_on safely so I would consider handling an exception in a sane way in validates_title.
I wound up building on #xlembouras's answer and added this method to my ApplicationRecord:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
end
Then I can do stuff like this in a controller:
if #post.valid_attributes?(:title, :date)
render :post_preview
else
render :new
end
Building on #coreyward's answer, I also added a validate_attributes! method:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
def validate_attributes!(*attributes)
valid_attributes?(*attributes) || raise(ActiveModel::ValidationError.new(self))
end
end

Resources