DRYing Views in Rails (number_to_currency) - ruby-on-rails

I have code similar to:
number_to_currency(line_item.price, :unit => "£")
littering my views in various models. Since my application deals only in GBP (£), should I not move this into each of my models so that line_item.price returns the string as it should be (i.e. number_to_currency(line_item.price, :unit => "£") and line_item.price are the same. I'm thinking that to do this I should:
def price
number_to_currency(self.price, :unit => "£")
end
but this doesn't work. If price is already defined in the model, then Rails reports 'stack level too deep', when I change def price to def amount, then it complains that number_to_currency is not defined?

If you want to change the default for your whole application, you can edit config/locales/en.yml
Mine looks like this:
# Sample localization file for English. Add more files in this directory for other locales.
# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
"en":
number:
currency:
format:
format: "%u%n"
unit: "£"
# These three are to override number.format and are optional
separator: "."
delimiter: ","
precision: 2
Everything except the unit is optional and will fall back to the default, but I put it in so I know what values I can change. you could also use the £ sign instead of £.

number_to_currency is a view helper, so it is not available in models.
You could save some key strokes by defining your own helper in application_helper.rb (so it is available to all views). Eg
def quid(price)
number_to_currency(price, :unit => "£")
end
Then call it in views:
quid(line_item.price)

The reason for the stack level too deep error is that when you say self.price in the price method you are creating an infinite recursive call to your price method as you have now overridden the normal accessor method. To avoid this you would need to access the value of the price field using the attributes hash. e.g. something like:
def price
number_to_currency(attributes['price'], :unit => "£")
end
except for the fact that number_to_currency is not available in model code for the reason Larry K describes.

Here was my approach to this problem ..
# /RAILS_ROOT/lib/app_name/currency_helper.rb
module AppName
module CurrencyHelper
include ActionView::Helpers::NumberHelper
def number_to_currency_with_pound(amount, options = {})
options.reverse_merge!({ :unit => '£' })
number_to_currency_without_pound(amount, options)
end
alias_method_chain :number_to_currency, :pound
end
end
in your models you can do this (and you won't be polluting your model with methods you aren't going to use)
class Album < ActiveRecord::Base
include AppName::CurrencyHelper
def price
currency_to_number(amount)
end
end
then for your views to all be updated include the module in one of your app helpers
module ApplicationHelper
# change default currency formatting to pounds..
include AppName::CurrencyHelper
end
Now everywhere you use the number to currency helper it will be formatted with a pound symbol, but you also have all the flexiblity of the original rails method so you can pass in the options as you did before ..
number_to_currency(amount, :unit => '$')
will convert it back to a dollar symbol.

The other answer regarding making another helper method quid(price) to simplify the repetition is probably the best approach.. however.. if you REALLY want to access view helpers in the model you can do something like:
# /RAILS_ROOT/lib/your_namespace/helper.rb
#
# Need to access helpers in the model?
# YourNamespace::Helper.instance.helper_method_name
module YourNamespace
class Helper
include Singleton
include ActionView::Helpers
end
end
then you should be able to do this in the model class:
def price
helper = YourNamespace::Helper.instance
helper.number_to_currency(read_attribute('price'), :unit => "£")
end

As of Rails 3
As Larry K describes but with this edit:
def quid(price)
number_to_currency(price, :unit => "£")
end

Related

Overwriting default accessors with options

I am using Ruby on Rails 4 and I would like to know what could be the pitfalls when I overwrite default accessors. The Rails' Official Documentation says (in the initial lines):
The mapping that binds a given Active Record class to a certain
database table will happen automatically in most common cases, but can
be overwritten for the uncommon ones.
More, in that documentation there is the "Overwriting default accessors" section which makes me think that I can do it without any problem. What do you think about?
In my case I would like to overwrite attribute accessors in order to provide some options, something like this:
# Given my Article model has :title and :content attributes
# Then I would like to overwrite accessors providing options this way:
class Article < ActiveRecord::Base
def title(options = {})
# Some logic...
end
def content(options = {})
# Some logic...
end
end
# So that I can run
#article.title # => "Sample Title"
#article.title(:parse => true) # => "Sample *Title*"
#article.content # => "Sample description very long text"
#article.content(:length => :short) # => "Sample description..."
Maybe this is more Ruby than Rails, but will be the #article.title calling the title(options => {}) method or it will call the Rails attribute accessor that access the related database table column value?
Update (after commenting)
Since it seems that in the above code default accessors are not overwritten, is there a way to provide options for those accessors so to reach what I am looking for? If so, how?
#article.title #=> will call your method
#article.title(:parse => true) #=> will call your method
There is no method overloading in ruby if that is what you are looking for.
Looking closer at the official documentation I see where your code diverges.
You forgot "=" when defining your method.
class Foo < ActiveRecord::Base
def self.bar=(value)
#foo = value
return 'OK'
end
end
Foo.bar = 3 #=> 3
WARNING: Never rely on anything that happens inside an assignment method,
(eg. in conditional statements like in the example above)

Add Facebook metadata to Rails pages with sub-pages

I've tried Facebook's Open Graph protocol in adding meta data on Rails pages. What I want to do now is to make my code not duplicated or D.R.Y.---instead of putting one meta-data header for each controller page I have, I'd like to create a base class called "MyMetaBuilder" which will be inherited by the sub-pages, but don't know where and how to start coding it...
Someone suggested that meta data property values must be dynamically generated depending on the context. For example, PlayMetaBuilder, CookMetaBuilder and so on...
Also, when unit testing the controller action, how do I verify for its existence?
Thanks a lot.
One thing is defining the tags, another is rendering them. I would do the following:
write a controller mixin (something like acts_as_metatagable) where I would define specific fields for each controller (and populate the remaining with defaults). These would be assigned to a class (or instance) variable and in this way be made accessible in the rendering step).
write an helper function which would take all my tags and turn them into html. This helper function would then be called in the layout and be rendered in the head of the document.
so, it would look a bit like this:
# homepage_controller.rb
class HomepageController < ActionController::Base
# option 1.2: include it directly here with the line below
# include ActsAsMetatagable
acts_as_metatagable :title => "Title", :url => homepage_url
end
# lib/acts_as_metatagable.rb
module ActsAsMetatagable
module MetatagableMethods
#option 2.2: insert og_tags method here and declare it as helper method
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
def self.included(base)
base.helper_method :og_tags
end
end
def acts_as_metagabable(*args)
include MetatagableMethods
# insert dirty work here
end
end
# option 1.1: include it in an initializer
# initializers/acts_as_metatagable.rb
ActiveController::Base.send :include, ActsAsMetatagable
# option 2.1: insert og_metatags helper method in an helper
module ApplicationHelper
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
end
What I did for Scoutzie, was put all metadata into a head partial, with if/else cases as such:
%meta{:type => 'Author', :content => "Kirill Zubovsky"}
%meta{'property' => "og:site_name", :content=>"Scoutzie"}
-if #designer
...
-elsif #design
...
-else
...
This way, depending on the variables that load, I know which page it is, and thereby know which metadata to include. This might not be an elegant solution, but it works and it's really simple.

Ruby on Rails: Why my class extension is not recognized?

In views/products/list.html.erb I use:
<%= product.power.power_in_kw.to_kw if ... %>
to_kw is defined in lib/my_extensions.rb along with other methods:
class Symbol
def pluralize
to_s.pluralize.to_sym
end
end
class BigDecimal
def to_kw
number_to_currency(self, :unit => "kw", :format => "%n%u", :precision => 1)
end
end
class Float
def to_dollar
number_to_currency(self)
end
end
config/environment.rb has the following line at the end:
require 'my_extensions'
However, I got the following error:
undefined method `to_kw' for #<BigDecimal:2704620,'0.555E2',8(8)>
What am I missing ?
I know it's been hours since you submitted this, but these functions might work once you restart your app. Items in lib generally are not reloaded automatically like those in app, so changes made will not be reflected in the application until performing a full restart.
Just throwing it out there :)
I'm also going to point out that, once you have these methods up and running, they probably will not work immediately. This is because your views are defined in the context of all of the Rails view helpers, like ActionView::Helpers::NumberHelper, which defines number_to_currency. Your extension in lib, however, is not defined in such a context, and therefore cannot access those helpers.
ActionView::Helpers::NumberHelper.number_to_currency might be more likely to work as expected.
You should include ActionView::Helpers::NumberHelper in your BigDecimal and Float:
class BigDecimal
include ActionView::Helpers::NumberHelper
def to_kw
number_to_currency(self, :unit => "kw", :format => "%n%u", :precision => 1)
end
end
class Float
include ActionView::Helpers::NumberHelper
def to_dollar
number_to_currency(self)
end
end
I think the error undefined method to_kw is caused by the undefined method number_to_currency.

How to deal with european number formats in rails forms?

So, here's the situation: a rails app with two idioms, one where users use the format 5,000.40 for writing numbers (standard english), and the other 5.000,40.
Now the question is, how do I deal with this? I'm using the i18n helpers for dealing with output, but I don't know how to process these numbers in my rails controller.
This is what I did to get this working:
Inside an initializer:
class ActiveRecord::Base
class << self
def handle_internationalization_numbers_for(*fields)
fields.each do |field_name|
define_method "#{field_name}=" do |other|
if valid_number?(other)
write_attribute(field_name, other)
else
# try converting it to a proper number
write_attribute(field_name, other.tr(".,", ",."))
end
end
end
end
end
private
def valid_number?(number)
number.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true
end
end
Then in your models:
handle_internationalization_numbers_for :field_1, :field_2
Use the localize method
=l record.amount
and define the specific format in the locale files.

rails - passing method name to helper function

docs say that options_from_collection_for_select should be used following way:
options_from_collection_for_select(collection, value_method, text_method, selected = nil)
so, in my case for example
options_from_collection_for_select(#messages,'id','title')
but i need to put more information to title, so what i tried to do was:
class Message < ActiveRecord::Base
def proper_title
self.name+", updated at "+self.updated_at
end
end
and it works, but thing is i need strings internationalized and it's a bit more difficult with models than with controllers.
now do i have to do model internationalization in this case or is it possible to get around somehow? thanks
You can still call I18n.translate() in the model. It will give you the same result as t() helper
# Message.rb
def proper_title
I18n.translate("message.proper_title", :name => self.name, :updated_at => self.updated_at)
end
# en.yml
en:
message:
proper_title: "{{name}}, updated at {{updated_at}}"
# view
options_from_collection_for_select(#messages,'id','proper_title')

Resources