Rails 3 - Loop through to get a collection? - ruby-on-rails

I'm implementing search and am having some difficulties with my sql/finding.
Basically, I have a favorites page, that gets a collection of favorites with:
#favorites = current_user.votes
In the view, I then loop through my favorites, and can call .voteable on them to get the actual object that was voted on. This is making search very difficult for me to write.
I was wondering if it was possible to change my original collection, so that I'm actually getting the .voteable objects each time to dry up my view/help me write my search. I cannot called current_user.votes.voteables but individually can do something like current_user.votes.first.voteable
What I've attempted is a loop like so:
#favorites = current_user.votes.each {|vote| vote.voteable }
Which is wrong, and I'm just getting my votes again, and not the actual voteable object. I was just wondering if there was a way to get these voteables from looping through my votes like this.
Any pointers would help, thanks.
EDIT:
Expansion what I mean by search:
I'm building a method in the model that searches self, here is an example:
def self.search(search)
if search
where('title LIKE ?', "%#{search}%")
else
scoped
end
end
I pass in search from the view, with a form like:
<div id="search_form">
<%= form_tag topic_links_path, :method => 'get', :id => "links_search" do %>
<%= text_field_tag :search, params[:search], :size => "35" %>
<%= submit_tag "Search", :name => nil %>
<% end %>
</div>
That way, when I make my collection in the controller, I can call .search(params[:search]) to get only the items that are like whatever the user entered in the form. I'm using the vote_fu gem for handling the votes/voteables.

You need to use map instead of each:
#favorites = current_user.votes.map {|vote| vote.voteable }
each simply loops through the elements and performs the operation on them, but it doesn't return the result in an array format. That's what map does.
On a side note, you can use a scope for search instead of a function. It might be a little cleaner:
scope :search, lambda{ |title| where('title LIKE ?', "%#{title}%") unless title.blank? }

Related

Ruby on Rails - Search through records with multiple attributes

I have written some code to search via a couple of attributes in all my Recipe records. The code works, but I would like some input on if it's ok, or how to make it better/faster.
I have a Recipe model with various attributes including name:string and ingredients:[array of integers] (postgres database). The ingredients are the ID's of a separate model Ingredient. This is a learning experience, I don't want to use any gems.
My form
index.html.erb
<%= form_tag recipes_path, :method => 'get' do %>
<p>
<%= text_field_tag :search, params[:search] %>
<%= collection_select( :ingredients, :ingredient_ids, Ingredient.all, :id, :name, {:include_blank => false, include_hidden: false}, { :multiple => true } ) -%>
<%= submit_tag "Search" %>
</p>
<% end %>
recipes_controller.rb
def index
#recipes = Recipe.search(params[:search], params[:ingredients])
end
recipe.rb
def self.search(search, ids)
array = []
if search && !search.empty?
meals = where('name ILIKE ?', "%#{search}%")
meals.each do |meal|
array.push(meal)
end
if ids && !ids.empty?
ingredients(array, ids)
else
return array
end
elsif ids && !ids.empty?
ingredients(all, ids)
else
all
end
end
def self.ingredients(meals, ids)
newarray = []
if ids
meals.each do |me|
a = me.ingredients
b = ids[:ingredient_ids].map(&:to_i)
if (b - a).empty?
newarray.push(me)
end
end
return newarray
else
return meals
end
end
This works fine at the moment as I don't have many records, but I don't trust that it'll be very fast if I had hundreds or thousands of records. Any advice on improving things?
If you know you're going to be searching on one or more columns frequently, try adding a database-level index for those columns. Without an index, any search will be O(n) time where n is the number of records. However, if you use an index, a search will be O(log(n)) time, because with the data ordered by your search column, you can binary search through it.
Take a look at http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/ for more information at how to check if your query performance can be improved.
Two other things to consider performance-wise:
1) For your all condition, you might be returning waaaay more records than you want. You may want to consider using pagination (I know you mentioned no other gems, but there are some great gems for pagination out there).
2) If you do actually want to return a ton of records at once, consider using ActiveRecord's batching (I usually use #find_each) to ensure you don't load everything into memory at once and end up OOM-ing.

How do I use a button to update the order of a filtered category?

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

ruby on rails search form

I'm new to RoR and I've managed to make a basic search form but keep getting errors when trying to expand the search tags (name).. I have a model with various data (location, website, email, telephone) and was wondering how I can add these to my current search code.
/models/ciir.rb
def self.search(search)
if search
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
else
find(:all)
end
end
static_pages_controller.rb
def home
#ciirs = Ciir.search(params[:search])
end
/home.html.erb
<%= form_tag ciirs_path, :method => 'get' do %>
<p>
<%= text_field_tag :search, params[:search] %>
<%= submit_tag " Search Database Records ", :name => nil %>
</p>
<% end %>
When clicking the submit button (no search terms) the url is:
ciirs?utf8=✓&search=
but when modifying the name condition to something like 'website' the url changes to
ciirs?utf8=✓&search=&commit=+Search+Database+Records+ –
Since you mentioned you are new to RoR, I must share the way I learned RoR was reading, using and analyzing one issue at a time. I would suggest you to take a look at following points one at a time and try & learn how RoR treats them and how these fit your question:
How form_tag works?
How text_field_tag works?
Once you have understood form_tag, difference between text_field_tag and f.text_field?
How params objects are created, and it uses names of form controls?
How and when to use GET and/or POST form methods? Inadvertently, what are different types of method and when to use them?
How URL are used in the form_tag and what components are they made of?
Sprinkle a bit of knowledge of Ruby language by learning between Arrays and Hashes? In fact, learn Ruby as much as you can.
Answering your question,
/home.html.erb
<%= form_tag "/static_pages/home", :method => 'post' do %>
<p>
<%= text_field_tag "search[name]", params.has_key?("search") && params[:search].has_key?("name") ? params[:search][:name] : "" %>
<%= submit_tag " Search Database Records " %>
</p>
<% end %>
/models/ciir.rb
def self.search(search)
if search
find(:all, :conditions => ["name LIKE '%?%'", search[:name]])
else
find(:all)
end
end
So I modified your form, and told RoR about search params containing data for name.
params is a Hash (which is a key-value pair) having key named search, which further is a Hash having key named name.
The same principle is followed in the model code. We passed the Hash of key search to the function and in there, used the value of key named name.
I also updated the url in form_tag, to point it to home action of your controller. Assuming that you have added it to your routes.rb file, it usually follows the pattern controller_name/action_name or the function name action_name_controller_name_path or action_name_controller_name_url. Run rake routes command at your root directory to list out all paths in your application.
Also note, I used POST method instead of original GET. You may wish to use GET here, so please change it back.
I hope this works.
I found no error in your code. the url changed to ciirs?utf8=✓&search=&commit=+Search+Database+Records+ is normal. submit_tag generates a button named "commit" defaultly, it will be parsed in the params. I see you add :name => nil , it will fix the problem, the other part of your code needn't to be modified. I copied your code and tested it, it ran smoothly.

rails search form that works on all pages of application

I have a search form which appears on all pages because I want someone to be able to search and be redirected to search results regardless of where they are on the site. To achieve this i placed the form in a partial which i included in the application layout. It looks like this
<form class="form-search center">
<%=form_tag search_url, method: :get do%>
<%=text_field_tag :query, params[:query] ,{:class=>"input-xxlarge search-query"}%>
<button type="submit" class="btn">Search</button>
<%end%>
</form>
I have created a controller called SearchResults with an index action to display the results.
The named route looks like this
match '/search', to: 'search_results#index'
The search works perfectly fine on the search page, but cannot work anywhere else.My index action looks like this
class SearchResultsController < ApplicationController
def index
#restaurants = Restaurant.text_search(params[:query]).page(params[:page]).per_page(3)
end
end
I want to be able to redirect to this action whenever i carry out a search regardless of where i am in the application.The text_search method is defined in the restaurant model like so
def self.text_search(query)
if query.present?
where("restaurant_name ilike :q or description ilike :q", q: "%#{query}%")
else
scoped
end
It doesn't work in that nothing happens when i search but if i go to the search page it returns the results.
I have noticed something interesting. When I search from the home page for a term like eats. The url looks like this localhost:3000/?utf8=✓&query=eats and when I search from the real search page which works perfectly it looks like this localhost:3000/search?utf8=✓&query=eats. How can i get it point to the latter url regardless of where am searching from?
Its because you have a nested form.
<form class="form-search center">
<%=form_tag search_path do%>
Get rid of the form you have in raw HTML and just let Rails generate it via the helper in the 2nd line.
Why don't you do a proper routing entry for your search controller?
resources :search_results, :only => :index
And then in your search form you let it POST to searches_path. Then you don't have to fiddle around with this :match routing.
Edit
I also noticed that you GET your form. Technically it doesn't really matter, but it's not very clean. So maybe, you could do something like this:
resources :searches_results do
post :query
end
And then in your controller you simply have the query function.
Edit 2
And I think the real problem here is that you are having is that you have a form tag in a form tag. The first form tag gets evaluated and it probably points to /. Therefore it works in the controller itself, but not anywhere else.
for my case i use somethig like this: in view
<%= form_tag url_for(:controller => '/searchresults', :action => 'index') , :method => 'get' do %>
<%= text_field_tag :query, params[:query] ,{:class=>"input-xxlarge search-query"} %>
<%= submit_tag 'search' %>
<% end %>
and in model
q = "%#{params[:query]}%"
where("restaurant_name LIKE ? or description LIKE ?", q, q)

Rails - json.erb template

I have been trying to figure out a way to customize JSON with special fields, custom formats, etc etc. I have created an as_json and to_xml method in my model to formulate the object how I need. This works well but it is sloppy because some of my helper methods had to move into the model, because I need the formats in the helpers and model. I also think it is sloppy code and makes the model out of control.
I have been able to get a format with json.erb working but don't think it is working 100% correct either and the callback doesn't append either. Anyone get this working
Here is what I got so far.
api calls format.json
template called is items.json.erb
<% #items.each do |item| %>
<%= { :item => { :id => item.id, :name => item.name }.to_json.html_safe %>
<% end %>
This works but seems odd. Anyone have suggestions or have a way to do this?
btw did this for the callback to work
<%= params[:callback]+"(" if params[:callback] %>
<% #items.each do |item| %>
<%= { :item => { :id => item.id, :name => item.name }.to_json.html_safe %>
<% end %>
<%= ")" if params[:callback] %>
I think the best way to do this would be to skip the erb template if you don't absolutely need if for some reason. Then you could do something like this:
items = Item.all
render :json => items.to_json(:only => [:id, :name]), :callback => params[:callback]
You can override the to_json method in your model to add fields or call methods.
Based on your answer to polarblau, you should override the as_json method and use the :methods parameter to include method results in your json
class Item
def date
return "1 year and 8 months" #obviously use logic here
end
def as_json(args={})
super(:methods=>[:date], :only=>[:id=>:name])
end
end
Most likely, you'll want to either:
use custom finder sql to alter column names/perform calculations (it's much faster than Ruby):
MyModel.select('col_name_that_needs_renamed AS new_name').order('some_col DESC')
or a more complicated example:
MyModel.find_by_sql('SELECT col_name_that_needs_renamed AS new_name, foo_col*50 AS math WHERE foo=bar ORDER some_col LIMIT 8')
if there's something you can't do (or can't figure out) in SQL, you may have to revert to Ruby (although not recommended because it's significantly slower)
API Dock for to_json

Resources