I am showing a list of questions when the user use the index action. I want to filter this list, showing only rejected questions, questions that only have images attached to them etc.
How do you do that? Do you just add code in the index action that checks if the different named parameters is in the request parameter hash and use them build a query.
myurl.com/questions?status=approved&only_images=yes
Or are there better ways?
You can use has_scope to do this elegantly:
# Model
scope :status, proc {|status| where :status => status}
scope :only_images, ... # query to only include questions with images
# Controller
has_scope :status
has_scope :only_images, :type => boolean
def index
#questions = apply_scopes(Question).all
end
To keep your controller thin and avoid spaghetti code you can try to use following way:
Controller:
def index
#questions = Question.filter(params.slice(:status, :only_images, ...) # you still can chain with .order, .paginate, etc
end
Model:
def self.filter(options)
options.delete_if { |k, v| v.blank? }
return self.scoped if options.nil?
options.inject(self) do |scope, (key, value)|
return scope if value.blank?
case key
when "status" # direct map
scope.scoped(:conditions => {key => value})
when "only_images"
scope.scoped(:conditions => {key => value=="yes" ? true : false})
#just some examples
when "some_field_max"
scope.scoped(:conditions => ["some_field <= ?", value])
when "some_field_min"
scope.scoped(:conditions => ["some_field >= ?", value])
else # unknown key (do nothing. You might also raise an error)
scope
end
end
end
So, I think there are places where you need to code to be good in such a scenario; the model and the controller.
For the model you should use scopes.
#Model
scope :rejected, lambda { where("accepted = false") }
scope :accepted lambda { where("accepted = true") }
scope :with_image # your query here
In the controller,
def index
#questions = #questions.send(params[:search])
end
You can send the method name from the UI and directly pass that to the scope in the model. Also, you can avoid an "if" condition for the .all case by passing it from the UI again.
But as this directly exposes Model code to view, you should filter any unwanted filter params that come from the view in a private method in the controller using a before_filter.
Related
Today i am trying to present the items from a list that were paginated:
users = User.order('name').paginate(:per_page => 10, :page => 1).map{|u| UserPresenter.new(e)}
The problem is that users is no longer (after mapping to my presenter) an array wrapped with will_paginate magic, so it does not contains such things like total_entries, per_page or page, then the will_paginate helper is not working in my view :S .
How should i proceed to paginate a list of post-processed objects?
I've tried doing the other way around but then if i have a huge amount of records in my table it will be really painful because it will paginate from a big result set that is already retrieved:
users = User.order('name').map{|u| UserPresenter.new(u)}.paginate(:per_page => 10, :page => 1)
Thanks in advance
Maybe I would try to create a UserCollectionPresenter and a UserPresenter this way
class UserCollectionPresenter
include Enumerable
delegate :paginate, to: :collection
attr_reader :collection
def initialize(collection)
#collection = collection
#_presentable_collection = collection.map { |user| UserPresenter.new(user) }
end
def each(&block)
#_presentable_collection.each(&block)
end
def total_count
#collection.limit(nil).offset(nil).count
end
end
And
class UserPresenter
def initialize(user)
#user = user
end
private
attr_reader :user
end
In the controller u should send the paginated AR collection and later u can use paginate. Not sure which methods u need from will_paginate but i would think of either delegating, "overriding" them or include them from will_paginate directly.
I would suggest that you still use users for will_paginate and add another instance variables for presenters.
#users = User.order('name').paginate(:per_page => 10, :page => 1)
#presenters = #users.map { |u| UserPresenter.new(u) }
For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.
Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end
There is a search action in RoR that can handle some params e.g.:
params[:name] # can be nil or first_name
params[:age] # can be nil or age
params[:city] # can be nil or country
params[:tag] # can be nil or country
The model name is Person. It also has_many :tags.
When finding persons I need like to AND all the conditions that are present. Of course, it not rational and not DRY.
What I tried to do:
conditions = []
conditions << [ "name like ?", params[:name]+"%" ] if params[:name].present?
conditions << [ "age = ?", params[:age] ] if params[:age].present?
conditions << [ "city = like ?", params[:city]+"%" ] if params[:city].present?
#persons = Person.all(:conditions => conditions )
#What about tags? How do include them if params[:tag].present?
Of course, I want my code to be DRY. Now it's not. Even more, it will cause an exception if params[:age] and params[:name] and params[:city] are not present.
How can I solve? And how do I include tags for persons filtered by tag.name=params[:tag] (if params[:tag].present?) ?
You should do something like this:
lets say filters parameters include this:
filters = {
:name_like => "Grienders",
:age_equal => 15
}
Now you define methods for each
class Person
def search_with_filters(filters)
query = self.scoped
filters.each do |key, values|
query = query.send(key, values)
end
return query
end
def name_like(name)
where("name like ?", name)
end
def age_equal(age)
where(:age => age
end
end
Do you see the method search_with_filters will be the "controlling" method that will take a set of query conditions (name_like, age_equal, etc...) and pass them out to matching method name using the send method, and along with that we also pass the condition which will be the parameter of the method.
The reason why this way is good is because you can scale to any number of conditions (your filter lets say get huge) and also the code is very clean because all you have to do is populate your filters parameter. The method is very readable and very modular and also easy to test
To include the tags condition in your query:
if params[:tag].present?
#persons = Person.all(:conditions => conditions).includes(:tags).where("tags.name = ?", params[:tag])
else
#persons = Person.all(:conditions => conditions)
end
As for there error you are getting when params[:age] .. is not present - This is very strange because it is supposed to return false incase the key is not set in params. Could you please paste the error you are getting?
I would work extensively with scopes for this. Starting with default scope, merge other scopes based on conditions.
Write scopes (or class methods):
class Person << AR::Base
...
NAME_PARTS = ['first_name', 'last_name']
scope :by_name, lambda { |q| where([NAME_PARTS.join(' LIKE :q OR ') + ' LIKE :q', { :q => "%#{q}%" }]) }
scope :by_email, lambda { |q| joins(:account).where(["accounts.email LIKE :q", { :q => "%#{q}%" }]) }
scope :by_age, where(["people.age = ?", q])
scope :tagged, lambda { |q| joins(:tags).where(["tags.name LIKE :q", { :q => "%#{q}%" }]) }
end
Refer:
Scopes in Rails 3
Scopes overhaul section in How Rails 3 Makes Your Life Better
Now, merge the scopes only when the condition is met. As I understand your condition, is the value for a particular search item is nil (like, age is not given), do not search for that scope.
...
def search(object)
interested_fields = ['name', 'age', 'email', 'tags']
criteria = object.attributes.extract!(*interesting_fields) # returns { :age => 20, ... }
scope = object.class.scoped
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.all
end
# This method actually builds the search criteria.
# Only keep param which has value and reject the rest.
def build(params)
required = ['name', 'age', 'email', 'tags']
params.delete_if { |k, v| required.include?(k) && v.blank? }
params
end
...
Refer:
extract!
I have an index action in rails that can handle quite a few params eg:
params[:first_name] # can be nil or first_name
params[:age] # can be nil or age
params[:country] # can be nil or country
When finding users I would like to AND all the conditions that are not nil. This gives me 8 permutations of the find conditions.
How can I can I keep my code DRY and flexible and not end up with a bunch of if statements just to build the conditions for the find. Keep in mind that if no conditions are specified I just want to return User.all
How about something like:
conditions = params.only(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
if conditions.empty?
User.all
else
User.all(:conditions => conditions)
end
I would normally use named scopes for something like this:
class User < ActiveRecord::Base
named_scope :name_like, lambda {|name| {:conditions => ["first_name LIKE ?", "#{name}%"]}}
named_scope :age, lambda {|age| {:conditions => {:age => age}}}
named_scope :in_country, lambda {|country| {:conditions => {:country => country}}}
end
class UsersController < ActionController
def index
root = User
root = root.name_like(params[:first_name]) unless params[:first_name].blank?
root = root.age(params[:age]) unless params[:age].blank?
root = root.country(params[:country]) unless params[:age].blank?
#users = root.paginate(params[:page], :order => "first_name")
end
end
That's what I normally do.
This seems to work quite nicely:
conditions = params.slice(:first_name, :age, :country)
hash = conditions.empty? ? {} : {:conditions => conditions}
#users = User.all hash
Using James Healy answer, I modify the code to be used in Rails 3.2 (in case anyone out there need this).
conditions = params.slice(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
#users = User.where(conditions)
You could try Ambition, or a number of other ActiveRecord extensions.
This works for me too
conditions = params[:search] ? params[:search].keep_if{|key, value| !value.blank?} : {}
User.all(:conditions => conditions)
If you happen to be on an ancient project (Rails 2.x) and very messy, you could do something like the following for adding new fields to the original query.
Original code:
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=?',
"#{name}%", age, country]
Adding a new dynamic condition on zip_code field:
zip_code = params[:zip_code] # Can be blank
zip_query = "AND zip_code = ?" unless zip_code.blank?
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=? #{zip_query}',
"#{name}%", age, country, zip_code].reject(&:blank?)
Adding a reject(&:blank?) to the conditions arrays will filter the nil value.
Note: The other answers are much better if you are coding from zero, or refactoring.