Rails doing a FIND with Conditions? - ruby-on-rails

In Rails 3, I created a Search Form that does a FIND with conditions in the Models file.
#projects = find(:all,
:select => 'projects.*',
:conditions => ['name = ?', search_name]
).first
This works great if a name is provided in the searchform (search_name). Problem is if search_name is blank, Rails Errors (can't say I blame it)...
What is the smart way to handle this situation? I'd like, if search_name is blank, to not error but return everything.
Suggestions? Thanks!

You can create a scope to handle this. In your Project model, add something like:
scope :search_by(name), lambda{|name| first.where(:name => name) unless name.blank?}
then in your controller, simply call:
Project.search_by(params[:search])
EDIT:
If you need to serach for multiple fields you can adapt the scope:
scope :search_by(name), lambda{|name| first.includes(:owner).where("projects.name LIKE ? OR owners.name LIKE ?", name, name) unless name.blank?}

if search_name.blank?
#projects = Project.order(:name)
else
#projects = Project.where(:name => search_name)
end

The cleanest way is using lazy loading with the new ActiveRecord functionalities like this:
#projects = Project.order(:name)
#projects = #projects.where(:name => search_name) if search_name
You can add as many conditions as you like this way. They won't be executed until you need the results anyway (with #projects.all or #projects.each, etc...)

Related

Ruby on Rails - Implementing Simple Search with scopes

I followed the Railscasts #37, very interesting. I tried to apply it to a search that already implies a scope and pagination. But it failed unsuccessfully. Being quite new at rails, I wonder if a better solution exists.
Here is my original controller, that works:
def index
#business_rules = BusinessRule.pgnd(current_playground).order("hierarchy ASC").paginate(page: params[:page], :per_page => paginate_lines)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #business_rules }
end
end
But trying to add the search function as defined in the RailsCast fails:
#business_rules = BusinessRule.pgnd(current_playground).search(params[:search]).order("hierarchy ASC").paginate(page: params[:page], :per_page => paginate_lines)
Error message is: undefined method order for #<Array:0x007fbf8cc62ca0>
Does your search function look like this, as in the railscast?
def self.search(search)
if search
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
else
find(:all)
end
end
If so, I'm guessing your getting back an array of results, which cannot be chained with additional query methods.
So when you use the query methods (where, order, limit, etc), it returns an ActiveRelation object, which is basically a proxy for your eventual result set. It won't actually hit your DB until you try to use the result set by calling .all, .first, .each, something like that.
You could write your search method like this instead:
def self.search(search)
if search
where('name LIKE ?', "%#{search}%")
end
end
So if search is present, you'll scope your AR object down to the matching results. Otherwise you won't, which will have the same result as returning everything. Then you can still order those results, limit them, paginate them, whatever.
The final script for the search function created in the controller is:
def self.search(search)
if not search.empty?
where('name like ?', "%#{search}%")
else
where('1=1')
end
end
Then it is possible to cahin the query methods as this:
#business_rules=BusinessRule.pgnd(current_playground).search(params[:search]).limit(10)
For more explanations, please refer to RailsCast #37, and note that the syntax for inserting a from_tag is <%= form_tag ...
Thanks to all for your help,
Best regards,
Fred

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)

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: Where does this code belong?

I currently have the following in my controller:
#items = Item.scoped
#items = #items.where('price >= ?', params[:price_min]) if params[:price_min]
#items = #items.where('price <= ?', params[:price_max]) if params[:price_max]
#items = #items.where(:size => params[:size]) if params[:size]
#items = #items.where(:colour => params[:colour]) if params[:colour]
# ...
#items = #items.paginate(:page => params[:page], :per_page => 10)
Is this the right place for this code, or should it really belong in my model with a single method call in the controller? i.e.
#items = Item.apply_filters(params)
I am trying to stick to convention as much as possible.
Many thanks.
You are correct that this all belongs in your model. This is very similar to someone's code that I reviewed the other day. The MetaWhere gem might be a good fit for your project as well.
https://codereview.stackexchange.com/questions/500/how-to-filter-search-a-model-with-multiple-params-in-hash/501#501
Also, if your items are always going to be scoped, you can make that the default_scope. In your model:
default_scope order("item_number"), where('price >= ?', 100)
(I'm not totally sure I got all of that syntax correct, but it's something like that.)
named_scope might also help you out.

Is it possible to filter by conditions before paginating?

I'm using ruby on rails 2.3.8 and will_paginate plugin.
I've just noticed that if I write something like this:
Announcement.paginate :page => params[:page], :per_page => 10, :conditions => some_condition
it will work.
But, if I write something like this:
announcements = Announcement.all :conditions => some_condition
#ann = announcements.paginate :page => params[:page], :per_page => 10
it won't recognize conditions.
EDIT:
I've developed a Search functionality and, due to a Sort functionality I had to implement, I had to put the search feat inside a model's method to call it from the controller every time I need either to search or sort by some field.
So, my model's methods look like this:
def self.search_by_relevance(words)
conditions = get_search_conditions(words)
Announcement.published.descend_by_featured.order_by_rate :conditions => conditions
end
where "published" and "order_by_rate" are named scopes and "descend_by_feature" belongs to "searchlogic" gem.
def self.get_search_conditions(words)
unless words.empty? or words.nil?
conditions = ''
words.each do |word|
if conditions.nil? or conditions.empty?
conditions = '(title like "%' + word + '%" or description like "%' + word + '%")'
else
conditions += ' and (title like "%' + word + '%" or description like "%' + word + '%")'
end
end
conditions
end
end
My controller's action looks like this:
def search
#announcements = Announcement.search_by_relevance(params[:txtSearch].to_s.split).paginate :page => params[:page], :per_page => 10 unless params[:txtSearch].nil? or params[:txtSearch].empty?
end
This syntax won't recognize the conditions specified in the model's method.
EDIT 2:
Thanks for the posts. Testing my code a little more I found out that if I write ".all" right after "order_by_rate" at this line Announcement.published.descend_by_featured.order_by_rate :conditions => conditions, in search_by_relevance method it will return the correct query, but will_paginate plugin will give me the following error(just if I add ".all"):
NoMethodError in AnnouncementsController#search
undefined method `to_i' for {:page=>nil, :per_page=>10}:Hash
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/collection.rb:15:in `initialize'
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb:37:in `new'
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb:37:in `paginate'
D:/Proyectos/Cursometro/www/app/controllers/announcements_controller.rb:276:in `search'
First of all, I don't understand why I have to add the ".all" to the query to work right, and second, I don't see why will_paginate won't work when I include ".all"(I also tried to add the following code but didn't work: :page => params[:page] || 1).
Also, if I include the ".all" syntax to the query, it will return:
SELECT * FROM announcements WHERE
((title like "%anuncio%" or
description like "%anuncio%")) AND
(announcements.state = 'published')
ORDER BY announcements.featured DESC
If I don't, it will return:
SELECT * FROM announcements WHERE
(announcements.state = 'published')
ORDER BY announcements.featured DESC
Do you see that no conditions are being included in the last one? This is causing the problem.
I don't know if this will work for you, but you can use paginate just like find, I mean, something like:
#announcements = Announcement.paginate_all_by_id params[:id],
:page => params[:page], :per_page => 10
Edit
#announcements is an array, right?
Well, I found this post and this other one that may help you.
Well, I kind of solve this by adding ".paginate"(instead of ".all") to my query in the model's method, passing by parameters the "page" and "per_page" values. It was not my idea to include pagination in models, but well...it's the solution I have for now. If you come up with a better one, I'll be glad to hear it :)

Resources