Code in Railscasts 111 -Advanced Search Form - ruby-on-rails

In one particular Railcasts episode Ryan talks about advanced search and in that he uses some code so as to find the conditions for the search. As its working isn't explained I wanted some clarification regarding it.
def products
#products ||= find_products
end
private
def find_products
Product.find(:all, :conditions => conditions)
end
def keyword_conditions
["products.name LIKE ?", "%#{keywords}%"] unless keywords.blank?
end
def minimum_price_conditions
["products.price >= ?", minimum_price] unless minimum_price.blank?
end
def maximum_price_conditions
["products.price <= ?", maximum_price] unless maximum_price.blank?
end
def category_conditions
["products.category_id = ?", category_id] unless category_id.blank?
end
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
def conditions_clauses
conditions_parts.map { |condition| condition.first }
end
def conditions_options
conditions_parts.map { |condition| condition[1..-1] }.flatten
end
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
I would welcome any information as to how this works especially the method products as he even calls it as products.name etc.

He defines some methods for the conditions in his search form: keyword_conditions, minimum_price_conditions ans so on. products.name means the field name from the table products.
The method
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
uses reflection to look at the private methods of this class which have the name that ends with _conditions (The regex /_conditions$/) and joins only those that don't return null (compact)
The method
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
adds a AND keyword between the conditions and passes the result to Product.find which makes the SQL query and returns the result set.

Related

how to check if there are any results in a query and add an if/else condition

in my home_controller, I have to show several lists.
I have this:
def subscriptions
#movies = current_user.followed_movies
.limit(12)
.order('movies.last_news DESC NULLS LAST').decorate
end
def watched
#movies = current_user
.watched_movies
.order_by_watched_date
.limit(12).decorate
end
I want to add an if condition in the def subscriptions.
For example
def subscriptions
#movies = if this query has no results... current_user.followed_movies
.limit(12)
.order('movies.last_news DESC NULLS LAST').decorate
else
to show the movies in the def watched
end
end
How to do?
it's not clear exactly what you're looking for, but I think you mean:
"if the subscriptions query is empty, use the watched query instead".
I'd probably do that like this:
def set_movies
#movies = subscriptions
#movies = watched if subscriptions.empty?
#movies = #movies.limit(12).decorate
end
def subscriptions
current_user.followed_movies.order_by_last_news
end
def watched
current_user.watched_movies.order_by_watched_date
end
and then in user.rb I might add:
scope :order_by_last_news, -> { order('movies.last_news DESC NULLS LAST') }
We can create scopes but for the simplicity, two separate methods can be created as below:
def movies_followed
current_user.followed_movies
end
def movies_watched
current_user.watched_movies
end
And then we can use those two methods in the below def subscriptions as below:
def subscriptions
#movies =
if movies_followed
movies_followed.limit(12).order('movies.last_news DESC NULLS LAST').decorate
else
movies_watched.order_by_watched_date.limit(12).decorate
end
end
Hope, it suites your requirement...

undefined method `where' for nil:NilClass when trying to do an advanced search ruby

I am trying to set up an advanced search on the contracts section of my landlord management application.
So far i have followed the following tutorial but still not able to get anywhere.
The search form displays correctly but as soon as i search anything i get error mentioned in title.
app/models/search.rb:11:in search_contracts'
app/views/searches/show.html.haml:3:in_app_views_searches_show_html_haml__746758187_75961032'
search.rb
class Search < ActiveRecord::Base
def search_contracts
#contracts = Contract.all
contracts = contracts.where(["first_name LIKE ?", first_name]) if first_name.present?
contracts = contracts.where(["last_name LIKE ?", last_name]) if last_name.present?
contracts = contracts.where(["balance >= ?", min_balance]) if min_balance.present?
contracts = contracts.where(["balance >= ?", max_balance]) if max_balance.present?
contracts = contracts.where(["unpaid_rent LIKE ?", unpaid_rent]) if unpaid_rent.present?
return contracts
end
end
search_controller
class SearchesController < ApplicationController
def new
#search = Search.new
end
def create
#search = Search.create(search_params)
redirect_to #search
end
def show
#search = Search.find(params[:id])
end
private
def search_params
params.require(:search).permit(:first_name, :last_name, :min_balance,
:max_balance, :unpaid_rent)
end
end
You instantiate #contracts, but not contracts:
class Search < ActiveRecord::Base
def search_contracts
#contracts = Contract.all
contracts = contracts.where(["first_name LIKE ?", first_name]) if first_name.present?
contracts = contracts.where(["last_name LIKE ?", last_name]) if last_name.present?
contracts = contracts.where(["balance >= ?", min_balance]) if min_balance.present?
contracts = contracts.where(["balance >= ?", max_balance]) if max_balance.present?
contracts = contracts.where(["unpaid_rent LIKE ?", unpaid_rent]) if unpaid_rent.present?
return contracts
end
end
So, contracts.where will throw the undefined method error since contracts is nil.
Also, it seems a little weird that Search inherits from ActiveRecord::Base based on the code you've posted. But, maybe there's more going on than meets the eye.

How can I get last ActiveRecord query data?

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

Help me refactor ruby next code

#people = People.scoped
#people = #people.where(...) if ...
#people = #people.where(...) if ...
#people = #people.where(...) if ...
#people = #people.where(...) if ...
Is any ruby existing solutions to make something like
#people = People.scoped
#people.???? do
where(...) if ...
where(...) if ...
where(...) if ...
end
PS: Thanks for answers. But solutions you provide looks like
def self.conditional_scope
where(...) if ...
where(...) if ...
where(...) if ...
end
I think i'll get only last where even if all "if" is true.
Am i right?
I think you should get yourself familiar with named_scopes:
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html
They are composable, so you can write something like:
People.tall.having_children.older_than(30)
where "tall", "having_children" and "older_than" are named scopes.
If I understand what you are asking, you only want to apply each scope if a condition exists... you could use a named scope with a lambda, and then chain them:
scope :one, lambda {|condition| condition ? where(...) : {}}
scope :two, lambda {|condition| condition ? where(...) : {}}
...
#people = Person.one(true).two(false)
def self.conditional_scope
where(...) if ...
where(...) if ...
where(...) if ...
end
Then:
Model.conditional_scope
Yes. You just need to move it to model:
# Controller
#people = People.find_my_guy
# Model
def self.find_my_guy
where(...) if ...
where(...) if ...
where(...) if ...
end
Obviously, you'll need to pass some environment variable to your model if they are used in your statements:
# Controller
#people = People.find_my_guy(params)
# Model
def self.find_my_guy(params)
where(:id => params[:id]) if params[:id]
where('title LIKE (?)', "%#{params[:search]}%") if parmas[:search]
where(...) if ...
end
As far as you're right about last where I can suggest only method chaining here (simmilar as #socjopata did(:
# Model
def self.with_name(name)
where(:name => name) if name.present?
end
def self.with_id_gt(id)
where('id >= ?', id) if id.to_i > 3
end
# Controller
Post.with_name(parms[:name]).with_id_gt(params[:id])
Maybe you're looking for a way to avoid explicitly assigning the new scope after every where clause? You might be interested in this railscast: http://railscasts.com/episodes/212-refactoring-dynamic-delegator. Ryan Bates uses a delegator to achieve code like this:
def self.search(params)
products = scope_builder
products.where("name like ?", "%" + params[:name] + "%") if params[:name]
products.where("price >= ?", params[:price_gt]) if params[:price_gt]
products.where("price <= ?", params[:price_lt]) if params[:price_lt]
products
end

refactor a method with a block that contains multiple blocks itself

I'm using Ruby 1.9.2
I have a class method called search that takes a block
e.g.
class MyClass
def self.search do
if criteria1
keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
if criteria2
keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
What I'd like to do is refactor the MyClass.search method and have a simple one-line method for each if/end statement
e.g. it would look something like this:
class MyClass
def self.search do
c1_method
c2_method
end
def self.c1_method
if criteria1
return keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
end
def self.c2_method
if criteria2
return keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
But the refactoring that I show above doesn't quite work. It looks like the "blocks" that I'm returning in c1_method and c2_method aren't really being returned and evaluated in the search method, but I'm not sure how to do that.
Well, you can use the method(sym) call in order to get at the body of a method.
>> def foo(bar); bar * 2; end
=> nil
>> def baz(bleep); method(:foo).call(bleep); end
=> nil
>> baz(6)
=> 12

Resources