How to have search functionality on association models as well - ruby-on-rails

By using a railscast video i create a simple search that works on same model. But now i have a model that shows associated model data as well and i would like to search on them as well.
Right now i managed to make it semi work, but i assume i have conflict if i add the field "name" into the joins as i have two models that have a column named "name"
def self.search(search)
if search
key = "'%#{search}%'"
columns = %w{ city station venue area country plate_number }
joins(:services).joins(:washer).joins(:location).where(columns.map {|c| "#{c} ILIKE #{key}" }.join(' OR '))
else
where(nil)
end
end
What do i need to change to be sure i can search across all columns?

I think when you have ambiguous field name after join then you can mention table_name.field_name so it remove the ambiguity and works. something like.
joins(:services).joins(:washer).where("services.name = ? or washer.name = ?", "test", "test")

Related

Activeadmin: how to filter for strings that match two or more search terms

Let's say I've got User class with an :email field. And let's say I'm using activeadmin to manage Users.
Making a filter that returns emails that match one string, e.g. "smith", is very simple. In admin/user.rb, I just include the line
filter :email
This gives me a filter widget that does the job.
However, this filter doesn't let me search for the intersection of multiple terms. I can search for emails containing "smith", but not for emails containing both "smith" AND ".edu".
Google tells me that activerecord uses Ransack under the hood, and the Ransack demo has an 'advanced' mode that permits multiple term searches.
What's the easiest way to get a multiple term search widget into activeadmin?
Ideally, I'd like a widget that would allow me to enter smith .edu or smith AND .edu to filter for emails containing both terms.
there is simple solution using ranasckable scopes
So put something like this in your model
class User < ActiveRecord::Base
....
scope :email_includes, ->(search) {
current_scope = self
search.split.uniq.each do |word|
current_scope = current_scope.where('user.email ILIKE ?', "%#{word}%")
end
current_scope
}
def self.ransackable_scopes(auth_object = nil)
[ :email_includes]
end
end
After this you can add filter with AA DSL
Like
filter :email_includes, as: :string, label: "Email"
UPD
should work if change email_contains_any to email_includes
I've figured out a solution but it's not pretty.
The good news is that Ransack has no trouble with multiple terms searches. These searches use the 'predicate' cont_all. The following line works for finding emails containing 'smith' and '.edu'.
User.ransack(email_cont_all: ['smith','.edu'] ).result
Since these searches are easy in Ransack, they're probably straightforward in Activeadmin, right? Wrong! To get them working, I needed to do three things.
I put a custom ransack method (a.k.a. ransacker) into User.rb. I named the ransacker email_multiple_terms.
class User < ActiveRecord::Base
# ...
ransacker :email_multiple_terms do |parent|
parent.table[:path]
end
I declared a filter in my activeadmin dashboard, and associated it with the ransacker. Note that the search predicate cont_all is appended to the ransacker name.
admin/User.rb:
ActiveAdmin.register User do
# ...
filter :email_multiple_terms_cont_all, label: "Email", as: :string
This line creates the filter widget in Activeadmin. We're nearly there. One problem left: Activeadmin sends search queries to ransack as a single string (e.g. "smith .edu"), whereas our ransacker wants the search terms as an array. Somewhere, we need to convert the single string into an array of search terms.
I modified activeadmin to split the search string under certain conditions. The logic is in a method that I added to lib/active_admin/resource_controller/data_access.rb.
def split_search_params(params)
params.keys.each do |key|
if key.ends_with? "_any" or key.ends_with? "_all"
params[key] = params[key].split # turn into array
end
end
params
end
I then called this method inside apply_filtering.
def apply_filtering(chain)
#search = chain.ransack split_search_params clean_search_params params[:q]
#search.result
end
This code is live in my own fork of activeadmin, here: https://github.com/d-H-/activeadmin
So, to get multiple term search working, follow steps 1 and 2 above, and include my fork of A.A. in your Gemfile:
gem 'activeadmin', :git => 'git://github.com/d-H-/activeadmin.git'
HTH.
If anyone's got a simpler method, please share!
Just add three filters to your model:
filter :email_cont
filter :email_start
filter :email_end
It gives you a flexible way to manage your search.
This filter executes next sql code:
SELECT "admin_users".* FROM "admin_users"
WHERE ("admin_users"."email" ILIKE '%smith%' AND
"admin_users"."email" ILIKE '%\.edu')
ORDER BY "admin_users"."id" desc LIMIT 30 OFFSET 0
I expect that exactly what you're looking for.

Ruby on Rails search 2 models

Right... I've spent 3 days trying to do this myself to no a vale.
I have 2 models called Film and Screenings. Screenings belongs_to Film, Film has_many Screenings.
The Film has certain attributes(:title, :date_of_release, :description, :genre).
The Screening has the attributes(:start_time, :date_being_screened, :film_id(foreign key of Film)).
What I am trying to do is create a Search against both of these models.
I want to do something like this...
#films = Film.advanced_search(params[:genre], params[:title], params[:start_time], params[:date_showing])
And then in the Film model...
def self.advanced_search(genre, title, start_time, date)
search_string = "%" + title + "%"
self.find(:all, :conditions => ["title LIKE ? OR genre = ? OR start_time LIKE ? OR date_showing = ?", title, genre, start_time, date], order: 'title')
end
end
I don't think this could ever work quite like this, but I'm hoping my explanation is detailed enough for anyone to understand what im TRYING to do?? :-/
Thanks for any help guys
I would extract the search capability into a separate (non-ActiveRecord) class, such as AdvancedSearch as it doesn't neatly fit into either the Film or Screening class.
Rather than writing a complex SQL query, you could just search the films, then the screenings, and combine the results, for example:
class AdvancedSearch
def self.search
film_matches = Film.advanced_search(...) # return an Array of Film objects
screening_matches = Screening.advanced_search(...) # return an Array of Screening objects
# combine the results
results = film_matches + screening_matches.map(&:film)
results.uniq # may be necessary to remove duplicates
end
end
Update
Let's say your advanced search form has two fields - Genre and Location. So when you submit the form, the params sent are:
{ :genre => 'Comedy', :location => 'London' }
Your controller would then something like:
def advanced_search(params)
film_matches = Film.advanced_search(:genre => params[:genre])
screening_matches = Screening.advanced_search(:location => params[:location])
# remaining code as above
end
i.e. you're splitting the params, sending each to a different model to run a search, and then combining the results.
This is essentially an OR match - it would return films that match the genre or are being screened at that specified venue. (If you wanted and AND match you would need to the work out the array intersection).
I wanted to write something but this cast says all http://railscasts.com/episodes/111-advanced-search-form
Almost the same case as yours.

Rails- Merge a find with 2 models

I want to build a rails request with 2 models.
I think it's quite simple, but I don't want to do a loop myself.
I'm in my country model:
def self.find_for_user(user_id)
wines = Wine.where("user_id = ?", user_id).group(:country_id)
where("countries.id IN ?", wines.map())
end
I want to get all countries depending the first request (the wines grouped by countries, I just need the countries)
I think I can do this in a single line where I put map() or another instruction. I just need to get all country_id fields for wines.
Thanks.
Assuming that you've got an association set up between wines and country (ie. has_many :wines in country.rb), I think this is what you're looking for:
def self.find_for_user(user_id)
joins(:wines).where('wines.user_id = ?', user_id).uniq
end
If all you want is all countries that have wine for a specific user, you can do that in SQL:
where("countries.id in (select country_id from wines where wines.user_id = ?)", user_id)

Rails basic search and PostgreSQL

I'm trying to create basic search in my web app. Here's code of search function.
def self.search(title, category_id, city_id)
if title || category_id || city_id
joins(:category).where('title LIKE (?) AND category.category_id IN (?) AND city.city_id IN (?)', "%#{title}%", "%#{category_id}%", "%#{city_id}%")
else
scoped
end
end
I have these associations in my model:
has_one :category
has_one :city
And I get this error
ActionView::Template::Error (PG::Error: ERROR: missing FROM-clause entry for ta
ble "category"
LINE 1: ..._id" = "events"."id" WHERE (title LIKE ('%%') AND category.c...
I'm using PostgreSQL. What I can do to remove this error?
The form of joins that you're using wants the association name, the SQL wants the table name. The table should be called categories.
A few other things:
I don't see you joining :city anywhere so your next error will be "Missing FROM-clause entry for table "city". The solution will be to .joins(:city) and use cities in the where. But keep reading anyway.
You don't need the parentheses around the value for LIKE, just title LIKE ? is fine.
You're using IN expressions for the category and city but you're giving them LIKE patterns and that won't work: the IDs will be numbers and you can use LIKE with numbers. If you're using IN then you'll usually want to supply a list of possible values, if you only want to match one value then just use = and a single value for the placeholder.
The categories table probably doesn't have a category_id column, similarly for the cities table and city_id column. Those two columns should be in your model's table.
Searching for a title when you don't have a title doesn't make much sense. Similarly for country and city.
That looks like a lot of problems but they can be fixed without too much effort:
def self.search(title, category_id, city_id)
rel = scoped
rel = rel.where('title like ?', "%#{title}%") if(title)
rel = rel.where('category_id = ?', category_id) if(category_id)
rel = rel.where('city_id = ?', city_id) if(city_id)
rel
end
and you don't even need joins or explicit table names at all.

Rails gem rails3-jquery-autocomplete: How do I query multiple fields

I'm using the rails3-jquery-autocomplete gem found here: http://github.com/crowdint/rails3-jquery-autocomplete
The instructions are clear for how to query a single attribute of a model and I am able to make that work without a problem.
My Person model has two attributes that I would like to combine and query, however. They are first_name and last_name. I would like to combine them into a pseudo-attribute called full_name. Currently, I receive this error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: full_name: SELECT "people".* FROM "people" WHERE (LOWER(full_name) LIKE 'cla%') ORDER BY full_name ASC LIMIT 10):
There is no full_name attribute of the Person model, though I have the following method in the Person model file:
def full_name
"#{self.first_name} #{self.last_name}"
end
How do I modify the Person model file so that calls to full_name queries the database to match a combination of first_name and last_name?
Your pseudo attribute works only on records already retrieved, but it has no bearing on searching for records. Probably the easiest solution is a named named scope like:
scope :search_by_name, lambda { |q|
(q ? where(["first_name LIKE ? or last_name LIKE ? or concat(first_name, ' ', last_name) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
Thus, a call like:
Person.search_by_name(params[:q])
will return an appropriate result set. It will also return all entries if no param was passed (or more specifically, this scope will add nothing extra), making it an easy drop-in for the index action.
Sadly, the scope method mentioned above didn't work for me. My solution was to simply overwrite the get_autocomplete_items method (formerly get_items).
For what it's worth, there is a MySQL function (other db's have it as well, but we're talking MySQL for the moment) that is better suited to the type of concatenation you're using:
def get_autocomplete_items(parameters)
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
end
MySQL's CONCAT_WS() is intended to join strings together with some sort of separator and is ideal for full names.
This chunk of code basically says return the "first last" formatted names of contacts that match whatever the user is searching by when we pair up the database's contact records by a concatenated pairs of first and last names. I feel it's better than the SQL statement above since it does a full search that will match first AND/OR last name in one statement, not three OR statements.
Using this "hn sm" would match "John Smith" since indeed "hm sm" is LIKE "John Smith". Furthermore, it has the added benefit of also returning the concatenated first and last name of each contact. You may want the full record. If that's the case, remove the select() query from the line above. I personally had the need for the user to search for a name and have an autocomplete field return all possible matches, not the records.
I know this is a bit late, but I hope it helps someone else!
Full implementation of multiple-field autocomplete :
Following my comment, my solution to integrate into jquery-autocomplete was to have a custom implementation of the "internal" autocomplete.
1. Query the database
If you're using ActiveRecord, you can use DGM's solution for your named_scope
If you're using Mongoid, you can use this syntax instead:
scope :by_first_name, ->(regex){
where(:first_name => /#{Regexp.escape(regex)}/i)
}
scope :by_last_name, ->(regex){
where(:last_name => /#{Regexp.escape(regex)}/i)
}
scope :by_name, ->(regex){
any_of([by_first_name(regex).selector, by_last_name(regex).selector])
}
EDIT : if you want a stronger autocomplete that can handle accents, matching parts of text, etc; you should try the mongoid text index. Credits to the original answer there
index(first_name: 'text', last_name: 'text')
scope :by_name, ->(str) {
where(:$text => { :$search => str })
}
And don't forget to build the indexes after adding that rake db:mongoid:create_indexes
So you can basically do User.by_name(something)
2. Create an autocomplete action in your controller
Because the one provided by jquery-autocomplete... ain't gonna do what we want.
Note that you'll have to convert the result to JSON so it can be used in the frontend jquery-autocomplete. For this I have chosen to use the gem ActiveModel::Serializer, but feel free to use something else if you prefer, and skip step 3
In your controller :
def autocomplete
#users = User.by_name(params[:term])
render json: #users, root: false, each_serializer: AutocompleteSerializer
end
3. Reformat the response
Your serializer using the gem activemodel:
I provided the link to the 0.9 version, as the master mainpage doesn't contain the full documentation.
class AutocompleteSerializer < ActiveModel::Serializer
attributes :id, :label, :value
def label
object.name
# Bonus : if you want a different name instead, you can define an 'autocomplete_name' method here or in your user model and use this implementation
# object.respond_to?('autocomplete_name') ? object.autocomplete_name : object.name
end
def value
object.name
end
4. Create a route for your autocompletion
In your routes :
get '/users/autocomplete', to: 'users#autocomplete', as: 'autocomplete_user'
5. Have fun in your views
Finally in your views you can use the default syntax of jquery-rails, but remember to change the path !
<%= form_tag '' do
autocomplete_field_tag 'Name', '', autocomplete_user_path, :id_element => "#{your_id}", class: "form-control"
end %>
RQ : I used some 2-level deep nested forms, so it was a bit tricky to get the right id element your_id. In my case I had to do somethig complicated, but most likely it will be simple for you. You can always have a look at the generated DOM to retrieve the field ID
This is a hack and I would very much like this function to be included in the gem but as slant said overwriting the get_autocomplete_items works as he wrote it but it will only return first_name and last_name from the model column. In order to restore functionality that frank blizzard asked for you also need to return the id of the row.
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name, id").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
The difference between mine and slant's answer is id as the last argument of the select method. I know it is late but I hope it helps somebody in the future.
If you don't need the functionality of comparing your search string against the concatenated string and are trying to just do a query on three separate columns you can use this fork of the gem : git://github.com/slash4/rails3-jquery-autocomplete.git. Oh also that fork will only work with ActiveRecord which is probably why they didn't pull it.
To perform a case-insensitive search using #Cyril's method for rails4-autocomplete, I did a slight modification to the named scope #DGM provided
scope :search_by_name, lambda { |q|
q.downcase!
(q ? where(["lower(first_name) LIKE ? or lower(last_name) LIKE ? or concat(lower(first_name), ' ', lower(last_name)) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
This converts the record's entry to lowercase and also converts the search string to lowercase as it does the comparison
An alternative to the previous suggestions is to define a SQL view on the table holding first_name and last_name. Your SQL code (in SQLite) might look like:
CREATE VIEW Contacts AS
SELECT user.id, ( user.first_name || ' ' || users.last_name ) AS fullname
FROM persons;
Then define a model for the table:
class Contact < ActiveRecord::Base
set_table_name "contacts"
end
You can now use rails3-jquery-autocomplete 'out-of-the box'. So in your controller you would write:
autocomplete :contact, :fullname
In your view file you can simply write:
f.autocomplete_field :contact, autocomplete_contact_fullname_path
I will leave configuration of the route as an exercise for the reader :-).

Resources