Search query in Filterrific Gem - ruby-on-rails

I am trying to add a search query in filterrific gem. I have user model with a method that returns all diagnosis of the user.
def diagnosis
self.consultations.map(&:diagnosis).uniq.compact
end
I want to filter based on this method. Filtering by name is working fine because it's attribute of user table.
Works
scope :with_name, -> (search_string) {
where("users.name ILIKE ?", (search_string.to_s.gsub('*', '%') + '%').gsub(/%+/, '%'))
}
Now are there any way to filter by diagnosis by adding it in query like? of course the below one doesn't work. Or by putting any SQL query?
Need to query based on the diagnosis method
scope :with_name, -> (search_string) {
where("users.diagnosis ILIKE ?", (search_string.to_s.gsub('*', '%') + '%').gsub(/%+/, '%'))
}

Related

Rails 5: use .where with two attributes

I'm combining the two .where like this:
#questions = (FirstQuestion.where(user_id: current_user) + SecondQuestion.where(user_id: current_user)).sort_by(&:created_at).reverse
Both .where searches are with one attribute... the user_id. But now I want to search with two attributes, the user_id and this created_at >= ?", Date.today + 60.days. So basically I want to find the object with a user_id: current_user and the objects that where created less then or equal to 60 days.
Any idea on how to implement this?
Please see my comment as well... because it is kind of a code smell when you have models names FirstQuestion, SecondQuestion. There's really no reason for having separate models. You could probably easily model the logic via an attribute question_depth or something (I don't know what you are trying to achieve exactly).
With regard to your question: ActiveRecord is quite a nice class, that allows for very customizable queries. In your case, you could easily write both conditions each in a separate where, or create a single where. That's totally up to you:
Question.where(user: current_user).where('created_at <= ?', 60.days.from_now)
Or in a single where
Question.where('user_id = ? AND created_at <= ?', current_user.id, 60.days.from_now)
Also, consider using scopes on your Question model for readability and reusability:
class Question < AppModel
scope :by_user, -> (user) { where(user: user) }
scope :min_age, -> (date) { where('created_at <= ?', date) }
end
And use it like:
Question.by_user(current_user).min_age(60.days.from_now)

Filter users post with user params

I'm working on an application and I try to implement a filter with filterrific on the post index.
The filter sort the post with user params : mother_tongue, locality, and availability.
I CreateJoinTableUsersPosts
Post belongs_to :user
User has_one :post
Post.rb :
filterrific(
default_filter_params: {},
available_filters: [
:with_mother_tongue,
:with_locality,
:with_start_time_gte
]
)
scope :with_mother_tongue, -> (search_string) { joins(:users).where("users.mother_tongue LIKE ?", (search_string.to_s.gsub('*', '%') + '%').gsub(/%+/, '%'))
}
scope :with_locality, -> (search_string) { joins(:users).where("users.locality LIKE ?", (search_string.to_s.gsub('*', '%') + '%').gsub(/%+/, '%'))
}
scope :with_start_time_gte, -> (ref_date) { joins(:availabilities).where('availabilities.start_time >= ?', ref_date) }
The filter works on users index but not on posts index with the error :
Can't join 'Post' to association named 'users'; perhaps you misspelled
it?
Do you know why there is this error ?
I think you don't need the join table between users and posts, drop it. And your join should start working.
Other than that, if you are using devise gem, you already have #user available in all controllers inherting from ApplicationController. If not, then it's a whole different story. Research that, there are plenty of resources on that topic.
Don't hesitate to comment, though.

Sort by a model method in a scope

In my model, I have this method which takes the last_name and first_name columns for an object and concatenates them into a string.
def name
last_name + " " + first_name
end
I want to define a scope that can sort my objects by that method. How would one go about doing that, using my method? I don't want to define some scope that first sorts by last_name, and then by first_name in the scope (if that's even possible). My understanding that you can only scope on actual columns in the rails framework? Is that incorrect?
Here's what I wrote, but obviously neither works, as there is no name field in my AdminUser table. Not sure why the second one doesn't work, but I'm guessing that the :name_field wouldn't work, as it's not actually in the model/database as a column.
scope :sorted, lambda { order("name ASC")}
scope :sorted, lambda { :name_field => name, order("name_field ASC")}
Unfortunately it is not possible to do this directly from Ruby/Rails to SQL. You could achieve what you want in two ways
You can load all the users into memory and sort them in Ruby
User.all.to_a.sort_by(&:name)
Or you can define an order in SQL as such
ORDER BY CONCAT(users.last_name, ' ', users.first_name) ASC;
In Rails, you'd have to do the following
scope :sorted, -> {
order("CONCAT(users.last_name, ' ', users.first_name) ASC")
}
Do note that this may not be portable between DBs.

Rails ActiveRecord - Search on Multiple Attributes

I'm implementing a simple search function that should check for a string in either the username, last_name and first_name. I've seen this ActiveRecord method on an old RailsCast:
http://railscasts.com/episodes/37-simple-search-form
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
But how do I make it so that it searches for the keyword in name, last_name and first name and returns the record if the one of the fields matched the term?
I'm also wondering if the code on the RailsCast is prone to SQL injections?
Thanks a lot!
I assumed your model name is Model - just replace it with your real model name when you do the actual query:
Model.where("name LIKE ? OR last_name LIKE ? OR first_name LIKE ?", "%#{search}%","%#{search}%","%#{search}%")
About your worries about SQL injections - both of code snippets are immune to SQL injections. As long as you do not directly embed strings into your WHERE clause you are fine. An example for injection-prone code would be:
Model.where("name LIKE '#{params[:name]}'")
Although the selected answer will work, I noticed that it breaks if you try to type a search "Raul Riera" because it will fail on both cases, because Raul Riera is not either my first name or my last name.. is my first and last name... I solved it by doing
Model.where("lower(first_name || ' ' || last_name) LIKE ?", "%#{search.downcase}%")
With Arel, you can avoid writing the SQL manually with something like this:
Model.where(
%i(name first_name last_name)
.map { |field| Model.arel_table[field].matches("%#{query}%")}
.inject(:or)
)
This would be particularly useful if the list of fields to match against was dynamic.
A more generic solution for searching in all fields of the model would be like this
def search_in_all_fields model, text
model.where(
model.column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
end
Or better as a scope in the model itself
class Model < ActiveRecord::Base
scope :search_in_all_fields, ->(text){
where(
column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
}
end
You would just need to call it like this
Model.search_in_all_fields "test"
Before you start.., no, sql injection would probably not work here but still better and shorter
class Model < ActiveRecord::Base
scope :search_all_fields, ->(text){
where("#{column_names.join(' || ')} like ?", "%#{text}%")
}
end
The best way to do this is:
Model.where("attr_a ILIKE :query OR attr_b ILIKE :query", query: "%#{query}%")

Search multiple columns - Rails

I am currently writing a search method for my rails applications, and at the moment it works fine. I have the following in my game.rb:
def self.search(search)
if search
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "#{search}", "#{search}"])
else
find(:all)
end
end
Now that searches fine, but my problem is that if there is a record in game_name that has the word 'playstation' in it, it will finish the search there. It only returns that record, rather than all games that have 'playstation' stored in console. Now I understand this is because I have 'OR' in my conditions, but I don't know an alternative. 'AND' requires all the conditions to match or none return at all. What is an alternative I can use to AND and OR? Help would be much appreciated.
If there is a solution that has separate search boxes and entries, then that would be fine, I don't necessarily require the search to find it all based on one search form.
If I understand your question correctly, your SQL looks good to me for what you are trying to do. An OR clause will return all records that match in column1, column2, or column3. It doesn't stop at the first match. I do see an issue with your parameters in that the first you are using LIKE with % but in the second two you aren't, maybe that is where your issue is coming from.
Should this be your find (% around second and third search)?
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
or better use DRY version (above will not work for Rails 4.2+):
Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: "%#{search}%")
What if you have 15 columns to search then you will repeat key 15 times. Instead of repeating key 15 times in query you can write like this:
key = "%#{search}%"
#items = Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: key).order(:name)
It will give you same result.
Thanks
I think this is a little bit of a cleaner solution. This allows you to add/remove columns more easily.
key = "%#{search}%"
columns = %w{game_name genre console}
#items = Item.where(
columns
.map {|c| "#{c} like :search" }
.join(' OR '),
search: key
)
A more generic solution for searching in all fields of the model would be like this
def search_in_all_fields model, text
model.where(
model.column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
end
Or better as a scope in the model itself
class Model < ActiveRecord::Base
scope :search_in_all_fields, ->(text){
where(
column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
}
end
You would just need to call it like this
Model.search_in_all_fields "test"
Before you start.., no, sql injection would probably not work here but still better and shorter
class Model < ActiveRecord::Base
scope :search_all_fields, ->(text){
where("#{column_names.join(' || ')} like ?", "%#{text}%")
}
end
I think this is a more efficient solution if you want to search an array of columns as I do.
First and most importantly you can add a private function to your model that creates a query template:
def self.multiple_columns_like_query(array)
array.reduce('') { |memo, x| #
unless memo == '' #
memo += ' or ' # This is the
end #
memo += "#{x} like :q" # core part
} #
end
Than you can use the function in your search function:
def self.search(query)
if fields = self.searched_fields && query
where multiple_like_query(fields), q: "%#{query}%"
end
end
Here you should also define self.searched_fields as an array of field names.

Resources