I have five dropdowns, and I need to put conditions on each of them. My code is:
def search(search, compare, year, rain_fall_type)
if search == 'All'
if rain_fall_type == 'All'
all
else
if year == 'All'
if rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
# all
where(Sector: rain_fall_type).order('id')
end
else
if rain_fall_type == "All"
order("#{year} ")
elsif rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
where(Sector: rain_fall_type).order("#{year} ")
end
end
# where(Year: year).order("#{rain_fall_type} ")
end
elsif compare != "None"
if year == 'All'
where('Sector = ? OR Sector = ?', rain_fall_type, compare).order(:id)
else
where('Sector = ? OR Sector = ?', rain_fall_type, compare).order(:id)
end
else
if rain_fall_type == 'All'
all.order('id')
else
if year == 'All'
if rain_fall_type == "None"
where('Sector = ? ', search).order('id')
else
where('Sector = ? ', rain_fall_type).order('id')
end
else
if rain_fall_type == "None"
if search == "All"
where('Sector = ? ', search).order('id')
else
where('Sector = ? ', search).order('id')
end
else
# all
where('Sector = ? ', rain_fall_type).order('id')
end
end
end
end
end
It has many if and else. I am trying to minimise the conditions. What can be the best way to shrink this code? Someone suggested that I should use switch case instead. Should I use it? If so, how?
You can use a guard statement which is basically return something if some_condition?. This only doable in specific scenarios (where one of the condition is executing a single statement:
Bad example:
if condition?
do_something
else
do_something_else
end
This could be written as:
return do_something if condition?
do_something_else
This will give you less code branching.
Also, another recommendation is to call another method with more conditions instead of nesting conditions in one single shot.
Bad example:
if condition?
if condition_two?
do_something_two
else
do_something
end
else
do_something_else
end
This could be written as:
if condition?
call_another_method
else
do_something_else
end
def call_another_method
if condition_two?
do_something_two
else
do_something
end
end
An example from your code could be:
if rain_fall_type == 'All'
all
else
if year == 'All'
if rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
# all
where(Sector: rain_fall_type).order('id')
end
else
if rain_fall_type == "All"
order("#{year} ")
elsif rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
where(Sector: rain_fall_type).order("#{year} ")
end
end
end
That could be converted to:
return all if rain_fall_type == 'All'
if year == 'All'
return where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id') if rain_fall_type == "None"
where(Sector: rain_fall_type).order('id')
else
return order("#{year} ") if rain_fall_type == "All"
return where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id') if rain_fall_type == "None"
where(Sector: rain_fall_type).order("#{year} ")
end
I hope this could help :)
NOTE: This is to answer the original question of How to simplify big conditions?. But the original post is not following Rails/Ruby way of doing search and filters and not making a good use of scopes.
This is probably the best explanation of how you should set this up.
class Product < ActiveRecord::Base
# custom_scope_1
scope :status, -> (status) { where status: status }
# custom_scope_2
scope :location, -> (location_id) { where location_id: location_id }
# custom_scope_3
scope :search, -> (name) { where("name like ?", "#{name}%")}
end
def index
#products = Product.where(nil) # creates an anonymous scope
#products = #products.status(params[:status]) if params[:status].present?
#products = #products.location(params[:location]) if params[:location].present?
#products = #products.search(params[:search]) if params[:search].present?
end
This can be cleaned up further by...
def index
#products = Product.where(nil)
filtering_params(params).each do |key, value|
#products = #products.public_send(key, value) if value.present?
end
end
private
# A list of the param names that can be used for filtering the Products
def filtering_params(params)
params.slice(:status, :location, :search)
end
This method uses ruby meta-programming to loop through parameters and dynamically call predefined scopes on a model
You can move this code into a module and include it into any model that supports filtering
app/models/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
end
end
app/models/product.rb
class Product
include Filterable
...
end
app/controllers/product_controller.rb
def index
#products = Product.filter(params.slice(:status, :location, :search))
end
You now have filtering and searching of your models with one line in the controller and one line in the model
First of all, some of your logic doesn't make sense:
def search(search, compare, year, rain_fall_type)
if search == 'All'
if rain_fall_type == 'All'
all
else
# rain_fall_type != 'All'
if year == 'All'
if rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
where(Sector: rain_fall_type).order('id')
end
else
# in rain_fall_type != 'All' branch, so meaningless 'if'
if rain_fall_type == "All"
order("#{year} ")
elsif rain_fall_type == "None"
where('Sector = ? OR Sector = ? OR Sector = ?', "Primary", 'Secondary', 'Tertiary').order('id')
else
where(Sector: rain_fall_type).order("#{year} ")
end
end
end
elsif compare != "None"
# both are same, so meaningless 'if'
if year == 'All'
where('Sector = ? OR Sector = ?', rain_fall_type, compare).order(:id)
else
where('Sector = ? OR Sector = ?', rain_fall_type, compare).order(:id)
end
else
# search != 'All'
if rain_fall_type == 'All'
all.order('id')
else
if year == 'All'
if rain_fall_type == "None"
where('Sector = ? ', search).order('id')
else
where('Sector = ? ', rain_fall_type).order('id')
end
else
if rain_fall_type == "None"
# in search != 'All' branch, so meaningless 'if'
# AND both are same, so again meaningless 'if'
if search == "All"
where('Sector = ? ', search).order('id')
else
where('Sector = ? ', search).order('id')
end
else
where('Sector = ? ', rain_fall_type).order('id')
end
end
end
end
end
There's more like that and I won't point it all out because we're throwing all that if stuff out, anyway.
Ultimately, we're going to defer the querying to the end of the method, like this:
def search(search, compare, year, rain_fall_type)
...
#query = all
#query = #query.where(Sector: #sectors) if #sectors
#query = #query.order(#order) if #order
#query
end
That way, you take all of your where and order statements, and do them only once at the end. That saves a lot of typing right there. See the comment from muistooshort for why (Sector: #sectors) works.
So, the trick is setting #sectors and #order. First, I'm going to assign the input variables to instance variables because I like it like that (and to avoid confusion between the variable #search and the method search):
def search(search, compare, year, rain_fall_type)
#search, #compare, #year, #rain_fall_type = search, compare, year, rain_fall_type
...
#query = all
#query = #query.where(Sector: #sectors) if #sectors
#query = #query.order(#order) if #order
#query
end
Now, this answer is going on too long already, so I won't drag you through all the gorey details. But, adding in a couple of helper methods (sectors_to_use, and order_to_use) and substituting them in for #sectors and #order, you basically end up with this:
def search(search, compare, year, rain_fall_type)
#search, #compare, #year, #rain_fall_type = search, compare, year, rain_fall_type
#query = all
#query = #query.where(Sector: sectors_to_use) if sectors_to_use
#query = #query.order(order_to_use) if order_to_use
#query
end
private
def sectors_to_use
return [#rain_fall_type, #compare] if #search != 'All' && #compare != 'None'
unless #rain_fall_type == 'All'
if #rain_fall_type == 'None'
#search == 'All' ? ['Primary', 'Secondary', 'Tertiary'] : [#search]
else
[#rain_fall_type]
end
end
end
def order_to_use
return nil if (#search == 'All') && (#rain_fall_type == 'All')
return #year if (#search == 'All') && !(#year == 'All')
return :id
end
That's less than half the lines of code, over a thousand fewer characters, and a whole lot fewer ifs.
module ApplicationHelper
def sortable(column, title = nil)
title ||= column
css_class = column == sort_column ? "current #{sort_direction}" : nil
direction = column == sort_column && sort_direction == "asc" ? "desc" : "asc"
sort_arrow = ""
if(column == sort_column)
if(direction == "desc")
sort_arrow = "▼"
elsif(direction == "asc")
sort_arrow = "▲"
end
end
link_to sort_arrow+title, {:sort => column, :direction => direction,
:search_form => #search_form.attributes
}, {:class => css_class}
end
end
this above is the method I would like to test, the problem is that the sort_column method is on the controller, therefore I get an error message method does not exist. How do I get rid of it or how do I engage the Controller to execute the method?
Edit:
Controller:
class ResultsController < ApplicationController
def sort_column
params[:sort] != nil ? params[:sort] : 'ID'
end
end
Easy way: move the sortable method to ResultsController with a helper_method declaration.
class ResultsController < ApplicationController
helper_method :sortable
def sort_column
params[:sort] != nil ? params[:sort] : 'ID'
end
def sortable(column, title = nil)
title ||= column
css_class = column == sort_column ? "current #{sort_direction}" : nil
direction = column == sort_column && sort_direction == "asc" ? "desc" : "asc"
sort_arrow = ""
if(column == sort_column)
if(direction == "desc")
sort_arrow = "▼"
elsif(direction == "asc")
sort_arrow = "▲"
end
end
link_to sort_arrow + title,
{:sort => column, :direction => direction,
:search_form => #search_form.attributes},
{:class => css_class}
end
end
Im newbie on ruby on rails)
Ok, I have controller with index action:
def index
condition = ['fulltime','parttime','remote','forone']. select{ |t| params.has_key?(t.to_sym) }.join(' | ')
Advert.search params[:search],:order => :created_at, :sort_mode => :desc, :conditions => {:employment_type=>condition}
But it peace of code:
['fulltime','parttime','remote','forone']. select{ |t| params.has_key?(t.to_sym) }
return nil
Why?:))
I don't want to write code with a bunch of checks like:
if !fulltime.nil? && !fulltime.blank?
condition = "fulltime"
end
if !parttime.nil? && !parttime.blank?
if !condition.nil? && !condition.blank?
condition = condition + " | parttime"
else
condition ="parttime"
end
end
But my way with array.select method is not working:(
Could you give me some advices?) Thanks!
I tried your code on irb as follows:
> params = { :fulltime => "1" }
=> {:fulltime=>"1"}
> ['fulltime','parttime','remote','forone']. select{ |t| params.has_key?(t.to_sym) }.join(' | ')
=> "fulltime"
So it is working fine. If you are getting nil from that code, then the params are being set to nil, or the params don't have any of ['fulltime','parttime','remote','forone'] as keys.
Check out Rails params explained? thread; likely to give an idea as to how to set the params correctly.
If i change the processing from client-side to server-side, i will get all information for the table, but I can't search and sort the columns. But its possible to go to the next page. I have only 2 columns for searching and sorting to test it. Hopefully you can help me.
Database:
t.text "comment"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "source_stock_id"
t.integer "destination_stock_id"
t.integer "order_id"
js.coffee-Code:
jQuery ->
$("#product_relocates_table").dataTable
bProcessing: true
bServerSide: true
sAjaxSource: $('#product_relocates_table').data('source')
"aaSorting": [[ 0, "desc" ]]
Datatable-Code:
class ProductRelocatesDatatable
delegate :params, :h, :link_to, to: :#view
def initialize(view)
#view = view
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: ProductRelocate.count,
iTotalDisplayRecords: product_relocates.total_count,
aaData: data
}
end
private
def data
product_relocates.map do |product_relocate|
[
h(product_relocate.created_at),
h(product_relocate.comment),
h(product_relocate.source_stock),
h(product_relocate.destination_stock),
h(product_relocate.quantity),
link_to('Show', [:admin, product_relocate])
]
end
end
def product_relocates
#product_relocates ||= fetch_product_relocates
end
def fetch_product_relocates
product_relocates = ProductRelocate.order("#{sort_column} #{sort_direction}")
product_relocates = product_relocates.page(page).per(per)
if params[:sSearch].present?
search_string = search_columns.map do |search_column|
"#{search_column} like :search"
end.join(" OR ")
product_relocates = product_relocates.where(search_string, search: "%#{params[:sSearch]}%")
end
product_relocates
end
def page
params[:iDisplayStart].to_i/per + 1
end
def per
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def search_columns
%w[product_relocates.created_at product_relocates.comment]
end
def sort_columns
%w[product_relocates.created_at product_relocates.comment]
end
def sort_column
sort_columns[params[:iSortCol_0].to_i]
end
def sort_direction
params[:sSortDir_0] == "desc" ? "desc" : "asc"
end
end
I refactored a superclass that handles server side multi-column searching and sorting:
https://gist.github.com/2936095
which is derived from:
http://railscasts.com/episodes/340-datatables
class Datatable
delegate :params, :h, :raw, :link_to, :number_to_currency, to: :#view
def initialize(klass,view)
#klass = klass
#view = view
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: #klass.count,
iTotalDisplayRecords: items.total_entries,
aaData: data
}
end
private
def data
[]
end
def items
#items ||= fetch_items
end
def fetch_items
items = filtered_list
items = selected_columns(items)
items = items.order(sort_order)
items = items.page(page).per_page(per_page)
if params[:sSearch].present?
items = items.where(quick_search)
end
items
end
def filtered_list
#klass.all
end
def selected_columns items
items
end
def quick_search
search_for = params[:sSearch].split(' ')
terms = {}
which_one = -1
criteria = search_for.inject([]) do |criteria,atom|
which_one += 1
terms["search#{which_one}".to_sym] = "%#{atom}%"
criteria << "(#{search_cols.map{|col| "#{col} like :search#{which_one}"}.join(' or ')})"
end.join(' and ')
[criteria, terms]
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def columns
[]
end
def sort_order
colnum = 0
sort_by = []
while true
break if !sorted?(colnum)
sort_by << "#{sort_column(colnum)} #{sort_direction(colnum)}"
colnum += 1
end
sort_by.join(", ")
end
def sorted? index=0
!params["iSortCol_#{index}"].nil?
end
def sort_column index=0
index = "iSortCol_#{index}"
columns[params[index].to_i]
end
def sort_direction index=0
index = "sSortDir_#{index}"
params[index] == "desc" ? "desc" : "asc"
end
end
I have a call on my posts_controller.rb index action:
#articles = Article.order("id desc")
I now want to be able to order by:
date
id
some_counter_attribute
My querystring will have sort=date/id/count like:
www.example.com/articles/?sort=date
How should I implement this in my controller now? Should I just use if statements?
if params[:sort] == "date"
#articles = Article.order("created_at desc")
elsif params[:sort] == "count"
#articles = ...
..
Or is there a better way?
Should this logic be in the controller or Model ideally?
Try this:
class ArticlesController
def index
#articles = Article.order(sort_order)
end
private
def sort_order
##sort_order ||= {
"date" => "created_at DESC",
"id" => "id DESC",
"comments" => "comment_count ASC"
}
##sort_order[params[:sort]]
end
end
Off course there are gems for doing this sort of things:
MetaSearch
SearchLogic
Straightforward approach could be:
#articles = Article.order("#{params[:sort]} desc")
But for "date" you have to sort by created_at. So try this:
mylist = {"date" => "created_at",
"id" => "id",
"counter" => "some_counter_attribute"}
#articles = Article.order("#{mylist[params[:sort]]} desc")