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.
In my Application Controller I am setting cookies in 3 places. In a before filter...
def set_abingo_identity
# skip bots
if request.user_agent =~ /#{#robot_regular_expression}/i
Abingo.identity = "robot"
elsif current_user
Abingo.identity = cookies[:abingo_identity] ||=
{ :value => current_user.id, :expires => 1.year.from_now }
else
Abingo.identity = cookies[:abingo_identity] ||=
{ :value => rand(10 ** 10), :expires => 1.year.from_now }
end
end
... and in two methods...
def remember_visit url
20.times do |i|
# Add cookie to referrer list
name = "referrer#{i}"
if cookies[name].blank?
cookies[name] = {:value => url, :expires => Time.now + 1.year }
break
elsif cookies[name] == url
break # skip
end
end
end
... and this one...
def set_referral_cookie val
ref_name = "referral_id"
offer_name = "offer_id"
cur = cookies[ref_name]
offer = cookies[offer_name]
affiliate = Affiliate.find_by_name val
if cur.blank? || offer.blank?
if affiliate
if cur.blank?
cookies[ref_name] = { :value => affiliate.id.to_s, :expires => Time.now + 1.year }
end
if offer.blank? && affiliate.offer? && !affiliate.expired?
cookies[offer_name] = { :value => affiliate.id.to_s, :expires => Time.now + 1.year }
end
end
end
return affiliate
end
Stepping through in the debugger, I see in the before filter, cookies["abingo_identity"] is set. Then in remember_visit(), both it and cookies["referrer0"] are set, and then in set_referral_cookie(), all 3 are set.
But when I get back to my functional test, only cookies["abingo_identity"] is set.
The cookies["abingo_identity"] part is new, and when I comment it out, the other cookies persist.
What is wrong with this code?
Update:
They are not getting deleted, but they are not all copied to the test case's #response.cookies
I have a pretty elaborate set-up to change the condition from passing to failing for four composing methods of a method called eligible?.
describe "#participant_age_eligible?" do
it "returns whether participant is age-eligible" do
#part.participant_age_eligible?(#pers).should == true
end
it "returns false if participant is not age eligible" do
q = #survey_section.questions.select { |q| q.data_export_identifier ==
"#{OperationalDataExtractor::PbsEligibilityScreener::
INTERVIEW_PREFIX}.AGE_ELIG" }.first
answer = q.answers.select { |a| a.response_class == "answer" && a.reference_identifier == "2" }.first
Factory(:response, :survey_section_id => #survey_section.id, :question_id => q.id, :answer_id => answer.id, :response_set_id => #response_set.id)
#part.participant_age_eligible?(#pers).should == false
end
end
describe "#participant_psu_county_eligible?" do
it "returns whether participant lives in eligible PSU" do
#part.participant_psu_county_eligible?(#pers).should == true
end
it "returns false if participant coes not live in an eligible PSU" do
q = #survey_section.questions.select { |q| q.data_export_identifier ==
"#{OperationalDataExtractor::PbsEligibilityScreener::
INTERVIEW_PREFIX}.PSU_ELIG_CONFIRM" }.first
answer = q.answers.select { |a| a.response_class == "answer" && a.reference_identifier == "2" }.first
Factory(:response, :survey_section_id => #survey_section.id, :question_id => q.id, :answer_id => answer.id, :response_set_id => #response_set.id)
#part.participant_psu_county_eligible?(#pers).should == false
end
end
There are two more methods just like those two. What I'd like to do is extract the
q = #survey_section.questions.select { |q| q.data_export_identifier ==
"#{OperationalDataExtractor::PbsEligibilityScreener::
INTERVIEW_PREFIX}.AGE_ELIG" }.first
answer = q.answers.select { |a| a.response_class == "answer" && a.reference_identifier == "2" }.first
Factory(:response, :survey_section_id => #survey_section.id, :question_id => q.id, :answer_id => answer.id, :response_set_id => #response_set.id)
portion into a method in the before block and then pass the relevant parameters, but I hesitiate because I have never seen someone define a method in a before block, not even sure you can do it, further, I'm not sure if you even should do it even if you can, maybe it's pointing to a problem that I'm not seeing. So I though I'd ask the unfathomably enormous expertise of the SO community. Thanks.
You can always define methods within your specific rspec file, and then call them from anywhere in that file. For example:
# your_file_spec.rb
describe MyModel do
before(:each) { setup_variables }
describe ...
end
# I usually put my helper methods at the bottom
def setup_variables
# Do some work
end
end
You can also sometimes use a 'scenario outline' approach to your work, for example:
# your_file_spec.rb
describe MyModel do
examples = [{:name => "Joe", :login => "joe18"}, {:name => "Grace", :login => "grace12"}]
examples.each do |example|
it "logs in #{example[:name]}." do
# Do some work
end
end
end
You may also find that useful.
I have this terribly large controller in my app. I'd really like to make it as skinny as possible. Below is some of the code, showing the types of things I'm currently doing.. I'm wondering what things I can move out of this?
A note - this is not my exact code, a lot of it is similar. Essentially every instance variable is used in the views - which is why I dont understand how to put the logic in the models? Can models return the values for instance variables?
def mine
#For Pusher
#push_ch = "#{current_user.company.id}"+"#{current_user.id}"+"#{current_user.profile.id}"
#Creating a limit for how many items to show on the page
#limit = 10
if params[:limit].to_i >= 10
#limit = #limit + params[:limit].to_i
end
#Setting page location
#ploc="mine"
#yourTeam = User.where(:company_id => current_user.company.id)
#Set the user from the param
if params[:user]
#selectedUser = #yourTeam.find_by_id(params[:user])
end
#Get all of the user tags
#tags = Tag.where(:user_id => current_user.id)
#Load the user's views
#views = View.where(:user_id => current_user.id)
if !params[:inbox]
#Hitting the DB just once for all the posts
#main_posts = Post.where(:company_id => current_user.company.id).includes(:status).includes(:views)
#main_posts.group_by(&:status).each do |status, posts|
if status.id == #status.id
if #posts_count == nil
#posts_count = posts
else
#posts_count = #posts_count + posts
end
elsif status.id == #status_act.id
if #posts_count == nil
#posts_count = posts
else
#posts_count = #posts_count + posts
end
end
end
if params[:status] == "All" || params[:status] == nil
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).order(sort_column + " " + sort_direction).where(:company_id => current_user.company.id, :status_id => [#status.id, #status_act.id, #status_def.id, #status_dep.id, #status_up.id]).limit(#limit).includes(:views)
else
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).order(sort_column + " " + sort_direction).where(:company_id => current_user.company.id).limit(#limit).includes(:views)
end
elsif params[:inbox] == "sent"
#yourcompanylist = User.where(:company_id => current_user.company.id).select(:id).map(&:id)
#yourcompany = []
#yourcompanylist.each do |user|
if user != current_user.id
#yourcompany=#yourcompany.concat([user])
end
end
if params[:t]=="all"
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
elsif params[:status]!="complete"
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
elsif params[:status]!=nil
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
end
end
respond_to do |format|
format.html # index.html.erb
format.js # index.html.erb
format.xml { render :xml => #posts }
end
end
You can start by moving logic into the model...
A line like this screams of feature envy:
#push_ch = "#{current_user.company.id}"+"#{current_user.id}"+"#{current_user.profile.id}"
I would recommend moving it into the model:
#user.rb
def to_pusher_identity
"#{self.company_id}#{self.id}#{self.profile_id}"
end
And then in your controller
#push_ch = current_user.to_pusher_identity
At this point you could even move this into a before_filter.
before_filter :supports_pusher, :only => :mine
Another thing you can do is create richer associations, so you can express:
#tags = Tag.where(:user_id => current_user.id)
as
#tags = current_user.tags
Another example would be for main posts, instead of
Post.where(:company_id => current_user.company.id).includes(:status).includes(:views)
you would go through the associations:
current_user.company.posts.includes(:status).includes(:views)
When I'm drying out a controller/action I try to identify what code could be (should be?) offloaded into the model or even a new module. I don't know enough about your application to really point to where these opportunities might lie, but that's where I'd start.
Few quick ideas:
Consider using respond_to/respond_with. This controller action can be splitted up to two separate ones - one for displaying #main_posts, another for params[:inbox] == "sent". The duplicate code can be removed using before_filters.
Also, a couple of gem suggestions:
use kaminari or will_paginate for pagination
meta_search for search and sorting
I have this call in my vote model:
fires :vote_updated, :on => :update,
:actor => :user,
:secondary_subject => :video,
:if => lambda { |vote| ((vote.value == 1) || (vote.value == -1)) && (vote.video.user != current_user)}
In case you aren't familiar, it works with the timeline_fu plugin.
I do not want the call to be fired if the user who owns the voted up video is the current user. That is where this line comes in:
:if => lambda { |vote| ((vote.value == 1) || (vote.value == -1)) && (vote.video.user != current_user)}
However, I do not have access to current_user here. How do I get around this?
Here's the create method in my votes controller (there actually is no update method):
def create
#video = Video.find(params[:video_id])
#vote = current_user.video_votes.find_or_create_by_video_id(#video.id)
if #vote.value.nil?
if params[:type] == "up"
#vote.value = 1
else
#vote.value = -1
end
elsif (params[:type] == "up" && #vote.value == 1) || (params[:type] == "down" && #vote.value == -1)
#vote.value = 0
elsif ((params[:type] == "up" && #vote.value == -1) || (params[:type] == "down" && #vote.value == 1)) || (#vote.value == 0)
if params[:type] == "up"
#vote.value = 1
else
#vote.value = -1
end
end
if #vote.save
respond_to do |format|
format.html { redirect_to #video }
format.js
end
else
respond_to do |format|
format.html
format.js
end
end
end
I believe the right thing to do would be validating this in controller. I would create a before filter for this case
UPDATE:
Just as a quick example:
before_filter :valid_vote, :only => :update
def update
#vote.update_attributes(params[:vote]) # or whatever
end
..
private
def valid_vote
#vote = Vote.find params[:id]
unless ( #vote.video.user.id != current_user.id )
render :text => 'You can't vote for your own video', :status => 403
end
end
So #vote is being declared and validated before your 'update' action is proccessed.
If it's not valid then your 'update' action stays untouched
UPDATE 2 :
not sure how you'll like it, but you could also do as follows:
in your Vote model:
attr_accessor :skip_timeline
then use the concept with before filter, but do #vote.skip_timeline = true instead of rendering text
then the statement might look as follows:
:if => lambda { |vote| ((vote.value == 1) || (vote.value == -1)) && !vote.skip_timeline }
You could also move ((vote.value == 1) || (vote.value == -1)) to your before filter :
def valid_vote
#vote = Vote.find params[:id]
unless ( [1,-1].include? #vote.value && #vote.video.user.id != current_user.id )
#vote.skip_timeline = true
end
end
and
:if => lambda { |vote| !vote.skip_timeline }
You are getting this error because it's typically not recommended to access current_user (or session information) in your model. I am not all that familiar with the timeline_fu gem, so this answer isn't going to be the greatest answer you may get. I'm merely going to show you how to access current_user from any model.
First go to your application controller. You'll want to make a method that sets the current user. You need to call the method in the before filter.
before_filter :loadCurrentUser
def loadCurrentUser
User.currentUser = current_user
end
Then in your User model, you need to define 'currentUser'.
def self.currentUser
Thread.currentUser[:user]
end
You don't necessarily have to declare the current_user in the application controller, but since it's a gem, I'm not sure if it has an easily accessible controller.
Edit: This way may be prone to problems, but I'm not entirely sure if you were asking how to make current_user available in models, or a completely different workaround so you do not have that problem... and reading the responses of the other answer, I'm thinking it's not what you were asking.