Rails 4 validates if num1 > num2 > num3 - ruby-on-rails

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.

Related

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.

Rails Validates less than 0 or greater than 0, or field not equal to 0

I've added validation to my Rails model for less_than and greater_than, but they obviously conflict each other.
I want to make sure Rails validates a field on model to never be 0. So less than OR greater than 0, but not both because that's not possible.
How can I do this?
There is already a validator for this called numericality
http://edgeguides.rubyonrails.org/active_record_validations.html#numericality
class Player < ApplicationRecord
validates :salary, numericality: { other_than: 0 }
end
validate :non_zero
def non_zero
if self.field_name == 0
self.errors.add(:field_name, "Field can't be zero")
end
end
validates :field, exclusion: { in: [0] }
https://guides.rubyonrails.org/active_record_validations.html#exclusion
works for fields that allow nil values; easy to test

Rails 4 active record validation - conditionally validate presence of 4 attributes if at least one is present while allowing none to be present

I have a form with 10 attributes.
Among them I have 4 attributes which I need to apply what I'd call a"mutually conditional presence" Active Record validation.
I want that
(A) if one at least is present, then the other 3 must be present
(B) still allow none to be present (if the other 3 are blank, then the fourth has the right be be blank)
Here are the four attributes:
address_line_1
zipcode
state
country
It means that if the user fills ONE of them then ALL the others have to be present. But if all are blank, it's ok.
So far I have only be able to manage to enforce (A). But I fail to implement (B).
Indeed when i try putting allow_blank: true on one of the 4 attributes validates, then it breaks (A) , as after that, it does not ensure that if on of the attributes is present, the others must be as well.
How to do this?
Here is my current code
spec/models/users
validates :address_line_1,
presence: true, if: :pa_subelements_mutual_presence?
length: { maximum: 100,
minimum: 3 }
validates :zipcode,
presence: true, if: :pa_subelements_mutual_presence?,
length: { maximum: 20,
minimum: 4}
validates :state,
presence: true, if: :pa_subelements_mutual_presence?,
validates :country,
presence: true, if: :pa_subelements_mutual_presence?,
length: { maximum: 50}
private
def pa_subelements_mutual_presence? # method to help set validates on mutually dependent for presence for postal address
lambda { self.address_line_1.present? } ||
lambda { self.zipcode.present? } ||
lambda { self.state.present? } ||
lambda { self.country.present? }
end
It seems to me, it has to be all four or none of them. Not tested, but this should work.
validate :all_or_none
private
def all_or_none
errors[:base] << "all or nothing, dude" unless
(address_line_1.blank? && zipcode.blank? && state.blank? && country.blank?) ||
(!address_line_1.blank? && !zipcode.blank? && !state.blank? && !country.blank?)
end
all_or_none will either be true if all four fields are blank or none of them is.

rails 4 validates presence and length on the same parameter

Is it a good idea to having validates like this:
validates :serialnumber, presence: true, length: {7..20}, format: {with: /\d{7,20/}
As you see it generates three errors if I don't type serialnumber.
I would like to see only one error.
If I type nothing, I would like to see 'serial number is required' only.
If I type 123ABC I would like to see 'wrong length' only
And if I type 123-ABC-123 I would like to see 'wrong format' only
How to do it?
Regards
You could split it into 2 validators, check if this would work
validates :serialnumber, presence: true
validates :serialnumber, length: {7..20}, format: { with: /\d{7,20}/ }, allow_blank: true
As I understand then you want to see only one error message at a time. If that's the case then custom validation method might help. For ex.
validate :serial_number_validation_one_by_one
private
def serial_number_validation_one_by_one
if !self.serial_number.present?
errors.add(:serial_number, "must be present!")
elsif self.serial_number.length < 7 || self.serial_number.length > 20
errors.add(:serial_number, "must have length between 7 and 20!")
elsif self.serial_number.match(<your regex pattern here>)
errors.add(:serial_number, "must be properly formatted!")
end
end
Keep in mind that custom validation methods are called by validate not by validates.

Resources