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')
Related
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)
I have some variables in a instance variable (for other methods can access the variable) which type is hash.
if I don't want all members in hash #iw2 applied attr_accessor
only #iw2[:dir] can be modified by others.
#iw2 ={}
#iw2[:dir] = "#{Rails.root}/#{ENV["module_handy_network_tools_src_path"]}"
#iw2[:prog_path] ="#{#iw2[:dir]}/#{ENV["module_handy_network_tools_prog_path"]}"
So I wrote that way,
attr_accessor :iw2[:dir]
But I got the error
TypeError (can't convert Symbol into Integer):
app/helpers/handy_network_tools_helper.rb:8:in `[]'
How to fix the problem, thanks in advance.
[2] pry(#<HandyNetworkToolsController>)> #iw2.class
=> Hash
Edit
When you find yourself having many methods with the same prefix (iw2 in this case), it is a sign that there's a hidden object in there. How about this? Better?
class Iw2
def initialize(hash)
#dir = hash[:dir]
#prog_path = hash[:prog_path]
end
attr_accessor :dir, :prog_path
end
class MyClass
def initialize
#iw2 = Iw2.new(:dir => "a rails path",
:prog_path => "some another rails path")
end
delegate :dir, :prog_path, :to => :#iw2
end
mc = MyClass.new
mc.dir # => "a rails path"
mc.prog_path # => "some another rails path"
Original answer
Well, attr_accessor doesn't work like that. You can always use old-fashioned getters/setters.
def iw2_dir
#iw2[:dir]
end
def iw2_dir=(dir)
#iw2[:dir] = dir
end
You can then implement your own attr_sub_accessor that will generate such methods for you.
attr_sub_accessor :iw2, :dir
attr_sub_accessor :iw2, :prog_path
(I think explicit getters/setters are better in this case)
I have a category model and I'm routing it using the default scaffolding of resources :categories. I'm wondering if there's a way to change the paths from /category/:id to /category/:name. I added:
match "/categories/:name" => "categories#show"
above the resources line in routes.rb and changed the show action in the controller to do:
#category = Category.find_by_name(params[:name])
it works, but the 'magic paths' such as link_to some_category still use the :id format.
Is there a way to do this? If this is a bad idea (due to some possible way in which rails works internally), is there another way to accomplish this? So that /categories/music, for example, and /categories/3 both work?
Rails has a nifty model instance method called to_param, and it's what the paths use. It defaults to id, but you can override it and produce something like:
class Category < ActiveRecord::Base
def to_param
name
end
end
cat = Category.find_by_name('music')
category_path(cat) # => "/categories/music"
For more info, check the Rails documentation for to_param.
EDIT:
When it comes to category names which aren't ideal for URLs, you have multiple options. One is, as you say, to gsub whitespaces with hyphens and vice versa when finding the record. However, a safer option would be to create another column on the categories table called name_param (or similar). Then, you can use it instead of the name for, well, all path and URL related business. Use the parameterize inflector to create a URL-safe string. Here's how I'd do it:
class Category < ActiveRecord::Base
after_save :create_name_param
def to_param
name_param
end
private
def create_name_param
self.name_param = name.parameterize
end
end
# Hypothetical
cat = Category.create(:name => 'My. Kewl. Category!!!')
category_path(cat) # => "/categories/my-kewl-category"
# Controller
#category = Category.find_by_name_param(param[:id]) # <Category id: 123, name: 'My. Kewl. Category!!!'>
If you don't want to to break existing code that relying on model id you could define your to_param like this:
def to_param
"#{id}-#{name}"
end
so your url will be: http://path/1-some-model and you still can load your model with Model.find(params[:id]) because:
"123-hello-world".to_i
=> 123
Although possibly more than you need, you may also want to look into 'human readable urls' support like friendly_id or one of the others (for instance, if you need unicode support, etc.) that are described here at Ruby Toolbox.
I've got a legacy table that my rails application shares with another application. It has a column called "class". The first time I reference any attribute in that model, I get an error. Subsequent references to attributes work. Is there a good workaround for this, or should I just go modify the other application that uses this table (ugh)?
>> Member::Ssg.find(:first)
=> #<Member::Ssg ssg_key: #<BigDecimal:10b169688,'0.253E3',4(8)>, org_id: 2, academic_year: 2006, class: true, next_due_date: "2011-06-01", submitted_date: "2006-02-13", notes: nil, owner_id: "1">
>> Member::Ssg.find(:first).notes
NoMethodError: undefined method `generated_methods' for true:TrueClass
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.8/lib/active_record/attribute_methods.rb:247:in `method_missing'
from (irb):2
>> Member::Ssg.find(:first).notes
=> nil
SOLUTION:
I went with a combination of the Bellmyer solution and adding the code below to my model
class << self
def instance_method_already_implemented?(method_name)
return true if method_name == 'class'
super
end
end
NOTE: Please see the updated solution at the end of this answer. Leaving the original outdated solution for historic reasons.
This has come up often enough (legacy column names interfering with ruby/rails) that I might just make a plugin out of this. Here's how you can fix it right away, though. Create this file in your app:
# lib/bellmyer/create_alias.rb
module Bellmyer
module CreateAlias
def self.included(base)
base.extend CreateAliasMethods
end
module CreateAliasMethods
def create_alias old_name, new_name
define_method new_name.to_s do
self.read_attribute old_name.to_s
end
define_method new_name.to_s + "=" do |value|
self.write_attribute old_name.to_s, value
end
end
end
end
end
And now, in your model:
class Member < ActiveRecord::Base
include Bellmyer::CreateAlias
create_alias 'class', 'class_name'
end
The first parameter to create_alias is the old method name, and the second parameter is the new name you want to call it, that won't interfere with rails. It basically uses the read_attribute and write_attribute methods to interact with the column instead of the ruby methods that get defined by ActiveRecord. Just be sure to use the new name for the field everywhere, like so:
member.class_name = 'helper'
This works with ruby 1.8, but I haven't tested with ruby 1.9 yet. I hope this helps!
UPDATE: I've found a better solution that works in Rails 3, the safe_attributes gem. I've written a blog post explaining how to use it, with example code snippets, and a full sample app you can download from github and play around with. Here's the link:
Legacy Database Column Names in Rails 3
The following works in Rails 6.0.2.2
class ReasonCode < ApplicationRecord
class << self
def instance_method_already_implemented?(method_name)
return true if method_name == 'class'
super
end
end
def as_json(options={})
add_class = attributes.keys.include?('class')
if add_class
if options[:only]
add_class = Array(options[:only]).map(&:to_s).include?('class')
elsif Array(options[:except])
add_class = Array(options[:except]).map(&:to_s).exclude?('class')
end
end
options[:except] = Array(options[:except])
options[:except].push('class')
json = super(options)
json['class'] = attributes['class'] if add_class
json
end
end
Adapted from this answer https://www.ruby-forum.com/t/activerecord-column-with-reserved-name-class/125705/2. The as_json method was added because rendering the record as json gave a SystemStackError (stack level too deep). I followed the serialization code in the Rails repo to only render the class attribute if specified in the as_json options.
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