Custom numericality validation in rails - ruby-on-rails

I needed to add a custom validation option for numericality that accepts , and . as separators and provides localized error messages, while the field itself is a string.

I create a custom validation inside of my model, which means that I have access to the field names (average and grade_scale in this case)
validate :average_valid_number
def average_valid_number
avg = average.gsub("," , ".")
case avg
when /\A0[xX]/
errors.add(:average, :not_a_number)
else
begin
Kernel.Float(avg)
rescue
errors.add(:average, :not_a_number)
end
if grade_scale == "CH"
errors.add(:average, :greater_than_or_equal_to, options = {:count => 4}) if avg.to_f < 4
errors.add(:average, :less_than_or_equal_to, options = {:count => 6}) if avg.to_f > 6
end
end
end

Related

Validation is not working if integer value of old attribute is same as that of new attribute

I've a model Cart having has_many relationship with cart_items.
# cart.rb:
accepts_nested_attributes_for :cart_items, allow_destroy: true
has_many :cart_items, dependent: :destroy, inverse_of: :cart
# cart_item.rb:
validates :quantity, presence: true, numericality: { greater_than: 0 }
# Controller code:
def update
current_cart.assign_attributes(params[:cart])
.....
current_cart.valid?
end
While updating cart_items, if integer (to_i) value of quantity is same as old value then validation is not working.
For example,
If old value of quantity is 4, now new value is updated to 4abc then quantity validation is not working and record is considered as valid.
Although, if new value is updated from 4 to 5abc then it shows a validation error, as expected.
Any suggestions why this all is happening?
EDIT 1:
Here's the output of rails console:
[3] pry(#<Shopping::CartsController>)> cart
=> #<Cart id: 12, created_at: "2017-06-22 13:52:59", updated_at: "2017-06-23 08:54:27">
[4] pry(#<Shopping::CartsController>)> cart.cart_items
[#<CartItem id: 34201, cart_id: 12, quantity: 4, created_at: "2017-06-23 05:25:39", updated_at: "2017-06-23 08:54:27">]
[5] pry(#<Shopping::CartsController>)> param_hash
=> {"cart_items_attributes"=>{"0"=>{"id"=>"34201", "quantity"=>"4abc"}}}
[6] pry(#<Shopping::CartsController>)> cart.assign_attributes param_hash
=> nil
[7] pry(#<Shopping::CartsController>)> cart.valid?
=> true
Here, previous quantity of cart_item is 4 and new value is 4abc, but still the cart is validated.
EDIT 2:
I've checked answers of How to validate numericality and inclusion while still allowing attribute to be nil in some cases?, as it is masked as duplicate in comments, but it does not seems to work.
As I mentioned above, validation is working fine if to_i of new quantity is different than previous quantity, but if it is same then validation is not working.
Also, I tried to apply a method for custom validation using validate, but I'm getting to_i value in that method. Something like:
Model code:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
puts quantity_changed? # => false
puts quantity # => 4
end
EDIT 3:
It seems like if to_i of new value is same as previous value then model is casting the value to integer and considering the field not even updated.
EDIT 4:
I'm getting answers & comments about the pupose of to_i. I know what to_i does.
I just want to know why I'm not getting the validation error if to_i of new quantity is similar to the quantity stored in the database.
I know quantity column is integer and ActiveRecord will cast it to integer BUT there must be a validation error as I've added that in model.
I'm getting the validation error if to_i of new value is different than quantity stored in db.
BUT
I'm not getting the validation error if to_i of new value is same than quantity stored in db.
First to answer your question why:
Problably in history it worked differently than today. Personally I suspect this commit (after a very brief search): https://github.com/rails/rails/commit/7500daec69499e4f2da2fc06cd816c754cf59504
And how to fix that?
Upgrade your rails gem... I can recommend Rails 5.0.3, which I tested and it works as expected.
Following the source code for numeric validation, it casts Integer type on the string
"4abc".to_i
=> 4
Hence the input is greater then 0
To resolve it, try to use <input type="numeric" /> in your view to force a user to type only integer values
You should use quantity_before_type_cast, as explained here, in section "Accessing attributes before they have been typecasted". For example:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
q = quantity_before_type_cast
return if q == q.to_i
errors.add(:quantity, 'invalid quantity') unless (q.to_i.to_s == q)
end
Since the quantity column is defined as integer, activerecord will typecast it with .to_i before assigning it to the attribute, however, quantity_changed? will be true in that case unlike what you have shown above, so that gives you a hint for solution 1, else you can check if param contains only integers or not in your controller as in solution 2.
Solution 1
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed?
if quantity_was == quantity || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
Solution 2
In your controller,
before_action :validate_cart_params, only: [:create, :update]
private
def validate_cart_params
unless cart_params[:quantity].scan(/\D/).empty?
render json: { errors: "Oops! Quantity should be numeric and greater than 0." }, status: 422 and return
end
end
UPDATE
I googled a bit, but late and found a helper _before_type_cast, it is also a good solution and #Federico has put it as a solution. I am also adding it to my list.
Solution 3
Using quantity_before_type_cast
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed? || ((actual_quantity = quantity_before_type_cast) != quantity_was)
if actual_quantity.scan(/\D/).present? || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
This type of issue is already reported in Github.
Reported issues:
Parent model not marked as invalid when associated record is invalid, but not dirty
Validations not triggered when attribute is changed from 0 to a string if it's a nested attribute
Validates_numericality_of skipped when changing 0 to "foo" through accepts_nested_attributes_for
Fixed in:
[v4.0]
https://github.com/rails/rails/commit/b9ec47da00e7f5e159ff130441585c3b42e41849
[v3.2]
https://github.com/rails/rails/commit/40617c7e539b8b70469671619e3c1716edcfbf59
All the issues are reported where previous value of an integer attribute is 0 and new value is some string (whose to_i will be 0). Thus the fix is also only for 0.
You can check the same in #changes_from_zero_to_string? in active_record/attribute_methods/dirty.rb, which is initially called from #_field_changed?
Solution:
Override #_field_changed? specifically for quantity field only (due to lack of time. In future, I'll override method for all integer fields).
Now if to_i value of some alphanumeric quantity is equal to current quantity value in database then below method will return true and will not type cast the value.
def _field_changed?(attr, old, value)
if attr == 'quantity' && old == value.to_i && value != value.to_i.to_s
return true
end
super
end
to_i(p1 = v1) public
Returns the result of interpreting leading characters in str as an integer base (between 2 and 36). Extraneous characters past the end of a valid number are ignored. If there is not a valid number at the start of str, 0 is returned. This method never raises an exception when the base is valid.
"12345".to_i #=> 12345
"99 red balloons".to_i #=> 99
"0a".to_i #=> 0
"0a".to_i(16) #=> 10
"hello".to_i #=> 0
"1100101".to_i(2) #=> 101
"1100101".to_i(8) #=> 294977
"1100101".to_i(10) #=> 1100101
"1100101".to_i(16) #=> 17826049
The problem happens because
"4".to_i => 4
"4abc".to_i => 4
That means your custom validation will pass and will not cause any error on the page.
Hope that helps you...

In RoR, how do I initialize fields in my model based on a field in my model that has no underlying database column?

I’m using Rails 4.2.7. I have an attribute in my model that doesn’t have a database field underneath it
attr_accessor :division
This gets initialized when I create a new object.
my_object = MyObject.new(:name => name,
:age => get_age(data_hash),
:overall_rank => overall_rank,
:city => city,
:state => state,
:country => country,
:age_group_rank => age_group_rank,
:gender_rank => gender_rank,
:division => division)
What I would like is when this field gets set (if it is not nil), for two other fields that do have mappings in the database to get set. The other fields would be substrings of the “division” field. Where do I put that logic?
I'd probably drop the attr_accessor :division and do it by hand with:
def division=(d)
# Break up `d` as needed and assign the parts to the
# desired real attributes.
end
def division
# Combine the broken out attributes as needed and
# return the combined string.
end
With those two methods in place, the following will all call division=:
MyObject.new(:division => '...')
MyObject.create(:division => '...')
o = MyObject.find(...); o.update(:division => '...')
o = MyObject.find(...); o.division = '...'
so the division and the broken out attributes will always agree with each other.
If you try to use one of the lifecycle hooks (such as after_initialize) then things can get out of sync. Suppose division has the form 'a.b' and the broken out attributes are a and b and suppose that you're using one of the ActiveRecord hooks to break up division. Then saying:
o.division = 'x.y'
should give you o.a == 'x' but it won't because the hook won't have executed yet. Similarly, if you start with o.division == 'a.b' then
o.a = 'x'
won't give you o.division == 'x.b' so the attributes will have fallen out of sync again.
I see couple of options here
You can add it in your controller as follows
def create
if params[:example][:division]
# Set those params here
end
end
Or you can use before_save In your model
before_save :do_something
def do_something
if division
# Here!
end
end

Validate Param Types in Rails

I've been looking all over the place and I'm wondering if I'm doing something wrong. And just to double check, I'll ask you guys!
So I'm receiving params in a Rails controller. One key, value pair is :status => true/false. However, I find that when I try to post status as a string like
:status => "THIS IS NOT A BOOLEAN"
and create my object in my controller, the :status attribute of my object becomes false.
Therefore, is there any clean way in rails to validate that my :status corresponds to a boolean?
Thanks!
This very strange method will to the trick
def is_boolean?(item)
!!item == item
end
params[:status] = 'some string'
is_boolean?(params[:status])
# => false
params[:status] = true
is_boolean?(params[:status])
# => true
A slightly more intuitive version would be
def is_boolean?(item)
item == false || item == true
end
Validation
The Rails way to do it is to validate in the model (from the docs):
#app/models/model.rb
Class Model < ActiveRecord::Base
validates :status, inclusion: { in: [true, false] }, message: "True / False Required!"
end
--
MVC
The reason for this is twofold:
DRY
MVC
If you want to keep your application DRY, you need to make sure you have only one reference to a validation throughout. Known as the "Single Source Of Truth", it means if you try and populate the model with other controllers / methods, you'll still invoke the same validation
Secondly, you need to consider the MVC (Model-View-Controller) pattern. MVC is a core aspect of Rails, and means you have to use your controller to collate data only - pulling & compiling data in the model. This is also true for validations -- always make sure you keep your validations with the data (IE in the model)
The above #Iceman solution is good if you are only doing it once place but you keep doing/repeating it in other places i suggest you to create to_bool method. i.e
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
and put this method in intializer or in library. And, you can simply do this
Mymodel.new(status: params[:status].to_s.to_bool)
we are doing to_s just because to convert nil to '' incase the status key isn't in params .

How to create a enum type and default to a specific value for new objects

I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)

Ruby validation based off of different field

I am new to rails validation.
I have two fields:
field :feed_entitlements, :type => Array
...
field :alert_news, :type => Boolean, :default => false
I want to put a validation on "alert_news" that requires a count > 0 in "feed_entitlements". Is this possible using a rails validation? It seems like all examples of validations that I can find are simple "can't be blank" type problems.
Thanks.
Add to your model this code
def validate
errors.add_to_base "count should be more then 0" if feed_entitlements.count < 0
end

Resources