How to dynamically call scopes with 'OR' clause from an array - ruby-on-rails

I have an array, and I'd like to call scopes with OR clause:
cars = ['bmw', 'audi', 'toyota']
class Car < AR
scope :from_bmw, -> { where(type: 'bmw') }
scope :from_audi, -> { where(type: 'audi') }
scope :from_toyota, -> { where(type: 'toyota') }
end
I'd like to achieve something like this:
Car.from_bmw.or(Car.from_audi).or(Car.from_toyota)
My cars array can change; in case: cars = ['toyota', 'audi'], my method should produce:
Car.from_toyota.or(Car.from_audi)
I have something like the following:
def generate(cars)
scopes = cars.map {|f| "from_#{f} "}
scopes.each do |s|
# HOW TO I ITERATE OVER HERE AND CALL EACH SCOPE???
end
end
I don't want to pass type as an argument to scope, there's a reason behind it.

def generate(cars)
return Car.none if cars.blank?
scopes = cars.map {|f| "from_#{f} "}
scope = Car.send(scopes.shift)
scopes.each do |s|
scope = scope.or(Car.send(s))
end
scope
end

Assuming the given array contains only valid type values, you could simply do that:
class Car
scope :by_type, -> (type) { where(type: type) }
end
types = ['bmw', 'audi', 'toyota']
Car.by_type(types) # => It'll generate a query using IN: SELECT * FROM cars WHERE type IN ('bmw', 'audi', 'toyota')
If you don't want to pass the array as an argument to scope for whatever reason, you could create a hash mapping the array values to valid by_type arguments.
VALID_CAR_TYPES = { volkswagen: ['vw', 'volkswagen'], bmw: ['bmw'], ... }
def sanitize_car_types(types)
types.map do |type|
VALID_CAR_TYPES.find { |k, v| v.include?(type) }.first
end.compact
end
Car.by_type(sanitize_car_types(types))

Related

Can scopes be chained conditionally in Rails?

Consider this code:
class Car
scope :blue, -> { where(color: "blue") }
scope :manual, -> { where(transmission: "manual") }
scope :luxury, -> { where("price > ?", 80000) }
end
def get_cars(blue: false, manual: false, luxury: false)
cars = Car.all
cars = cars.blue if blue
cars = cars.manual if manual
cars = cars.luxury if luxury
end
Is there a way to chain these scopes like Car.blue.manual.luxury conditionally? I.e. only scope if the arg is true?
You can use yield_self(read more here), new functionality added in ruby 2.5 for it.
In your example:
class Car
scope :blue, -> { where(color: "blue") }
scope :manual, -> { where(transmission: "manual") }
scope :luxury, -> { where("price > ?", 80000) }
end
def get_cars(blue: false, manual: false, luxury: false)
cars = Car.all
.yield_self { |cars| blue ? cars.blue : cars }
.yield_self { |cars| manual ? cars.manual : cars }
.yield_self { |cars| luxury ? cars.luxury : cars }
end
ActiveRecord scopes can be applied conditionally, like this:
scope :blue, -> { where(color: 'blue') if condition }
Where condition is something you define that returns true or false. If the condition returns true, the scope is applied. If the condition is false, the scope is ignored.
You can also pass values into a scope:
scope :blue, ->(condition) { where(color: 'blue') if condition }
So, you could do something like this:
Task.blue(color == 'blue')
Which is similar to what the OP requested. But, why would you?
A better approach is something like this:
scope :color, ->(color) { where(color: color) if color.present? }
Which would be called like this:
Car.color('blue') # returns blue cars
Car.color(nil) # returns all cars
Car.color(params[:color]) # returns either all cars or only cars of a specific color, depending on value of param[:color]
Car.color(params[:color]).transmission(params[:transmission]).price(params[:price])
Your mileage may vary.

Sorting by integers represented as strings

I would like sort array of ActiveRecord objects by related object's attribute value. Meaning something like this:
Item has one product which has an attribute SKU. The SKU is mostly integer stored as a string, but could be alphanumeric as well.
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
For now in case of error the items with original order returns.
What would I like to do?
Extend the Array class to achieve something like:
items.numeric_sort { |item| item.product.sku }
What I did so far?
1. Building a lambda expression and passing it
class Array
def numeric_sort(&lambda)
if lambda.respond_to? :call
self.sort_by(&lambda) rescue self
else
self.sort_by { |el| Integer(el) } rescue self
end
end
end
product_bin = lambda { |task_item| Integer(item.product.bin) }
items.numeric_sort(&product_bin)
2. Building lambda expression from methods chain
class Object
def send_chain(keys)
keys.inject(self, :send)
end
end
class Array
def numeric_sort_by(*args)
(args.length == 1) ? lam = lambda {|el| Integer(el.send(args))} : lam = lambda {|el| Integer(el.send_chain(args))}
self.sort_by(&lam) rescue self
end
end
items.numeric_sort_by(:product, :sku)
Is it all makes any sense?
Can you please point me in the right direction to implement the syntax I mentioned above, if it is possible at all.
Thanks.
EDIT: the sku could be alphanumeric as well. Sorry for the confusion.
Try this solution.
There is no error handling.
It's just an idea to develop if you like it.
class Array
def numeric_sort_by(*args)
self.sort_by do |element|
object = element
args.size.times { |n| object = object.send(args[n]) }
object.to_f
end
end
end
items.numeric_sort_by 'product', 'sku'
So the straightforward implementation was:
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
And the desired was:
items.numeric_sort_by { |item| item.product.sku }
I was manage to achieve it by yielding a block into the sort_by:
class Array
def numeric_sort_by(&block)
return to_enum :numeric_sort_by unless block_given?
self.sort_by { |element| Integer(yield(element)) } rescue self
end
end

How can I pass a hash for my where clause?

I am listing products and I want to be able to pass a hash as my where clause so I can do something like:
filter = {}
filter[:category_id] = #category.id
filter[:is_active] = true
#products = Products.where(filter)
Is it possible to do this somehow?
I also need to add something like this in my where clause:
WHERE price > 100
How could I add that to a filter?
The reason I want to do this is because in the UI I will have a set of optional filters, so then I will use if clauses in my controller to set each filter.
You can pass a hash to where exactly like you did:
filter = {
category_id: #category_id,
is_active: true
}
#products = Product.where(filter)
Using a hash only works for equality (e.g. category_id = 123), so you can't put something like price > 100 in there. To add that criteria, just add another where to the chain:
#product = Product.where(filter).where('price > 100')
Or...
#product = Product.where(filter)
if params[:min_price]
#product = #product.where('price > ?', min_price)
end
You could have a bit of fun with scopes: write a scope that's actually a mini predicate builder, sanitizing and pattern-matching strings, and delegating to the standard predicate builder for other scalar types. E.g.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(params) {
params.inject(self) do |rel, (key, value)|
next rel if value.blank?
case value
when String
rel.where arel_table[key].matches '%%%s%%' % sanitize_sql_like(value)
when Range, Numeric, TrueClass, FalseClass
rel.where key => value
else
raise ArgumentError, "unacceptable search type"
end
end
}
end
end
# app/models/product.rb
class Product < ApplicationRecord
include Searchable
then you can
filter = { name: 'cheese', description: 'aged', age: 42.. }
Product.search(filter) #=> SELECT "products".* FROM products WHERE "products"."name" ILIKE '%cheese%' AND "products"."description" ILIKE '%aged%' AND "products"."age" >= 42

get models name and count

I have this in models_name:
model_names = Object.constants.collect { |sym| Object.const_get(sym) }.select { |constant| constant.class == Class && constant.include?(Mongoid::Document) }.collect { |klass| klass.name }
the result is:
["Model_name1","Model_name2","Model_name3"]
I need something like:
[{"Model1":"Count"},{"Model2":"Count"},{"Model3":"Count"}]
I need show in a chart all models and object counter inside each model is working with morris.js
You can see the example:
http://jsbin.com/uzosiq/2/embed?javascript,live
Thank you very much!
that last one:
collect { |klass| klass.name }
rewrite it as
collect { |klass| [klass.name, klass.count] }
this should return an array of arrays of 2 elements (classname and count). If the variable to which this is assigned is called a, just do this:
Hash[a]
now you have a hash at your disposal. Now you can do this:
Hash[a].map do |k, v|
{k => v}
end
and now you have an array of hashes of only one key-value assoc each. Which I think it is what you want.
module Foo
end
class Bar
include Foo
end
1.upto(5) { Bar.new }
model_names = Object.constants.collect { |sym| Object.const_get(sym) }.
select { |constant| constant.class == Class && constant.include?(Foo) }.
inject({}) do |m,klass|
m.update klass.name => ObjectSpace.each_object(klass).to_a.size
end
p model_names
{"Bar"=>5}
Will work only on MRI based rubies, so no jRuby nor Runbinius.
Here is a live demo

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