Optional mix of filter parameters in a search the Rails way - ruby-on-rails

I've got a simple list page with a couple of search filters status which is a simple enumeration and a test query which I want to compare against both the title and description field of my model.
In my controller, I want to do something like this:
def index
conditions = {}
conditions[:status] = params[:status] if params[:status] and !params[:status].empty?
conditions[???] = ["(descr = ? or title = ?)", params[:q], params[:q]] if params[:q] and !params[:q].empty?
#items = Item.find(:all, :conditions => conditions)
end
Unfortunately, it doesn't look like I can mix the two types of conditions (the hash and the paramatized version). Is there a "Rails Way" of doing this or do I simply have to do something awful like this:
has_status = params[:status] and !params[:status].empty?
has_text = params[:q] and !params[:q].empty?
if has_status and !has_text
# build paramatized condition with just the status
elsif has_text and !has_status
# build paramatized condition with just the text query
elsif has_text and has_status
# build paramatized condition with both
else
# build paramatized condition with neither
end
I'm migrating from Hibernate and Criteria so forgive me if I'm not thinking of this correctly...
Environment: Rails 2.3.4

You can mix hash and array conditions using scopes:
hash_conditions = {}
# build hash_conditions
items_scope = Item.scoped(:conditions => hash_conditions)
unless params[:q].blank?
items_scope = items_scope.scoped(:conditions => ["(descr = ? or title = ?)", params[:q], params[:q]])
end
...
items = items_scope.all
So you can mix and match any types of conditions, and the query will be executed only when you do items_scope.all

a=[],b=[]
unless params[:status].blank?
a << "status = ?"
b << params[:status]
end
unless params[:q].blank?
a << "(descr = ? or title = ?)"
b << params[:q] << params[:q]
end
#items = Item.all( :conditions => [a.join(" AND "), b] )

A better search on my part turned up something called "named scopes" which looks like is exactly what I'm looking for. I'm about to see if it will work with the will_paginate gem....
Reference:
http://edgerails.info/articles/what-s-new-in-edge-rails/2010/02/23/the-skinny-on-scopes-formerly-named-scope/

Related

Ignore uppercase, downcase and accent in search

I have a search system with filter here. This system work like a charm but I have some problem with downcase / uppercase and accent.
For example if I search "marée" I have result but if I search "MAREE" or "Marée" or "maree". I don't have result.
I want to fix that. How I can fix this ? Thank you.
my controller
def resultnohome
if params[:query].blank?
redirect_to action: :index and return
else
#campings = Camping.searchi(params[:query], params[:handicap], params[:animaux], params[:television], params[:plage], params[:etang], params[:lac])
if params[:query] == "aube"
#pub = Camping.find_by_id(1)
else
end
end
end
My model
def self.searchi(query, handicap, animaux, television, plage, etang, lac)
return scoped unless query.present?
result = left_outer_joins(:caracteristiquetests, :situations).where('nomdep LIKE ? OR name LIKE ? OR nomregion LIKE ? OR commune LIKE?', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%")
result = result.where('handicap LIKE ?', "%#{handicap}%") if handicap
result = result.where('animaux LIKE ?', "%#{animaux}%") if animaux
result = result.where('television LIKE ?', "%#{television}%") if television
result = result.where('plage LIKE ?', "%#{plage}%") if plage
result = result.where('etang LIKE ?', "%#{etang}%") if etang
result = result.where('lac LIKE ?', "%#{lac}%") if lac
return result
end
If you insist on using SQLite then you don't have many good options. The most common suggestion is to have extra columns in your database, that are normalized values so if your plage column contains "Marée" then you also have a column plage_ascii that contains "maree"
you need to create the additional columns with migrations, then you would have a before_save action in your model...
before_save :create_normalized_strings
def create_normalized_strings
self.handicap_ascii = handicap.downcase.mb_chars.normalize(:kd).gsub(/[^x00-\x7F]/n, '').to_s
self.animaux_ascii = animaux.downcase.mb_chars.normalize(:kd).gsub(/[^x00-\x7F]/n, '').to_s
# etc etc
end
Then in your search do...
if handicap
test_handicap = handicap.downcase.mb_chars.normalize(:kd).gsub(/[^x00-\x7F]/n, '').to_s
result = result.where('handicap_ascii LIKE ?', "%#{handicap}%")
end
It's not great, as it basically forces you to duplicate data in your database into extra columns. If you can consider more sophisticated databases other than SQLite then you'd be better off... personally I wouldn't ever use SQLite in a production environment.

I need a search form with a large number of fields. How do I do this cleanly in Ruby on Rails?

I'm building a database application that tracks a lot of data for a Person, such as first_name, last_name, DOB, and 20+ more fields.
Users will need to be able to search for all of these fields. I'm having trouble writing clean code for this. The code below is what I have so far in my people_controller:
The data is submitted from a form_tag
def search
#people = Person.all
general_info_string = String.new
if(params[:first_name] != "") then general_info_string << 'people.first_name = "' + params[:first_name] + '" AND ' end
if(params[:last_name] != "") then general_info_string << "people.last_name = '" + params[:last_name] + "' AND " end
... Lots more of similar clauses
general_info_string = general_info_string[0, general_info_string.length - 5]
# ^This line removes the trailing " AND " from the string
#people = #people.where(general_info_string)
end
general_info_string is so called because there are more "where" clauses(not shown) and separate strings that I build to search for them.
The problem here is that the code looks like a mess and seems like a "hacky" way to do something that should be well supported by Rails. How could I perform this operation in a cleaner way?
That's not just hacky - it leaves you open to a string injection attack.
You need a :conditions using the ? template, like this example:
:conditions => ["key1 = ?", var]
but you need to make the template part into a string that grows once per parameter, and you need to make the var into an array that grows with each parameter's value. That gives you something like this:
template = []
values = []
if params[:first_name].present?
template.push 'people.first_name = ?'
values.push params[:first_name]
end
if params[:last_name].present?
template.push 'people.last_name = ?'
values.push params[:last_name]
end
template = template.join(' AND ')
:conditions => [template, *values]
From there, you should DRY up all those ifs, such as with a table of value keys. Then you'd loop through the table, check the key, and push its results into the arrays:
fields = [:first_name, :last_name, :shoe_size, ...]
fields.each do |field|
if params[field].present?
template.push "people.#{field} = ?"
values.push params[field]
end
end

Active Record query when some conditions are absent

I have the following hash conditions for retrieving products from the database:
#products = Product.where(category: #category, color: params[:color], brand_id: params[:brand], size: params[:size]).
Is there a way to get the query result when one or more of the conditions are not supplied, for example
if params[:color] is absent, how can i retrieve the products with the other conditions?, or how can i restructure the conditions to achieve this?
I'm going to base myself on techvineet's answer (editing was becoming messy).
Try this:
# This line throws away the :action and :controller arguments
params.except!(:action, :controller) # notice the ! now
params.delete_if {|k,v| v.nil?} # throw out any empty args
params[:category] = #category
#products = Product.where(params)
You could also use something like this:-
params.except(:action, :controller)
params[:category] = #category
#products = Product.where(params)
You can try Periscope gem
Or Ransack
You can build your query using conditions like this:
product_scope = Product.where(:category => #category)
product_scope = product_scope.where(:color => params[:color]) if params[:color]
# (...)
#products = product_scope.all
That way, only the valid and present conditions are being added to the scope. Or you could build a hash with your options in a similar way and add only the keys with valid params, then call Product.where(conditions_hash).all

Rails 3: Search method returns all models instead of specified

What I'm trying to do: I have a model "Recipe" in which I defined a method "search" that takes an array of strings from checkboxes (I call them tags), and a single string. The idea is to search the db for recipes that has anything in it's 'name' or 'instructions' that contains the string, AND also has any of the tags matching it's 'tags' property.
Problem: The search method return all the recipes in my db, and doesn't seem to work at all at finding by the specific parameters.
The action method in the controller:
def index
#recipes = Recipe.search(params[:search], params[:tag])
if !#recipes
#recipes = Recipe.all
end
respond_to do |format|
format.html
format.json { render json: #recipe }
end
end
The search method in my model:
def self.search(search, tags)
conditions = ""
search.present? do
# Condition 1: recipe.name OR instruction same as search?
conditions = "name LIKE ? OR instructions LIKE ?, '%#{search[0].strip}%', '%#{search[0].strip}%'"
# Condition 2: if tags included, any matching?
if !tags.empty?
tags.each do |tag|
conditions += "'AND tags LIKE ?', '%#{tag}%'"
end
end
end
# Hämtar och returnerar alla recipes där codition 1 och/eller 2 stämmer.
Recipe.find(:all, :conditions => [conditions]) unless conditions.length < 1
end
Any ideas why it return all records?
if you are using rails 3, then it is easy to chain find conditions
def self.search(string, tags)
klass = scoped
if string.present?
klass = klass.where('name LIKE ? OR instructions LIKE ?', "%#{string}%", "%#{string}%")
end
if tags.present?
tags.each do |tag|
klass = klass.where('tags LIKE ?', "%#{tag}%")
end
end
klass
end
When you do
search.present? do
...
end
The contents of that block are ignored - it's perfectly legal to pass a block to a function that doesn't expect one, however the block won't get called unless the functions decides to. As a result, none of your condition building code is executed. You probably meant
if search.present?
...
end
As jvnill points out, it is in general much nicer (and safer) to manipulate scopes than to build up SQL fragments by hand

DRYing Search Logic in Rails

I am using search logic to filter results on company listing page. The user is able to specify any number of parameters using a variety of named URLs. For example:
/location/mexico
/sector/technology
/sector/financial/location/argentina
Results in the following respectively:
params[:location] == 'mexico'
params[:sector] == 'technology'
params[:sector] == 'financial' and params[:location] == 'argentina'
I am now trying to cleanup or 'DRY' my model code. Currently I have:
def self.search(params)
...
if params[:location]
results = results.location_permalink_equals params[:location] if results
results = Company.location_permalink_equals params[:location] unless results
end
if params[:sector]
results = results.location_permalink_equals params[:sector] if results
results = Company.location_permalink_equals params[:sector] unless results
end
...
end
I don't like repeating the searchs. Any suggestions? Thanks.
This is how I would write it:
[params[:location], params[:sector]].reject(&:nil?).each do |q|
results = (results ? results : Company).location_permalink_equals q
end
There's plenty of other ways, just an idea. Has the benefit of making it easy to add say params[:street] or something.
I don't think you can really DRY that up much when sticking to SearchLogic... I'd suggest to refine your routes to directly emit *_permalink as parameter names and do something like this:
Company.all :conditions => params.slice(:location_permalink, :sector_permalink)
or
Company.find :all, :conditions => params.slice(:location_permalink, :sector_permalink)
Documentation link: http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Hash/Slice.html

Resources