Another way to output helper instead of eval - ruby-on-rails

(#products + #collections + #users + #questions).map do |r|
#results << {
:label => ["Product", "Collection", "User"].include?(r.class.name) ? r.name : r.question,
:category => r.class.name,
:href => eval("#{r.class.name.downcase}_path(r)")
}
end
I am currently looking if there is a way not to use eval on the string to convert it to a helper.
Note: This code is currently in the controller. Rails 2.3.11

Use polymorphic_path
#results = (#products + #collections + #users + #questions).map do |r|
{
:label => ["Product", "Collection", "User"].include?(r.class.name) ? r.name : r.question,
:category => r.class.name,
:href => polymorphic_path(r)
}
end

You can try something like this:
:href => self.send("#{r.class.name.downcase}_path".to_sym, r)
Since I'm not totally sure of the context here I'm not 100% confident this will work, but if this is a method you're trying to reference, then self is the most likely target for it.

There's 3 ways you can dynamically call methods with benchmarks shown here. I'l summarize the article below:
One way to invoke a method dynamically in ruby is to send a message to the object :
p s.send(:length) #=> 6
p s.send(:include?,"hi") #=> true
A second way is instantiate a method object and then call it:
method_object = s.method(:length)
p method_object.call #=> 6
method_object = s.method(:include?)
p method_object.call('hi') #=> true
And the third way is to use the eval method:
eval "s.length" #=> 6
eval "s.include? 'hi'" #=>true
According to the benchmarks results the SLOWEST is eval so I'd use send instead.
#######################################
##### The results
#######################################
#Rehearsal ----------------------------------------
#call 0.050000 0.020000 0.070000 ( 0.077915)
#send 0.080000 0.000000 0.080000 ( 0.086071)
#eval 0.360000 0.040000 0.400000 ( 0.405647)
#------------------------------- total: 0.550000sec
# user system total real
#call 0.050000 0.020000 0.070000 ( 0.072041)
#send 0.070000 0.000000 0.070000 ( 0.077674)
#eval 0.370000 0.020000 0.390000 ( 0.399442)

There are a few things you should be doing different, some of which have been alluded to or explicitly stated in other answers, but I think this does a better job:
#results = (#products + #collections + #users + #questions).map do |r|
{
:label => r.try(:question) || r.name,
:category => r.class.model_name.human,
:href => send(:"#{r.class.model_name.underscore}_path", r)
}
end
You don't need to build your result array manually — you're already using map, just assign the result to #results.
There's no need to do all the manual work of determining whether or not you should be posting the question or the name. You could give all these models a to_label method, which would be pretty easy, but assuming that questions don't have names using try and a binary or is just a dead simple way around the problem.
Single word model names convert pretty easily to display titles, but when you end up with a class like BedSheet you're going to want it to display as "Bed Sheet". Might as well do it right to start.
The other side of that same problem is converting the class name to the path/url helper method: you want bed_sheet_path, not bedsheet_path.
Using send over eval, as explained in other answers. No need to use an explicit to_sym though since Ruby supports using double-quotes to create a symbol outright.
Another quick note, I don't know if Rails 2.x was the same, but in Rails 3 you don't need to use the path helper to link to an instance of a model since most html helper methods will convert automatically (e.g. link_to 'A Product', #product).
Voila.

You could use a look-up table:
class_procs = {
Product => {
:path => lambda { |r| product_path(r) },
:label => lambda { |r| r.name }
},
Collection => {
:path => lambda { |r| collection_path(r) },
:label => lambda { |r| r.name }
}
User => {
:path => lambda { |r| user_path(r) },
:label => lambda { |r| r.name }
},
Question => {
:path => lambda { |r| question_path(r) },
:label => lambda { |r| r.question }
}
}
(#products + #collections + #users + #questions).map do |r|
procs = class_procs[r.class]
#results << {
:label => procs[:label].call(r),
:category => r.class.name,
:href => procs[:path].call(r)
}
end
And if your needs get more complicated then you could easily convert the per-class Hashes to individual classes to keep the inner Hashes from getting too big and complicated.

Related

Datagrid Gem in Rails 4 returns PG:UndefinedColumn: ERROR:

No hair left on my head (and I have had lots :) ), I have been pulling out my hair and for the life of me I can't figure this out.
I have a one to many relations between 2 tables. I have installed the Datagrid Gem for reporting. I need to get the report from one model based on the other one.
Please have a look at my code.
reports_grid.rb
class ReportsGrid
include Datagrid
scope do
Land.includes(:estate)
end
filter(:estate, :enum, :select => proc { Estate.group("title").select("title").map {|c| [c.title] }})
column(:id, :header => "Land ID")
column(:current_stage, :header => "Stage")
column(:price)
column(:status)
end
reports_controller.rb
class ReportsController < ApplicationController
def index
#grid = ReportsGrid.new(params[:reports_grid]) do |scope|
if params[:reports_grid].present?
if params[:reports_grid][:estate].present?
scope.joins(:estate).where("estates.title = ? ",params[:reports_grid][:estate]).page(params[:page])
**# when I get the #grid.assets here all good and return correct number of rows**
else
scope.page(params[:page])
end
else
scope.page(params[:page])
end
end
end
end
Land.rb
belongs_to :estate
estate.rb
has_many :lands
Now when I go to /reports and try to run the filter I get the following error
PG::UndefinedColumn: ERROR: column lands.estate does not exist LINE 1: ..._id" WHERE (estates.title = 'Olive Gardens' ) AND "lands"."e... ^ : SELECT COUNT(*) FROM "lands" INNER JOIN "estates" ON "estates"."id" = "lands"."estate_id" WHERE (estates.title = 'Olive Gardens' ) AND "lands"."estate" = 'Olive Gardens'
Why is the Gem tries to add "lands"."estate" = 'Olive Gardens' to the query when I have defined it at the instance.
Please let me know if you need me to add anything. Thank you in advance.
Edit:
This is what I have done and worked in the Filter:
I have done this:
filter(:estate_id, :enum,
:select => lambda {Estate.all.map {|p| [p.title, p.id]}},
:multiple => false,
:include_blank => true
) do |value|
self.where(:lands => {:estate_id => value})
end
Do you it is a good approach?
I guess in the scope I could say Land.joins(:estate) then use the scope.all.map... in the query.
Datagrid filter designed to filter data but not to just be by default.
If you have some reason why estate should not filter data by itself then add :dummy => true option:
filter(:estate, :enum, :select => ..., :dummy => true)
But I'would recommend it. Do this instead and your hair will start growing instantly:
filter(:estate, :enum, :select => ...) do |scope, value|
scope.joins(:estate).where("estates.title = ? ", value)
end
It seems obvious from documentation here:
https://github.com/bogdan/datagrid/wiki/Filters#filter-block
Try using references
Land.includes(:estate).references(:estates)

How to understand this code?

def event_calendar_options
{
:year => #year,
:month => #month,
:event_strips => #event_strips,
:month_name_text => I18n.localize(#shown_month, :format => "%B %Y"),
:previous_month_text => "<< " + month_link(#shown_month.prev_month),
:next_month_text => month_link(#shown_month.next_month) + " >>"
}
end
def event_calendar
calendar event_calendar_options do |args|
event = args[:event]
%(#{h(event.name)})
end
end
Here the whole event_calendar_options is enclosed by {}, so does it simply return the hash table?
Secondly, why event_calendar_options, a module method, can be passed as a parameter to calendar?
Assuming they're part of the same class (e.g. controller or model), event_calendar_options can be called by event_calendar. This is common practice within classes. Even if they're not, they can be declared as public and called by other classes as well (which is less common).
And, yes, event_calendar_options returns the hash table. In ruby, the final statement will be returned unless a return statement is supplied. In this case, it will return a hash table that will be iterated over by the event_calendar method.

How can I pass multiple attributes to find_or_create_by in Rails 3?

I want to use find_or_create_by, but this statement does NOT work. It does not "find" or "create" with the other attributes.
productproperty = ProductProperty.find_or_create_by_product_id(:product_id => product.id, :property_id => property.id, :value => d[descname])
There seems to be very little, or no, information on the use of dynamic finders in Rails 3. "and"-ing these together gives me a an unknown method error.
UPDATE:
Originally I couldn't get the following to work. Please assume I'm not an idiot and "product" is an instance of Product AR model.
product.product_properties.find_or_create_by_property_id_and_value(:property_id => 1, :value => "X")
The error methods was:
no such keys: property_id, value
I couldn't figure that out. Only this morning did I find the reference to passing the values like this instead:
product.product_properties.find_or_create_by_property_id_and_value(1, "X")
And voilá, it works fine. I would have expected a hash to work in the same situation but I guess not.
So I guess you get a down vote if you miss something on the internet?
If you want to search by multiple attributes, you can use "and" to append them. For example:
productproperty = ProductProperty.find_or_create_by_product_id_and_property_id_and_value(:product_id => product.id, :property_id => property.id, :value => d[descname])
There is one minor catch to be aware of. It will always return the object you've specified, even if that object can't be saved due to validation errors. So make sure you check to see if the returned object has an id (or is_valid?). Don't assume its in the database.
Alternatively, you can use the 'bang' version of the method to raise an error if the object cannot be saved:
http://guides.rubyonrails.org/active_record_querying.html#find-or-create-by-bang
This applies to Rails 3.
See http://api.rubyonrails.org/classes/ActiveRecord/Base.html:
With single query parameter:
productproperty = ProductProperty.find_or_create_by_product_id(product.id) { |u| u.property_id => property_id, u.value => d[descname] } )
or extended with multiple parameters:
productproperty = ProductProperty.find_or_create_by_product_id(:product_id => product.id, :property_id => property_id, :value => d[descname]) { |u| u.property_id => property_id, u.value => d[descname] } )
Would work with:
conditions = { :product_id => product.id,
:property_id => property.id,
:value => d[descname] }
pp = ProductProperty.find(:first, :conditions => conditions) || ProductProperty.create(conditions)
In Rails 4, you can use find_or_create_by(attr1: 1, attr2: 2) to find or create by multiple attributes.
You can also do something like:
User.create_with(
password: 'secret',
password_confirmation: 'secret',
confirmation_date: DateTime.now
).find_or_create_by(
email: 'admin#domain.com',
admin: true
)
If you need to create the user with some attributes, but cannot search by those attributes.
You could also use where(...).first_or_create - ActiveRecord::Relation#first_or_create.
product_property_attrs = { product_id: product.id,
property_id: property.id,
value: d[descname] }
product_property = ProductProperty.where(product_property_attrs).first_or_create
I've found in Rails 3.1 you do not need to pass the attributes in as a hash. You just pass the values themselves.
ProductProperty.find_or_create_by_product_id_and_property_id_and_value(
product.id, property.id, d[descname])

Is there find_or_create_by_ that takes a hash in Rails?

Here's some of my production code (I had to force line breaks):
task = Task.find_or_create_by_username_and_timestamp_and_des \
cription_and_driver_spec_and_driver_spec_origin(username,tim \
estamp,description,driver_spec,driver_spec_origin)
Yes, I'm trying to find or create a unique ActiveRecord::Base object. But in current form it's very ugly. Instead, I'd like to use something like this:
task = Task.SOME_METHOD :username => username, :timestamp => timestamp ...
I know about find_by_something key=>value, but it's not an option here. I need all values to be unique. Is there a method that'll do the same as find_or_create_by, but take a hash as an input? Or something else with similat semantics?
Rails 3.2 first introduced first_or_create to ActiveRecord. Not only does it have the requested functionality, but it also fits in the rest of the ActiveRecord relations:
Task.where(attributes).first_or_create
In Rails 3.0 and 3.1:
Task.where(attributes).first || Task.create(attributes)
In Rails 2.1 - 2.3:
Task.first(:conditions => attributes) || Task.create(attributes)
In the older versions, you could always write a method called find_or_create to encapsulate this if you'd like. Definitely done it myself in the past:
class Task
def self.find_or_create(attributes)
# add one of the implementations above
end
end
I also extend the #wuputah's method to take in an array of hashes, which is very useful when used inside db/seeds.rb
class ActiveRecord::Base
def self.find_or_create(attributes)
if attributes.is_a?(Array)
attributes.each do |attr|
self.find_or_create(attr)
end
else
self.first(:conditions => attributes) || self.create(attributes)
end
end
end
# Example
Country.find_or_create({:name => 'Aland Islands', :iso_code => 'AX'})
# take array of hashes
Country.find_or_create([
{:name => 'Aland Islands', :iso_code => 'AX'},
{:name => 'Albania', :iso_code => 'AL'},
{:name => 'Algeria', :iso_code => 'DZ'}
])

Checking if ActiveRecord find returns a result

I'm trying to check if a find method returns a result. My find method is the following:
post = Post.find(:all, :conditions => { :url => params['url'] }, :limit => 1)
What would be a good way to check that post contains a result?
find :all returns an empty array ([]) if no rows are returned, so you can just use it this way:
post = Post.find(:all, :conditions => { :url => params['url'] }, :limit => 1)
unless post.empty?
# do something...
end
By the way, if you do find :all you're going to get an array, not a single row. If you're trying to get just one Post, it would be cleaner to use the find_by helper or find :first or just first instead:
post = Post.find_by_url params['url']
# or
post = Post.first :conditions => { :url => params['url'] }
# then...
if post
# do something...
end
You can try ActiveRecord::Base.exists? before
Post.exists?(:conditions => { :url => params['url'] })
Use the BANG! version of the find_by_url method to get it to raise an exception of it could not be found and then rescue it later on in that same method/action.
def show
Post.find_by_url!(params[:url])
rescue ActiveRecord::RecordNotFound
flash[:notice] = "The URL you were looking for could not be found."
redirect_to root_path
end
end
If you didn't raise an exception here I believe that Rails would show the public/404.html page.
if post doesn't contain any result it will be an empty list and then:
post.empty?
will return true.
it may be as simple as changing your finder to:
post = Post.find(:first, :conditions => { :url => params['url'] })
With this finder, post will either return a single value or nil. Because nil behaves like false in a condition statement, you can say something like the following:
if post
# do something
else
# do something else
end
Post.find_by_id(id_column_value)
will return nil rathering than blowing up your program when it can't find a record.
Of course, there's
x = Post.where(:any_column_name => value)
which always returns an array of results. In which case you could just run an
x.each {|t| f(t) }
or
y = x.map {|t| f(t)}
or of course,
x[0], x[1], etc
Sorry I got a little carried away there
Another way to do it is checking with ActiveRecord#any?.

Resources