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.
Related
I have a record called Feeds that contains the field 'last_visited' and 'last_modified', both are timestamps.
I'm trying to render a list in a view of alls Feeds where last_modified > last_visited.
I currently have this:
Controller
#feeds = #user.feeds
#feeds_hot = #feeds.where(['#feeds.last_modified > ?', #feeds.last_visited])
Have a feeling I'm way off track here. Should I also being using a model class to do this?
Any helps is greatly appreciated.
Edit:
Here's my model
class Feed < ActiveRecord::Base
attr_accessible :feed_id, :feed_url, :last_modified, :title, :url, :last_visited, :user_id
belongs_to :user
scope :hottest, lambda {where('last_modified > ?', :last_visited)}
def fetch_feed!
feed = Feedzirra::Feed.fetch_and_parse(feed_url) # probably want some eror handling here
self.title = feed.title
self.url = feed.url
self.last_modified = feed.last_modified
self.last_visited = feed.last_modified
self #or nil if you like
end
def self.check_for_update(feed)
fetched_feed = Feedzirra::Feed.fetch_and_parse(feed.feed_url)
entry = fetched_feed.entries.first
feed.last_modified = entry.published
end
def update_visit_date!
date = Time.now
update_attribute(:last_visited, date)
self
end
end
Edit
Updated code
Controller
def home
#user = current_user
#feeds = #user.feeds
#feeds_hot = #feeds.hottest
end
Model
attr_accessible :feed_id, :feed_url, :last_modified, :title, :url, :last_visited, :user_id
belongs_to :user
scope :hottest, lambda {where("'last_modified' > ?", 'last_visited')}
View
%ol.feeds.feeds_hot
- #feeds_hot.each do |feed|
%li.feed.hot[feed]
= render :partial => 'feeds/feed_link', :locals => {:feed => feed}
Unfortunately it's still not rendering the hot feeds in the view when I have a feed with the following data:
Last Modified 2013-06-14 23:49:07 UTC
Last Visited 2013-06-14 23:47:55 UTC
Last Modified is a few hours > than Last Visited
If you're looking to get a list of all feeds that have been modified more recently than the most recent visit on any feed, the following will work:
last_visit = Feed.order("last_visited").last.last_visited
#feeds = #user.feeds
#hot_feeds = Feed.where("last_modified > ?", last_visited)
EDIT:
Based on your comments and a re-reading of your question, the code posted above will not accomplish what you're trying to do. Since you're trying to get a list of all invites where each has been modified since it was last visited, you'll want to create a model scope to do the lookup with ActiveRecord. The following code should work:
# app/models/feed.rb
class Feed < ActiveRecord::Base
scope :hottest, lambda {where('last_modified > ?', :last_visited)}
end
Then, you can run the following in a console, controller, or view:
#user.invites.hottest
#=> array of all the user's invites that have been modified more recently than they have been viewed
So I am trying to implement multiple autocomplete using this gem and simple_form and am getting an error.
I tried this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
This is the error I get:
undefined method `to_i' for ["Alley Park, Madison"]:Array
In my params, it is sending this in neighborhood_id:
"search"=>{"neighborhood_id"=>["Alley Park, Madison"],
So it isn't even using the IDs for those values.
Does anyone have any ideas?
Edit 1:
In response to #jvnill's question, I am not explicitly doing anything with params[:search] in the controller. A search creates a new record, and is searching listings.
In my Searches Controller, create action, I am simply doing this:
#search = Search.create!(params[:search])
Then my search.rb (i.e. search model) has this:
def listings
#listings ||= find_listings
end
private
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
#truncated for brevity
listings
end
First of all, this would be easier if the form is returning the ids instead of the name of the neighborhood. I haven't used the gem yet so I'm not familiar how it works. Reading on the readme says that it will return ids but i don't know why you're only getting names. I'm sure once you figure out how to return the ids, you'll be able to change the code below to suit that.
You need to create a join table between a neighborhood and a search. Let's call that search_neighborhoods.
rails g model search_neighborhood neighborhood_id:integer search_id:integer
# dont forget to add indexes in the migration
After that, you'd want to setup your models.
# search.rb
has_many :search_neighborhoods
has_many :neighborhoods, through: :search_neighborhoods
# search_neighborhood.rb
belongs_to :search
belongs_to :neighborhood
# neighborhood.rb
has_many :search_neighborhoods
has_many :searches, through: :search_neighborhoods
Now that we've setup the associations, we need to setup the setters and the attributes
# search.rb
attr_accessible :neighborhood_names
# this will return a list of neighborhood names which is usefull with prepopulating
def neighborhood_names
neighborhoods.map(&:name).join(',')
end
# we will use this to find the ids of the neighborhoods given their names
# this will be called when you call create!
def neighborhood_names=(names)
names.split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
# view
# you need to change your autocomplete to use the getter method
<%= f.input :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, input_html: { data: { delimiter: ',', multiple: true, class: "span8" } %>
last but not the least is to update find_listings
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline).includes(:neighborhood)
if keywords.present?
listings = listings.where("listings.headline LIKE :key OR neighborhoods.name LIKE :key", { key: "#{keywords}")
end
if neighborhoods.exists?
listings = listings.where(neighborhood_id: neighborhood_ids)
end
listings
end
And that's it :)
UPDATE: using f.input_field
# view
<%= f.input_field :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, data: { delimiter: ',' }, multiple: true, class: "span8" %>
# model
# we need to put [0] because it returns an array with a single element containing
# the string of comma separated neighborhoods
def neighborhood_names=(names)
names[0].split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
Your problem is how you're collecting values from the neighborhood Model
Neighborhood.order(:name)
will return an array of names, you need to also collect the id, but just display the names
use collect and pass a block, I beleive this might owrk for you
Neighborhood.collect {|n| [n.name, n.id]}
Declare a scope on the Neighborhood class to order it by name if you like to get theat functionality back, as that behavior also belongs in the model anyhow.
edit>
To add a scope/class method to neighborhood model, you'd typically do soemthing like this
scope :desc, where("name DESC")
Than you can write something like:
Neighborhood.desc.all
which will return an array, thus allowing the .collect but there are other way to get those name and id attributes recognized by the select option.
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.