How to use the numericality validation with an integer column? - ruby-on-rails

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.

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.

Conditional case_sensitive validation in model

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.

Which takes precedence: Rails type casting or validation?

I am very curious about what this expected behavior is for Rails 4.2 and I have not been able to find an answer to my question.
I'm adding validation to a model on all the time attributes. I want them to ONLY accept integers, not numerical strings. The data type in my schema for this attribute is an integer. I have my validation like so:
RANGE = 0..59
validates :start_minute, inclusion: { in: RANGE }, numericality: true
I've tried these other validations as well. I get the same result.
validates_numericality_of :start_minute, inclusion: { 0..59, only_integer: true }
validates :start_minute, inclusion: { in: 0..59 }, numericality: { only_integer: true }
When I pass my params to my controller from the request spec, start_minute is "12". BUT when I look at the created object, the start_minute is 12.
According to this article by ThoughtBot:
"This is because Active Record automatically type casts all input so that it matches the database schema. Depending on the type, this may be incredibly simple, or extremely complex."
Shouldn't the object not be able to be created? Is the typecasting taking precedence of my validation? Or is there something wrong with my validation? I appreciate any insight to this as I haven't been able to determine what is happening here. I've also created a model spec for this and I'm still able to create a new object with numerical strings.
Thank you for any insight you can give on this. I am still learning the magic of Rails under the hood.
From the rails docs it says,
If you set :only_integer to true, then it will use the
/\A[+-]?\d+\z/
What it(only_integer validator) does is that it validates that the format of value matches the regex above and a string value that contains only numbers like '12' is a match(returns a truthy value which is 0 and passes the validation).
2.3.1 :001 > '12' =~ /\A[+-]?\d+\z/
=> 0

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).

Resources