I'm fairly new to rails and having a problem that I'm sure is not unique, but can't seem to find an elegant solution to on my own:
I have a model with a :tax_rate decimal attribute that is used to create a total later on using some multiplication in the model.
This works just fine if the user enters the tax rate as '0.09' in the tax rate field, but obviously creates an erroniously large amount when the user enters '9'.
I'm looking to create some code that will handle a whole number being entered into my form.
My form:
<td><%= f.input :tax_rate, label: false, input_html: { class: 'taxtarget', precision: 2 }%></td>
The calcualtions in the model:
before_save :persist_calculations
validates_presence_of :price, :units
def calculated_total
price * units + calculated_tax
end
def calculated_tax
if self.tax_rate.present?
self.price * self.tax_rate
else
self.price
end
end
def persist_calculations
self.total = calculated_total
self.tax_amount = calculated_tax
end
I would like to either handle the whole number by preventing a whole number (requiring a decimal point) or by converting a whole number into a decimal by adding two decimal places to it.
Anyone have any good resources or ideas for how to accomplish this?
Either add a validation to your model for tax_rate so that it cannot be a larger than 1.0:
validates :tax_rate, less_than: 1.0
and/or add a before_validation to your model that converts large numbers to small ones:
before_validation ->(obj) {
obj.tax_rate = obj.tax_rate / 100 if obj.tax_rate > 1
}
and/or handle it your controller, which is generally preferable to using callbacks.
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
What is the purpose of defining methods inside a model like the example here? What does this get me? I was under the impression that only the fields of a model are defined in the model.
class Bean
include Mongoid::Document
field :name, type: String
field :roast, type: String
field :origin, type: String
field :quantity, type: Float
has_many :pairings
# has_many :pastries
def pastries
Pastry.find pastry_ids
end
#accepts_nested_attributes_for :pastries
def pastry_ids
pastry_ids_array = []
self.pairings.each do |one_pairing|
if one_pairing.pastry_id
pastry_ids_array.push one_pairing.pastry_id
end
end
pastry_ids_array
end
def pastry_ids=(list)
self.pairings.destroy
list.each do |pastry_id|
self.pairings.create(pastry_id: pastry_id)
end
end
# some way of showing a list
def pastry_list
pastries_string = ""
pastries.each do |one_pastry|
pastries_string += ", " + one_pastry.name
end
pastries_string.slice(2,pastries_string.length - 1)
pastries_string
end
end
I don't know if you know enough ruby but let's say you don't. This is a basic Class question? Defining methods on a model it's like having an helper. Let's say that you have
class CanadianPopulation
attr_accessor :population, :number_of_french_speaker, :number_of_english_speaker
def initialize(a,b,c)
#population = a
#number_of_french_speaker = b
#number_of_english_speaker = c
end
def total_people_that_have_a_different_mother_tongue
#Canadian who speak english or french but have a different mother tongue
self.population - (self.number_of_french_speaker + self.number_of_english_speaker)
end
end
census_2014 = CanadianPopulation.new(34_000_000, 4_000_000, 12_000_000)
let's say that you didn't have the method total_people_that_have_a_different_mother_tonguehow will you do to retrieve the total number of Canadians that have a different mother tongue? you will do the caculation yourself like for a view
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census = #census.population - (#census.number_of_english_speaker + #census.number_of_french_speaker) %>
</p>
Your view or your controller shouldn't do much logic (calculations) so that's one of the reason why you have a method inside the model (or class) it should be like this
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census.total_people_that_have_a_different_mother_tongue %>
</p>
For the second part of your question what does those methods do. rails c -s on your terminal than call or create a new instance model Bean and check to see what it does (the output/results)
Bean.first
b = _
b.pastries
b.pastry_ids
b.pastry_list
edit: #paul-richer recommends to maintain a thin controller
I'm creating rails API and want to want to add validation for countries field which contains an ISO 3166-1 code on model level.
For example if use gem carmen-rails, it provides only helper country_select. Is that way to use validation for accordance country for ISO 3166-1 code in the model?
Here is the newest syntax for validation with the countries gem:
validates :country, inclusion: { in: ISO3166::Country.all.map(&:alpha2) }
You are just trying to validate that the country code entered is appropriate? this should work with carmen
validates :country, inclusion: { in: Carmen::Country.all.map(&:code) }
But if this is all you need seems like the countries gem might work well too. With countries you could do
validates :country, inclusion: { in: Country.all.map(&:pop) }
Or
validate :country_is_iso_compliant
def country_is_iso_compliant
errors.add(:country, "must be 2 characters (ISO 3166-1).") unless Country[country]
end
Update
For Region and State you can validate all 3 at the same time like this.
validates :country, :region, :state, presence: true
validate :location
def location
current_country = Country[country]
if current_country
#valid regions would be something Like "Europe" or "Americas" or "Africa" etc.
errors.add(:region, "incorrect region for country #{current_country.name}.") unless current_country.region == region
#this will work for short codes like "CA" or "01" etc.
#for named states use current_country.states.map{ |k,v| v["name"}.include?(state)
#which would work for "California" Or "Lusaka"(it's in Zambia learn something new every day)
errors.add(:state, "incorrect state for country #{current_country.name}.") unless current_country.states.keys.include?(state)
else
errors.add(:country, "must be a 2 character country representation (ISO 3166-1).")
end
end
Although Region seems unnecessary as you could imply this from the country like
before_validation {|record| record.region = Country[country].region if Country[country]}
Create a Fixture with the data provided by Wikipedia on ISO-3166-1 and validate the country based on that data.
Also you can create an auto-complete feature easing the input. You can look at the auto-complete provided here for guidance.
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