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...
I thought this would be easier to find, but I'm quite surprised that it isn't.
How on Earth do I test if a string is a number (including decimals) outside a Model?
e.g.
is_number("1") # true
is_number("1.234") # true
is_number("-1.45") # true
is_number("1.23aw") #false
In PHP, there was is_numeric, but I can't seem to find an equivalent in Ruby (or Rails).
So far, I've read the following answers, and haven't gotten any closer:
Ruby on Rails - Validate a Cost
Ruby/Rails - How can you validate against decimal scale?
invalid decimal becomes 0.0 in rails
You could borrow the idea from the NumericalityValidator Rails uses to validate numbers, it uses the Kernel.Float method:
def numeric?(string)
# `!!` converts parsed number to `true`
!!Kernel.Float(string)
rescue TypeError, ArgumentError
false
end
numeric?('1') # => true
numeric?('1.2') # => true
numeric?('.1') # => true
numeric?('a') # => false
It also handles signs, hex numbers, and numbers written in scientific notation:
numeric?('-10') # => true
numeric?('0xFF') # => true
numeric?('1.2e6') # => true
You could use Regular Expression.
!!("1" =~ /\A[-+]?[0-9]+(\.[0-9]+)?\z/) # true
!!("1.234" =~ /\A[-+]?[0-9]+(\.[0-9]+)?\z/) # true
!!("-1.45" =~ /\A[-+]?[0-9]+(\.[0-9]+)?\z/) # true
!!("1.23aw" =~ /\A[-+]?[0-9]+(\.[0-9]+)?\z/) # false
You can use it like this or make a method in a module or add this in the String class
class String
def is_number?
!!(self =~ /\A[-+]?[0-9]+(\.[0-9]+)?\z/)
end
end
You can use this site to test your expression : Rubular: a Ruby regular expression editor and tester
I can explain much more the expression if needed.
Hope this helps.
I have recently noticed that the method changed? on ActiveRecord objects has changed between Rails 3.2.13 and Rails 4.0.1. The problem is with fields connected to integer fields in database. Let's assume I have model Model with number integer field:
# Rails 3.2.13
m = Model.last
m.number #=> 5
m.number = '5hello'
m.number #=> 5
m.number_changed? #=> true
m.changed? #=> true
m.changes #=> {:number => [5,5]}
# Rails 4.0.1
m = Model.last
m.number #=> 5
m.number = '5hello'
m.number #=> 5
m.number_changed? #=> false
m.changed? #=> false
m.changes #=> {}
This causes a number of extremely annoying issues with form validations - if a user is trying to change the integer value to have invalid characters (but the type cast will result in the same value as initial one), rails will not invoke save method and none of the validation (including numericality: { only_integer: true }) will be run.
I have managed to get around this problem by overriding number_changed? method to super || number.to_s != number_before_type_cast, however this is extremely ugly.
The question is: why was this changed? Is it a bug or is intentional change? How can it be fixed without overriding all meta methods for integer columns?
I'm not sure on how you're running your validations, but I have a model on my rails app which is called ExtraField and have the following validation:
class ExtraField < ActiveRecord::Base
# stuff
validates :display_order, numericality: { only_integer: true }
# more stuff
end
I'm using rails 4.0.5 and I can do the following:
e = ExtraField.first
e.display_order #=> 1
e.valid? #=> true
e.errors.messages #=> {}
e.display_order = '1banana'
e.display_order #=> 1
e.display_order_changed? #=> false
e.changed? #=> false
e.valid? #=> false
e.errors.messages #=> {:display_order=>["is not a number"]}
So, although the record indeed isn't marked as changed ( what seems correct IMHO ), I can still run my validations and check that the model is not valid. Doesn't look like a bug to me, but just an intentional improvement.
If your form is only validating if the model is responding true to changed? maybe you should check your controller code. Or if you use a gem to help building the form it could be a bug in that gem I guess.
I have a Price model with 4 different fields:
t.decimal "amount"
t.decimal "amount_per_unit"
t.decimal "unit_quantity"
t.string "unit"
I'm trying to make a custom validation that allows either the amount or amount_per_unit fields (this includes unit quantity and unit) to be filled but not both of them. So to make a word diagram of what I mean.
amount = YES
amount_per_unit + unit + unit_quantity = YES
amount_per_unit (alone or amount.present) = NO
unit_quantity (alone or amount.present) = NO
unit (alone or amount.present) = NO
amount and amount_per_unit + unit + unit_quantity = NO
if you still are confused, just know that its either the amount itself thats filled in or the amount per unit fields that are (1 or 3).
So far I tried this validation in my Price model:
validates :amount, :numericality => true
validates :amount_per_unit, :numericality => true
validates :unit_quantity, :numericality => true
validates :unit, :inclusion => UNITS
validate :must_be_base_cost_or_cost_per_unit
private
def must_be_base_cost_or_cost_per_unit
if self.amount.blank? and self.amount_per_unit.blank? and self.unit.blank? and self.unit_quantity
# one at least must be filled in, add a custom error message
errors.add(:amount, "The product must have a base price or a cost per unit.")
return false
elsif !self.amount.blank? and !self.amount_per_unit.blank? and !self.unit.blank? and !self.unit_quantity
# both can't be filled in, add custom error message
errors.add(:amount, "Cannot have both a base price and a cost per unit.")
return false
else
return true
end
end
This validation doesn't work though as all fields are blank it results to a numericality error and if I fill all of them, it creates the price with all fields filled. What needs to be fixed?
I think your values are coming in as nil, not blank.
Try changing the second condition to:
elsif !self.amount.to_s.blank? and !self.amount_per_unit.to_s.blank? and !self.unit.to_s.blank? and !self.unit_quantity.to_s.blank?
Also, it seems you have a typo on the last condition on both statements (e.g. !self.unit_quantity instead of !self.unit_quantity.to_s.blank?
I hope that helps.
I'm working on a very basic shopping cart system.
I have a table items that has a column price of type integer.
I'm having trouble displaying the price value in my views for prices that include both Euros and cents. Am I missing something obvious as far as handling currency in the Rails framework is concerned?
You'll probably want to use a DECIMAL type in your database. In your migration, do something like this:
# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, :precision => 8, :scale => 2
In Rails, the :decimal type is returned as BigDecimal, which is great for price calculation.
If you insist on using integers, you will have to manually convert to and from BigDecimals everywhere, which will probably just become a pain.
As pointed out by mcl, to print the price, use:
number_to_currency(price, :unit => "€")
#=> €1,234.01
Here's a fine, simple approach that leverages composed_of (part of ActiveRecord, using the ValueObject pattern) and the Money gem
You'll need
The Money gem (version 4.1.0)
A model, for example Product
An integer column in your model (and database), for example :price
Write this in your product.rb file:
class Product > ActiveRecord::Base
composed_of :price,
:class_name => 'Money',
:mapping => %w(price cents),
:converter => Proc.new { |value| Money.new(value) }
# ...
What you'll get:
Without any extra changes, all of your forms will show dollars and cents, but the internal representation is still just cents. The forms will accept values like "$12,034.95" and convert it for you. There's no need to add extra handlers or attributes to your model, or helpers in your view.
product.price = "$12.00" automatically converts to the Money class
product.price.to_s displays a decimal formatted number ("1234.00")
product.price.format displays a properly formatted string for the currency
If you need to send cents (to a payment gateway that wants pennies), product.price.cents.to_s
Currency conversion for free
Common practice for handling currency is to use decimal type.
Here is a simple example from "Agile Web Development with Rails"
add_column :products, :price, :decimal, :precision => 8, :scale => 2
This will allow you to handle prices from -999,999.99 to 999,999.99
You may also want to include a validation in your items like
def validate
errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01
end
to sanity-check your values.
Just a little update and a cohesion of all the answers for some aspiring juniors/beginners in RoR development that will surely come here for some explanations.
Working with money
Use :decimal to store money in the DB, as #molf suggested (and what my company uses as a golden standard when working with money).
# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, precision: 8, scale: 2
Few points:
:decimal is going to be used as BigDecimal which solves a lot of issues.
precision and scale should be adjusted, depending on what you are representing
If you work with receiving and sending payments, precision: 8 and scale: 2 gives you 999,999.99 as the highest amount, which is fine in 90% of cases.
If you need to represent the value of a property or a rare car, you should use a higher precision.
If you work with coordinates (longitude and latitude), you will surely need a higher scale.
How to generate a migration
To generate the migration with the above content, run in terminal:
bin/rails g migration AddPriceToItems price:decimal{8-2}
or
bin/rails g migration AddPriceToItems 'price:decimal{5,2}'
as explained in this blog post.
Currency formatting
KISS the extra libraries goodbye and use built-in helpers. Use number_to_currency as #molf and #facundofarias suggested.
To play with number_to_currency helper in Rails console, send a call to the ActiveSupport's NumberHelper class in order to access the helper.
For example:
ActiveSupport::NumberHelper.number_to_currency(2_500_000.61, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
gives the following output
2500000,61€
Check the other options of number_to_currency helper.
Where to put it
You can put it in an application helper and use it inside views for any amount.
module ApplicationHelper
def format_currency(amount)
number_to_currency(amount, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
end
end
Or you can put it in the Item model as an instance method, and call it where you need to format the price (in views or helpers).
class Item < ActiveRecord::Base
def format_price
number_to_currency(price, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
end
end
And, an example how I use the number_to_currency inside a contrroler (notice the negative_format option, used to represent refunds)
def refund_information
amount_formatted =
ActionController::Base.helpers.number_to_currency(#refund.amount, negative_format: '(%u%n)')
{
# ...
amount_formatted: amount_formatted,
# ...
}
end
If you are using Postgres (and since we're in 2017 now) you might want to give their :money column type a try.
add_column :products, :price, :money, default: 0
Use money-rails gem. It nicely handles money and currencies in your model and also has a bunch of helpers to format your prices.
Using Virtual Attributes (Link to revised(paid) Railscast) you can store your price_in_cents in an integer column and add a virtual attribute price_in_dollars in your product model as a getter and setter.
# Add a price_in_cents integer column
$ rails g migration add_price_in_cents_to_products price_in_cents:integer
# Use virtual attributes in your Product model
# app/models/product.rb
def price_in_dollars
price_in_cents.to_d/100 if price_in_cents
end
def price_in_dollars=(dollars)
self.price_in_cents = dollars.to_d*100 if dollars.present?
end
Source: RailsCasts #016: Virtual Attributes: Virtual attributes are a clean way to add form fields that do not map directly to the database. Here I show how to handle validations, associations, and more.
Definitely integers.
And even though BigDecimal technically exists 1.5 will still give you a pure Float in Ruby.
If someone is using Sequel the migration would look something like:
add_column :products, :price, "decimal(8,2)"
somehow Sequel ignores :precision and :scale
(Sequel Version: sequel (3.39.0, 3.38.0))
My underlying APIs were all using cents to represent money, and I didn't want to change that. Nor was I working with large amounts of money. So I just put this in a helper method:
sprintf("%03d", amount).insert(-3, ".")
That converts the integer to a string with at least three digits (adding leading zeroes if necessary), then inserts a decimal point before the last two digits, never using a Float. From there you can add whatever currency symbols are appropriate for your use case.
It's definitely quick and dirty, but sometimes that's just fine!
I am using it on this way:
number_to_currency(amount, unit: '€', precision: 2, format: "%u %n")
Of course that the currency symbol, precision, format and so on depends on each currency.
You can pass some options to number_to_currency (a standard Rails 4 view helper):
number_to_currency(12.0, :precision => 2)
# => "$12.00"
As posted by Dylan Markow
Simple code for Ruby & Rails
<%= number_to_currency(1234567890.50) %>
OUT PUT => $1,234,567,890.50