Help me refactor ruby next code - ruby-on-rails

#people = People.scoped
#people = #people.where(...) if ...
#people = #people.where(...) if ...
#people = #people.where(...) if ...
#people = #people.where(...) if ...
Is any ruby existing solutions to make something like
#people = People.scoped
#people.???? do
where(...) if ...
where(...) if ...
where(...) if ...
end
PS: Thanks for answers. But solutions you provide looks like
def self.conditional_scope
where(...) if ...
where(...) if ...
where(...) if ...
end
I think i'll get only last where even if all "if" is true.
Am i right?

I think you should get yourself familiar with named_scopes:
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html
They are composable, so you can write something like:
People.tall.having_children.older_than(30)
where "tall", "having_children" and "older_than" are named scopes.

If I understand what you are asking, you only want to apply each scope if a condition exists... you could use a named scope with a lambda, and then chain them:
scope :one, lambda {|condition| condition ? where(...) : {}}
scope :two, lambda {|condition| condition ? where(...) : {}}
...
#people = Person.one(true).two(false)

def self.conditional_scope
where(...) if ...
where(...) if ...
where(...) if ...
end
Then:
Model.conditional_scope

Yes. You just need to move it to model:
# Controller
#people = People.find_my_guy
# Model
def self.find_my_guy
where(...) if ...
where(...) if ...
where(...) if ...
end
Obviously, you'll need to pass some environment variable to your model if they are used in your statements:
# Controller
#people = People.find_my_guy(params)
# Model
def self.find_my_guy(params)
where(:id => params[:id]) if params[:id]
where('title LIKE (?)', "%#{params[:search]}%") if parmas[:search]
where(...) if ...
end
As far as you're right about last where I can suggest only method chaining here (simmilar as #socjopata did(:
# Model
def self.with_name(name)
where(:name => name) if name.present?
end
def self.with_id_gt(id)
where('id >= ?', id) if id.to_i > 3
end
# Controller
Post.with_name(parms[:name]).with_id_gt(params[:id])

Maybe you're looking for a way to avoid explicitly assigning the new scope after every where clause? You might be interested in this railscast: http://railscasts.com/episodes/212-refactoring-dynamic-delegator. Ryan Bates uses a delegator to achieve code like this:
def self.search(params)
products = scope_builder
products.where("name like ?", "%" + params[:name] + "%") if params[:name]
products.where("price >= ?", params[:price_gt]) if params[:price_gt]
products.where("price <= ?", params[:price_lt]) if params[:price_lt]
products
end

Related

Ruby metaprogramming to achieve dynamic methods?

Want to achieve the following code using metaprogramming.
#resource = {}
#voters = {}
#is_upvoted = {}
def resource(comment)
#resource[comment.id]
end
def voters(comment)
#voters[comment.id]
end
def is_upvoted(comment)
#is_upvoted[comment.id]
end
How can I create these methods using ruby metaprogramming and access the hash?
Can you tell me what is wrong in my code ?
['resource', 'voters', 'is_upvoted'].each do |attribute|
define_method("#{attribute}") do |comment|
instance_variable_set("##{attribute}", comment.id)
end
end
This bit seems redundant:
#resource = {}
#voters = {}
#is_upvoted = {}
Since you're already looping an array to do your metaprogramming.
You might try something like:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |comment|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
instance_variable_get("##{attr_sym}")[comment.id]
end
end
end
Which I believe will give you methods roughly like:
class Foo
def resource(comment)
#resource ||= {}
#resource[comment.id]
end
end
Personally, it seems not great to me to have comment.id in your method. Because what if someday you want to use a different attribute (or something else altogether) as the key?
So, I think I would do:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |key|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
instance_variable_get("##{attr_sym}")[key]
end
end
end
Now, it seems like you're going to want an easy way to set key-value pairs on your instance variable, so I guess I would try something like:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |key=nil|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
hsh = instance_variable_get("##{attr_sym}")
return hsh[key] if key
hsh
end
end
end
In which case you should be able to do (assuming you have a #comment variable that responds to id):
#comment.id
=> 1
foo = Foo.new
=> #<Foo:0x000056536d7504b0>
foo.resource
=> {}
foo.resource[#comment.id] = :bar
=> :bar
foo.resource
=> {1=>:bar}
foo.resource[#comment.id]
=> :bar
Can you tell me what is wrong in my code ?
It's doing the equivalent of this:
def resource(comment)
#resource = comment.id
end
instance_variable_get would be a better choice.
This is how I used it and it works
['resource', 'voters', 'is_upvoted'].each do |attribute|
define_method("#{attribute}") do |comment|
instance_variable_get("##{attribute}")[comment.id]
end
end

Correct way to validate the query string parameter in controller?

I have a User controller and method named 'index' for listing users. And in view there is a filtering option (with status pending, active, deleted). I wrote the code and its working fine. But I need to know the code that I wrote is correct or not, or is any easy method to code. Is that correct method for validating status in controller(validating if the query string status include in a set of statuses). Please help
I used following code:
class UsersController < ApplicationController
def index
#filter_field = ''
if ((not params[:status].nil?) && ['pending', 'active', 'deleted'].include?(params[:status]))
#filter_field = params[:status]
end
#users = User.select_all(#filter_field)
end
end
class User < ActiveRecord::Base
def self.select_all filter
if filter.empty?
User.find(:all)
else
User.find(:all, :conditions => ['status = ?', filter]))
end
end
end
You can do this:
def index
filter = %w(pending active deleted).include?(params[:status]) ? params[:status] : ''
#users = User.select_all(filter)
end
It makes the check easier to understand + no need for an instance variable right?
BTW, I feel like:
['pending', 'active', 'deleted']
Should be given by a method, to avoid a magic array

how to chain where query in rails 3 active record

I need to add conditions depending on params data.
#users = User.where('id', params[:id]) unless params[:id].nil?
#users = User.where('email', params[:email]) unless params[:email].nil?
#users = User.limit(10)
But it does not work for some reason.
Thanks
Each of your statements is replacing the #users variable, and as ActiveRecord evaluates each lazily, the first two are never being called.
If you want to maintain three separate queries and build things up that way you can do:
#users = User.limit(10)
#users = #users.where('id', params[:id]) if params[:id]
#users = #users.where('email', params[:email]) if params[:email]
It isn't the prettiest, but it will work. However, I recommend keeping it to a single method call and defining it in the model.
# In the model
def self.by_id_and_email(id, email)
users = limit(10)
users = users.where('id', id) if id.present?
users = users.where('email', email) if email.present?
users
end
# In the controller / out of the model
User.by_id_and_email(params[:id], params[:email])
That way you can use the method again, refine it, and write speed(ier) tests against it.
You could add scopes to your model that don't do anything if the param isn't provided e.g.
(the scoped call just returns the current scope)
# in the model
def self.by_id(id)
return scoped unless id.present?
where(:id => id)
end
def self.by_email(email)
return scoped unless email.present?
where(:email => email)
end
# in the controller
User.by_id(params[:id]).by_email(params[:email])
You could do this simply by:
wheres = [:id, :email].map{|key| params.has_key?(key) ? {key => params[key]} : {} }\
.inject({}){|hash, injected| hash.merge!(injected)}
#users = User.where(wheres).limit(10)
Further, you could always abstract the above into a scope.
This is a perfect case for scopes. Check out this asciicast for a more in depth tutorial: http://asciicasts.com/episodes/215-advanced-queries-in-rails-3
By using scopes you can keep the where clauses as separate elements that you can reuse individually, but can also chain them just like the where clauses.
You can do this via something like...
if params[:id].exists? && params[:email].exists?
#users = User.where('id = ? AND email = ?', params[:id], params[:email]).limit(10)
elsif #if only one of id/email exists then...
#users = User.where( #conditions for only one of id/email ).limit(10)
else
#raise some errors
Actually is better if you create Scopes for example, This is a excellent article that explain this
in your model
scope :by_id, -> id { where(id: id) if id.present? }
scope :by_email, -> email { where(email: email) if email.present? }
and in your method you can call in this way
def my_function()
User.by_id(id).by_email(email)
end

refactor a method with a block that contains multiple blocks itself

I'm using Ruby 1.9.2
I have a class method called search that takes a block
e.g.
class MyClass
def self.search do
if criteria1
keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
if criteria2
keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
What I'd like to do is refactor the MyClass.search method and have a simple one-line method for each if/end statement
e.g. it would look something like this:
class MyClass
def self.search do
c1_method
c2_method
end
def self.c1_method
if criteria1
return keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
end
def self.c2_method
if criteria2
return keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
But the refactoring that I show above doesn't quite work. It looks like the "blocks" that I'm returning in c1_method and c2_method aren't really being returned and evaluated in the search method, but I'm not sure how to do that.
Well, you can use the method(sym) call in order to get at the body of a method.
>> def foo(bar); bar * 2; end
=> nil
>> def baz(bleep); method(:foo).call(bleep); end
=> nil
>> baz(6)
=> 12

Code in Railscasts 111 -Advanced Search Form

In one particular Railcasts episode Ryan talks about advanced search and in that he uses some code so as to find the conditions for the search. As its working isn't explained I wanted some clarification regarding it.
def products
#products ||= find_products
end
private
def find_products
Product.find(:all, :conditions => conditions)
end
def keyword_conditions
["products.name LIKE ?", "%#{keywords}%"] unless keywords.blank?
end
def minimum_price_conditions
["products.price >= ?", minimum_price] unless minimum_price.blank?
end
def maximum_price_conditions
["products.price <= ?", maximum_price] unless maximum_price.blank?
end
def category_conditions
["products.category_id = ?", category_id] unless category_id.blank?
end
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
def conditions_clauses
conditions_parts.map { |condition| condition.first }
end
def conditions_options
conditions_parts.map { |condition| condition[1..-1] }.flatten
end
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
I would welcome any information as to how this works especially the method products as he even calls it as products.name etc.
He defines some methods for the conditions in his search form: keyword_conditions, minimum_price_conditions ans so on. products.name means the field name from the table products.
The method
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
uses reflection to look at the private methods of this class which have the name that ends with _conditions (The regex /_conditions$/) and joins only those that don't return null (compact)
The method
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
adds a AND keyword between the conditions and passes the result to Product.find which makes the SQL query and returns the result set.

Resources