Calling a ActiveRecord class method for ActiveRecord_Relation as a receiver - ruby-on-rails

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

Related

Chaining ActiveRecord_Relation in PORO

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

Method to return a collection - Ruby

Say, I have a method called posted_listings, which is supposed to run an ActiveRecord query and return a collection of User.listings where posted: true, with posted? being a Listing class method. So far I have been doing:
class Bar < ActiveRecord::Base
def posted_listings
posted_listings = []
listings.each { |listing| posted_listings << listing if listing.posted? }
posted_listing
end
end
but each time this query runs I start feeling really bad about my skills (or lack of thereof). What is the most efficient way to return a collection of posted listings?
Edit:
posted? is not an attribute, its a class method:
class Listing < ActiveRecord::Base
def posted?
true if quantity >= 1 && has_sellers?
end
end
def has_sellers?
sellers.count >=1 #association to Seller
end
I would recommend adding a scope to your Listing model like this:
scope :posted, -> { where(posted: true) }
Then you can get all posted listings like this:
#user.listings.posted
You can learn more about scopes here if you are interested.
UPDATE
Try this scope instead:
def self.posted
joins(:sellers)
.where('posted = ? AND quantity > ?', true, 0)
.group('listings.id')
.having('COUNT(sellers.id) > ?', 0)
end
Your question is not so clear for me.
You may try:
User.listings.where(posted: true)
to get all users' posted Listings.
Or, saying #useris an User instance:
#user.listings.where(posted: true)
to get posted Listings from an specific user.

Similarity in rails

I am trying to implement a similar posts functionality in my rails app for this i have this code in my posts controller
#related_Posts = Post
.where('posts.id != ?', #post.id)
.where(:post_title=>#post.post_title)
.where(:category_id=>#post.category).limit(5)
this work fine, but I'm wondering if there is a way to consider a post is similar to the other when only 50% of title are similar instead of the full title
Using a full-text search engine like solr/sunspot is probably what you want. It can be configured to find related posts that have similar words in the title field.
Simply you can use sql LIKE function, in rails (matches) it will seems as following:
class Post < ActiveRecord::Base
scope :by_not_ids, ->(ids = []) { where.not id: ids }
scope :by_title_like, ->(title = '') {
arel_table[:title].matches("%#{title}%")
}
scope :by_category_ids, ->(category_ids = []) {
where category_id: category_ids
}
end
class PostsController < ApplicationController
def index
#related_posts = related_scoping
end
private
def related_scoping
Post.by_not_ids([#post.id])
.by_title_like(#post.title)
.by_category_ids([#post.category_id])
.limit(5)
end
end

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

How to specify a Rails 3 scope.limit - with an offset?

So I have some posts, and would like to show n..m most recent entries in the sidebar (these numbers being set in a config)
I can get the latest n records easily enough
class Post < ActiveRecord::Base
default_scope :order => "created_at DESC"
scope :published, lambda { where("blog_entries.created_at <= ?", Time.zone.now) }
scope :latest, lambda { |n| published.limit(n) }
end
#posts = Post.latest(6)
But what I'd like is
#posts = Post.published.limit(6, 12)
but this gives wrong number of arguments, so is there any way in AR? Right now I'm playing with will_paginate, but it seems hacky to use it for this.
Ok, so the answer is, I think:
#posts = Post.published.limit(6).offset(5)
It will retrieve 6 posts, starting from the sixth.
edit2: About the limit([6, 12]), I find that strange:
attr_accessor :limit_value
def limit(value)
relation = clone
relation.limit_value = value
relation
end
def build_arel
...
arel.take(connection.sanitize_limit(#limit_value)) if #limit_value
...
end
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
elsif limit.to_s =~ /,/
Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',')
else
Integer(limit)
end
end
So I don't really see how it works with an array. But I obviously missed something. Do you see what?
For rails 5 (not sure for rails 4). offset(x).limit(y) works correctly. limit(y).offset(x) still behaves as described in other answers.

Resources