call that calls result of call - ruby-on-rails

Using a framework I need 2 ActiveRecord scopes:
scope :tagged_with, lambda { |tag| {:conditions => [" tags like ? ", "% #{tag} %"] } }
scope :tagged_with_any, lambda { |tag_array | [HERE NEW IMPLEMENTATION] }
I want the second scope to be based on the first scope. If you would do it hard coded, you would do for a 2 element array:
lambda { | tag_array | tagged_with(tag_array[0]).tagged_with(tag_array[1]) }
which works, but how do I do it generic
lambda { | tag_array | tags.each { |t| tagged_with(t) } }
clearly doesn't do the job.

Is this acceptable?
named_scope :tagged_with_all, lambda { |tag_array| tag_array.inject(self, :tagged_with) }
[edit] renamed to tagged_with_all since it's what it really does. For a tagged_with_any, Vanilla named scopes do not implement OR-concatenations; concatenating ORs conditions "manually" from scopes is doable but a bit messy. Note that you have libraries like Arel or Metawhere.

Related

Rails and Arel and Scopes - simplify multiple OR's on the same table/field match

I have a requirement where the user can type in a search box and the Rails api should search any of the customer fields for a possible match, so I started like this and realised this was not such a great solution and seemed quite repetitive for all 5 fields:
scope :filter, -> (term) { where(
"lower(customers.name) LIKE ? OR
lower(customers.email) LIKE ? OR
lower(customers.business_name) LIKE ? OR
lower(customers.phone) LIKE ? OR
lower(customers.doc_id) LIKE ? OR",
"%#{term.downcase}%", "%{term.downcase}%", "%#{term.downcase}%",
"%#{term.downcase}%", "%#{term.downcase}%"
) }
So I learned about Arel and tried this instead:
customers = Customer.arel_table
scope :filter, -> (term) { Customer.where(
customers[:name].matches("%#{term.downcase}%")).
or(customers[:email].matches("%#{term.downcase}%")).
or(customers[:phone].matches("%#{term.downcase}%")).
or(customers[:business_name].matches("%#{term.downcase}%").
or(customers[:doc_id].matches("%#{term.downcase}%"))
) }
but that is just as repetitive.
Is there a way to simply either version? I was thinking maybe for Arel I could do this:
scope :filter, -> (term) { Customer.where(
customers[:name, :email, :phone, :business_name, :doc_id].matches("%#{term.downcase}%")
) }
UPDATE
Apologies but I forgot to mention - I was trying to keep this simple! - that if there is a simpler solution, it would still need to be a chainable scope, because I am using this filter in a chain of other scopes, like this in the controller:
if params[:filter].present?
#cards = current_api_user.account.cards.new_card(:false).search(params.slice(:filter))
else ...
where 'search' is a concern that simply sends the filter params key/value pair to scopes in the model. For example, here is the cards model scopes (you can see it's filter scope then calls the filter_customer scope, which then calls Customer.filter which is the one the question is about). This might seem complex but it means I have complete composability of all scopes for all these related models:
scope :new_card, -> value { where(is_new: value) }
scope :filter_template, -> (term) { Card.where(template_id: Template.filter(term)) }
scope :filter_customer, -> (term) { Card.where(customer_id: Customer.filter(term)) }
scope :filter, -> (term) { Card.filter_customer(term).or(Card.filter_template(term)) }
Option 1:
Build a condition string with many ORs
fields = ["name", "email", "phone", "business_name", "doc_id"]
filter = fields.map { |field| "lower(#{field}) LIKE '#{term.downcase}'" }.join(' OR ')
#customers = Customer.where(filter)
Option 2:
Concatenate searches using simple conditions
fields = ["name", "email", "phone", "business_name", "doc_id"]
#customers = []
fields.each do |field|
filter = "lower(#{field}) LIKE '#{term.downcase}'"
#customers.concat(Customer.where(filter))
end
Scope:
With a small change you can use the first method as a scope
Class Customer
scope :filter_customer, -> (term) { Customer.where(Customer.build_filter(term)) }
def self.build_filter term
fields = ["name", "email", "phone", "business_name", "doc_id"]
filter = fields.map { |field| "lower(#{field}) LIKE '#{term.downcase}'" }.join(' OR ')
end
Notes: Your first post was based on Customer and I made all code based on this model. After your update, the answer needs some changes to use in Cards, but it should be trivial.

Combine many scopes

I would like to combine two different scopes in my model. I have this:
Post_model
scope :with_tasks, -> { where(cat: 3).includes(:user).includes(task: :users) }
scope :with_events, -> { where(cat: 4).includes(:user).includes(event: :users) }
scope :with_comments, -> {where(comented: true).includes(comments: :user)}
Post_controller
def index
#posts = current_user.posts.with_tasks + current_user.posts.with_events
end
But I think it is not a really elegant way to achieve it, and I cannot include the comments scope.
Do you know a method to join this scopes into a new one (like the example below)?
scope :with_elements, -> { self.with_tasks.merge(self.with_events) }
What would allow me to call this method into my post#index:
#posts = current_user.posts.with_elements
TASKS = 3
EVENTS = 4
scope :with_tasks_and_or_events, ->(cat) {
cond = {}.tap do |c|
c.merge!(task: :users) if cat.include? TASKS
c.merge!(event: :users) if cat.include? EVENTS
end
where(cat: cat).includes(:user).includes(**cond)
}
And use it like:
with_tasks_and_or_events([TASKS])
with_tasks_and_or_events([TASKS, EVENTS])
Or, better, use Relational Algebra.
Or, even better, revise your database structure.

Are these Rails query syntaxes interchangable?

>scope :a, -> { joins(:b).where('bs.c': false) }
>scope :a, -> { joins(:b).where('bs.c = ?', false) }
Just wanted to ask whether those 2 lines do the same thing? The first one seemed to work fine in development but gave me a syntax error when I tried to push to Heroku. Is the first one deprecated?
I believe these are more interchangeable, without syntax errors:
scope :a, -> { joins(:b).where(bs: { c: false }) }
scope :a, -> { joins(:b).where('bs.c' => false }) }
scope :a, -> { joins(:b).where('bs.c = ?', false) }
scope :a, -> { joins(:b).where('bs.c = :q', { q: false }) }
Personally, the first line is my preferred because you can list several columns within that nested hash without needing to keep repeating the table name/alias.
Only Ruby 2.2 and above allow use the JSON like hash syntax with quoted keys, ie
{'foo' : bar}
Instead of
{foo: bar}
Of course in your case not quoting the key probably won't work either because of the . in the key.
This suggests you are running different Ruby versions locally and on heroku.
Other than that, they should be equivalent.

Drying up multiples scopes with simialar queries

I am noticing a trend with my scopes and trying to figure out how to make it dry
scope :newest, -> { order('created_at DESC') }
scope :top_sold, -> { order('qty_sold DESC') }
scope :most_viewed, -> { order('qty_viewed DESC') }
scope :most_downloaded, -> { order('qty_download DESC') }
scope :most_favorited, -> { order('qty_favorited DESC') }
I would like to pass in the column I want sorted so that I can call it on Photo. I tried this, but running into problems
scope :sort_photos, -> type { order('type DESC') }
Photo.sort_photos('qty_download')
Am I on the right path or is there a smarter way to accomplish this?
Pass type as a scope parameter and use that in order clause with string interpolation:
scope :sort_photos,->(type) { order("#{type} DESC") }
Then do:
Photo.sort_photos('qty_download')
The order method takes a String or a Hash. So instead of order('created_at DESC') you can do order(created_at: :desc), for example. So, to accomplish what you want, it's as simple as changing the key to your type variable:
scope :sort_photos, -> type { order(type => :desc) }
I would also recommend using a sentinel for your order scopes such as by_. So that the scope by_sort_photos doesn't get overridden by definition of a sort_photos method or assoication later.
Finally, it's good to have a public interface full of methods, as opposed to requiring knowledge of the class attributes and passing those attribute names into a public interface method. So I'd keep the many different scopes that you have, but perhaps have them all refer to the one, general scope as we've defined here. So:
scope :newest, -> { by_most_recent_type('created_at') }
scope :top_sold, -> { by_most_recent_type('qty_sold') }
scope :by_most_recent_type, -> type { order(type => :desc) }

Is it possible to have a scope with optional arguments?

Is it possible to write a scope with optional arguments so that i can call the scope with and without arguments?
Something like:
scope :with_optional_args, lambda { |arg|
where("table.name = ?", arg)
}
Model.with_optional_args('foo')
Model.with_optional_args
I can check in the lambda block if an arg is given (like described by Unixmonkey) but on calling the scope without an argument i got an ArgumentError: wrong number of arguments (0 for 1)
Ruby 1.9 extended blocks to have the same features as methods do (default values are among them):
scope :cheap, lambda{|max_price=20.0| where("price < ?", max_price)}
Call:
Model.cheap
Model.cheap(15)
Yes. Just use a * like you would in a method.
scope :print_args, lambda {|*args|
puts args
}
I used scope :name, ->(arg1, arg2 = value) { ... } a few weeks ago, it worked well, if my memory's correct. To use with ruby 1.9+
You can conditionally modify your scope based on a given argument.
scope :random, ->(num = nil){ num ? order('RANDOM()').limit(num) : order('RANDOM()') }
Usage:
Advertisement.random # => returns all records randomized
Advertisement.random(1) # => returns 1 random record
Or, you can provide a default value.
scope :random, ->(num = 1000){ order('RANDOM()').limit(num) }
Usage:
Product.random # => returns 1,000 random products
Product.random(5) # => returns 5 random products
NOTE: The syntax shown for RANDOM() is specific to Postgres. The syntax shown is Rails 4.
Just wanted to let you know that according to the guide, the recommended way for passing arguments to scopes is to use a class method, like this:
class Post < ActiveRecord::Base
def self.1_week_before(time)
where("created_at < ?", time)
end
end
This can give a cleaner approach.
Certainly.
scope :with_optional_args, Proc.new { |arg|
if arg.present?
where("table.name = ?", arg)
end
}
Use the *
scope :with_optional_args, -> { |*arg| where("table.name = ?", arg) }
You can use Object#then (or Object#yield_self, they are synonyms) for this. For instance:
scope :cancelled, -> (cancelled_at_range = nil) { joins(:subscriptions).merge(Subscription.cancelled).then {|relation| cancelled_at_range.present? ? relation.where(subscriptions: { ends_at: cancelled_at_range }) : relation } }

Resources