Sort by a model method in a scope - ruby-on-rails

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.

Related

Rails non hash conditions lost table reference

When using array or string conditions inside Rails query, for example:
Scope in Location model:
scope :name_like, ->(keyword) {where("name ilike ?", keyword)}
It will have problem when using it with join table who also has column name. It is like:
Location.joins(:users).name_like('main')
It will report ambiguous column name conflicts at name.
How should I address this issue, thanks!
Change your name_like scope to use explicit name of locations. I suggest to change it as below:
scope :name_like, -> (keyword) { where("locations.name ilike ?", keyword) }
You need to use like this
scope :by_name, -> { joins(:users).where("users.name like '%?%'",'FirstName' ) }
Refer this link below.
https://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/scope

In Rails5, how do I return the union of two scopes or combine them into one?

I'm struggling to either return the union of two scopes or combine them into one in my Rails app. The outcome I'm looking for is to retrieve all orders that satisfy either of the scopes: Order.with_buyer_like or the Order.with_customer_like. That is I'm looking for the union of the sets. Importantly, I need these to be returned as an ActiveRecord::Relation, rather than an array. Otherwise, I'd just do
results = Order.with_buyer_like + Order.with_customer_like
I've also tried to use the "or" operator but get an error:
Order.with_seller_like('pete').or(Order.with_customer_like('pete'))
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
I've also tried combining the scopes into one, but I can't quite get it to work.
Here's my setup:
class Company
has_many :stores
end
class Store
belongs_to :company
end
class Order
belongs_to :buying_store, class_name: 'Store', foreign_key: 'buying_store_id', required: true
belongs_to :selling_store, class_name: 'Store', foreign_key: 'selling_store_id', required: true
scope :with_buyer_like, ->(search_term) { joins(buying_store: [:company], :customer).where(['stores.name LIKE ? OR companies.name LIKE ?', "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%"]) }
scope :with_customer_like, ->(search_term) { joins(:customer).where(['first_name LIKE ? OR last_name LIKE ? OR mobile_number LIKE ? OR email LIKE ?', "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%"] ) }
end
This blog has good explanation of issue you are facing.
To summarize here, you have to make sure the joins, includes, select (in short structure of AR data to be fetched) is consistent between both the scopes.
This syntax is the way to go as long as you make sure both the scopes have same joins.
Order.with_seller_like('pete').or(Order.with_customer_like('pete'))
To take first step, check if this works(not recommended though):
Order.where(id: Order.with_seller_like('pete')).or(Order.where(id: Order.with_customer_like('pete')))
I would recommend to move away from joins and use sub-query style of querying if you are looking for only Order data in output.

Dynamic Method with ActiveRecord, passing in hash of conditions

I am struggling with the best way to meta program a dynamic method, where I'll be limiting results based on conditions... so for example:
class Timeslip < ActiveRecord::Base
def self.by_car_trans(car, trans)
joins(:car)
.where("cars.trans IN (?) and cars.year IN (?) and cars.model ILIKE ?", trans, 1993..2002, car)
.order('et1320')
end
end
Let's say instead of passing in my arguments, i pass in an array of conditions with key being the fieldname, and value being the field value. so for example, I'd do something like this:
i'd pass in [["field", "value", "operator"],["field", "value", "operator"]]
def self.using_conditions(conditions)
joins(:car)
conditions.each do |key, value|
where("cars.#{key} #{operator} ?", value)
end
end
However, that doesn't work, and it's not very flexible... I was hoping to be able to detect if the value is an array, and use IN () rather than =, and maybe be able to use ILIKE for case insensitive conditions as well...
Any advice is appreciated. My main goal here is to have a "lists" model, where a user can build their conditions dynamically, and then save that list for future use. This list would filter the timeslips model based on the associated cars table... Maybe there is an easier way to go about this?
First of all, you might find an interest in the Squeel gem.
Other than that, use arel_table for IN or LIKE predicates :
joins( :car ).where( Car.arel_table[key].in values )
joins( :car ).where( Car.arel_table[key].matches value )
you can detect the type of value to select an adequate predicate (not nice OO, but still):
column = Car.arel_table[key]
predicate = value.respond_to?( :to_str ) ? :in : :matches # or any logic you want
joins( :car ).where( column.send predicate, value )
you can chain as many as those as you want:
conditions.each do |(key, value, predicate)|
scope = scope.where( Car.arel_table[key].send predicate, value )
end
return scope
So, you want dynamic queries that end-users can specify at run-time (and can be stored & retrieved for later use)?
I think you're on the right track. The only detail is how you model and store your criteria. I don't see why the following won't work:
def self.using_conditions(conditions)
joins(:car)
crit = conditions.each_with_object({}) {|(field, op, value), m|
m["#{field} #{op} ?"] = value
}
where crit.keys.join(' AND '), *crit.values
end
CAVEAT The above code as is is insecure and prone to SQL injection.
Also, there's no easy way to specify AND vs OR conditions. Finally, the simple "#{field} #{op} ?", value for the most part only works for numeric fields and binary operators.
But this illustrates that the approach can work, just with a lot of room for improvement.

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 :-).

Variable field name in named_scope?

In a Rails model I am trying to acheive a named_scope that filters on a start_date and end_date. This is easy. But I am going to have to do it on lots of different fields on many occasions.
Is this asking for trouble? If so why (SQL injection?) and is there another way to acheive this.
named_scope :between, lambda {|start_date, end_date, field|
{ :conditions => ["#{field} >= ? AND #{field} <= ?", start_date, end_date] }
}
EDIT: Solution Used
Using Eggdrop's line of thinking I went with:
##valid_fields = %w(fields in here)
named_scope :between, lambda{ |start_date, end_date, field_name|
field = (##valid_fields.include?(field_name)) ? (field_name) : raise (ActiveRecord::StatementInvalid)
{ :conditions => ["#{field} >= ? AND #{field} <= ?", start_date, end_date]}
}
Now I can reuse my named_scope for fields I wish to filter on date range without rewriting essentially the same scope over and over and whitelist the field names to avoid any mucking about with my column names and tricky SQL injection should the code ever get exposed to user input in the future.
Maybe you could write a method in your model to validate 'field':
If table x, then 'field' must be a particular existing date field in that table.
In other words you don't allow external input into 'field' directly - the external input has to map to known attributes and defined conditions specified in your validate method.
In general, though, this overall direction would not seem to be recommended.

Resources