Dynamic Model Validation - ruby-on-rails

I want to create some validations for one of my models which contain location information(street, locality, postal_code, etc). I want to be able to change the validation rules based on which country is selected.
For example, the validation rules for postal_code will be different for the US & Canada. Furthermore, some countries don't have postal_codes so no validation rules would be needed.
How would I go about implementing something like this?

Put this in your model to run any custom logic for validation.
validate :location_should_be_valid
def location_should_be_valid
# run all your custom logic here
# if it isn't valid, add an error indicating why
if country == "Canada"
errors.add(:postal_code, "Invalid postal code for Canada") if postal_code.length != 7
end
end
Read more about this in the Rails Guides:
http://guides.rubyonrails.org/active_record_validations_callbacks.html#creating-custom-validation-methods

validates :postal_code, :presence => true, :if => :check_country
def check_country
["US", "Canada"].include?(self.country)
end

Related

How can I use a custom predicate to validate multiple fields with dry-validation?

I have an address form that I want to validate as a whole rather than validating each input on its own. I can only tell if the address is valid by passing the line1, city, state, zip to the custom predicate method so that it can check them as a unit.
How can I do this? I only see how to validate individual fields.
It seems that this "High-level Rules" example could help you :
schema = Dry::Validation.Schema do
required(:barcode).maybe(:str?)
required(:job_number).maybe(:int?)
required(:sample_number).maybe(:int?)
rule(barcode_only: [:barcode, :job_number, :sample_number]) do |barcode, job_num, sample_num|
barcode.filled? > (job_num.none? & sample_num.none?)
end
end
barcode_only checks 3 attributes at a time.
So your code could be :
rule(valid_address: [:line1, :city, :state, :zip]) do |line, city, state, zip|
# some boolean logic based on line, city, state and zip
end
Update—this is for ActiveRecords rather than dry-validation gem.
See this tutorial, http://guides.rubyonrails.org/active_record_validations.html
Quoting from the tutorial,
You can also create methods that verify the state of your models and add messages to the errors collection when they are invalid. You must then register these methods by using the validate (API) class method, passing in the symbols for the validation methods' names.
class Invoice < ApplicationRecord
validate :discount_cannot_be_greater_than_total_value
def discount_cannot_be_greater_than_total_value
if discount > total_value
errors.add(:discount, "can't be greater than total value")
end
end
end

How can I implement dynamic validation in activerecord

What is the best way to adjust your validation of a model based on a parameter or action? Say I am entering a lead into a system, so all that is required is basic contact info. But then later I may enter a new contract for that user at which point I need more detailed information. So I may need phone number when just entering a lead but once they are entering into a contract I might need their birthdate and alternate phone number.
I considered using a state machine but the user could actually enter into two contracts at the same time so state doesn't really work for this scenario.
I also considered storing the extra info with the contract but since the user can have more than one contract, it needs to live with the user so it is not redundant.
So basically when saving a contract, it would tell me that the attached user is invalid if said user doesn't have the extra fields.
Check out conditional validations:
class Person
validates_presence_of :given_name, family_name
validates_presence_of :phone_number, :email_address, :if => :registered
with_options :if => :registered do |person|
# validations in this block are scoped to a registered user
person.validates_presence_of :dob
end
end
The :if option can take:
a symbol that corresponds to a method on the class that evaluates to true or false
a proc or lambda that returns a value that evaluates to true or false
a string containing ruby code (god knows why you'd want to do that)
You also have access to an :unless option which works in a similar fashion.
You can also encapsulate the logic to determine the current state of the user and use that to determine what validation steps you can take:
class Person
validates_presence_of :email_address, :if => ->(p) { p.state == :pending_confirmation }
# I actually prefer validations in this format
validate do # stricter validations when user is confirming registration
if confirming_membership && (given_name.blank? || family_name.blank?
errors.add(:base, 'A full name is required')
end
end
def state
# your logic could be anything, this is just an example
if self.confirmed_email
:registered
elsif confirming_membership
:pending_confirmation
else
:new_user
end
end
def confirming_membership
# some logic
end
end
You can use conditional validation for example:
validates_presence_of :phone, :if => Proc.new { |p| p.lead? }
In whatever action the lead posts to, you could just do this:
#object.save(validate: false)
Then, when they need to enter the contract, leave off that validate: false option to ensure that those validations run.
Also, see this post if you want to skip only certain validations.

Devise: Unable to limit model validation to specific def's

So I'm working on the registration aspect of the site currently. I have a main sign up which is just full name, email and password. (aka new.html.erb)
After you fill in that information I direct you to a new site (setup.html.erb) and ask for more info like city, country etc.
On that you also have the edit profile account.
I am trying to make my app more secure and adding restrictions and presence etc in the model. However how can I limit them.
Currently if I do
validates :email, presence: true,
and I go to a form that doesn't even contain the email for nor permits it I get an error up that I need to add an email.
Also how do I fix this: I make presence true, I input require in html5. But still if I go to my source code and just remove the form and push submit it saves and I can bypass adding info.
Currently if I do validates :email, presence: true,
and I go to a form that doesn't even contain the email for nor permits it I get an error up that I need to add an email.
Fix:
what you need is a conditional validation. If we look at rail guides it says
Sometimes it will make sense to validate an object only when a given predicate is satisfied. You can do that by using the :if and :unless options, which can take a symbol, a string, a Proc or an Array.
So in your model you could do something like:
class Order < ActiveRecord::Base
validates :email, presence: true, if: :need_to_validate?
def need_to_validate?
#your condition to check whether you want email validation or not
end
end
Update:
You can use params[:action] and params[:controller] smartly to check in which action and controller(hence which view) you currently are in so your method would be:
def need_to_validate?
params[:action] == your_view_action && params[:controller] == your_controller_name #your condition to check whether you want email validation or not
end

Rails Validation on presence of two or more entities

I've got two columns by name,
product_available_count (integer) and product_available_on (date).
I need to perform a model level validation on these columns.
The validation should check that if product_required is true then either of fields should be populated.
When a Product Manager fill in the catalogue, we need to perform a model level validation that checks that he should fill in either of the fields.
Suggest me any elegant way of writing a custom validation for my requirement.
I've tried this approach
validates :product_available_count_or_product_available_on if product_required?
def product_available_count_or_product_available_on
//logic ???
end
Is Custom validation the only way forward to my requirement. Can I use Proc or any other approach to write a better code.
I think Custom validation is best approach for this kind of problem
validate :product_available_count_or_product_available_on if product_required?
def product_available_count_or_product_available_on
if [product_available_count, product_available_on].compact.blank.size == 0
errors[:base] << ("Please select alteast one.")
end
end
but if you really donot want to write custom validation then try this
validates :product_available_count, :presence => { :if => product_required? && product_available_on.blank? }
validates :product_available_on, :presence => { :if => product_required? && product_available_count.blank? }

Validations that rely on associations being built in Rails

A Course has many Lessons, and they are chosen by the user with a JS drag-n-drop widget which is working fine.
Here's the relevant part of the params when I choose two lessons:
Parameters: {
"course_lessons_attributes"=>[
{"lesson_id"=>"43", "episode"=>"1"},
{"lesson_id"=>"44", "episode"=>"2"}
]
}
I want to perform some validations on the #course and it's new set of lessons, including how many there are, the sum of the lessons' prices and other stuff. Here's a sample:
Course Model
validate :contains_lessons
def contains_lessons
errors[:course] << 'must have at least one lesson' unless lessons.any?
end
My problem is that the associations between the course and the lessons are not yet built before the course is saved, and that's when I want to call upon them for my validations (using course.lessons).
What's the correct way to be performing custom validations that rely on associations?
Thanks.
looks like you don't need a custom validation here, consider using this one:
validates :lessons, :presence => true
or
validates :lessons, :presence => {:on => :create}
You can't access the course.lessons, but the course_lessons are there, so I ended up doing something like this in the validation method to get access to the array of lessons.
def custom validation
val_lessons = Lesson.find(course_lessons.map(&:lesson_id))
# ...
# check some things about the associated lessons and add errors
# ...
end
I'm still open to there being a better way to do this.

Resources