How to distinct which validator failed ?
I have multiple validations on the same field:
class User < ActiveRecord::Base
validates :name, presence: true, length: {minimum: 1, maximum: 20 }
validates_uniqueness_of :name
end
When I save the user as user.save - I want to distinct what failed.
if user.__not_valid_name_length__?
# name length wrong
# do smth 1
end
if **user.__not_valid_name_unique__?
# name is not unique
# do smth 2
end
I can access user.errors[:name] and see all error messages for the field.
But I don't want to rely on message text which can change.
Is there any way to know which validator failed?
A feature to return machine-parseable symbols instead of strings was committed to Rails almost a year ago, but it's still not available in the 4-x-stable branch. You can use it if you use the edge version, and it will be available in Rails 5.
Example:
user = User.new
user.valid?
user.errors.details[:name] # returns: [{error: :blank}, {error: :too_short}]
More info:
https://github.com/rails/rails/pull/18322
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb
There are no built-in callbacks for rails validation fails, for rails validation available callbacks are:
before_validation
after_validation
To learn more about callback, please read this call_back
Checking validation fail base on the message is no good approach. It may change in different scenarios i.e internationalization. Do it by defining the method for validation i.e
# check presence_of validation
def is_name_present?
self.name.present?
end
# check uniqueness validation
def is_name_uniqe?
User.where(name: self.name).count == 0
end
Use one which suit you best, I suggest use after_validation
after_validation :post_validatiom
def post_validation
unless is_name_present?
# do_someting
end
unless is_name_uniqe?
# do_something
end
end
Related
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.
I have several before_validation callbacks that operate on the attributes being set on my model. I run into trouble when I have a situation like this:
class Foo < ActiveRecord::Base
before_validation :capitalize_title
validates :title, :presence => true
def capitalize_title
title.upcase
end
end
I write a test to ensure that 'nil' title is not allowed, but the test gets an error because the nil.upcase is not defined. I'd like to handle this error, but I already have error handling that runs after before_validation callbacks.
I don't want to put checks around all of my before_validation callbacks to make sure the data exists, if I can avoid it.
Is there a clean or accepted way to deal with this type of situation?
Just check if you have a title. And don't forget to save the modified title.
def capitalize_title
title = title.upcase if title
end
If you need to patch things up with a before_validation hook then you're stuck with taking care of invalid data in two places. If your validation was complicated you could factor it into two pieces: one piece that has to be true before the before_validation can run and one piece that has to be true after the before_validation has run:
before_validation :mangle_data
validate :data_is_okay
#...
def mangle_data
return if(!data_is_mangleable)
#... mangle away
end
def date_is_okay
if(!data_is_mangleable)
# complain
end
if(!data_is_properly_mangled)
# complain some more
end
end
def data_is_mangleable
return false if(thing.nil?)
# etc.
end
def data_is_properly_mangled
# check that stuff that the before_validation hook doesn't
# have to care about.
end
I need to validate a model only for a certain action (:create). I know this is not a good tactic, but i just need to do this in my case.
I tried using something like :
validate :check_gold, :if => :create
or
validate :check_gold, :on => :create
But i get errors. The problem is that i cannot have my custom check_gold validation execute on edit, but only on create (since checking gold has to be done, only when alliance is created, not edited).
Thanx for reading :)
I'm appending some actual code :
attr_accessor :required_gold, :has_alliance
validate :check_gold
validate :check_has_alliance
This is the Alliance model. :required_gold and :has_alliance are both set in the controller(they are virtual attributes, because i need info from the controller). Now, the actual validators are:
def check_gold
self.errors.add(:you_need, "100 gold to create your alliance!") if required_gold < GOLD_NEEDED_TO_CREATE_ALLIANCE
end
def check_has_alliance
self.errors.add(:you_already, "have an alliance and you cannot create another one !") if has_alliance == true
end
This works great for create, but i want to restrict it to create alone and not edit or the other actions of the scaffold.
All ActiveRecord validators have a :on option.
validates_numericality_of :value, :on => :create
Use the validate_on_create callback instead of validate:
validate_on_create :check_gold
validate_on_create :check_has_alliance
Edit:
If you use validates_each you can use the standard options available for a validator declaration.
validates_each :required_gold, :has_alliance, :on => :create do |r, attr, value|
r.check_gold if attr == :required_gold
r.check_has_alliance if attr == :has_alliance
end
Like Sam said, you need a before_create callback. Callbacks basically mean 'execute this method whenever this action is triggered'. (More about callbacks here : http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html).
This is what you want in your model:
before_create :check_gold
# other methods go here
private # validations don't need to be called outside the model
def check_gold
# do your validation magic here
end
The above method is the simplest to do what you want, but FYI there's also a way to use a before_save callback to execute additional actions on creation:
before_save :check_gold_levels
# other methods
private
def check_gold_levels
initialize_gold_level if new? # this will be done only on creation (i.e. if this model's instance hasn't been persisted in the database yet)
verify_gold_level # this happens on every save
end
For more info on 'new?' see http://api.rubyonrails.org/classes/ActiveResource/Base.html#method-i-new%3F
You need to look into callbacks. Someone once told me this and I didn't understand what they meant. Just do a search for rails callbacks and you will get the picture.
In your model you need to do a callback. The callback you need is before_create and then before a object is created you will be able to do some logic for check for errors.
model.rb
before_create :check_gold_validation
def check_gold_validation
validate :check_gold
end
def check_gold
errors.add_to_base "Some Error" if self.some_condition?
end
I want to be able to replace a field error with a warning when saving/updating a model in rails. Basically I want to just write a wrapper around the validation methods that'll generate the error, save the model and perhaps be available in a warnings hash (which works just like the errors hash):
class Person < ActiveRecord::Base
# normal validation
validates_presence_of :name
# validation with warning
validates_numericality_of :age,
:only_integer => true,
:warning => true # <-- only warn
end
>>> p = Person.new(:name => 'john', :age => 2.2)
>>> p.save
=> true # <-- able to save to db
>>> p.warnings.map { |field, message| "#{field} - #{message}" }
["age - is not a number"] # <-- have access to warning content
Any idea how I could implement this? I was able to add :warning => false default value to ActiveRecord::Validations::ClassMethods::DEFAULT_VALIDATION_OPTIONS
By extending the module, but I'm looking for some insight on how to implement the rest. Thanks.
The validation_scopes gem uses some nice metaprogramming magic to give you all of the usual functionality of validations and ActiveRecord::Errors objects in contexts other than object.errors.
For example, you can say:
validation_scope :warnings do |s|
s.validates_presence_of :some_attr
end
The above validation will be triggered as usual on object.valid?, but won't block saves to the database on object.save if some_attr is not present. Any associated ActiveRecord::Errors objects will be found in object.warnings.
Validations specified in the usual manner without a scope will still behave as expected, blocking database saves and assigning error objects to object.errors.
The author has a brief description of the gem's development on his blog.
I don't know if it's ready for Rails 3, but this plugin does what you are looking for:
http://softvalidations.rubyforge.org/
Edited to add:
To update the basic functionality of this with ActiveModel I came up with the following:
#/config/initializer/soft_validate.rb:
module ActiveRecord
class Base
def warnings
#warnings ||= ActiveModel::Errors.new(self)
end
def complete?
warnings.clear
valid?
warnings.empty?
end
end
end
#/lib/soft_validate_validator.rb
class SoftValidateValidator < ActiveModel::EachValidator
def validate(record)
record.warnings.add_on_blank(attributes, options)
end
end
It adds a new Errors like object called warnings and a helper method complete?, and you can add it to a model like so:
class FollowupReport < ActiveRecord::Base
validates :suggestions, :soft_validate => true
end
I made my own gem to solve the problem for Rails 4.1+: https://github.com/s12chung/active_warnings
class BasicModel
include ActiveWarnings
attr_accessor :name
def initialize(name); #name = name; end
warnings do
validates :name, absence: true
end
end
model = BasicModel.new("some_name")
model.safe? # .invalid? equivalent, but for warnings
model.warnings # .errors equivalent
I know I can require a field by adding validates_presence_of :field to the model. However, how do I require at least one field to be mandatory, while not requiring any particular field?
thanks in advance
-- Deb
You can use:
validate :any_present?
def any_present?
if %w(field1 field2 field3).all?{|attr| self[attr].blank?}
errors.add :base, "Error message"
end
end
EDIT: updated from original answer for Rails 3+ as per comment.
But you have to provide field names manually.
You could get all content columns of a model with Model.content_columns.map(&:name), but it will include created_at and updated_at columns too, and that is probably not what you want.
Here's a reusable version:
class AnyPresenceValidator < ActiveModel::Validator
def validate(record)
unless options[:fields].any?{|attr| record[attr].present?}
record.errors.add(:base, :blank)
end
end
end
You can use it in your model with:
validates_with AnyPresenceValidator, fields: %w(field1 field2 field3)
Add a validate method to your model:
def validate
if field1.blank? and field2.blank? and field3.blank? # ...
errors.add_to_base("You must fill in at least one field")
end
end
I believe something like the following may work
class MyModel < ActiveRecord::Base
validate do |my_model|
my_model.my_validation
end
def my_validation
errors.add_to_base("Your error message") if self.blank?
#or self.attributes.blank? - not sure
end
end
Going further with #Votya's correct answer, here is a way to retrieve all columns besides created_at and updated_at (and optionally, any others you want to throw out):
# Get all column names as an array and reject the ones we don't want
Model.content_columns.map(&:name).reject {|i| i =~ /(created|updated)_at/}
For example:
1.9.3p327 :012 > Client.content_columns.map(&:name).reject {|i| i =~ /(created|updated)_at/}
=> ["primary_email", "name"]
If you only have two fields, this will get the job done:
validates :first_name, presence: true, if: :nick_name.blank?
validates :nick_name, presence: true, if: :first_name.blank?
This does not scale up well with more fields, but when you only have two, this is perhaps clearer than a custom validation method.
n.b. If they omit both, the error message will appear more restrictive than you intend. (e.g. First Name is required. Nick Name is required.) ¯\(ツ)/¯