Rails 3: Search method returns all models instead of specified - ruby-on-rails

What I'm trying to do: I have a model "Recipe" in which I defined a method "search" that takes an array of strings from checkboxes (I call them tags), and a single string. The idea is to search the db for recipes that has anything in it's 'name' or 'instructions' that contains the string, AND also has any of the tags matching it's 'tags' property.
Problem: The search method return all the recipes in my db, and doesn't seem to work at all at finding by the specific parameters.
The action method in the controller:
def index
#recipes = Recipe.search(params[:search], params[:tag])
if !#recipes
#recipes = Recipe.all
end
respond_to do |format|
format.html
format.json { render json: #recipe }
end
end
The search method in my model:
def self.search(search, tags)
conditions = ""
search.present? do
# Condition 1: recipe.name OR instruction same as search?
conditions = "name LIKE ? OR instructions LIKE ?, '%#{search[0].strip}%', '%#{search[0].strip}%'"
# Condition 2: if tags included, any matching?
if !tags.empty?
tags.each do |tag|
conditions += "'AND tags LIKE ?', '%#{tag}%'"
end
end
end
# Hämtar och returnerar alla recipes där codition 1 och/eller 2 stämmer.
Recipe.find(:all, :conditions => [conditions]) unless conditions.length < 1
end
Any ideas why it return all records?

if you are using rails 3, then it is easy to chain find conditions
def self.search(string, tags)
klass = scoped
if string.present?
klass = klass.where('name LIKE ? OR instructions LIKE ?', "%#{string}%", "%#{string}%")
end
if tags.present?
tags.each do |tag|
klass = klass.where('tags LIKE ?', "%#{tag}%")
end
end
klass
end

When you do
search.present? do
...
end
The contents of that block are ignored - it's perfectly legal to pass a block to a function that doesn't expect one, however the block won't get called unless the functions decides to. As a result, none of your condition building code is executed. You probably meant
if search.present?
...
end
As jvnill points out, it is in general much nicer (and safer) to manipulate scopes than to build up SQL fragments by hand

Related

How to query records with and without params in Rails?

I have model Places and I have the index method in a controller. I need to get all places via request
/places
And filter places via request with query
/places?tlat=xxxx&tlong=xxxx&blat=xxxxx&blong=xxxx
What the best way to get this records? Should I check an existence of each param or are there Rails way?
#places = if params[tlat]&&params[blat]....
Places.all.where("lat > ? AND long > ? AND lat < ? AND long < ?", tlat, tlong, blat, blong)
else
Places.all
If you want to set WHERE clauses depending on params, you can use Ursus' code which is fine.
However, if you need to apply those WHERE clauses only if a set of params are present, you can use the following:
#places = Place.all
if params[:blat].present? && params[:tlat].present?
#places = #places.where(blat: params[:blat], tlat: params[:tlat])
end
# etc.
You could use an array of arrays to pair the associated params, kind of like what Ursus did.
I'd do something like this if possible. Important to note the this is just one query, composed dynamically.
#places = Place.all
%i(tlat tlong blat blong).each do |field|
if params[field].present?
#places = #places.where(field => params[field])
end
end
IMO, truly the "Rails way" (but actually just the "Ruby way") would be to extract this long conditional, and the query itself, out to their own private method. It becomes much easier to understand what's going on in the index action
class MyController < ApplicationController
def index
#places = Place.all
apply_geo_scope if geo_params_present?
end
private
def geo_params_present?
!!(params[:tlat] && params[:blat] && params[:tlong] && params[:blong])
end
# A scope in the model would be better than defining this in the controller
def apply_geo_scope
%i(tlat tlong blat blong).each do |field|
#places = #places.where(field => params[field])
end
end
end

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}

How to refactor complex search logic in a Rails model

My search method is smelly and bloated, and I need some help refactoring it. I'm new to Ruby, and I haven't figured out how to leverage it effectively, which leads to bloated methods like this:
# discussion.rb
def self.search(params)
# If there is a search query, use Tire gem for fulltext search
if params[:query].present?
tire.search(load: true) do
query { string params[:query] }
end
# Otherwise grab all discussions based on category and/or filter
else
# Grab all discussions and include the author
discussions = self.includes(:author)
# Filter by category if there is one specified
discussions = discussions.where(category: params[:category]) if params[:category]
# If params[:filter] is provided, user it
if params[:filter]
case params[:filter]
when 'hot'
discussions = discussions.open.order_by_hot
when 'new'
discussions = discussions.open.order_by_new
when 'top'
discussions = discussions.open.order_by_top
else
# If params[:filter] does not match the above three states, it's probably a status
discussions = discussions.order_by_new.where(status: params[:filter])
end
else
# If no filter is passed, just grab discussions by hot
discussions = discussions.open.order_by_hot
end
end
end
STATUSES = {
question: %w[answered],
suggestion: %w[started completed declined],
problem: %w[solved]
}
scope :order_by_hot, order('...') DESC, created_at DESC")
scope :order_by_new, order('created_at DESC')
scope :order_by_top, order('votes_count DESC, created_at DESC')
This is a Discussion model that can be filtered (or not) by a category: question, problem, suggestion.
All discussions or a single category can be filtered further by hot, new, votes, or status. Status is a hash in the model and it has several values depending on the category (status filter only appears if params[:category] is present).
Complicating matters is a fulltext search feature using Tire
But my controller looks nice and tidy:
def index
#discussions = Discussion.search(params)
end
Can I dry this up/refactor it a little, maybe using meta programming or blocks? I managed to extract this out of the controller, but then ran out of ideas. I don't know Ruby well enough to take this further.
For starters, "Grab all discussions based on category and/or filter" can be a separate method.
params[:filter] is repeated many times, so take that out at the top:
filter = params[:filter]
You can use
if [:hot, :new, :top].incude? filter
discussions = discussions.open.send "order_by_#{filter}"
...
Also, factor out if then else if case else statements. I prefer break into separate methods and return early:
def do_something
return 'foo' if ...
return 'bar' if ...
'baz'
end
discussions = discussions... appears many times, but looks weird. Can you use return discussions... instead?
Why does the constant STATUSES appear at the end? Usually constants appear at the top of the model.
Be sure to write all your tests before refactoring.
To respond to the comment about return 'foo' if ...:
Consider:
def evaluate_something
if a==1
return 'foo'
elsif b==2
return 'bar'
else
return 'baz'
end
end
I suggest refactoring this to:
def evaluate_something
return 'foo' if a==1
return 'bar' if b==2
'baz'
end
Perhaps you can refactor some of your if..then..else..if statements.
Recommended book: Clean Code

Rails 3 displaying tasks from partials

My Tasks belongs to different models but are always assigned to a company and/or a user. I am trying to narrow what gets displayed by grouping them by there due_at date without doing to many queries.
Have a application helper
def current_tasks
if user_signed_in? && !current_company.blank?
#tasks = Task.where("assigned_company = ? OR assigned_to = ?", current_company, current_user)
#current_tasks = #tasks
else
#current_tasks = nil
end
end
Then in my Main view I have
<%= render :partial => "common/tasks_show", :locals => { :tasks => current_tasks }%>
My problem is that in my task class I have what you see below. I have the same as a scope just named due_today. when I try current_tasks.due_today it works if I try current_tasks.select_due_today I get a undefined method "select_due_tomorrow" for #<ActiveRecord::Relation:0x66a7ee8>
def select_due_today
self.to_a.select{|task|task.due_at < Time.now.midnight || !task.due_at.blank?}
end
If you want to call current_tasks.select_due_today then it'll have to be a class method, something like this (translating your Ruby into SQL):
def self.select_due_today
select( 'due_at < ? OR due_at IS NOT NULL', Time.now.midnight )
end
Or, you could have pretty much the same thing as a scope - but put it in a lambda so that Time.now.midnight is called when you call the scope, not when you define it.
[edited to switch IS NULL to IS NOT NULL - this mirrors the Ruby in the question, but makes no sense because it will negate the left of the ORs meaning]

How to chain optional Mongoid criteria in separate statements?

I'm trying to chain criteria based on optional rails
parameters.
I want to be able to simultaneously filter based on selected tags as
well as searching.
Here is the current code that works in all situations:
if params[:tag] and params[:search]
#notes = Note.tagged_with_criteria(params[:tag]).full_text_search(params[:search])
elsif params[:tag]
#notes = Note.tagged_with_criteria(params[:tag])
elsif params[:search]
#notes = Note.full_text_search(params[:search])
end
I tried simply using
#notes = Note.tagged_with_criteria(params[:tag]).full_text_search(params[:search])
without the if statement, but then if only one of the params was
given, then all notes are returned.
Each of the methods (tagged_with_criteria and full_text_search) are
returning Note.criteria if their parameter is nil / empty.
Is there a simpler, more elegant way to chain Mongoid criteria like this?
I'd rather keep tacking on criteria one-by-one instead of having to do
the weird "if params[...] and params[...]" thing..
UPDATE: here are the current methods:
def tagged_with_criteria(_tags)
_tags = [_tags] unless _tags.is_a? Array
if _tags.empty?
criteria
else
criteria.in(:tags => _tags)
end
end
def self.full_text_search(query)
if query
begin
regex = /#{query}/
# supplied string is valid regex (without the forward slashes) - use it as such
criteria.where(:content => regex)
rescue
# not a valid regexp -treat as literal search string
criteria.where(:content => (/#{Regexp.escape(query)}/))
end
else
# show all notes if there's no search parameter
criteria
end
end
In a situation like that, I would modify the scopes to do nothing when passed in blank values.
I think what might be happening is you are getting empty strings from the params hash, which is causing your code to think that something was entered. Try the scopes with these edits.
def tagged_with_criteria(_tags)
_tags = Array.wrap(_tags).reject(&:blank?)
if _tags.empty?
criteria
else
criteria.in(:tags => _tags)
end
end
def self.full_text_search(query)
if query.present?
begin
regex = /#{query}/
# supplied string is valid regex (without the forward slashes) - use it as such
criteria.where(:content => regex)
rescue
# not a valid regexp -treat as literal search string
criteria.where(:content => (/#{Regexp.escape(query)}/))
end
else
# show all notes if there's no search parameter
criteria
end
end

Resources