Mongoid dynamic query - ruby-on-rails-3

This must be an easy one, but I'm stuck...
So I'm using Rails#3 with Mongoid and want to dynamically build query that would depend upon passed parameters and then execute find().
Something like
def select_posts
query = :all # pseudo-code here
if (params.has_key?(:author))
query += where(:author => params[:author]) # this is pseudo-code again
end
if (params.has_key?(:post_date))
query += where(:create_date => params[:post_date]) # stay with me
end
#post_bodies = []
Post.find(query).each do |post| # last one
#post_bodies << post.body
end
respond_to do |format|
format.html
format.json { render :json => #post_bodies }
end
end

You have a few different options to go with here - depending on how complex your actual application is going to get. Using your example directly - you could end up with something like:
query = Post.all
query = query.where(:author => params[:author]) if params.has_key?(:author)
query = query.where(:create_date => params[:post_date]) if params.has_key?(:post_date)
#post_bodies = query.map{|post| post.body}
Which works because queries (Criteria) in Mongoid are chainable.
Alternatively, if you're going to have lots more fields that you wish to leverage, you could do the following:
query = Post.all
fields = {:author => :author, :post_date => :create_date}
fields.each do |params_field, model_field|
query = query.where(model_field => params[params_field]) if params.has_key?(params_field)
end
#post_bodies = query.map{|post| post.body}
And finally, you can take it one level further and properly nest your form parameters, and name the parameters so that they match with your model, so that your params object looks something like this:
params[:post] = {:author => "John Smith", :create_date => "1/1/1970", :another_field => "Lorem ipsum"}
Then you could just do:
#post_bodies = Post.where(params[:post]).map{|post| post.body}
Of course, with that final example, you'd want to sanitize the input fields - to prevent malicious users from tampering with the behaviour.

Related

Ruby on Rails - search in database based on a query

I have a simple form, where I set up a query that I want to browse, for example panasonic viera.
This is on how I search the term in database:
Product.where("name ilike ?", "%#{params[:q]}%").order('price')
The query looks like %panasonic viera%, but I would need to search the query this way: %panasonic%viera% - I need to find all products, where is in the title the word panasonic or viera... but how to make this query?
One solution would be to break up your query into individual terms and build a set of database queries connected by OR.
terms = params[:q].split
query = terms.map { |term| "name like '%#{term}%'" }.join(" OR ")
Product.where(query).order('price')
If you're using PostgreSQL, you can use pg_search gem. It's support full text search, with option any_word:
Setting this attribute to true will perform a search which will return all models containing any word in the search terms.
Example from pg_search:
class Number < ActiveRecord::Base
include PgSearch
pg_search_scope :search_any_word,
:against => :text,
:using => {
:tsearch => {:any_word => true}
}
pg_search_scope :search_all_words,
:against => :text
end
one = Number.create! :text => 'one'
two = Number.create! :text => 'two'
three = Number.create! :text => 'three'
Number.search_any_word('one two three') # => [one, two, three]
Number.search_all_words('one two three') # => []
How about via ARel
def self.search(query)
words = query.split(/\s+/)
table = self.arel_table
predicates = []
words.each do |word|
predicates << table[:name].matches("%#{word}%")
end
if predicates.size > 1
first = predicates.shift
conditions = Arel::Nodes::Grouping.new(predicates.inject(first) {|memo, expr| Arel::Nodes::Or.new(memo, expr)})
else
conditions = predicates.first
end
where(conditions).to_a
end
This isn't working?
WHERE name LIKE "panasonic" OR name LIKE "viera"

Passing array to Rails where method

So I want to dynamically pass filter parameters to my where method so basically I have this
#colleges = College.where(#filter).order(#sort_by).paginate(:page => params[:page], :per_page => 20)
And the #where is just a string built with this method
def get_filter_parameters
if params[:action] == 'index'
table = 'colleges'
columns = College.column_names
else
table = 'housings'
columns = Housing.column_names
end
filters = params.except(:controller, :action, :id, :sort_by, :order, :page, :college_id)
filter_keys = columns & filters.keys
#filter = ""
first = true
if filter_keys
filter_keys.each do |f|
if first
#filter << "#{table}.#{f} = '#{filters[f]}'"
first = false
else
#filter << " AND #{table}.#{f} = '#{filters[f]}'"
end
end
else
#filter = "1=1"
end
The problem is I don't know how good it is to drop raw SQL into a where method like that. I know normally you can do stuff like :state => 'PA', but how do I do that dynamically?
UPDATE
Okay so I am now passing a hash and have this:
if params[:action] == 'index'
columns = College.column_names
else
columns = Housing.column_names
end
filters = params.except(:controller, :action, :id, :sort_by, :order, :page, :college_id)
filter_keys = columns & filters.keys
#filter = {}
if filter_keys
filter_keys.each do |f|
#filter[f] = filters[f]
end
end
Will that be enough to protect against injection?
in this code here:
College.where(:state => 'PA')
We are actually passing in a hash object. Meaning this is equivalent.
filter = { :state => 'PA' }
College.where(filter)
So you can build this hash object instead of a string:
table = "colleges"
field = "state"
value = "PA"
filter = {}
filter["#{table}.#{field}"] = value
filter["whatever"] = 'omg'
College.where(filter)
However, BE CAREFUL WITH THIS!
Depending on where this info is coming from, you be opening yourself up to SQL injection attacks by putting user provided strings into the fields names of your queries. When used properly, Rails will sanitize the values in your query. However, usually the column names are fixed by the application code and dont need to be sanitized. So you may be bypassing a layer of SQL injection protection by doing it this way.

How to convert postgresql column into a float for a scoped search in rails

I am trying to search my postgresql db in rails. I followed the Railscasts #111 Advanced Search tutorial and it is working for the name and category of my items column in plain text. However, I want to set a min/max price on my search as well which is where I come into my problem. In my db my price is stored as a string in the format "AU $49.95". Can I convert this into a float on the fly in my scoped search? If so how? If not, what should I do?
Here is the code:
search.rb
class Search < ActiveRecord::Base
attr_accessible :keywords, :catagory, :minimum_price, :maximum_price
def items
#items ||= find_items
end
private
def find_items
scope = Item.scoped({})
scope = scope.scoped :conditions => ["to_tsvector('english', items.name) ## plainto_tsquery(?)", "%#{keywords}%"] unless keywords.blank?
scope = scope.scoped :conditions => ["items.price >= ?", "AU \$#{minimum_price.to_s}"] unless minimum_price.blank?
# scope = scope.scoped :conditions => ["items.price <= ?", "AU \$#{maximum_price.to_s}"] unless maximum_price.blank?
scope = scope.scoped :conditions => ["to_tsvector('english', items.catagory) ## ?", catagory] unless catagory.blank?
scope
end
end
searches_controller.rb
class SearchesController < ApplicationController
def new
#search = Search.new
end
def create
#search = Search.new(params[:search])
if #search.save
redirect_to #search, :notice => "Successfully created search."
else
render :action => 'new'
end
end
def show
#search = Search.find(params[:id])
end
end
Thanks for reading this far!
Use the data type numeric or money for exact numerical calculation without rounding errors - and sorting as a number (not as text).
Converting string literal to numeric should not be a performance problem at all.

One efficient SQL call instead of 500

In my view, I've got a fiddly loop which creates 500 SQL queries (to get the info for 500 books). How can I avoid lots of SQL queries by loading a variable up in the controller?
My current (pseudo) code:
controller index action:
#books = Book.scoped.where(:client_id => #client.id).text_search(params[:query])
#feature_root = Book.multiple_summary_details_by_category( #books )
#...returns a hash of books
#features = #feature_root.to_a.paginate(:page => params[:page], :per_page => 4)
index.html.haml
= render :partial => "feature", :locals => { :features => #features }
_features.html.haml
- features.each_with_index do |(cat_name, array_of_books), i|
%h2
= cat_name
- array_of_books[0..10].each do |feature|
= link_to image_tag(feature[:cover], :class => "product_image_tiny"), book_path(feature[:book])
# more code
- array_of_books.sort_by{ |k, v| k["Author"] }.each do |feature|
- feature.each do |heading,value|
%span.summary_title
= heading + ':'
%span.summary_value
= value
What have you tried so far? It should be quite easy with standard ActiveRecord queries as documented in http://guides.rubyonrails.org/active_record_querying.html.
Also, instead of
array_of_books.sort_by{ |k, v| k["Author"] }
try something like
Book.order("author DESC")
(not sure about your exact model here) to let the db do the sorting rather than putting them in an array and let ruby handle it.

Refactor this controller?

class ArticlesController < ApplicationController
def index
#articles = Article.by_popularity
if params[:category] == 'popular'
#articles = #articles.by_popularity
end
if params[:category] == 'recent'
#articles = #articles.by_recent
end
if params[:category] == 'local'
index_by_local and return
end
if params[:genre]
index_by_genre and return
end
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #articles }
end
end
def index_by_local
# 10 lines of code here
render :template => 'articles/index_by_local'
end
def index_by_genre
# ANOTHER 10 lines of code here
render :template => 'articles/index_by_genre'
end
end
As you can see from above. My controller is not exactly thin. What its doing is, depending on the params that were passed, it interacts with the model to filter out records.
And if params[:local] or params[:genre] was passed. Then it calls its own methods respectively (def index_by_local and def index_by_genre) to do further processing. These methods also load their own template, instead of index.html.erb.
Is this quite typical for a controller to look? Or should I be refactoring this somehow?
We can move the first few lines into the model(article.rb):
def get_by_category(category)
# Return articles based on the category.
end
In this way we can completely test the article fetching logic using unit tests.
In general move all the code related to fetching records inside model.
Controllers in general
should authorize the user
get records using the params and assign them to instance variables
[These must typically be function
calls to model]
Render or redirect
I would define scopes for each of the collections you want to use.
class Article < ActiveRecord::Base
...
scope :popular, where("articles.popular = ?", true) # or whatever you need
scope :recent, where(...)
scope :by_genre, where(...)
scope :local, where(...)
...
def self.filtered(filter)
case filter
when 'popular'
Article.popular, 'articles/index'
when 'recent'
Article.recent, 'articles/index'
when 'genre'
Article.by_genre, 'articles/index_by_genre'
when 'local'
Article.local, 'articles/index_by_local'
else
raise "Unknown Filter"
end
end
end
Then in your controller action, something like this:
def index
#articles, template = Article.filtered(params[:category] || params[:genre])
respond_to do |format|
format.html { render :template => template }
format.xml { render :xml => #articles }
end
end