I need a search form with a large number of fields. How do I do this cleanly in Ruby on Rails? - 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

Related

Rails Conditional Select Contents

I'm populating a select tag dynamically within my form. The select tag is populated by a query to Payee. I have a couple fields I'd like to display as the text value of the element. However it must be done conditionally. For example.
fields:
:company
:first_name
:last_name
I can concatenate those three in the text box like so:
f.select :payee_id, Payee.all.collect { |p| [p.company.to_s + " " + p.first_name.to_s + " " + p.last_name.to_s, p.id] }
The above doesn't work the way I desire. If company exists, I would like to leave out first_name and last_name.
if company is blank, I'd like to display only first_name and last_name
currently if company and first_name exist it concatenates the two together, which I do not desire.
I know how to query the database for each case separately, but I don't know how to combine the results so the select tag will display correctly.
Here's what I have of my helper so far. I know it's currently a mess, but I need help understanding the method to get this done.
def expense_payee_id_field(f)
#payee_with_company = Payee.where("company IS NOT NULL and company !='' ")
#payee_without_company = Payee.where("(first_name != '' or last_name != '') and company = '' ")
f.select :payee_id, Payee.all.collect { |p| [p.company.to_s + " " + p.first_name.to_s + " " + p.last_name.to_s, p.id] }
end
You should just create a method in your Payee model that does this logic for you:
# Payee.rb
# I've extracted this into its own method
# because you very well may need it later
def full_name
[first_name, last_name].join(" ")
end
# This can be written more concisely, but I've
# kept it as-is to allow for better readability
def display_name
if company_name.blank?
full_name
else
company_name
end
end
Now, you can simply call payee.display_name and it will print the desired result.

Search every field or attribute in model Rails

I have a simple model called Company with atributes such as Name, contact email, address, etc. I've created a search form where a user can find the company by searching for any attribute i.e., city, name of business, etc.
I'm new to Rails but I've figured out how to get it working like so in my controller.
if(params[:searchstring].present?)
logger.debug "*** Running search --> "
term = params[:searchstring]
#companies = Company.where('name LIKE ? OR name LIKE ? OR contactemail LIKE ? OR city LIKE ?' , "%#{term}%", "%#{term}%", "%#{term}%","%#{term}%" ).paginate(page: params[:page]).order('id DESC')
else
logger.debug "*** Running search --> get em all "
#companies = Company.all.paginate(page: params[:page], per_page: 5).order('id DESC')
end
end
This works fine but I'm guessing there is a much simplier way. I'd prefer not to modify the query every time I add an attribute. Any ideas on how to improve this?
You could go with something like this (It is just an example, might be a better way, but it is fonctionnal)
# company.rb
def self.search_all_properties term
query = ''
properties = Company.column_names
properties.each_with_index do |prop, index|
if index < properties.count - 1
query = query + prop + ' LIKE :term OR '
else
query = query + prop + ' LIKE :term'
end
end
term = "%" + term + "%"
Company.where(query, :term => term)
end
and use it like this
Company.search_all_properties term

Rails - where with multiple like options

I am trying pull information from a contacts table based on multiple like conditions. So far I have come up with the following
conditions = ""
conditions << "email_address LIKE '%#{params[:email_address]}%'" unless params[:email_address].blank?
conditions << " AND first_name LIKE '%#{params[:first_name]}%'" unless params[:first_name].blank?
conditions << " AND last_name LIKE '%#{params[:last_name]}%'" unless params[:last_name].blank?
conditions.sub!(/^AND/, '')
if !conditions.blank?
#contacts = Contact.where(conditions).page(params[:page]).per(10)
else
#contacts = Contact.all.page(params[:page]).per(10)
end
What I was wondering is ... is this the best way to do this? I would have thought there would be a nice way to add multiple conditions in the form of a hash and somehow specify that I want to use OR/AND and like.
I am fairly new to rails and google is not really helping much.
Thanks.
Just append the where calls directly to a scope:
#contacts = Contact.scoped
#contacts = #contacts.where("email_address LIKE '%?%'", params[:email_address]) if params[:email_address].present?
#contacts = #contacts.where("first_name LIKE '%?%'", params[:first_Name]) if params[:first_name].present?
#contacts = #contacts.where("last_name LIKE '%?%'", params[:last_name]) if params[:last_name].present?
You can use a simple loop to make it less repetative:
%(email_address first_name last_name).each do |field|
#contacts = #contacts.where("#{field} like '%?%'", params[field]) if params[field].present?
end
And do not build queries by hand by directly substituting user input into your query string. Rails makes that hard to do on purpose: You're bypassing all of Rails' sanitization and opening yourself to SQL injection.
I would have thought there would be a nice way to add multiple conditions in the form of a hash and somehow specify that I want to use OR/AND and like.
There is, but it only works with AND and =:
#contacts.where(first_name: "bob", last_name: "smith")
# select ... where first_name = 'bob' and last_name = 'smith'

Rails 4, Postgresql - Custom query incorrectly constructed inside controller, correctly in console

I'm trying to write a query that will match last names that at least start with the submitted characters. No matter how I construct the query though, whether I use ILIKE, LIKE, ~*, or ##, the query that's constructed when the method is called from the controller changes the query to a simple = operator.
The full method:
def self.search(params)
if params.empty?
return Evaluation.none
end
results = nil
if params.include?(:appointment_start_date) && params.include?(:appointment_end_date)
results = Evaluation.where(appointment_time: params.delete(:appointment_start_date)..params.delete(:appointment_end_date))
end
params.each do |key, value|
case key
when :l_name
results = (results || Evaluation).where("to_tsquery('english', '#{value}:*') ## to_tsvector('english', l_name)")
when :ssn
results = (results || Evaluation).joins(:candidate).where(candidates: { ssn: value })
else
results = (results || Evaluation).where("#{key} = ?", value)
end
end
results.includes(:evaluation_type, :agency, :psychologist)
end
But the pertinent piece is:
.where("to_tsquery('english', '#{value}:*') ## to_tsvector('english', l_name)")
If I call the method from the Rails console, the expected SQL query is generated and the correct records are returned. Now here's the controller action:
def search
authorize! :read, Evaluation
#evaluations = Evaluation.search(evaluation_search_params).paginate(page: params[:page]).decorate
end
Let's say the user submits 'kel' for the search. For some flippin' reason, the generated query is
SELECT "evaluations".* FROM "evaluations" WHERE (l_name = 'kel') ORDER BY "evaluations"."appointment_time" ASC LIMIT 30 OFFSET 0
(There's a default_scope on Evaluation, hence the ORDER BY, so that's not part of the mystery.)
I've tried removing the pagination and the Draper decoration to no avail, and both of those also work fine in the console. So, my question is, why the heck is the generated query different when called from the controller? I'm seriously at my wit's end with this.
Are you param keys strings instead of symbols? That might be affecting which branch of the case statement is selected. Try debugging and stepping through the code if you are unsure.
You may just need to change to string values:
params.each do |key, value|
case key
when "l_name"
results = (results || Evaluation).where("to_tsquery('english', '#{value}:*') ## to_tsvector('english', l_name)")
when "ssn"
results = (results || Evaluation).joins(:candidate).where(candidates: { ssn: value })
else
results = (results || Evaluation).where("#{key} = ?", value)
end
end

Optional mix of filter parameters in a search the Rails way

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/

Resources