Custom validator with validation helpers in it - ruby-on-rails

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

Related

Virtual attributes Rails - Unknown Attribute Error

I'm making a Rails 4 application. Each incident belongs to a user. Each incident has many events. I want that I can create an Incident with current_user.incidents.new and pass it an attribute message which exists in the Event model. On creation, I wish to create a new event with the said message.
Here's my Incident model.
class Incident < ActiveRecord::Base
# The 'incident' model.
# Incidents are created by Users (belongs_to :user)
# Incidents may be public or private.
# Incidents have many events (identified, fixing, etc.)
belongs_to :user
has_many :events
validates :name, presence: true, length: {maximum: 256}
validates_presence_of :component
validates_presence_of :public
validates_presence_of :user_id
attr_accessor :message
validates_associated :message, presence: true
def message
Event.find_by_incident_id(self.id).message
end
def message=(value)
self.events.new(message: value, status: self.status)
end
private
def incident_params
params.require(:incident).permit(:name, :status, :user_id, :message)
end
end
But, when I run #i = Incident.new(message: 'something'), I get ActiveRecord::UnknownAttributeError: unknown attribute 'message' for Incident.
Please help me figure this out.
The problem is that you're passing the values to the default ActiveRecord::new method for Incident, which won't care about your getters, setters and accessors, it goes straight to the columns.
Some people override or adapt the build method that can implement clever logic like you require, without affecting the base initialize method.
Because your Incident model inherits from ActiveRecord::Base, it inherits the constructor. The ActiveRecord constructor only accepts model attributes (db columns) as params. You are trying to call an arbitrary instance method. You could create a class method that creates an instance then adds the message:
def self.create_with_message(msg)
i = Incident.new
i.message(msg)
return(i)
end

Rails: calling standard validations from a custom validator

Is there a way to call standard Rails validators from within a custom validator?
I have a combination of OAuth/email signup/sign in and I want to be able to call certain validators on each method of authentication. For instance, I want to be able to call validates_uniqueness_of :email if the user signs up through email and then call a single validator, for instance validates_with UserValidator.
If there isn't a way to do this I'm just going to use state tracking and a series of :if validations.
I believe there's no way to call other validators from custom one, this also may possibly cause circular dependency which is dangerous.
You have to go with conditional validations, but keep in mind that you can scope them like this (taken from Rails Guides)
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
If your goal is to call some combination of custom and standard rails validators you can do that with the validates method provided by ActiveModel::Validations
For example, you've created a custom Email Validator:
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add attribute, (options[:message] || "is not an email") unless
value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
And you want to include it in your Person class. You can do like so:
class Person < ApplicationRecord
attr_accessor :email
validates :email, presence: true, email: true
end
which will call your custom validator, and the PresenceValidator. All examples here are taken from the docs ActiveModel::Validations::Validates
I'm not sure if this a recent change in Rails but in 6.1 it is possible to call the standard validators from a custom validator:
class VintageYearValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ActiveModel::Validations::NumericalityValidator.new({
attributes: attributes,
only_integer: true,
greater_thank_or_equal_to: 1900,
less_than_or_equal_to: 2100
}).validate_each(record, attribute, value)
# Your custom validation
errors.add() unless bla?
end
end
Those standard Validators are not really documented (https://apidock.com/rails/v6.1.3.1/ActiveModel/Validations/NumericalityValidator) so use at your own risk. But there doesn't seem to be a risk of circular dependency. Both your custom validator and the standard Validator inherit from EachValidator.

Refactoring custom validation for multiple fields in model

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.

Rails attr_readonly doesn't work

According to this question and the documentation of attr_readonly the following should be possible:
class MyModel < ActiveRecord::Base
attr_accessible :foo
attr_readonly :bar
end
m = MyModel.create(foo: '123', bar: 'bar') # Should work
m.update_attributes(bar: 'baz') # Should not work
However the first one fails, saying that I can't mass-assign bar. What am I mising?
From documentation
attr_accessible takes a list of attributes that will be accessible.
All other attributes will be protected.
So attr_accessible made bar attribute as protected from mass-assignment.
You can make the attribute , suppose ,key as:-
attr_accessible :key
and then add one more validation
validate :check_if_key_changed, :on=> :update
private
def check_if_key_changed
if self.key_changed?
errors.add(:key,"cant change key")
end
end
In this way you will be able to mass-assign it once on creation and can also make sure that it do not get updated.

Custom validation errors on nested models

class Parent
has_one :child
accepts_nested_attributes_for :child
end
class Child
belongs_to :parent
end
Using a nested object form, I need to add some extra validations to the child model. These are not always run on Child, so I cannot put them in a validate method in Child. It seems sensible to do the checking in a validate method in Parent, but I'm having trouble adding error messages correctly.
This does work:
class Parent
...
def validate
errors[ :"child.fieldname" ] = "Don't be blank!"
end
But we lose nice things like I18n and CSS highlighting on the error field.
This doesn't work:
def validate
errors.add :"child.fieldname", :blank
end
You should keep them in the child model since that's the one validated, however, you can set conditionals with if: and unless:
class Order < ActiveRecord::Base
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
You can do several variations on this, read more in the rails documentation http://edgeguides.rubyonrails.org/active_record_validations.html#conditional-validation
I guess you could add an attribute, created_by to child, and make Child select which validations to use depending on that one. You can do that like they do in this answer: Rails how to set a temporary variable that's not a database field

Resources