I have the following code that I use in my search forms. I want to be able to chain the scoped method with the by_title, but I fail to see how. I want to have the by_title as a method instead of just doing:
# Arel helpers
class << self
def by_city(city)
where(['city_id = ?', city])
end
def by_title(title)
where('title LIKE ?', "%#{title}%")
end
end
def self.search(search_params)
experiences = scoped
experiences.self.by_title(search_params[:title]) if search_params[:title]
end
Why don't you play with scopes this way:
scope :by_title, lambda { |title| where('title LIKE ?', "%#{title}%") }
scope :by_city, lambda { |city| where('city_id = ?', city) }
By just removing self it should work I think:
experiences = scoped
experiences.by_title(search_params[:title]) if search_params[:title]
The scoped method returns an anonymous scope which can be chained with other scopes/class-methods.
Related
In a Rails 5.1 app, I have a query object (PORO) named CoolProducts.
class CoolProducts
def self.call(relation = Product.all)
...
# return an instance of Product::ActiveRecord_Relation
end
end
Now I need to limit the found Products based on the fact the name matches a string.
The following works
CoolProducts.call.where("name ILIKE ?", "%#{string}%")
However, I'd like to encapsulate the matching login within the CoolProducts class allowing to do something like
CoolProducts.call.including_in_name(string)
But I'm not sure where to start from.
Any ideas?
It will be difficult if you want any of your methods to be chainable or return ActiveRecord::Relation.
If you consider explicitly fetching the records when you're done chaining being ok, this should work:
class CoolProducts
def initialize(relation)
#relation = relation
end
def self.call(relation = Product.all)
new(relation).apply_scopes
end
attr_reader :relation
alias_method :fetch, :relation
def including_in_name(string)
tap { #relation = relation.where("name ILIKE ?", string) }
end
def apply_scopes
tap { #relation = relation.where(price: 123) }
end
end
Usage:
CoolProducts.call.including_in_name(string).fetch
I'm making a search page where I have a couple of filters on the side and I'm trying to integrate them with Searchkick to query products.
These are my scopes I'm using for the products
models/product.rb
scope :in_price_range, ->(range) { where("price <= ?", range.first) }
scope :in_ratings_range, -> (range) { where("average_rating >= ?", range.first) }
def self.with_all_categories(category_ids)
select(:id).distinct.
joins(:categories).
where("categories.id" => category_ids)
end
This is where I'm actually calling the scopes
controllers/search_controller.rb
#results = Product.search(#query)
#results = #results.with_all_categories(params[:category_ids]) if params[:category_ids].present?
#results = #results.in_price_range(params[:price]) if params[:price].present?
#results = #results.in_ratings_range(params[:rating]) if params[:rating].present?
After running it, I get an error saying the searchkick model doesn't have any methods with the name of my scope.
undefined method `with_all_categories' for #Searchkick::Results:0x00007f4521074c30>
How do I use scopes with my search query?
You can apply scopes to Searchkick results with:
Product.search "milk", scope_results: ->(r) { in_price_range(params[:price]) }
See "Run additional scopes on results" in the readme.
However, if you apply ActiveRecord where filters, it will throw off pagination. For pagination to work correctly, you need to use Searchkick's where option:
Product.search(query, where: {price_range: 10..20})
The error (unknown to me at the time of writing this answer) might be because you defined with_all_categories as a class method on Product, but in your controller you call it on #results which must be an ActiveRecord::Relation.
Turning it into a scope should fix the issue:
Change this:
def self.with_all_categories(category_ids)
select(:id).distinct.
joins(:categories).
where("categories.id" => category_ids)
end
to:
scope :with_all_categories, -> (category_ids) { select(:id).distinct.joins(:categories).where("categories.id" => category_ids) }
I want to create a class method for a class inherits ActiveRecord:Base.
What the method need to do is add where clauses based on the options and it works well.
class Article < ActiveRecord::Base
def self.list_by_params(params={})
articles = self
articles = articles.where(author_id: params[:author_id]) unless params[:author_id].blank?
articles = articles.where(category_id: params[:category_id]) unless params[:category_id].blank?
articles = articles.where("created_at > ?", params[:created_at].to_date) unless params[:created_at].blank?
articles
end
end
This code works fine in case of the call such as:
articles = Article.list_by_params({author_id: 700})
#=> Works fine as I expected.
articles = Article.joins(:authors).list_by_params({author_id: 700})
#=> Works fine as I expected.
However, the problem is that, if I want to call the list_by_params without filtering params, then it lose its former relations. For example:
articles = Article.joins(:authors).list_by_params({})
#=> articles is just a `Article` (not an ActiveRecord_Relation) class itself without joining :authors.
Is there any chance that I made a mistake?
Thanks in advance.
What you are looking for is a scope.
I would do something like this
scope :for_author, lambda { |author| where(author_id: author) unless author.blank? }
scope :in_category, lambda { |category| where(category_id: category) unless category.blank? }
scope :created_after, lambda { |date| where('created_at > ?', date.to_date) unless date.blank? }
scope :list_by_params, lambda do |params|
for_author(params[:author_id])
.in_category(params[:category_id])
.created_after(params[:created_at])
end
Now you can reuse the components of your query. Everything has a names and it gets easier to read the code.
For the self explanation, I've solved the problems by using where(nil).
Actually, Model.scoped returned anonymous scope but the method has been deprecated since Rails version 4. Now, where(nil) can replace the functionality.
class Article < ActiveRecord::Base
def self.list_by_params(params={})
articles = where(nil) # <-- HERE IS THE PART THAT I CHANGED.
articles = articles.where(author_id: params[:author_id]) unless params[:author_id].blank?
articles = articles.where(category_id: params[:category_id]) unless params[:category_id].blank?
articles = articles.where("created_at > ?", params[:created_at].to_date) unless params[:created_at].blank?
articles
end
end
I have this class method:
def self.default_column
"created_at"
end
How can I rewrite the following function, so that I can make use of my default_column method?
def next
User.where("created_at > ?", created_at).order('created_at ASC').first
end
I tried things like these...
def next
User.where("#{default_column} > ?", default_column).order('#{default_column} ASC').first
end
... but I must be awfully wrong here because it doesn't work at all.
Thanks for any help.
You can use:
def next
User.where("#{User.default_column} > ?", self.send(User.default_column)).order("#{User.default_column} ASC").first
end
Or even better
def next
klass = self.class # This is supposing you are inside User model
# Otherwise just use klass = User
klass.where("#{klass.default_column} > ?", self.send(klass.default_column))
.order(klass.arel_table[klass.default_column].asc)
end
Notice that if you handle the method in this way, you cannot chain it: like User.where(name: 'something').next
If you want to achieve this, you have to move next to be def self.next and in that case, you have to pass an instance of the user to it, like this:
def self.next(user)
klass = user.class
klass.where("#{klass.default_column} > ?", user.send(klass.default_column))
.order(klass.arel_table[klass.default_column].asc)
end
In this way you can write something like: User.where(name: 'test').next(#user). You can optionally chain .first to get directly the result, but in this way you will not be able to chain other things, like User.where(name: 'test').next(#user).where(email: 'my#mail.com')
Finally, if you want pure AREL (for portability)
def self.next(user)
klass = user.class
arel = klass.arel_table
column = klass.default_column # This helps cleaning up code
column_value = user.send(column)
klass.where(arel[column].gt(column_value))
.order(arel[column].asc)
end
def next
default_column = self.class.default_column
User
.where("#{default_column} > ?", send(default_column))
.order("#{default_column} ASC")
.first
end
In my Widget model I have the following:
scope :accessible_to, lambda { |user|
if user.has_role?('admin')
self.all
else
roles = user.roles
role_ids = []
roles.each { |r| role_ids << r.id }
self.joins(:widget_assignments).where('widget_assignments.role_id' => role_ids)
end
}
Ideally, I would like to use this scope as a filter for Ransack's search results, so in my controller I have:
def index
#q = Widget.accessible_to(current_user).search(params[:q])
#widgets = #q.result.order('created_at DESC')
end
Doing this generates the following error:
undefined method `search' for Array:0x007ff9b87a0300
I'm guessing that Ransack is looking for an ActiveRecord relation object and not an array. Is there anyway that I can use my scope as a filter for Ransack?
Change the self.all for self.scoped. all returns an array.
Update for Rails 4: all will now return a scope.