Common refactoring pattern for Rails model - ruby-on-rails

Is there a pattern to refactor such a construct into a readable single line?
def show_section
#news = News.all_active
#news = #news.where(:section => params[:section]) unless params[:section] == "all"
#news = #news.all
end
I use Rails 3 and Ruby 1.9.2

#news = News.all_active.where(params[:section] == "all" ? nil : {:section => params[:section]})
You can get rid of #news.all - in Rails 3 query will be executed when you use the resulting ActiveRecord::Relation object (for example when you call each or first on it). Passing nil to where method will do nothing.
If all_active is a method, you can refactor it into a scope and then call it in a chain.
Great resources on Rails 3 queries:
Active Record Queries in Rails 3
Advanced Queries in Rails 3
Arel README

You can turn the where clause into a method on your News model:
class News
def self.for_section(section)
where(section == "all" ? nil : {:section => section})
end
end
Then in your controller, you can chain it all together like so:
News.for_section(params[:section]).all_active
This of course assumes that all_active is also a scope, and not a resultset.

Related

Passing multiple scopes to Concern Method - Ruby on Rails

In the process of drying my Rails app code, I have created the following concern that is used to generate the contents of an index method.
define_method(:generate_index) do |string, scope|
instance_variable_set( "##{string}", string.camelize.constantize.public_send(scope))
end
I use this code to generate something like the following:
def index
generate_index("foo", "all")
# #foo = Foo.all
end
What I'd like to do is to have the define method accept a number of scopes. I tried passing in an array of scopes but that results in an error.
Any ideas?
Thanks
You could use the splash * operator:
define_method(:generate_index) do |klass, *scopes|
scope = klass.to_s.camelize.constantize
scopes.each { |s| scope = scope.send(s) }
instance_variable_set("##{string}", scope)
end
def index
generate_index(:foo, :all, :where_not_test)
# #foo = Foo.all.where_not_test
end

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Update on record would update all other records in model, global ordering in rails 3.1

I would like to update all records in a rails (3.1) model when i update an attribute on a single record.
Like self.update_attribute(:global_order => 1) then before or after save a would like to update all other records to update thier global_order (1, 2, 3, 4).
Right now with on after_save callback I get caught in a recursive loop, is skip callbacks the way to go? I would like the app to throw exceptions if anything seems strange in global_order.
Or are there any 3.1 gems that would solve my issue.
after_save :set_global_order
def set_global_order
#products = self.class.all(:order => :global_order)
#products.sort! {|a,b| a.global_order <=> b.global_order}
#products.reverse!
#products.each_with_index do |p, index|
p.update_attributes!({:global_order => index + 1})
end
end
Not sure if there's a gem, but you could definitely refactor this with the following considerations:
No need to pollute the object with an instance variable when a local one will do
The first three lines are sorting the same set, why not do that once?
...
def set_global_order
products = self.class.order('global_order DESC')
products.each_with_index do |p, index|
p.update_column(:global_order, index + 1)
end
end

Moving of will_paginate to model

On my Question model I have some scopes
scope :recent, order("created_at DESC")
scope :approved, where("status = ?", "approved")
scope :answered, approved.recent.where("answers_count > ?", 0)
On my question controller I'm retrieving questions using the scopes
example 1:
#questions = Question.approved.recent
example 2:
#questions = User.find(session[:user_id]).topics.map { |t| t.questions.approved.recent }.flatten.uniq
I'm trying to put will_paginate on my model to make things easier on the controller but the 2nd example is very tricky as it is using mapping to retrieve questions according to preferences.
I've tried to add this on my model
def self.pagination(page = 1)
self.paginate(:page => page, :per_page => 5)
end
and then on my controller I have
#questions = Question.approved.recent.pagination.(params[:page])
That works fine for the 1st example but I Dont know how to implement that on the 2nd example
Any hints?
This looks like Rails 3. Be sure to use the ~> 3.0.pre2 version of the will_paginate gem.
You can use the paginate method at the end of your chain of scopes. For example, your "example 1" would be:
#questions = Question.approved.recent.paginate(:page => params[:page], :per_page => 20)
I see you created a custom method (pagination) to wrap this pattern, but it's best that you keep this syntax in original form for now, especially since you're dealing with scopes and Relation objects in Rails 3 and will_paginate doesn't have proper support for this yet (but it's coming).
In your "example 2" it seems you only need to fetch the first few recent questions from each topic and that you won't perform a full-blown pagination here (like, going to page 2 and forward). You don't have to use the paginate method here; you can simply use ActiveRecord's limit:
current_user = User.find(session[:user_id])
#questions = current_user.topics.map { |topic|
topic.questions.approved.recent.limit(5).to_a
}.flatten.uniq

Rails 3 displaying tasks from partials

My Tasks belongs to different models but are always assigned to a company and/or a user. I am trying to narrow what gets displayed by grouping them by there due_at date without doing to many queries.
Have a application helper
def current_tasks
if user_signed_in? && !current_company.blank?
#tasks = Task.where("assigned_company = ? OR assigned_to = ?", current_company, current_user)
#current_tasks = #tasks
else
#current_tasks = nil
end
end
Then in my Main view I have
<%= render :partial => "common/tasks_show", :locals => { :tasks => current_tasks }%>
My problem is that in my task class I have what you see below. I have the same as a scope just named due_today. when I try current_tasks.due_today it works if I try current_tasks.select_due_today I get a undefined method "select_due_tomorrow" for #<ActiveRecord::Relation:0x66a7ee8>
def select_due_today
self.to_a.select{|task|task.due_at < Time.now.midnight || !task.due_at.blank?}
end
If you want to call current_tasks.select_due_today then it'll have to be a class method, something like this (translating your Ruby into SQL):
def self.select_due_today
select( 'due_at < ? OR due_at IS NOT NULL', Time.now.midnight )
end
Or, you could have pretty much the same thing as a scope - but put it in a lambda so that Time.now.midnight is called when you call the scope, not when you define it.
[edited to switch IS NULL to IS NOT NULL - this mirrors the Ruby in the question, but makes no sense because it will negate the left of the ORs meaning]

Resources