Make search NOT case sensitive on my rails app - ruby-on-rails

Using this gem: http://filterrific.clearcove.ca/ I have successfully setup search on my app. Here is the scope that is used.
scope :search_by_name, ->(name){ where(name: name) }
Lets say there is a name of 'Jonathon' and you search for 'jon' I would like for it to bring that result up. Also, as it stands now if you search for 'jonathon' (change J to j) it doesn't show the result. You have to have it exactly as the entry.

To stay portable between databases, consider changing your scope to a named method and use AREL matches.
def search_by_name(name)
where(arel_table[:name].matches(name))
end
Though maybe a scope could work:
scope :search_by_name, ->(name){ where(arel_table[:name].matches(name)) }
Since a named scope is the same as a method, especially when you pass it a lambda variable, there is no disadvantage of it being a method instead of a scope.
You may need to prepend or append a % (or both) to the name argument for it to look for anything containing the argument (instead of only exact case-insensitive matches. So perhaps...
def search_by_name(name)
where(arel_table[:name].matches('%' + name + '%'))
end

Related

Is there something like an `ILIKE` method in Rails 4?

I used to do this with an array condition inside the where method:
Article.where('title ILIKE ?','%today%')
This worked in Postgres but ILIKE is not present in MySQL and other DBMS.
What I need is to be able to perform case insensitive queries using a code like
Article.ilike(title:'%today%',author:'%john%')
Even if there's not builtin method to perform case insensitive queries, you can use the Arel library and the matches method, like in:
Article.where(Article.arel_table[:title].matches('%today%'))
This is DB agnostic and SQL Injection proof.
I've written an ilike method in my common scope file, that allows you to call it with a list of attributes and values, that's it:
module CommonScopes
extend ActiveSupport::Concern
module ClassMethods
def ilike( options={} )
raise ArgumentError unless options.is_a? Hash or options.empty?
if options.one?
where(arel_table[options.keys.first].matches(options.values.first))
else
key, value = options.shift
ilike( {key=>value} ).merge( ilike( options ) )
end
end
end
end
You can place this inside app/models/concerns/common_scopes.rb and include where you need it.
No, there isn't. You need to write driver-specific SQL to achieve this.
ActiveRecord's goal is to make database access fast and easy for 90% of usecases, not to make your models completely database-agnostic. Switching your entire database backend from one system to another is not something they optimize for.
You might consider looking at another gem like DataMapper which provides a Ruby-syntax for wrapping things like like (but which may or may not provide an equivalent to ilike):
# If the value of a pair is an Array, we do an IN-clause for you.
Person.all(:name.like => 'S%', :id => [ 1, 2, 3, 4, 5 ])
Rails don't have the direct case sensitive search. It's dependent on the DB level. For MySQL you can use LOWER method.
YourModel.where('lower(column_name) = ?', str.downcase)

Can I have the same URL, but different dynamic segments?

Is it possible to use the same URL, but with different dynamic segments?
My issue is: I want to be able add object A to objects B and C. So I want to have 2 Rails routes, A/new/:b_id AND A/new/:c_id. Which I tried.
In my routes.rb:
controller :A do
get 'A/new/:b_id', to: 'A#new', as: :new_b_a
get 'A/new/:c_id', to: 'A#new', as: :new_c_a
end
Problem is that the value being passed into the new page is always params[:b_id]! (I can print out the value from the URL using params[:b_id].)
So it seems like maybe I can't have 2 similar routes with different dynamic segments..? If so, how would I do this?
A better way to accomplish this would be using nested resources.
You always get :b_id because Rails matches routes in the order they appear in your file. Since a B ID is an integer indistinguishable from a C ID, there's no way for it to know if you want one or the other.
But, since you do have Bs andCs already, and perhaps those also need to be created, shown, etc., you can differentiate their paths RESTfully, which is what Rails wants you to do.
# config/routes.rb
resources :bs do
resources :as
end
resources :cs do
resources :as
end
This will build you the paths you're creating manually, but turned around a bit:
/bs/:b_id/as/new
/cs/:c_id/as/new
As you can see, the paths now start with the object type you want to add an A too, so Rails can tell them apart. The helper methods generated for this look the same as the ones you're currently defining manually:
new_b_a_path(b)
new_c_a_path(c)
Both paths will route you to the AsController, and then you'll need to look up the correct B or C based on the parameter present:
# AsController#new
#parent = B.find(params[:b_id]) if params[:b_id]
#parent = C.find(params[:c_id]) if params[:c_id]
#a = parent.build_a # Assuming has_one or has_many from B and C to A
Rails has spent a long time developing a particular way to do this sort of thing. You can always dive in and do it a different way, but at best you'll be wasting effort, and at worst you'll be fighting the framework. The framework is less compromising and usually wins.
The routing system works by trying to match the current path to each of the registered routes, from top to bottom. The dynamic :b_id part means "anything that goes here in the path will be passed as a parameter called :b_id to the controller". So making a request to "A/new/anything" will always match the first route, and since you renamed the parameter to :new_b_a, that's how it's called in the params hash.
If you really want to use the same route, you'll need to pass an extra argument specifying the class you want to create the relationship with, though I'd not recommend doing that. It could be something like get 'A/new/:klass/:id', so in the controller you could match the parameter to the desired classes:
def new
case params[:klass]
when 'B' then # do stuff
when 'C' then # do stuff
else raise "Invalid class: #{params[:klass]}"
end
end

How to avoid SQL Injection with Rails #order method

I haven't been able to find any resources online about prevent SQL injections when using #order. There's no trouble using ?-placeholders for the where-clause, but it doesn't seem to work for the order-clause.
Here's an example:
query = Foo.where("ST_DISTANCE(coords, ?) < ?", point, distance)
# The line below works:
.order("ST_DISTANCE(coords, ST_GeomFromText('#{point}'))")
# This line doesn't work:
.order("ST_DISTANCE(coords, ST_GeomFromText(?))", point)
Just to be clear: the line that doesn't work returns a PGError which logs ST_DISTANCE(coords, ST_GeomFromText(?)) literally.
Is this a known issue?
Are you trying to pass something like POINT(-71.064544 42.28787) in GET/POST params? I saw example here http://www.postgis.org/docs/ST_GeomFromText.html
I think better to
order("ST_DISTANCE(coords, ST_GeomFromText('POINT(%f %f))" % [lat, lon])
% is shorthand for Kernel::sprintf
The QueryMethod order calls preprocess_order_args which expects it will just be given a list of fields and optionally directions.
One option would be to call sanitize_sql which can be done from within an ActiveRecord class method:
# Inside Foo class
def self.order_by_distance(point)
order(sanitize_sql(["ST_DISTANCE(coords, ST_GeomFromText(?))", point]))
end

Rails scope searching with wild cards

I would like to add the ability to use wildcards to my search. I'm using rails 3 with SQLite3. My search is built into the index action of my controller using a scope defined in the model.
scope :by_drawing_number, lambda { |drawing_number| where('drawing_number LIKE ?', "#{drawing_number}") unless drawing_number.nil? }
I would like to modify this so if the user enters a '?' it is replaced by a '_' for a single character wildcards, and a '*' is replaced by a '%' for multiple character wildcards.
Is there a way to incorporate these substitutions in my scope or will I need to rewrite the scope as a method? If so, what should the method look like?
You can put any logic you want inside a scope. scope is basically just syntactic sugar for defining a class method. Something like this ought to work:
scope :by_drawing_number, lambda {|drawing_number|
break if drawing_number.nil?
match = drawing_number.tr('?*', '_%')
where('drawing_number LIKE ?', match)
}

Scope and Field as Parameter in Rails

I've added a scope to a Rails model that allows for searching based on a specified parameter field using a range. Here is what it looks like:
scope :upcoming, lambda { |field|
where("to_char(#{field}, 'DDD') BETWEEN :alpha AND :omega",
alpha: Time.now.advance(days: 4).strftime('%j'),
omega: Time.now.advance(days: 8).strftime('%j'),
)
}
Event.upcoming(:registration) # Query all events with registration shortly.
Event.upcoming(:completion) # Query all events with completion shortly.
The above works fine, however in creating I read the Ruby on Rails Guides and found the following:
Putting the variable directly into the conditions string will pass the variable to the database as-is. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
Although the scope is currently never called with a user parameter, I am curious if a way exists of setting the field without using the interpolation in order to better conform with the above recommendation. I've tried using another named parameter, however this will escape the field with quotes (and thus cause it to fail). Any ideas?
I would recommend validating the field parameter against the model's attributes, essentially using the model as a whitelist for values that are allowed to be passed. Something like this:
scope :upcoming, lambda { |field|
if column_names.include? field.to_s
where("to_char(#{field}, 'DDD') BETWEEN :alpha AND :omega",
alpha: Time.now.advance(days: 4).strftime('%j'),
omega: Time.now.advance(days: 8).strftime('%j'),
)
else
# throw some error or return nil
end
}
Okay, reading all the way to the end might help(thanks rubyprince). It looks like you are doing a between query on a field that is storing a date in Oracle. The problem is that to_char is looking for a variable, not a string. And the act of escaping a variable in rails turns it into a string. So, in this particular case you might convert :alpha and :omega into the format of the value stored in field. That way you can escape field in a straightforward manner. Of course there is the issue with Oracle treating dates as Time. I'm guessing that is why you converted to day-of-year for the compare. If you are using the Oracle Enhanced Adaptor you can set
self.emulate_dates_by_column_name = true
to make sure that the field is treated like a date. Then use the to_date function(which takes a string) with :alpha and :omega
scope :upcoming, lambda { |field|
where(":field BETWEEN to_date(:alpha,'yyyy/mm/dd') AND to_date(:omega,'yyyy/mm/dd')",
field: field,
alpha: Time.now.advance(days: 4).strftime('%Y/%m/%d'),
omega: Time.now.advance(days: 8).strftime('%Y/%m/%d'),
)
}
I have no way of testing this so I might be off in the weeds here.
Validating user input as per Jordan is always a good idea.

Resources