How to refactor complicated logic in create_unique method? - ruby-on-rails

I would like to simplify this complicated logic for creating unique Track object.
def self.create_unique(p)
f = Track.find :first, :conditions => ['user_id = ? AND target_id = ? AND target_type = ?', p[:user_id], p[:target_id], p[:target_type]]
x = ((p[:target_type] == 'User') and (p[:user_id] == p[:target_id]))
Track.create(p) if (!f and !x)
end

Here's a rewrite of with a few simple extract methods:
def self.create_unique(attributes)
return if exists_for_user_and_target?(attributes)
return if user_is_target?(attributes)
create(attributes)
end
def self.exists_for_user_and_target?(attributes)
exists?(attributes.slice(:user_id, :target_id, :target_type))
end
def self.user_is_target?(attributes)
attributes[:target_type] == 'User' && attributes[:user_id] == attributes[:target_id]
end
This rewrite shows my preference for small, descriptive methods to help explain intent. I also like using guard clauses in cases like create_unique; the happy path is revealed in the last line (create(attributes)), but the guards clearly describe exceptional cases. I believe my use of exists? in exists_for_user_and_target? could be a good replacement for find :first, though it assumes Rails 3.
You could also consider using uniqueness active model validation instead.

##keys = [:user_id, :target_id, :target_type]
def self.create_unique(p)
return if Track.find :first, :conditions => [
##keys.map{|k| "#{k} = ?"}.join(" and "),
*##keys.map{|k| p[k]}
]
return if p[##keys[0]] == p[##keys[1]]
return if p[##keys[2]] == "User"
Track.create(p)
end

Related

less than or greater than query in RoR

Is there a way to generate a less than or greater than query in rails like the between range query. I have multiple query params and hence I do not want to use string literal for comparison.
if params["end_time"]
if params["start_time"]
params["end_time"] = params["end_time"].to_datetime
query[:created_at] = ((params["start_time"])..params["end_time"])
else
query[:created_at] = #Need help with this
end
end
def with_duration
if(params['start_time'] && params['end_time'])
{created_at: params['start_time']..params['end_time']}
elsif(params['start_time'])
return ["created_at > ?", "#{params['start_time']}"]
elsif(params['end_time'])
return ["created_at < ?", "#{params['end_time']}"]
else
return {}
end
end
ModelName.where(with_duration)
As I understood you need to do nothing if params empty. So you can try this implementation.
def query_method(scope)
return scope unless params["end_time"] && params["start_time"]
scope.where.not(created_at: (params["start_time"]..params["end_time"]))
end

How to construct where clause in ruby using if

I am finding something like below. Constructing a where clause using condition. Is it possible in ruby? or I need to separate it into two where clause?
Post
.where(tag: "A") if condition A
.where(tag: "B") if condition B
.where(user_id: 1)
.order(....)
Actually, my case is like this. Is there any way to handle?
def this_function
#questions = Question.joins(:comment_threads)
.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
.where(index_where_clause)
.where("questions.created_at < ?", query_from_date_time)
.order(created_at: :desc).limit(5)
end
def index_where_clause
where_clause = {}
where_clause[:user_detail_id] = current_user_detail.id if params[:type] == "my_question"
where_clause[:comments] = {user_detail_id: current_user_detail.id} if params[:type] == "my_answer"
where_clause[:wine_question_score_id] = params[:wine_question_score_id] if params[:wine_question_score_id].present?
where_clause
end
The methods you're using return relations so you can say things like this:
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = #questions.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
#questions = #questions.where(:user_detail_id => current_user_detail.id) if params[:type] == "my_question"
#questions = #questions.where(:comments => { user_detail_id: current_user_detail.id}) if params[:type] == "my_answer"
#questions = #questions.where(:wine_question_score_id => params[:wine_question_score_id]) if params[:wine_question_score_id].present?
#questions = #questions.order(created_at: :desc).limit(5)
and build the query piece by piece depending on what you have in params.
I'd probably break it down a little more:
def whatever
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = with_tag(#questions, tag_variable)
#...
#questions = #questions.order(created_at: :desc).limit(5)
end
private
def with_tag(q, tag)
if tag.present?
q.tagged_with(tag, wild: true, any: true)
else
q
end
end
#...
and bury all the noisy bits in little methods to make things cleaner and easier to read. If you're doing this more than once then you could use scopes to hide the noise in the model class and re-use it as needed.
#tap can be helpful for modifying an object in place to apply conditional logic, in this case the object would be your .where conditions:
Post
.where(
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
)
.order(...)
Or, perhaps it's a little cleaner if you create a helper method:
def specific_conditions
{ user_id: 1 }.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
end
Post.where(specific_conditions).order(...)
But as a side note, if there's a case where condition A and condition B can both be true, the second conditions[:tag] = ... line will override the first. If there is not a case where both can be true, you might try to use some kind of collection to look up the proper value for tag.
CONDITION_TAGS = {
a: 'A'.freeze,
b: 'B'.freeze,
}.freeze
def specific_conditions
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = CONDITION_TAGS[condition_value] if condition_value
end
end
Post.where(specific_conditions).order(...)
#in Question class
scope :with_user_detail, -> (user_detail_id, flag=true) do
where("user_detail_id = ?", user_detail_id) if flag
end
scope :with_user_detail_comments, -> (user_detail_id, flag=true) do
joins(:comment_threads).where("comments.user_detail_id = ?", user_detail_id) if flag
end
scope :with_wine_question_score, -> (wine_question_score_id) do
where("wine_question_score_id = ?", wine_question_score_id) if wine_question_score_id.present?
end
scope :tagged_with_condition, -> (tag_variable, wild, any) do
tagged_with(tag_variable, wild, any) if tag_variable.present?
end
def this_function
my_question_flag = params[:type] == "my_question"
my_answer_flag = params[:type] == "my_answer"
Question.with_user_detail(current_user_detail.id, my_question_flag)
.tagged_with_condition(tag_variable, wild: true, any: true)
.with_user_detail_comments(current_user_detail.id, my_answer_flag)
.with_wine_question_score(params[:wine_question_score_id])
.order(created_at: :desc).limit(5)
end
You can do the following:
condition = {:tag => "A"} if condition A
condition = {:tag => "B"} if condition B
Post
.where(condition)
.where(:user_id => 1)
.order(....)
you have to use scope :
scope :my_scope, -> (variable) { where(some: vatiable) if my_condition }

A better way to do conditional ActiveRecord statements?

I'm trying to figure out a better way to have one query here. I want to be able to send something to last where statement a wildcard so I can select all vendors. Right now if i don't include that line it doesn't filter by vendor so I essentially get all the purchase requests.
Any thoughts of a cleaner way to do these sorts of queries?
if #vendor == "0" #checks for vendor
#purchase_requests = PurchaseRequest.includes(:purchase_order)
.where(:created_at => #date_start..#date_end)
.where(:total_cost => #cost_beginning..#cost_end)
else
#purchase_requests = PurchaseRequest.includes(:purchase_order)
.where(:created_at => #date_start..#date_end)
.where(:total_cost => #cost_beginning..#cost_end)
.where("purchaseorder.VendorRef_ListID = ?", #vendor)
end
there must be some better solution, but try this
#purchase_requests = PurchaseRequest.includes(:purchase_order).where(created_at: #date_start..#date_end, total_cost: #cost_beginning..#cost_end)
#purchase_requests = #purchase_requests.where("purchaseorder.VendorRef_ListID = ?", #vendor) unless #vendor == "0"
Here is a simplified version:
#purchase_requests = PurchaseRequest
.includes(:purchase_order)
.where(created_at: #date_start..#date_end)
.where(total_cost: #cost_beginning..#cost_end)
#purchase_requests = #purchase_requests.where('purchase_orders.VendorRef_ListID = ?', #vendor) unless #vendor == '0'

Better way to build multiple conditions in Rails 2.3

I'm developing with Rails 2.3.8 and looking for a better way to build find conditions.
On search page, like user search, which user sets search conditions, find conditions are depends on the condition which user have chosen, e.g age, country, zip-code.
I've wrote code below to set multiple find conditions.
# Add condition if params post.
conditions_array = []
conditions_array << ['age > ?', params[:age_over]] if params[:age_over].present?
conditions_array << ['country = ?', params[:country]] if params[:country].present?
conditions_array << ['zip_code = ?', params[:zip_code]] if params[:zip_code].present?
# Build condition
i = 0
conditions = Array.new
columns = ''
conditions_array.each do |key, val|
key = " AND #{key}" if i > 0
columns += key
item_master_conditions[i] = val
i += 1
end
conditions.unshift(columns)
# condiitons => ['age > ? AND country = ? AND zip_code = ?', params[:age], params[country], prams[:zip_code]]
#users = User.find(:all,
:conditions => conditions
)
This code works fine but it is ugly and not smart.
Is there better way to build find conditions?
Named scopes could make it a bit more readable, albeit bulkier, while still preventing SQL injection.
named_scope :age_over, lambda { |age|
if !age.blank?
{ :conditions => ['age > ?', age] }
else
{}
end
}
named_scope :country, lambda { |country|
if !country.blank?
{ :conditions => ['country = ?', age] }
else
{}
end
}
named_scope :zip_code, lambda { |zip_code|
if !zip_code.blank?
{ :conditions => ['zip_code = ?', age] }
else
{}
end
}
And then when you do your search, you can simply chain them together:
#user = User.age_over(params[:age_over]).country(params[:country]).zip_code(params[:zip_code])
I have accidentally run on your questions, and even it is old one, here is the answer:
After defining your conditions, you could use it like this:
# Add condition if params post.
conditions_array = []
conditions_array << ["age > #{params[:age_over]}"] if params[:age_over].present?
conditions_array << ["country = #{params[:country]}"] if params[:country].present?
conditions_array << ["zip_code = #{params[:zip_code]}"] if params[:zip_code].present?
conditions = conditions_array.join(" AND ")
#users = User.find(:all, :conditions => conditions) #Rails 2.3.8
#users = User.where(conditions) #Rails 3+

Multiple scope in rails 3.0

I am a beginner in Rails and i have a problem with scope.
I have my class with 2 scopes :
class Event < ActiveRecord::Base
belongs_to :continent
belongs_to :event_type
scope :continent, lambda { |continent|
return if continent.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('continent_id IN ( ? )', continent).all
return composed_scope
}
scope :event_type, lambda { |eventType|
return if eventType.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('event_type_id IN ( ? )', eventType).all
return composed_scope
}
end
And in my controller i want to use this 2 scopes at the same time. I did :
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
But i doesn't work, I have this error :
undefined method `event_type' for #<Array:0x7f11248cca80>
It's because the first scope return an array.
How can I do to make it work?
Thank you !
You should not append '.all' in your scopes:
It transforms a chainable ActiveRelation into an Array, by triggering the SQL query.
So simply remove it.
Bonus:
Some refactoring:
scope :continent, lambda { |continent|   
self.scoped.where('continent_id IN ( ? )', continent) unless continent.blank?
}
I don't think you need .scoped in your scopes.
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
on the code above you already have everything returning as 'scoped'.
Plus, your scopes wouldnt need an 'unless' on them, since they will only be called if your params arent blank. So your scopes could become something like this
scope :continent, lambda { |continent|
where('continent_id IN ( ? )', continent)
}
or, on a more Rails 3 way,
scope :continent, lambda { |continent_id|
where(:continent_id => continent_id)
}
which is much shorter :)

Resources