I'm new to Ruby on Rails, so you forgive myself if the question is silly ...
I have a table and two forms, the first form is to filter (through checkboxes) and the second one is to search into the table.
What I would like is to search into the table with the filters already applied. I do not really know to do... how I can access from the controller search method to "already filtered" data.
Source code:
prices_controller.rb
def filter()
#prices = Price.where(date: Date.today).order(price: :asc)
if params[:filter1] != nil
#prices = #prices.where('filter1_field IN (?)', params[:filter1])
end
if params[:filter2] != nil
#prices = #prices.where('filter2_field IN (?)', params[:filter2])
end
respond_to do |format|
format.js
end
end
def search()
# Here I would like to use filtered prices, not all prices
#prices = Price.where(date: Date.today).order(price: :asc)
if params[:search] != nil
#prices = #prices.where('name LIKE ?', "%#{params[:search]}%")
end
end
It works, but I am not using filtered values...
Are there any way to save the last query or something like that. I try to return to the search method the ActiveRecord::Relation but method where does not exists in this class. Other alternative is use the same form to filtering and searching.
I accept any kind of recommendation.
Thanks a lot.
You can't do this directly because
The request that applies the filters and the request that triggers the search are 2 different requests.
Rails initializes a new controller instance for each request, so the controller for filtering and the controller for searching are 2 different objects. They don't share any instance variables.
You can however store the IDs returned by the filter phase in session, and use those IDs in search phase.
def filter
#prices = Price.where(date: Date.today).order(price: :asc)
if params[:filter1] != nil
#prices = #prices.where('filter1_field IN (?)', params[:filter1])
end
if params[:filter2] != nil
#prices = #prices.where('filter2_field IN (?)', params[:filter2])
end
session[:price_ids] = #prices.pluck(:id)
respond_to do |format|
format.js
end
end
def search
#prices = session[:price_ids].present? ?
Price.where(id: session[:price_ids]) :
Price.all
#prices = #price.where(date: Date.today).order(price: :asc)
if params[:search] != nil
#prices = #prices.where('name LIKE ?', "%#{params[:search]}%")
end
end
This approach has a drawback that you have to remove price IDs from sessions at the proper time, which can be hard do define.
Another approach is to ensure the browser sending the filter every time it requests a search (unless your Rails app is an API for single page application). This approach requires a redirect whenever filter is changed.
def filter
redirect_to search_path(params.slice(:filter1, :filter2))
end
def search
#prices = Price.where(date: Date.today).order(price: :asc)
if params[:filter1] != nil
#prices = #prices.where('filter1_field IN (?)', params[:filter1])
end
if params[:filter2] != nil
#prices = #prices.where('filter2_field IN (?)', params[:filter2])
end
if params[:search] != nil
#prices = #prices.where('name LIKE ?', "%#{params[:search]}%")
end
end
And your search form should contain 2 hidden fields for the filters
<%= form_tag '/prices/search', method: :get do %>
<%= hidden_field_tag :filter1, params[:filter1] %>
<%= hidden_field_tag :filter2, params[:filter2] %>
<!-- other form stuff ... -->
<% end %>
There's a very bad bug here: your IN is broken.
ActiveRecord does SQL sanitization (to prevent SQL injection), so when you write:
query.where("a in ?", "hey")
You get WHERE a in "hey" on the SQL side.
It's the same in your query: your IN's parameter will get stringified.
If your filter1 is a,b,c, the query will look like this:
WHERE a iN ("a,b,c")
Which is not what you want at all.
ActiveRecord is able to generate IN when you pass an array. So this:
query.where(a: [1, 2, 3])
will generate:
WHERE q IN (1, 2, 3)
which is what you want!
Now, with regards to the code itself...
The simplest way to do what you want is to move the code to another method, and reuse it in both places:
def filter()
#prices = build_filter_query(Date.today, params[:filter1], params[:filter2])
respond_to do |format|
format.js
end
end
def search()
#prices = build_filter_query(Date.today, params[:filter1], params[:filter2], params[:search]) #pass in an extra param here!
end
private
def build_filter_query(date, filter1, filter2, search = nil)
# use a local variable here
prices = Price.where(date: Date.today).order(price: :asc)
if filter1
prices = prices.where(filter_field1: filter1)
end
if filter2
prices = prices.where(filter2_field: filter2)
end
if search
prices = prices.where('name LIKE ?', "%#{search}%")
end
return prices
end
Related
My idea for an #index method of a controller is to set things = Thing.all and then if there are filter params, check for them one by one and chain them on so that at the end, you're left with a single query to execute. But the following queries get executed as they are called:
def things_controller
def index
things = Thing.all #<-- db call #1
if params[:color]
things = things.where(color: params[:color]) #<-- db call #2
end
render json: things #<-- I would like to make a single db call here instead
end
end
How can I prevent multiple unnecessary db calls? Is there some convention for filter params that I should be using?
You just need to reorganise the code like this:
def index
things = if params[:color]
Thing.where(color: params[:color])
# ...else if
else
Thing.all
end
render json: things
end
Updated
If you want to chain where clauses, do this:
def index
valid_params_keys = %w(color size)
filtered_keys = valid_params_keys.select { |key| params.keys.include?(key) }
# Don't need conditional check anymore :).
#products = filtered_keys.inject(Product.all) do |scope, key|
scope.where(key => params[key])
end
end
Since things is an array, you can do this, which is only an array operation.
def index
things = Thing.all
if params[:color]
things = things.select!{ |thing| thing.color == params[:color]}
end
render json: things
end
def index
#users = User.all.paginate(page: params[:page])
#users = User.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
#users = User.countryname(params[:country]).paginate(page: params[:page]) if params[:country].present?
#users = User.gender(params[:gender_type]).paginate(page: params[:page]) if params[:gender_type].present?
end
The following code works fine if only :name or :country or :gender_type is present. But it does not work if multiple params are present. What is the DRY way of writing this code for multiple params? Obviously, I do not want to create a different line of code for each possible combination of params.
Here are the scopes:
class User
scope :countryname, -> (country) { where("country ILIKE ?", "%#{country}%")}
scope :gender, -> (gender_type) { where gender_type: gender_type}
scope :named, -> (name) { where("name ILIKE ?", "%#{name}%")}
If I have a query string of
example.com/users?name=sam&gender_type=male
it simply returns all users with names like sam and ignores their gender... I would need to code:
#users = User.gender(params[:gender_type]).named(params[:name]).paginate(page: params[:page]) if params[:gender_type] && params[:name].present?
but I do not want to have to write a new line of code for every single combination of parameters.
You could use the ruby try method. For example, you could write something like
#users = User.try(:gender, params[:gender_type]).try(:paginate, page: params[:page])
Look at try in api docs for other ways to use it.
the problem was the code should be
#users = User.all.paginate(page: params[:page])
#users = #users.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
etc
Rails will then allow chain scoping automatically. the previous code creates separate independent non-chained instance variables.
I'm putting filtering functionality into an application (Rails 4.1beta) - I did this by creating scopes on the Item model, passing a scope through the request params and doing a case statement in the index action. It all works but there's a code smell I'm trying to get rid of in the index action of one of the controllers;
def index
case params[:scope]
when "recent"
#items = Item.recent
when "active"
#items = Item.active
when "inactive"
#items = Item.inactive
else
#items = Item.all
end
end
It all feels a little too rigid / verbose. I'd really like to just do something like this;
def index
#items = Item.send(params[:scope])
end
but then I leave the application wide open to people calling methods on the Item class. Whacking conditions in there kinda defeats the point of what I'm trying to achieve.
Is there some rails magic I'm missing that can help me here?
You can use different controllers to do each of these.
inactive_items_controller.rb
def index
#items = Item.inactive
end
recent_items_controller.rb
def index
#items = Item.recent
end
etc.
Or you can just move you logic you have above to the model
Item model
def self.custom_scope(scope)
case scope
when "recent"
Item.recent
when "active"
Item.active
when "inactive"
Item.inactive
else
Item.all
end
end
or
def self.custom_scope(scope)
recent if scope == 'recent'
active if scope == 'active'
inactive if scope == 'inactive'
scoped if scope.blank?
end
And then in your index
#items = Item.custom_scope params[:scope]
something like this:
if ['recent', 'active', 'inactive'].include?(params[:scope])
#items = Item.send(params[:scope])
else
#items = Item.all
end
UPDATE: I've found a way that works, though it's not very flexible, by taking the params hash and comparing its keys to my model's column names. I then take that array of names and map it to a hash, that I then use in my ActiveRecord query. There must be a better way?
def index
if params
hash = {}
attributes = User.column_names & params.keys
attributes.each do |attribute|
hash.merge!(attribute.to_sym => params[attribute.to_sym])
end
#users = User.where(hash)
else
#users = User.all
end
respond_with #users
end
BACKGROUND: I've hooked up an Ember app to a Rails JSON API and have figured out how to query the database using Ember-Data. Below is an example in Coffeescript:
App.UsersRoute = Ember.Route.extend
model: ->
# Step 1: Query database for all users
#store.find('user')
# Step 2: Filter results (keep male users named "Steve")
#store.filter 'user', (user)->
user.get('name') == "Steve" && user.get('gender') == "Male"
OBJECTIVE: I'm wondering if this is the best way to go about this? Wouldn't querying for all users get increasingly difficult as the number of users increases?
I'm thinking a good alternative would be to include the query as parameters on my initial query, like so:
#store.find 'user', {name: "Steve", gender: "Male"}
# Sends JSON request to /users.json?name=Steve&gender=Male
If this is a better approach, I am stumped as to how to make Rails take these two parameters and query the database for them. Below is my Rails controller that responds to the above request:
class Api::V1::UsersController < ApplicationController
respond_to :json
def index
respond_with User.all
end
end
In order to accommodate the above request, I'd have to do something like this:
def index
respond_with User.where(name: params[:name], gender: params[:gender])
end
But this would not accommodate any additional queries, or queries that don't have both of these params set. Which of these two approaches is best?
You can try doing like this, it allows you to customize your where and other clauses depending upon input params:-
def index
#user = User
if params[:name].present? && params[:gender].present?
#user = #user.where(name: params[:name], gender: params[:gender])
end
#user = #user.all
.....
end
So, I'm building an educational resource search engine Rails app. This app will display results (e.g. Calculus videos) in a block-type layout meant to leverage Isotope (for cool filtering/sorting transitions and the like).
In addition to querying our database for appropriate Resources, we are also querying Google via their Custom Search Engine API. On top of that, eventually we will want to place some ad blocks into the grid view.
So my question is, how can I combine the #resources returned from querying our database with the JSON results of the Google query and also eventually with ads? I want to do this to make the results view (search.html.erb) as clean as possible. Additionally, I want to be able to sort/filter all of the results. That is, I'd like to be able to merge the ActiveRecord results with the Google query results. This would also allow me to do things like:
(Boxx is the generic class I'm thinking of)
<% #boxxes.each do |boxx| %>
<div class=<%= boxx.type %>>
<h2><%= boxx.title %></h2>
<h3><%= boxx.description %></h3>
...
...
...
</div>
<% end %>
My Resource controller is below. Basically, I want to combine the #resource with the results of a Google query into one enumerable with a common interface and that I can sort according to the sort type specified by the user.
What is the best way to go about this? Should I create a Boxx class at the bottom of the controller and be able to initialize it with either a Resource, google JSON or Ad? Then I could keep a type variable and then be able to sort them all together?
Here's my Resource controller
require 'will_paginate/array'
class ResourcesController < ApplicationController
def index
#resources = Resource.all
end
def create
# Usability concern here... need to make sure that they are redirected back here once they log in or something
if current_user == nil
flash[:alert] = "You must log in to submit a resource!"
redirect_to resources_path
return
else
params[:resource][:user_id] = current_user.id
end
# Check to see if this resource unique
params[:resource][:link] = Post::URI.clean(params[:resource][:link])
if unique_link?(params[:resource][:link])
#resource = Resource.new(params[:resource])
#resource[:youtubeID] = self.isYoutube(#resource[:link])
#resource.save
else
flash[:alert] = "This resource has already been added!"
end
redirect_to resources_path
end
def vote
value = params[:type] == "up" ? 1 : -1
#resource = Resource.find(params[:id])
#resource.add_or_update_evaluation(:votes, value, current_user)
respond_to do |format|
format.html { redirect_to :back, notice: "Thank you for voting" }
format.json { render :status=>200, :json=>{:success=>true}}
end
end
def isYoutube(youtube_url)
regex = %r{http://www.youtube.com}
if youtube_url[regex]
youtube_url[/^.*((v\/)|(embed\/)|(watch\?))\??v?=?([^\&\?]*).*/]
youtube_id = $5
thumbnail_Link = "http://img.youtube.com/vi/#{youtube_id}/1.jpg"
else
thumbnail_Link = nil
end
thumbnail_Link
end
def unique_link?(url)
Resource.find_by_link(url) == nil
end
def search
#resource = Resource.full_search(params[:q])
# raise params.to_s
#resource = #resource.reject!{|r| !r.media_type.eql? params[:filter][0][:media_type].downcase } if params[:filter] && !params[:filter][0][:media_type].blank?
if params[:filter]
case params[:filter][0][:sort].downcase
when 'newest'
then #resource = #resource.sort_by{|r| r.created_at}
when 'votes'
then #resource = #resource.sort_by!{|r| r.reputation_for(:votes).to_i}.reverse
else
end
end
#resource = #resource.paginate(:page => (params[:page] || 1), :per_page => 15)
end
def google(q, filter)
# Authenticating into Google's API
client = Google::APIClient.new(:key => 'secret', :authorization => nil)
# Discover the Custom Search API
search = client.discovered_api('customsearch')
# Search Google CSE
response = client.execute(
:api_method => search.cse.list,
:parameters => {
'q' => "#{q} #{filter}",
'key' => 'secret',
'cx' => 'secret'
}
)
# Decode the results
results = ActiveSupport::JSON.decode(response.body, {:symbolize_names => true})
# Return an empty array if Google CSE limit has been met.
results["items"] == nil ? [] : results["items"]
end
def make_boxxes(resources, google_results, ads)
end
end
EDIT #1: Wait, can just make a GoogleResult class, then do
#items = #resources | google_results
?
Because I could just make GoogleResult follow the same interface as Resources. But then how do I sort them? Hmmm...
I found what I needed by reading about "Designing Helpers in Ruby on Rails"
http://techspry.com/ruby_and_rails/designing-helpers-in-ruby-on-rails/