Rails simple filtering using LIKE - ruby-on-rails

I'm looking for a simple way to build up a filter query using given column/value hash. All of the values should be used in LIKE queries. I can build up the query and the parameters in arrays and pass it to where, like this:
def self.filter(filters=nil)
if filters.nil? || filters.empty?
return all
end
query = []
value_array = []
filters.each do |key, value|
query << "#{key} LIKE ?"
value_array << "%#{value}%"
end
where([query.join(' AND ')] + value_array)
end
But I was wondering if there is a better way of doing this either built into Rails (using version 4) or if there is a super simple gem that can easily accept a has and turn into a LIKE filter?

a nice way to play nice with hashes in queries is to make use of the Hash#slice method and scopes:
...
filtering_params(params).each do |key, value|
#products = #products.public_send(key, value) if value.present?
end
def filtering_params(params)
params.slice(:status, :location, :starts_with)
end
class Product < ActiveRecord::Base
scope :status, -> (status) { where status: status }
scope :location, -> (location_id) { where location_id: location_id }
scope :starts_with, -> (name) { where("name like ?", "#{name}%")}
end
Taken from here. After all you might need to restrict you logic to some specific queries in your DSL.

Related

How can i make this generic and make all the available attribute as filtering params?

I have multiple controllers. and I have a method to filter like this.
def filter(filtering_params)
results = where(nil)
filtering_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
and from controllers, I will call index or show APIs using filtering params for example like this.
def filtering_params
params.slice(:status, :created_at, :id, :transaction_datetime, :portfolio_external_reference_id, :file_id, :file_name)
end
which I will use while fetching from API like this.
def index
records = Module::Class.filter(filtering_params)
render json: {
data: records
}
end
I want to write a generic method that can be used in cases where I want all the available attributes in the particular model as the filtering_params. is there any better way than writing all the attributes?
I solved it and it's working for me.
def filter_params(params)
values = params.slice(*column_names.map(&:to_sym))
filter(values)
end

Rails 5 - Scope with latest has_many association

I'm creating a filtered table for my user model. I've created a few scopes to filter them. I'm :
class User < ApplicationRecord
has_many :invoices
scope :application_approved, -> { ... }
scope :application_denied, -> { ... }
scope :latest_invoice_paid, -> { ... }
scope :latest_invoice_not_paid, -> { ... }
def self.__self__
self
end
end
and in the controller:
def index
filters = params[:statuses] || {}
application_status = filters[:application_status].presence
payments_status = filters[:payments_status].presence
#vehicles = Vehicle.send(application_status || :__self__)
.send(payment_status || :__self__)
.paginate(:page => params[:page], :per_page => 10)
.order('created_at DESC')
end
All of the filters work in isolation, however when chained, filters that are not applied seem to cancel out the earlier filters.
For example, if I set the filter to only show users who have paid, it works. But if I set the filter to only show users who have been approved/unapproved, all users are returned all the time. It seems as though returning self when a filter is not applied just returns all of the users.
So, how can I skip the scope if a filter is not applied for it?
Something like this should do the trick, it also helps secure your send method. Since only whitelisted methods can be executed. The code below does the following:
First create a whitelist with the allowed keys and allowed values.
Get the params[:statuses] or if it doesn't exist create a new Parameters object.
Permit only the allowed keys.
Remove all key-value instances that don't have whitelisted values.
Convert the allowed parameters into a hash.
Reduce the resulting collection. Start with Vehicle.all and send the whitelisted methods (chaining them together). If a key or value isn't present, it won't be looped over so there is no need to call :__self__, or :itself.
Do the rest of your logic.
def index
whitelist = ActiveSupport::HashWithIndifferentAccess.new(
application_status: %w[application_approved application_denied],
payments_status: %w[latest_invoice_paid latest_invoice_not_paid],
)
filters = params[:statuses] || ActionController::Parameters.new
#vehicles =
filters
.permit(*whitelist.keys)
.select { |key, value| whitelist[key].include?(value) }
.to_h
.reduce(Vehicle.all) { |vehicles, (_key, value)| vehicles.send(value) }
.order(created_at: :desc)
.paginate(page: params[:page], per_page: 10)
end
References:
ActiveSupport::HashWithIndifferentAccess
ActionController::Parameters (permit, select and to_h can all be found here)
Enumerable#reduce
The splat operator * in .permit(*whitelist.keys)

Ignore parameters that are null in active record Rails 4

I created a simple web form where users can enter some search criteria to look for venues e.g. a price range. When a user clicks "find" I use active record to query the database. This all works very well if all fields are filled in. Problems occur when one or more fields are left open and therefore have a value of null.
How can I work around this in my controller? Should I first check whether a value is null and create a query based on that? I can imagine I end up with many different queries and a lot of code. There must be a quicker way to achieve this?
Controller:
def search
#venues = Venue.where("price >= ? AND price <= ? AND romance = ? AND firstdate = ?", params[:minPrice], params[:maxPrice], params[:romance], params[:firstdate])
end
You may want to filter out all of the blank parameters that were sent with the request.
Here is a quick and DRY solution for filtering out blank values, triggers only one query of the database, and builds the where clause with Rails' ActiveRecord ORM.
This approach safeguards against SQL-injection, as pointed out by #DanBrooking. Rails 4.0+ provides "strong parameters." You should use the feature.
class VenuesController < ActiveRecord::Base
def search
# Pass a hash to your query
#venues = Venue.where(search_params)
end
private
def search_params
params.
# Optionally, whitelist your search parameters with permit
permit(:min_price, :max_price, :romance, :first_date).
# Delete any passed params that are nil or empty string
delete_if {|key, value| value.blank? }
end
end
I would recommend to make method in Venue
def self.find_by_price(min_price, max_price)
if min_price && max_price
where("price between ? and ?", min_price, max_price)
else
all
end
end
def self.find_by_romance(romance)
if romance
where("romance = ?", romance)
else
all
end
end
def self.find_by_firstdate(firstdate)
if firstdate
where("firstdate = ?", firstdate)
else
all
end
end
And use it in your controller
Venue
.find_by_price(params[:minPrice], params[:maxPrice])
.find_by_romance(params[:romance])
.find_by_firstdate(params[:firstdate])
Another solution to this problem, and I think a more elegant one, is using scopes with conditions.
You could do something like
class Venue < ActiveRecord::Base
scope :romance, ->(genre) { where("romance = ?", genre) if genre.present? }
end
You can then chain those, which would work as an AND if there is no argument present, then it is not part of the chain.
http://guides.rubyonrails.org/active_record_querying.html#scopes
Try below code, it will ignore parameters those are not present
conditions = []
conditions << "price >= '#{params[:minPrice]}'" if params[:minPrice].present?
conditions << "price <= '#{params[:maxPrice]}'" if params[:maxPrice].present?
conditions << "romance = '#{params[:romance]}'" if params[:romance].present?
conditions << "firstdate = '#{params[:firstdate]}'" if params[:firstdate].present?
#venues = Venue.where(conditions.join(" AND "))

Rails - Pass collection to ActiveModel object

I am using rails to make a datatable that paginates with Ajax, and I am following railscast #340 to do so.
This episode makes use of a normal ActiveModel Class called ProductsDatatable or in my case OrdersDatatable to create and configure the table. My question has to do with ruby syntax in this class. I am trying to pass a collection of orders to the OrdersDatatable object, from the controller. I want to access this collection in the fetch_orders method.
I create the table object like this in order.rb:
#datatable = OrdersDatatable.new(view_context)
#datatable.shop_id = #current_shop.id
#datatable.orders_list = #orders # which is Order.in_process
And my OrdersDatatable class looks like this: (the important parts which probably need to change is the second line in initialize and the first line in fetch_orders)
class OrdersDatatable
include Rails.application.routes.url_helpers
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
delegate :params, :h, :link_to, :number_to_currency, to: :#view
attr_accessor :shop_id, :orders_list
def initialize(view)
#view = view
#orders_list = self.orders_list
end
def current_shop
Shop.find(shop_id)
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: orders.count,
iTotalDisplayRecords: orders.count,
aaData: data
}
end
private
def data
orders.map do |order|
[
order.id,
order.name,
h(time_tag(order.date_placed.in_time_zone)),
order.state,
order.source,
order.payment_status,
h(order.delivered? ? 'shipped' : 'unshipped'),
h(number_to_currency order.final_total, unit: order.currency.symbol),
h(link_to 'details', edit_admin_shop_order_path(current_shop, order)),
h(link_to 'delete', admin_shop_order_path(current_shop, order), method: :delete, data: { confirm: 'Are you sure?' } ),
]
end
end
def orders
#orders ||= fetch_orders
end
def fetch_orders
orders = orders_list.order("#{sort_column} #{sort_direction}")
orders = orders.page(page).per_page(per_page)
if params[:sSearch].present?
orders = orders.where("title like :search", search: "%#{params[:sSearch]}%")
end
orders
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def sort_column
columns = %w[id name date_placed state source payment_status delivered final_total]
columns[params[:iSortCol_0].to_i]
end
def sort_direction
params[:sSortDir_0] == "desc" ? "desc" : "asc"
end
end
When I change the first line in fetch_orders to this
orders = Order.in_process.order("#{sort_column} #{sort_direction}")
which is the hard-coded equivalent, it does work. So I just need the correct syntax
Short answer: If you've got an array, and want to sort it, use the sort_by method:
orders = orders_list.sort_by{|order| "#{order.sort_column} #{order.sort_direction}"}
Long answer: The reason your original code doesn't work is that in this case
Order.in_process.order("#{sort_column} #{sort_direction}")
you are building a query. in_process is a named scope (passing in some conditions), and .order tells rails what to order the query by. Then, when it runs out of chained methods, the query executes (runs some sql) and gets the records out of the DB to build a collection of objects.
Once you are working with a collection of objects, you can't call the .order method on it, as that's just used to assemble an sql query. You need to use Array#sort_by instead. sort_by takes a code block, into which is passed each object in the collection (as order in my example but you could call it anything, it's just a variable name).
BTW, if you just want to call a method on all the objects to sort them, you can use a "shortcut syntax" like .sort_by(&:methodname). This uses a little trick of ruby called Symbol#to_proc (http://railscasts.com/episodes/6-shortcut-blocks-with-symbol-to-proc).
So, for example, if there was a method in Order like so
def sort_string
"#{self.sort_column} #{self.sort_direction}"
end
then you could change your code to
orders = orders_list.sort_by(&:sort_string)
which is neat.
If you have an array, then you can sort like this.
orders = orders_list.sort! {|a,b| a.sort_column <=> b.sort_direction}

Is there a more performant way of creating this array?

for one of my views I want to include a search field with jqueries typeahead function.
The array should contain all the attribute values of a client.
The array for the query is generated the following way:
#clients = []
Client.each do |client|
#clients << client.attributes.values.join(' ')
end
Is this approach performant enough for a dataset of about 3000 entries?
Or is there a better and faster solution?
Thanks in advance.
Cheers,
Patrick
Update
One user mentioned to implement it like this:
#clients = Client.map do |client|
client.attributes.values.join(' ')
end
This is another way to do it. But a benchmark reveals that this is no improvement in performance.
This leaves me with the question: Maybe there is a more performant way, but speaking about a maxium of 3000 records - does it really matter?
You could use .map:
#clients = Client.map do |client|
client.attributes.values.join(' ')
end
Even if ActiveRecord models would implement the map method (which they don't i believe), the two solutions suggested by the OP and #xdazz are time- and memory-complexity-wise equivalent. This can be observed with this simple benchmark:
require 'fruity'
# Dummy client class
class Client < Struct.new(:first_name, :last_name, :position, :company)
class << self
include Enumerable
def each(&block)
5000.times do
yield Client.new('Firstname', 'Lastname', 'CEO', 'Company Inc.')
end
end
end
alias_method :attributes, :to_h
end
compare do
schnika do
clients = []
Client.each do |client|
clients << client.attributes.values.join(' ')
end
nil
end
xdazz do
clients = Client.map do |client|
client.attributes.values.join(' ')
end
nil
end
end
Which will output
schnika is similar to xdazz
Also, when you look at the implementation of map (synonymous to collect), it becomes clear that really nothing else happens than in the OP's method:
static VALUE
rb_ary_collect(VALUE ary)
{
long i;
VALUE collect;
RETURN_ENUMERATOR(ary, 0, 0);
collect = rb_ary_new2(RARRAY_LEN(ary));
for (i = 0; i < RARRAY_LEN(ary); i++) {
rb_ary_push(collect, rb_yield(RARRAY_PTR(ary)[i]));
}
return collect;
}
This translates to:
class Array
def collect
collect = []
self.each do |el|
collect << yield(el)
end
collect
end
end
You probably don't need to retrieve all the attributes (for example 'updated_at'), so the following may be faster:
#clients = Client.select([:name, :email, :id]).map do |client|
client.attributes.values.join(' ')
end
Added the id in case you need to link to the client.

Resources