ActiveRecord validation statement containing || - ruby-on-rails

I've done some searching online, and understand how ActiveRecord validations can be built with "if" statements and separately defined methods. However, I'm wondering if it's possibly to simply combine two validations together, and if either is true, the whole thing passes.
What I'm trying to do is have a user input a contact field that can either be an email or a phone number, but not both. Obviously the code I have below isn't working, but I'm wondering if something similar to it could work?
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
VALID_PHONE = /\d{10}/
validates :contact, presence: true, format: { with: VALID_EMAIL_REGEX } || presence: true, length: { is: 10 }, format: { with: VALID_PHONE }

I was going to recommend using a standard :if option for the validates callback, but having done a little more research, I found something you may benefit from:
Custom Validations
According to the Rails guide:
#app/models/concerns/my_validator.rb
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
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
This allows you to create your own validation method, allowing you to append error messages directly into the instance variable (which is then shown on the form). For your question, I'd to do this:
#app/models/concerns/phone_email_validator.rb
class PhoneEmailValidator < ActiveModel::Validator
def validate(record)
contact = record.contact
phone = /\d{10}/
email = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
if contact.validate(phone) || contact.validate(email)
if contact.validate(phone) && contact.length < 10
error = "Length too short for phone number!"
end
else
error = 'Needs To Be Phone Or Email '
end
record.errors[:contact] << error
end
end
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
The function is too verbose, and might not work with the validation regex; but it's an idea either way!

Related

Rails: how to validate an object field's value before save?

I'm writing a Redmine plugin that should check if some fields of an Issue are filled depending on values in other fields.
I've written a plugin that implements validate callback, but I don't know how to check field values which are going to be saved.
This is what I have so far:
module IssuePatch
def self.included(receiver)
receiver.class_eval do
unloadable
validate :require_comment_when_risk
protected
def require_comment_when_risk
risk_reduction = self.custom_value_for(3)
if risk_reduction.nil? || risk_reduction.value == 0
return true
end
comment2 = self.custom_value_for(4)
if comment2.nil? || comment2.value.empty?
errors.add(:comment2, "Comment2 is empty")
end
end
end
end
end
The problem here is that self.custom_value_for() returns the value already written to the DB, but not the one that is going to be written, so validation doesn't work. How do I check for the value that was passed from the web-form?
Any help will be greatly appreciated.
The nice thing about rails is that in your controller you don't have to validate anything. You are suppose to do all of this in your model. so in your model you should be doing something like
validates :value_that_you_care_about, :numericality => { :greater_than_or_equal_to => 0 }
or
validates :buyer_name, presence: true, :length => {:minimum => 4}
or
validates :delivery_location, presence: true
If any of these fail this will stop the object from being saved and if you are using rails scaffolding will actually highlight the field that is incorrect and give them and error message explaining what is wrong. You can also write your own validations such as
def enough_red_flowers inventory
if inventory.total_red_flowers-self.red_flower_quantity < 0
self.errors.add(:base, 'There are not enough Red Flowers Currently')
return false
end
inventory.total_red_flowers = inventory.total_red_flowers-self.red_flower_quantity
inventory.save
true
end
To write your own custom message just follow the example of self.errors.add(:base, 'your message')
You can find more validations here
Better way it's create custom validator
class FileValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# some logic for validation
end
end
then in model:
validates :file, file: true

Use value in override error message in Rails 4 for custom Validator

I am on Rails 4.2 I have written a custom Validator which will check if a value being entered exists in another table. I have been reviewing some other posts and it seems there is either a context specific or rails version preferred way to reuse the value being validated. In the rails docs i see examples such as:
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
however, if I try to use %{value} in my custom message override it does not interpolate, but just prints "%{value}". I have seen various ways of calling "value". I also could not get %{value} to work in my Validator definition, but could get #{value} to work (New to ruby, if#{value} getting it from validate_each?).
I have also been struggling with various formats of the validation statement and putting in the custom message. Some things which look repeatable from the docs are not. If the way I am declaring my custom message is causing the error, please let me know how to correct?
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors[attribute] << (options[:message] || "#{value} is not a valid
group code")
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, :existing_group => {:message => "The code you have enterd ( **what goes here?** ) is not a valid code, please check with your teacher or group
leader for the correct code." }
end
Rails automatically puts the value at the beginning of the phrase, so you can just do this:
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(code: value).any?
record.errors[attribute] << (options[:message] || 'is not a valid group code')
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, existing_group: {message: 'is not a valid code, please check with your teacher or group leader for the correct code.' }
end
Separately, note that it is always "#{1+1}" to interpolate not %.
Rails is using Internationalization style string interpolation to add the value to the message. You can use the I18n.interpolate method to accomplish this. Something like this should do the trick:
class ExistingGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors[attribute] << (I18n.interpolate(options[:message], {value: value}) || "is not a valid group code")
end
end
end
class Example < ActiveRecord::Base
validates :group_code, presence: true
validates :group_code, :existing_group => {:message => "The code you have entered, "%{value}", is not a valid code, please check with your teacher or group leader for the correct code." }
end

Changing validation rules on-the-fly

I need a conditional validation in some parts of my app. Right now I am using the following scheme:
User.create
User::WithPassword.create
User::WithPhone.create
It would be cool if I could change class behaviour on the fly like this:
User.with_phone.with_password.create
So I tried to do it like this:
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
def self.with_phone
define_method(:phone_required?) { true }
self
end
private
def phone_required?
false
end
end
So it can be used like this where needed:
User.with_phone.create(user_params)
The problem with this approach is that all instances of User get new behaviour since the actual class changes.
Is there a way to return only the modified copy of User class with new instance method phone_required? without affecting the "base" class?
Update
Thank you for the comments as this was more of an idea, the requirement is that I create users without certain validation automatically, and then when they edit profile, they are dealing with pristine User model. I create with_/without_ on the fly in method missing when needed.
Here's my next iteration:
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
def self.with_password
define_singleton_method(:password_required?) { true }
self
end
def password_required?
self.class.try :password_required?
end
end
Apparently it's not any better as the singleton method stays there all the time.
Why not simply use an instance variable initialized at creation time?
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
#phone_required = false
def self.create_with_phone(params)
obj = self.create(params)
obj.phone_required = true
end
private
def phone_required=(v)
#phone_required = v
end
def phone_required?
#phone_required
end
end
User.create_with_phone(user_params)

Rails 4 - concerns for generic validation

I just ran across Rails concerns and I want to use them for the validations of my models. But I want the validations to be generic, so that the validation is used only if the Class in which I include my concern has the attribute. I thought it would be easy, but I have tried many ways like using column_names, constantize, send and many other but nothing works. What is the right way to do it? The code:
module CommonValidator
extend ActiveSupport::Concern
included do
validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") },
format: { with: /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i,
message: I18n.t(:"validations.commons.email_wrong_format"),
allow_blank: true } if self.column_names.include? :email
end
end
class Restaurant < ActiveRecord::Base
include CommonValidator
.
.
.
end
Restaurant of course has an email attribute. Is it possible to check the existence of an attribute in the class in which in include my concern? I want include my CommonValidations into many models which will not have email attribute. I'm using rails 4.
You can use respond_to? on the current instance as follows:
validates :email, presence: { message: I18n.t(:"validations.commons.email_missing") },
format: { with: /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i,
message: I18n.t(:"validations.commons.email_wrong_format"),
allow_blank: true },
if: lambda { |o| o.respond_to?(:email) }
Another option as suggested by #coreyward is to define a class extending EachValidator. For example, for email validation:
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i
record.errors[attribute] << (options[:message] || I18n.t(:"validations.commons.email_wrong_format"))
end
end
end
Then you could update the validation call as:
validates :email,
presence: { message: I18n.t(:"validations.commons.email_missing") },
email: true,
allow_blank: true
I was looking for something similar, but with custom validations.
I ended up with something that I think could be shared, including generic tests.
First, set up the concern app/models/concern/my_concern.rb.
Please note that we don't define the validate_my_field into a ClassMethods module.
module MyConcern
extend ActiveSupport::Concern
included do
validate :my_field, :validate_my_field
end
private
def validate_my_field
...
end
end
Include concern into your model app/models/my_model.rb
class MyModel < ActiveRecord::Base
include MyConcern
end
Load concerns shared examples in spec/support/rails_helper:
…
Dir[Rails.root.join('spec/concerns/**/*.rb')].each { |f| require f }
…
Create concern shared examples spec/concerns/models/my_field_concern_spec.rb:
RSpec.shared_examples_for 'my_field_concern' do
let(:model) { described_class } # the class that includes the concern
it 'has a valid my_field' do
instance = create(model.to_s.underscore.to_sym, my_field: …)
expect(instance).not_to be_valid
…
end
end
Then finally call shared examples into your model spec spec/models/my_model_spec.rb:
require 'rails_helper'
RSpec.describe MyModel do
include_examples 'my_field_concern'
it_behaves_like 'my_field_concern'
end
I hope this could help.

on an ActiveModel Object, how do I check uniqueness?

In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects to abstract away multi-layer forms and stop using accepts_nested_attributes_for.
Edit: see below for a solution.
I've almost exactly duplicated his code sample, as I had the same problem to solve:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations doesn't have a uniqueness validator, as it's supposed to be a non-database backed variant of ActiveRecord.
I figured there are three ways to handle this:
Write my own method to check this (feels redundant)
Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
Or add the constraint in the data storage layer
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
I could do something like (metaprogramming, I would need to modify some other areas):
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
But now I have two checks running in my class, first I use valid? and then I use a rescue statement for the data storage constraints.
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
A simplified approach...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
#klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
You can use it in your ActiveModel classes like so:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model class has validations as well. Those validations aren't run when you call Signup.new.save, so you will have to check those some other way. You can always use save(validate: false) inside the above persist! method, but then you have to make sure all validations are in the Signup class, and keep that class up to date, when you change any validations in Account or User.

Resources