Conditional case_sensitive validation in model - ruby-on-rails

I have one model validation like below
validates :value, presence: true, allow_blank: false, uniqueness: { scope: [:account_id, :provider] }
I want to add one more condition of case_sensitive inside uniqueness like below
validates :value, presence: true, allow_blank: false, uniqueness: { scope: [:account_id, :provider], case_sensitive: :is_email? }
def is_email?
provider != email
end
In short, it should not validate case_sensitive when email provider is not email, But currently, it's not working it is expecting true or false only not any method or any conditions.
How can I achieve this in rails? I already wrote custom validation because it was not working.
UPDATE
if I add another validation like below
validates_uniqueness_of :value, case_sensitive: false, if: -> { provider == 'email' }
It's giving me same error for twice :value=>["has already been taken", "has already been taken"]

In the specific case of case_sensitive, the value passed to the option will always be compared against its truthy value.
As you can see in the class UniquenessValidator, when the relation is built, it uses the options passed to check if the value of case_sensitive is truthy (not false nor nil), if so, it takes the elsif branch of the condition:
def build_relation(klass, attribute, value)
...
if !options.key?(:case_sensitive) || bind.nil?
klass.connection.default_uniqueness_comparison(attr, bind, klass)
elsif options[:case_sensitive] # <--------------------------------- sadly, this returns true for :is_email?
klass.connection.case_sensitive_comparison(attr, bind)
else
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(attr, bind)
end
...
end
As you're passing the method name is_email? to case_sensitive, which is in fact a symbol, the condition takes that branch.
tl;dr;. You must always use true or false with case_sensitive.

Related

How to validate with comparison against a blank value?

I have a Rails ActiveModel with two fields date_from and date_to and I want the model to be valid when (and only when)
either of these fields or both are blank
date_from < date_to
In other words, the model should be invalid only when both fields are set but they're in the wrong order. In that case I also want both fields to be marked as invalid.
I tried with
validates :date_from, comparison: { less_than_or_equal_to: :date_to }, allow_blank: true
validates :date_to, comparison: { greater_than_or_equal_to: :date_from }, allow_blank: true
But that fails when exactly one of the fields is set with
#<ActiveModel::Error attribute=date_to, type=comparison of Date with nil failed, options={}>
How can I make the comparison validation pass when the referenced field is blank?
It can be done with two separate validates calls by adding a conditional check with if option
validates :date_from,
comparison: { less_than_or_equal_to: :date_to },
allow_blank: true,
if: :date_to # make sure `date_to` is not `nil`
validates :date_to,
comparison: { greater_than_or_equal_to: :date_from },
allow_blank: true,
if: :date_from
This will skip these validations if one of the dates is nil. When both dates are present it runs both validations and adds two separate errors, which may be not quite right, since it is essentially one error.
To make the intent of this validation more obvious, a validate method is a better fit
validate :date_from_is_less_than_date_to
def date_from_is_less_than_date_to
return unless date_from && date_to # valid if date(s) are missing
unless date_from < date_to
errors.add(:base, 'Date range is invalid')
# NOTE: to add errors to show on form fields
# errors.add(:date_from, 'must come before Date to')
# errors.add(:date_to, 'must come after Date from')
# NOTE: to add errors only for date that changed
# requires ActiveModel::Dirty
# errors.add(:date_from, 'must come before Date to') if date_from_changed?
# errors.add(:date_to, 'must come after Date from') if date_to_changed?
end
end

A nil value is transformed to 0 instead of triggering a validation error. How can I prevent this?

Model Plan has a jsonb column :per_unit_quantities_configuration . It is a hash with 3 keys/values pairs, min, max and step.
class Plan < ApplicationRecord
store_accessor :per_unit_quantities_configuration, :min, :max, :step, prefix: true
end
I have validations in place to prevent awkward configurations and/or infinite loops building an array of options based on those configuration settings.
First, I cast the values to from string to float before_validation
before_validation :cast_per_unit_quantities_config_values
def cast_per_unit_quantities_config_values
return unless per_unit_quantities_configuration_changed?
self.per_unit_quantities_configuration_min = per_unit_quantities_configuration_min&.to_f
self.per_unit_quantities_configuration_max = per_unit_quantities_configuration_max&.to_f
self.per_unit_quantities_configuration_step = per_unit_quantities_configuration_step&.to_f
end
And then I have each individual fields values' validations:
validates :per_unit_quantities_configuration_min,
numericality: { greater_than_or_equal_to: 0 }, allow_nil: false
validates :per_unit_quantities_configuration_max,
numericality: { greater_than: lambda { |p| p.per_unit_quantities_configuration_min } }
validates :per_unit_quantities_configuration_max
numericality: { greater_than_or_equal_to: 0 }, allow_nil: false
validates :per_unit_quantities_configuration_step,
numericality: { greater_than: 0 }, allow_nil: false
The problem I'm having is that when the user tries to send the form with the min field empty (nil), it is transformed to 0 which is a valid value for the field but is not appopiate since API users would receive no feedback that the change is being made.
What is converting the nil value to 0 ? And why is the allow_nil: false validation not triggered instead?
What is converting the nil value to 0?
The call to .to_f:
nil.to_f
=> 0.0
If you're starting with an empty string instead of a nil then the safe navigation operator won't save you:
nil&.to_f
=> nil
""&.to_f
=> 0.0
And why is the allow_nil: false validation not triggered instead?
You changed the value in a before_validation callback. The validations will run against the new value.
You may want to remove the before_validation. This way, you can use the Rails numericality validations to filter out any invalid values (nil, "", "x", etc.). If you still need to do something to the value before it gets stored in the database you may want to use an after_validation callback instead.

How to use the numericality validation with an integer column?

Given an integer column how do I prevent a alphanumeric being stored as zero?
I tried using the numericality validator but it seems that the value is checked post typecast (using to_i) so a string like 'dd1' is typecast to 0 and thus passes the validation.
validates :portfolio_number, numericality: { only_integer: true, allow_nil: true }
You can create your own validation which checks the value for typecasting:
validate :portfolio_number_is_numeric
private
def portfolio_number_is_numeric
return unless value.present?
errors.add(:portfolio_number, 'must be numeric') unless read_attribute_before_type_cast(:portfolio_number).match?(/\A[0-9]*\z/)
end
This could potentially be extracted in to a validator object for reusability.

Rails model attribute allow nil but validate when not

I want to validate an input box value in two cases:
If nil? then save successfully, no errors
If not nil? then validate its format
I have a simple line here:
validates :ip_addr, format: { with: Regexp.union(Resolv::IPv4::Regex)}
This will work in all cases but won't allow a nil/empty value as it throws an exception. But:
validates :ip_addr, format: { with: Regexp.union(Resolv::IPv4::Regex)}, allow_nil: true
and
validates :ip_addr, format: { with: Regexp.union(Resolv::IPv4::Regex)}, allow_blank: true
Will allow nil/empty values but If the input is invalid e.g. "33####$" then it returns "true".
How could I include both cases ? Is that possible ?
EDIT: It seems that Regexp.union(Resolv::IPv4::Regex).match("something") returns nil so If the validation works the same way, it will return nil in wrong values and allow_nil: true will allow them to be persisted like this.
Try this
validates :ip_addr, format: { with: Regexp.union(Resolv::IPv4::Regex)}, if: :ip_addr
I am not sure whether the above one will work. If it doesn't, do this
validate :ip_addr_format_check, if: :ip_addr
def ip_addr_format_check
unless ip_addr =~ Regexp.union(Resolv::IPv4::Regex)
errors.add(:base, "Error you want to show")
end
end
Typecasting seems to be the problem.
The solution below works fine:
validates :ip_addr, format: { with: Resolv::IPv4::Regex },
presence: false, unless: Proc.new { |ifc| ifc.ip_addr_before_type_cast.blank? }
Most probably we need to check If the variable's value is blank before casting it to inet (postgreSQL).

Rails 4 validates if num1 > num2 > num3

I'm currently facing 2 problems using custom validation on Rails 4. First problem, how can I make the following code more generic and efficient (if it's possible) ?
validates :p1, presence: true, numericality: { only_integer: false }
validate :p1_is_greater_than_p2_and_p3
validate :p2_between_p1_and_p3
validate :p3_is_less_than_p2_and_p1
def p1_is_greater_than_p2_and_p3
if self.p1.present?
errors.add(:p1, 'p1 must be greater than everything') unless
(self.p1 > self.p2) && (self.p1 > self.p3)
end
true
end
def p2_between_p1_and_p3
if self.p3.present?
errors.add(:p2, 'p2 bewteen p1 and p3') unless
self.p2.between?(self.p1, self.p3)
end
true
end
def p3_is_less_than_p2_and_p1
if self.p2.present? and self.p3.present?
errors.add(:p3, 'p3 must be inferior to eveything') unless
(self.p2 > self.p3) && (self.p1 > self.p3)
end
true
end
It's really bloated and dirty, isn't it?
Second issue, on errors.add, I can pass a symbol and an error message. However, if I don't pass any message, how can I define a custom yml key for my locales ? such as :
en:
activerecord:
errors:
models:
prices:
attributes:
custom_key_message_here: 'p1 must be greater than everything'
I want to keep this seperation of concern between locales and model. However, if I don't pass any message, it's show me is invalid. I would like something more explixit.
Thanks for your help.
From a quick look at the numericality validator, could you not just use:
validates :p1, presence: true, numericality: { greater_than: :p2 }
validates :p2, presence: true, numericality: { greater_than: :p3 }
validates :p3, presence: true
As long as p1 > p2 and p2 > p3, you shouldn't need to compare p1 and p3 directly. This is assuming all three values must be present, but you could probably adjust things to work if they're optional.

Resources