Overwriting default accessors with options - ruby-on-rails

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)

Related

How to apply attr_accessor for a variable in hash in Rails 3

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)

Can't understand Ruby's magic

In railscasts project you can see this code:
before(:each) do
login_as Factory(:user, :admin => true)
end
The corresponding definition for the function is:
Factory.define :user do |f|
f.sequence(:github_username) { |n| "foo#{n}" }
end
I can't understand how the admin parameter is passing to function, while in the function there's no word about admin parameter. Thanks
Factory.define is not a function definition, it is a method that takes a symbol or string (in this case user) and a block that defines the factory you are making. Factory(:user, :admin => true) makes a User object, with admin attributes. It is not calling the code in your second snippet, it is calling Factory() which initializes a factory, and selects one (in this case the one defined in second snippet). Then it passes options in hash form to Factory as well.
Factory selects the :user factory which is very generic. The option :admin=>true just tells Factory to set the admin instance variable on User to true.
This is actually what it is calling in factory.rb in factory girl
def initialize(name, options = {}) #:nodoc:
assert_valid_options(options)
#name = factory_name_for(name)
#options = options
#attributes = []
end
So Factory(name,options) is equivalent to Factory.new(name,options) in this code.
http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and String etc have similar constructs. I am trying to figure out how they did that now.
This is all confusing even for decent Ruby programmers. I recommend strongly the book "Metaprogramming Ruby" It is probably the best book I have read in ruby and it tells you a lot about this magic stuff.
Michael Papile's response is essentially correct. However, I'd like to elaborate upon it a bit as there are some technical nuances that you might wish to be aware of. I looked over the code for railscasts and factory_girl and I believe there are a few extra pieces to the puzzle that explain how the :admin => true arg ends up creating the admin attribute of the user factory. The attribute addition does not actually happen by way of Factory's initialize() method, although as Michael pointed out that method is indeed being called in service of building the new user factory object.
I'm going to include in this explanation all the steps I took in case you'd like to see how to go about investigating similar questions you might have.
Since your original post is dated Feb. 17th I looked at the version of railscasts that closely matches that date.
I looked in its Gemfile:
https://github.com/ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/Gemfile
Line 18:
gem "factory_girl_rails"
I then checked out the commit of factory_girl_rails that most closely matched the Feb 17th date.
https://github.com/thoughtbot/factory_girl_rails/blob/544868740c3e26d8a5e8337940f9de4990b1cd0b/factory_girl_rails.gemspec
Line 16:
s.add_runtime_dependency('factory_girl', '~> 2.0.0.beta')
factory_girl version 2.0.0.beta was actually not so easy to find. There are no github tags with that name so I just checked out the closest in terms of commit date.
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/vintage.rb
Lines 122-128:
# Shortcut for Factory.default_strategy.
#
# Example:
# Factory(:user, :name => 'Joe')
def Factory(name, attrs = {})
Factory.default_strategy(name, attrs)
end
So the Factory invocation in railscasts is actually calling a convenience method which invokes the "default strategy", which is located in the same file:
Lines 39-52:
# Executes the default strategy for the given factory. This is usually create,
# but it can be overridden for each factory.
#
# Arguments:
# * name: +Symbol+ or +String+
# The name of the factory that should be used.
# * overrides: +Hash+
# Attributes to overwrite for this instance.
#
# Returns: +Object+
# The result of the default strategy.
def self.default_strategy(name, overrides = {})
self.send(FactoryGirl.find(name).default_strategy, name, overrides)
end
Note that FactoryGirl.find is invoked to get the object on which to invoke default_strategy. The find method resolves to here:
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/registry.rb
Lines 12-14:
def find(name)
#items[name.to_sym] or raise ArgumentError.new("Not registered: #{name.to_s}")
end
Here the name is :user. Thus we wish to invoke default_strategy on the user factory. As Michael Papile pointed out, this user factory was defined and registered by the railscasts code that you originally thought was the class definition for Factory.
https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb
Lines 23-25:
Factory.define :user do |f|
f.sequence(:github_username) { |n| "foo#{n}" }
end
So in investigating what the default strategy is for the user factory, I looked around in the railscasts project and found this:
https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb
Lines 43-45:
def default_strategy #:nodoc:
#options[:default_strategy] || :create
end
:create is the default strategy. We go back to factory_girl to find the def for create.
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/methods.rb
Lines 37-55:
# Generates, saves, and returns an instance from this factory. Attributes can
# be individually overridden by passing in a Hash of attribute => value
# pairs.
#
# Instances are saved using the +save!+ method, so ActiveRecord models will
# raise ActiveRecord::RecordInvalid exceptions for invalid attribute sets.
#
# Arguments:
# * name: +Symbol+ or +String+
# The name of the factory that should be used.
# * overrides: +Hash+
# Attributes to overwrite for this instance.
#
# Returns: +Object+
# A saved instance of the class this factory generates, with generated
# attributes assigned.
def create(name, overrides = {})
FactoryGirl.find(name).run(Proxy::Create, overrides)
end
The create strategy calls the run method defined here:
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/factory.rb
Lines 86-97:
def run(proxy_class, overrides) #:nodoc:
proxy = proxy_class.new(build_class)
overrides = symbolize_keys(overrides)
overrides.each {|attr, val| proxy.set(attr, val) }
passed_keys = overrides.keys.collect {|k| FactoryGirl.aliases_for(k) }.flatten
#attributes.each do |attribute|
unless passed_keys.include?(attribute.name)
attribute.add_to(proxy)
end
end
proxy.result(#to_create_block)
end
A translation/summarization of what this code is doing:
First, the proxy object is built by calling new on the proxy_class, which in this case is Proxy::Create, which is defined here:
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/create.rb
Basically all you need to know is that proxy is building a new user factory object and invoking callbacks before and after the factory object is created.
Going back to the run method, we see that all the extra args that were originally passed into the Factory convenience method (in this case, :admin => true) are now being labelled as overrides. The proxy object then invokes a set method, passing in each attribute-name/value pair as args.
The set() method is part of the Build class, the parent class of Proxy.
https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/build.rb
Lines 12-14:
def set(attribute, value)
#instance.send(:"#{attribute}=", value)
end
Here #instance refers to the proxied object, the user factory object.
This then, is how :admin => true is set as an attribute on the user factory that the railscasts spec code creates.
If you want, you can google "programming design patterns" and read about the following patterns: Factory, Proxy, Builder, Strategy.
Michael Papile wrote:
http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and
String etc have similar constructs. I am trying to figure out how they
did that now.
If you are still curious, the Array and String you see in the Kernel doc are actually just factory methods used to create new objects of those types. That's why no new method invocation is needed. They are not actually constructor calls per se, but they do allocate and initialize Array and String objects, and hence under the hood they are doing the equivalent of calling initialize() on objects of those types. (In C though, of course, not Ruby)
I don't think that second snippet is the definition for the function. Function definitions have def and end. I think that second snippet looks like a function or method being called with an argument of :user and a block that takes an f parameter.
Of course with metaprogramming you can never really be sure what the hell is going on.

Pretty Paths in Rails

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.

Legacy table with column named "class" in Rails

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.

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