I have three related models, an Account which has one program and has one profile. When an account is created, profile.email can be nil so long as program.upgraded == 'f'. But how can I, and where should I, validate that profile.email has been set, i.e. is not nil, before setting program.upgraded to 't'.
So I've tried creating a custom validation, but I may not be implementing this properly. I have a service that calls
program.update!(
upgraded: 't',
)
inside a transaction block that rescues on an exception. Here's my validation:
class Account::Program < ApplicationRecord
validate :upgraded, :upgraded_email_present
...
private
def upgraded_email_present
return unless ppc_p == 't' && account.profile.auto_email.blank?
errors[:upgraded] << "profile auto_email must be set"
end
...
end
The update! statement appears to be failing, and it triggers the rescue in that the transaction block is in with the correct error. However, when I check the model instance, it's still been upgraded! Totally confused on what's wrong here.
EDIT
Finally realized what's going on. I was testing the validation in the context of my specs and had to do a reload on the instance. This solution works.
You can use a custom validator (Rails docs) for more complex validations like what you're after. An ActiveModel::EachValidator implements .validate_each, which takes the arguments:
the record being validated
the attribute being validated
the current value of that attribute
class UpgradeEmailPresentValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# All good if :upgraded is false or if the profile email is present
return unless value? && record.account.profile.email.blank?
record.errors[attribute] << (options[:message] || "profile email must be set")
end
end
class Program < ApplicationRecord
validates :upgraded, presence: true, upgrade_email_present: true
end
You didn't say if Program is associated with a Profile via has_one :through, but if it does you can shorten record.account.profile to just record.profile.
EDIT
I neglected to mention in my original that you can also use a custom validation method on the model as well.
class Program < ApplicationRecord
validate :upgraded, :upgrade_email_present
def upgrade_email_present
return unless upgraded? && account.profile.email.blank?
errors[:upgraded] << "profile email must be set"
end
end
Related
I have created a custom validator which has it's own specific unit tests to check that it works.
Using should-matchers there was a suggestion to add a validates_with matcher, so you could write:
subject.validates_with(:custom_validator)
Quite rightly the suggestion was declined, since it does not really test the behaviour of the model.
But my model has 4 fields that use the custom validator, and I want that behaviour to be tested - ie that those 4 fields are being validated, just as I am testing that they are being validated for presence:
describe '#attribute_name' do
it { is_expected.to validate_presence_of(:attribute_name) }
end
So how can I write a test that basically does the same thing, something sort of like this:
describe '#attribute_name' do
it { is_expected.to use_custom_validator_on(:attribute_name) }
end
This question asks the same thing and the answer suggests building a test model. However, my validator requires an option, it is used like this:
\app\models\fund.rb
class Fund < ActiveRecord
validates :ein, digits: { exactly: 9 }
end
So if I build a test model, and test it as suggested:
it 'is has correct number of digits' do
expect(build(:fund, ein: '123456789')).to be_valid
end
it 'is has incorrect number of digits' do
expect(build(:fund, ein: '123').to be_invalid
end
I receive RecordInvalid error (from my own validator! lol) saying I did not supply the required option for the validator. That option is called 'exactly'.
1) Fund#ein validates digits
Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
ActiveRecord::RecordInvalid:
Record invalid
So is Rspec not 'seeing' the value '9' defined in the model file?
Obviously it makes no sense to define that in the test as that is the defined behaviour I am trying to test for. Think of it like the validates_length_of testing for the { length: x } option.
Surely there must be a way to test that this custom validator option is set on the model?
The validator code
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] << (options[:message] || error_msg(length))
end
private
def error_msg(length)
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
end
Interesting side note
Obviously if I remove the 'raise' line from the DigitsValidator then both the tests succeed. Is there something wrong with my code that I cannot see?
I think you would have to add a return statement, no? :-)
def error_msg(length)
return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
Alternatively, remove that method and use a guard after setting length:
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
raise ActiveRecord::RecordInvalid if length.nil?
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] <<
(options[:message] ||
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
end
end
I think you should not aim for testing whether the model is using a specific validator. Rather check if the model is valid/invalid in specific cases. In other words, you should be able to test the behaviour of the model without knowing the implementation.
So in this case, you should setup you model correctly with you 'exactly' option for the validator and test if the model validation is sufficient overall.
On the other hand, if you are worried about that someone will misuse the validator in the future and 'exactly' is a required option for the validator, then you should raise error every time when the option is not present and test the validator in isolation like explained here: How to test a custom validator?
I like the idea of not including tests on the model that assume knowledge of exactly what the custom validator is validating. (Otherwise, we'll be repeating logic in the tests for the custom validators, and the tests for the model.)
I solved this by using Mocha (mocking library for Ruby) to set up expectations that the validate_each method of each my custom validators were being called on the correct corresponding field of my model. Simplified example:
Model class:
class User
include ActiveModel::Model
attr_accessor :first_name, :last_name
validates :first_name, first_name: true
validates :last_name, last_name: true
end
Custom validator classes:
class FirstNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
class LastNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
Model test class:
class UserTest < ActiveSupport::TestCase
def test_custom_validators_called_on_the_appropriate_fields
user = User.new(first_name: 'Valued', last_name: 'Customer')
FirstNameValidator.any_instance.expects(:validate_each).with(user, :first_name, 'Valued')
LastNameValidator.any_instance.expects(:validate_each).with(user, :last_name, 'Customer')
assert_predicate user, :valid?
end
end
I need to validate credit card number.
model Billing
validates :card, credit_card_number: true, allow_nil: true
Validation gem code:
def validate_each(record, attribute, value)
record.errors.add(attribute, options[:message] || :invalid) unless credit_card_valid?(value, extract_brands(record, options))
end
It works ok.
But then I try to override geter this way:
def card
"****#{self[:card][-4,4]}" if self[:card]
end
Validation fails.
when I monkey patched validates_each like that:
def validate_each(record, attribute, value)
value = record[attribute]
record.errors.add(attribute, options[:message] || :invalid) unless credit_card_valid?(value, extract_brands(record, options))
end
It back to work well.
Is it correct validation behavior, to validates getters instead of persisted values (validates_each first variant is follows guide).
Or what is preffered way to solve my problem?
Update: Activemodel/Activerecord version: 4.2.3
Firstly, the method you defined is not a model concern - it is a view concern and as such should be moved to a helper or presenter. This is however very common practice to have such methods within the model, so I wouldn't say that's a huge problem.
You can easily get around the whole problem, by picking the other name for your method:
def starred_card
card && "****#{card[-4,4]}"
end
When you override the getter method (as you have done here), you have to write a custom validation method.
For example, here is a simple validation that allows nil and ensures the card number is a string of 16 digits.
validate :correct_credit_card_number
def correct_credit_card_number
if self[:card] && self[:card] !~ /^\d{16}$/
errors.add(:card, "is not in the right format")
end
end
I have a form with 3 ActiveRecord fields. One of those fields has kind of goofy, and STATE-DEPENDENT validation requirements. (For example, I only validate the field if the object is being created on a setup wizard form.)
In my POST handler to create the object, I thought I could call errors.add to insert a special error condition
#foo = Foo.new(params[:foo])
if goofy_conditions(params[:foo][:goofy_field])
#foo.errors.add(:goofy_field, "doesn't meet the goofy conditions" )
end
respond_to do |format|
if #foo.save
...
else
... redirect back to form (with error fields hilited)
However, doing #foo.errors.add() in the controller doesn't seem to do anything... it doesnt prevent the save() if the other fields pass validations.
An alternative is to put a custom validation handler into the model... I know using errors.add(:field, 'msg') works fine there... but in that case how can my controller 'pass' info to the validator telling it whether or not the field needs to be validated.
That is model logic. Look at custom validations
class GoofyThing < ActiveRecord::Base
validate :goofy_attribute_is_goofy
def goofy_attribute_is_goofy
if goofy_conditions(self.goofy_field)
self.errors.add(:goofy_field, "doesn't meet the goofy conditions" )
end
end
end
Then it'll act just like any other validation.
Edit
You can conditionally validate with the :if option:
attr_accessible :via_wizard
validate :goofy_attribute_is_goofy, :if => lambda { self.via_wizard }
and in your controller:
class WizardController < ApplicationController
before_filter :get_object, :set_wizard
#...
def get_object
#object = GoofyThing.find(params[:id])
end
def set_wizard
#object.via_wizard = true
end
end
In my rails app I have a User model.
In that model I have some custom validation and a before save block as below
Class User < AvtiveRecord::Base
before_save :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Now, I just wanted to know that whether the validate block executes first or the validate. Because there are other validations based on the user role. So if the validate block executes first and there are no validation errors and then the before save executes and modifies the values. Are those values again validated?
Thanks in Advance.
Validations are called before before_save callbacks. If you want it to execute before the validations then you can use before_validation_on_create or before_validation_on_update, like this:
class User < ActiveRecord::Base
before_validation_on_create :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Those values will not be validated again. Validation happens once as does save, otherwise you could end up in a looping condition anytime you changed a value.
This is the guide you want: http://guides.rubyonrails.org/active_record_validations_callbacks.html
Based on section 10, it looks as if validation happens first.
I don't think the values will be validated again—there's nothing that would cause that to happen.
For example, if I have a user model and I need to validate login only (which can happen when validating a form via ajax), it would be great if I use the same model validations defined in the User model without actually instantiating a User instance.
So in the controller I'd be able to write code like
User.valid_attribute?(:login, "login value")
Is there anyway I can do this?
Since validations operate on instances (and they use the errors attribute of an instance as a container for error messages), you can't use them without having the object instantiated. Having said that, you can hide this needed behaviour into a class method:
class User < ActiveRecord::Base
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
unless mock.valid?
return mock.errors.has_key?(attr)
end
true
end
end
Now, you can call
User.valid_attribute?(:login, "login value")
just as you intended.
(Ideally, you'd include that class method directly into the ActiveRecord::Base so it would be available to every model.)
Thank you Milan for your suggestion. Inspired by it I created a simple module one can use to add this functionality to any class. Note that the original Milans suggestion has a logic error as line:
return mock.errors.has_key?(attr)
should clearly be:
return (not mock.errors.has_key?(attr))
I've tested my solution and it should work, but ofc I give no guarantees. And here's my glorious solution. Basically a 2-liner if you take away the module stuff.. It accepts method names as stings or symbols.
module SingleAttributeValidation
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def valid_attribute?(attr, value)
mock = self.new(attr => value)
(not mock.valid?) && (not mock.errors.has_key?(attr.class == Symbol ? attr : attr.to_sym))
end
end
end
To use your standard validation routines:
User.new(:login => 'login_value').valid?
If that does not work for you, build a custom class method for this:
class User < ActiveRecord::Base
validate do |user|
user.errors.add('existing') unless User.valid_login?(user.login)
end
def self.valid_login?(login)
# your validation here
!User.exist?(:login=> login)
end
end
I had a hell of a time getting this to work in Rails 3.1. This finally worked. (Not sure if it's the best way to do it, I'm kind of a newb.). The problem I was having was that value was being set to type ActiveSupport::SafeBuffer, and was failing validation.
def self.valid_attribute?(attr, value)
mock = User.new(attr => "#{value}") # Rails3 SafeBuffer messes up validation
unless mock.valid?
return (not mock.errors.messages.has_key?(attr))
end
return true
end
I have gone with the custom class solution but I just wanted to make sure there was no better way
class ModelValidator
def self.validate_atrribute(klass, attribute, value)
obj = Klass.new
obj.send("#{attribute}=", value)
obj.valid?
errors = obj.errors.on(attribute).to_a
return (errors.length > 0), errors
end
end
and I can use it like
valid, errors = ModelValidator.validate_attribute(User, "login", "humanzz")
class User < ActiveRecord::Base
validates_each :login do |record, attr, value|
record.errors.add attr, 'error message here' unless User.valid_login?(value)
end
def self.valid_login?(login)
# do validation
end
end
Just call User.valid_login?(login) to see if login itself is valid
An implementation of the 'valid_attribute' method you are suggesting:
class ActiveRecord:Base
def self.valid_attribute?(attribute, value)
instance = new
instance[attribute] = value
instance.valid?
list_of_errors = instance.errors.instance_variable_get('#errors')[attribute]
list_of_errors && list_of_errors.size == 0
end
end
How about:
User.columns_hash.has_key?('login')