I have a method that will provide an array of model object. Some of those model's attributes are easy to sort by using some help from SQL. I mean, using .find(:condition => {})
The problem is some of the other attributes isn't. It has to be calculated, modified, or did something else before showing. Then, I found the way to sort it using collection sorting. Which is, collection.sort{|a,b| a.something_to_sort <=> b.something_to_sort}
OK! That's works. But the problem is, is it possible to make that something_to_sort part to become a dynamic variable? For example, I want to take a parameter from the UI and assign it to that something_to_sort like following,
HTML
<select name="sort">
<option value="name">Name</option>
<option value="age">Age</option>
<option value="activity.category">Activity</option>
<option value="tax.calculate_personal_income_tax">Income Tax</option>
<option value="tax.calculate_withholding_tax">Withholding Tax</option>
<option value="doctor.name">Doctor's Name</option>
</select>
Rails
params[:sort] ||= "Age"
#people = Person.find(:all)
#people.sort{|a,b| a.params[:sort] <=> b.params[:sort]} #Note that this is an incorrect syntax
Is there any way to do this by not having to write a sort block for each of sorting option?
Additional #1
I've just tried this and it was working for some sorting option
#people.sort{|a,b| a.send(params[:sort]) <=> b.send(params[:sort])}
This works for name and age as they are people's attribute (and it works for peopls's method too). But for the association's such as tax.calculate_personal_income_tax, it's not.
So, my colleague told me to create new people's method, calculate_personal_income_tax and then change the option value from tax.calculate_personal_income_tax to calculate_personal_income_tax so that the send method will be able to find this method ...
def calculate_personal_income_tax
tax.calculate_personal_income_tax
end
But, this is not really what I want. Because I believe that there must be a way to access this association method without having to define a new model method. If I choose to do this and one day people model become larger, more attribute, more information to sort and display. Then, there will be a lot of method like this.
The other reason is, if I have to define a new method, that method should have to do some logic, calculation or else, not just retrieve a data from other model.
Additional #2
Finally, I've just found the way to access other model attribute by adding args to the find method, :join and :select.
#people = Person.find(:all, :select => "persons.*, tax.tax_rate AS tax_rate", :joins => :tax, :condition => {some conditions})
The result of that will be a table of person joining with tax. Then, I use the send(params[:sort]) thing to help me sorting the attribute that I want.
#people.sort {|a, b| a.send(params[:sort]) <=> b.send(params[:sort])}
params[:sort] ||= "age"
method_chain = params[:sort].split(".")
#people = Person.find(:all)
#sorted_people =
#people.sort_by do |person|
method_chain.inject(person) do |memo,method|
memo.send(method)
end
end
This assumes that the values passed in params[:sort] are always valid method names which can be chained together in the order specified. You'd want to keep in mind that anything can be passed in the params hash, so this would be very unsafe if exposed to untrusted users (e.g. params[:sort] = "destroy". Oops.)
Also, if you can do this in SQL instead you'll get better performance.
In fact, the more I look at this, the more it seems like a bad idea.
I have a loot system that is doing this on a calculated value, that is based on a reference table which can change. As the value is calculated, it could come from anywhere.
My players/index.html.rb looks something like.
<h1>Players</h1>
<table>
<tr>
<th>Name</th>
<%= generate_headings(params) %>
</tr>
<% players.each do |player| %>
<tr>
<td><%= player.name %></td>
<% LootType.all.each do |loot_type| %>
<td><%= player.loot_rate(loot_type.name) %></td>
<% end %>
<tr>
<% end %>
</table>
The loot_rate function is calculating on two related tables (player.raids and player.items) as "player raids / loot of type + 1".
The generate_headings function is in players_helper.rb, and it looks like:
def generate_headings(params = {})
sort = params[:sort]
headings = ""
LootType.all.each do |loot_type|
heading_text = "#{loot_type.name} Rate"
if (sort == loot_type.name)
column_heading = "<th>#{heading_text}</th>"
else
heading_url = "/players?sort=#{loot_type.name}"
column_heading = "<th>#{link_to heading_text, heading_url}</th>"
end
headings += column_heading
end
headings.html_safe
end
In my index action of players_controller.rb, I have:
def index
sort = params[:sort]
if !sort
#players.sort! { |a,b| a.name <=> b.name }
else
#players.sort! { |a,b| b.loot_rate(sort) <=> a.loot_rate(sort) } # Reverse sort (for my purposes)
end
respond_to do |format|
format.html
format.json {render :json => #players }
end
end
This means that when I add or remove "loot types" in my system, and the player list automagically allows users to sort by those types.
Long winded answer, but hope it helps.
Related
I am new to Rails, but slowly making progress. I can't quite wrap my head around how to achieve my next task.
I have a controller (IdeasController) with an index that looks like this:
def index
if params[:round].blank? && params[:challenge].blank?
#ideas = Idea.all.order(params[:sort])
# #ideas = Idea.all.order(created_at: :desc, cached_votes_up: :desc)
end
if params[:round].present?
#round_id = Round.find_by(name: params[:round]).id
#ideas = Idea.where(round_id: #round_id).order("created_at DESC")
end
if params[:challenge].present?
#challenge_id = Challenge.find_by(name: params[:challenge]).id
#ideas = Idea.where(challenge_id: #challenge_id).order("created_at DESC")
end
end
I am updating the view and filtering by category with the above :round and :challenge with the code below in my index.html.erb:
<%= link_to "All", ideas_path %>
<% Round.all.each do |round| %>
<%= link_to round.name, ideas_path(round: round.name) %>
<% end %>
<% Challenge.all.each do |challenge| %>
<%= link_to challenge.name, ideas_path(challenge: challenge.name) %>
<% end %>
Now, my problem is that I want to create a button that orders by created_at DESC or ASC. I want the button to essentially be a toggle. I also want another button to order by cached_weighted_average DESC or ASC. This is from acts_as_votable so I can sort by vote counts.
The problem I am running into is that I can create a link or button that orders by created_at or cached_weighted_average, but it replaces all of the URL that was previously filtered by :round or :challenge. For example, if a user clicks "Round 1" and sees all ideas marked for "Round 1" and then they click the link to order by cached_weighted_average, the URL replaces:
/ideas?round=Round+1
With this:
/ideas?sort=cached_weighted_average+ASC
What I want is:
/ideas?round=Round+1&?sort=cached_weighted_average+ASC
I know this is a very new question, but everything I have tried has failed so far. It feels like I am missing something very easy. What I noticed I can do easily is inside the controller I can do something like:
if params[:round].present?
#round_id = Round.find_by(name: params[:round]).id
#ideas = Idea.where(round_id: #round_id).order("cached_weighted_average DESC")
end
Which is perfect. This button just needs to switch between cached_weighted_average DESC and created_at DESC.
Any help is appreciated, thanks.
passing multiple parameters is one way to handle:
<%= link_to object.name, object_path(first: something, second: something_else) %>
then alter your conditionals to contemplate presence of multiple params.
to differentiate between round and challenge when attempting to allow the user to choose how they'd like to sort you could use the same name and then pass it different values.
something like:
params["round_or_challenge"]
this would change your conditional to something like:
if params["round_or_challenge"] == "round" && params["asc_or_desc"] == "asc"
# query
elsif params["round_or_challenge"] == "challenge"
# query
end
or whatever. it's basically the same...just pass the values you need. you can also pass the existing parameters from the view the same way you access them in the controller.
Thanks for the response, #toddmetheny. I didn't implement your solution, but your solution helped me understand passing multiple parameters a bit more.
I ended up creating a helper, sortable. I also used the url_for to append at the end of whatever the current URL might be. I liked this approach because it meant I could sort on any parameter. I'm not sure that it's the best solution, but it works.
def sortable (name, sort)
link_to name, url_for(params.merge(sort: sort))
end
This may sound strange, but none the less I want to learn how to do it and I need some help getting there. I'm not sure how to approach this. I'm hoping to get some dev love on this.... Let me explain by giving an example. (Btw thank you---you are awesome!)
Instead of this in my view:
<table>
#users.map do |user|
...
</table>
I want to extract it away into a helper that I can reuse for other collections.
So I want to say instead:
#users.to_table({
template: "simple_template",
header: ["Full Name","Email"],
column: ["name", "email"]
})
So in my application_helper I have something like this: (pseudo-ish code)
class ActiveRecord::Relation
def to_table *args
load args.template
self.map do |j|
args.header do |header|
j.header
end
args.column do |column|
j.column
end
end
end
end
I have no idea how to wire this up. (helper or table template) Definitely an order of magnitude above my current skill level. Need some serious direction.. I'm asking this because I feel like I hit a learning plateau and need help busting through to something more challenging (hence this question)... Hope it's clear, if not ask for clarification. Thanks for reading... Thanks for helping! =)
Not guaranteeing this will work it is just to show the syntax issues:
class ActiveRecord::Relation
def to_table(options={})
load options[:template]
self.map do |j|
Hash[
args[:headers].zip(args[:columns].map{ |column| j.send(column) }
]
end
end
end
Not Sure about the load part I think this should be handled outside of the relation as it is a view issue and has nothing to do with the ActiveRecord::Relation but this method will return an Array of Hashes like
[{"Full Name" => "USER 1 Name", "Email" => "USER1#email.com},{"Full Name" => "USER 2 Name", "Email" => "USER2#email.com"}]
In your current method args which is an array now based on the * will not respond to things like template or column. Like I said I have never really tried to implement anything in this way but the syntax change might get you headed in the right direction. Also handling should be put in place for when template is not passed or headers.count != columns.count.
Best bet is probably something like this
<%= render "template", obj: #user.to_table(headers: ["Full Name","Email"],columns: ["name", "email"]) %>
in _template.rb
<table>
<thead>
<tr>
<% obj.first.keys.each do |header|
<th><%= header %></th>
<% end %>
</tr>
</thead>
<tbody>
<% obj.each do |row|
<tr>
<% row.values.each do |cell| %>
<td><%= cell %></td>
<% end %>
</tr>
<% end %>
</tbody>
Although if I had more time to think there are probably far simpler implementations of this maybe something like
<%= render 'template', locals:{collection: #users, headers: ["Full Name","Email"], columns: ["name", "Email"]} %>
UPDATE
I think making a view helper might be better in this instance like this
def make_table(collection,options={})
content_tag(:table,options[:table_options]) do
content_tag(:thead) do
content_tag(:tr) do
options[:headers].map do |header,header_options|
content_tag(:th,header,header_options,false)
end.join.html_safe
end
end
content_tag(:tbody,options[:body_options]) do
collection.map do |obj|
content_tag(:tr,options[:row_options]) do
options[:columns].map do |column,cell_options|
content_tag(:td,obj.public_send(column),cell_options,false)
end.join.html_safe
end
end.join.html_safe
end
end
end
call as
<%= make_table(#users,columns:{name:{class: "name"},email:{}},headers:{"Full Name"=>{class:"name_header"},"Email"=>{}}) %>
or without formatting
<%= make_table(#users,columns:[:name,:email],headers:["Full Name","Email"]) %>
This method requires an object collection and will accept the following through the options Hash
:table_options as a Hash to pass to the content tag for formatting the table
:headers as an Array or Hash (for formatting header rows)
:body_options as a Hash to pass to the content tag for formatting the table body
:row_options as a Hash to pass to the content tag for formatting the rows
:columns as an Array or Hash (for formatting the individual cells)
You can place this method in helpers/application_helper.rb and you will have access to it throughout the application. Although I have not fully vetted this method and it is currently more conceptual than anything else.
In my web application the user can select certain instances of an entity. For instance on the class BubbleGum, now the user can select certain instances of BubbleGum by adressing their identifier:
gums/details?show=3532667
Now, in addition I also want to make it possible to display all BubbleGums. For this I have introduced the convention of using * to identify all
gums/details?show=*
This works nice so far, but often I have to add a bit code to process the * selection. Is there a nice way to represent an all-instances object in Ruby and/or Rails?
I have thought of using simply a dedicated symbol, constants, or an actual object of the class BubbleGum that represents all the other bubble gums.
To display all the entities in a rails application generally we use a index page.
bubble_gums_controller.rb
def index
#bubble_gums = BubbleGum.all
end
views/bubble_gums/index.html.erb
<% #bubble_gums.each do |bubble_gum| %>
<tr>
<td><%= bubble_gum.name %></td>
<td><%= bubble_gum.price %></td>
</tr>
<% end %>
Refer this for further details.
http://guides.rubyonrails.org/getting_started.html#listing-all-posts
I think you want to use the query string param show.
So, you can try in your gums controller:
def details
if params[:show] == "*"
#bubble_gums = BubbleGum.all
# ...
elsif params[:show]
#bubble_gum = BubbleGum.find(params[:show])
# ...
else
render :status => 404
end
end
Ok, I know this is for the Saas course and people have been asking questions related to that as well but i've spent a lot of time trying and reading and I'm stuck. First of all, When you have a model called Movie, is it better to use Ratings as a model and associate them or just keep Ratings in an array floating in space(!). Second, here's what I have now in my controller:
def index
#movies = Movie.where(params[:ratings].present? ? {:rating => (params[:ratings].keys)} : {}).order(params[:sort])
#sort = params[:sort]
#ratings = Ratings.all
end
Now, I decided to create a Ratings model since I thought It would be better. Here's my view:
= form_tag movies_path, :method => :get do
Include:
- #ratings.each do |rating|
= rating.rating
= check_box_tag "ratings[#{rating.rating}]"
= submit_tag "Refresh"
I tried everything that is related to using a conditional ternary inside the checkbox tag ending with " .include?(rating) ? true : "" I tried everything that's supposed to work but it doesn't. I don't want the exact answer, I just need guidance.Thanks in advance!
Update (the controller's method):
Here is the index method within my controller that is reading this hash - for clarity. I apologize for the fuzziness before.
def index
#all_stores = Product.all_stores
#selected_stores = params[:stores] || session[:stores] || {}
if #selected_stores == {}
#selected_stores = Hash[#all_stores.map {|store| [store, store]}]
end
if params[:stores] != session[:stores]
# session[:stores] = #selected_stores
session[:stores] = params[:stores]
redirect_to :stores => #selected_stores and return
end
#products = Product.order("created_at desc").limit(150).find_all_by_store(#selected_stores.keys).group_by { |product| product.created_at.to_date}
. . . etc
Couple of things
1) .order(params[:sort]) opens you up for sql injection attacks,
someone could potentially delete all of your users by putting this in the query string
http://localhost:3000/movies?sort=email%3B+DELETE+from+users+--
see rails 3 activerecord order - what is the proper sql injection work around?, this problem does not exist with .where, rails will sanitize the input for where method
2) minor thing, AREL delays making the actual call to the db until you iterate on the collection, so you can 'build' up queries, this syntax might be easier to understand then using ternary operator?
#movies = Movie.order(xxxxxx)
#movies = #movies.where(:rating => params[:ratings].keys) if params[:ratings].present?
3) for your ratings, like cdesrosiers says constant is fine, then your view
Movie::RATINGS.each do |rating|
check_box_tag "ratings[]", rating # <input type="checkbox" name="ratings[]" value="PG" />
EDIT: maintain selected values
# controller
#selected_ratings = (params[:ratings].present? ? params[:ratings] : [])
# view
Movie::RATINGS.each do |rating|
check_box_tag "ratings[]", rating, #selected_ratings.include?(rating)
# <input type="checkbox" name="ratings[]" value="PG" checked="checked" />
note the [] in the naming of the checkbox, this will give you params[:ratings] as an array in your controller action
#movies = #movies.where("rating IN (?)", params[:ratings]) if params[:ratings].present? and params[:ratings].any?
some links
http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-include-3F
http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-check_box_tag
To start, I'm a pretty new to Rails
I've created some methods and put them into my model, but it looks messy and just wondered if the code belongs in the model or the controller? What makes my code unique (not one model per controller anyhow) is that I have only one model "Products" but have 3 controllers that interact with it, "Merchants, Categories, Brands". Maybe there is an easier way I have completely overlooked?? I don't really want to split the data up into 3 tables / models with links between.
p.s. This is the first time I have slipped away from the comfort of a Rails book so please go easy on me! Any other general suggestions to my code will be much appreciated.
Product model
class Product < ActiveRecord::Base
validates :brand, :presence => true
def product_name
name.capitalize.html_safe
end
def product_description
description.html_safe
end
#Replace '-' with ' ' for nice names
def brand_name
brand.capitalize.gsub('-',' ')
end
def category_name
category.capitalize.gsub('-',' ')
end
def merchant_name
merchant.capitalize.gsub('-',' ')
end
#Replace ' ' with '-' for urls
def brand_url
brand.downcase.gsub(' ','-')
end
def category_url
category.downcase.gsub(' ','-')
end
def merchant_url
merchant.downcase.gsub(' ','-')
end
end
Merchants Controller
class MerchantsController < ApplicationController
def index
#merchants = Product.find(:all, :select => 'DISTINCT merchant')
end
def show
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
end
end
Merchant view (index)
<h1>Merchant list</h1>
<%= #merchants.count%> merchants found
<% #merchants.each do |merchant| %>
<p><%= link_to merchant.merchant_name, merchant.merchant_url %></p>
<% end %>
Merchant view (show)
<h1>Products from merchant: <%= #merchant %></h1>
<%= #products.count%> products found
<% #products.each do |product| %>
<h3><%= product.product_name %></h3>
<p>
<img src="<%= product.image %>" align="right" alt="<%= product.product_name %>" />
<%= product.product_description %>
</p>
<p><%= product.price %></p>
<p>Brand: <%= product.brand_name %></p>
<p>Category: <%= product.category_name %></p>
<p>Sub category: <%= product.sub_category %></p>
<p>Merchant: <%= product.merchant_name %></p>
<p>More information</p>
<hr />
<% end %>
So your data model does seem to be getting to the point where you might at least want to split merchants out. You can tell this from the select 'DISTINCT merchant' query. If your merchants are user-based input and are saved inside your products table it seems like a good time to move them into their own model so that they are easily searchable and manageable. As you get more merchants and more products it will get harder and harder to perform this query. Once you want to add additional merchant information you'll be in a worse position as well. Just keep in mind Rails was made for easy refactoring. Making this change shouldn't be daunting, it should just be another regular task in your agile development process.
What the above change would also allow you to do is change these lines:
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
into:
#merchant = Merchant.find_by_name(params[:name])
#products = #merchant.products
You could then have a capitalize and gsub name with a model function:
#merchant.display_name
The next step would be to DRY up your model code a little bit, for example:
class Product
def brand_name
make_name brand
end
def category_name
make_name category
end
def merchant_name
make_name merchant
end
private
def make_name name
name.capitalize.gsub('-', ' ')
end
end
You could do something similar to the _url functions as well. If you wanted to venture further you could clean this up using meta-programming as well.
Final Thoughts: make sure you actually want to be calling html_safe on your strings. If they are user based input it's best to let them go through the h function in your views. Do you want users to be able to enter HTML strings as brands, merchants and categories? If so, then leave the html_safe string there, otherwise let the strings be made html_safe in your views.
In general you are on the right path: Skinny Controllers and Views and Fat Models is the way to go. This means put your logic and your heavy-lifting into your Models and let your Controllers and Views be small and simple.
You should probably normalise your database. You need 3 tables instead of one: Products, Merchants and Brands. Your product table will then have references to merchant and brand tables. You can then have separate models (with belongs_to/has_many relationships between them) and separate controllers.
You will still be able to write things like product.merchant.name but some of your code will be simpler.
Conventions are just that, conventional. There is no right or wrong no matter who in Atlanta tells you that there are. F#$k him.
Anyways, if you are going with a Skinny Controller Fat model, then yeah, you're on the right track.
As they say, do all the heavy lifting in your model.
I'd look to refactor those methods personally in the model. All those places where you are calling *.downcase.gsub...
Also look into to_param, a method you can overwrite to get purdy urls.