Can scopes be chained conditionally in Rails? - ruby-on-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.

Related

How to dynamically call scopes with 'OR' clause from an array

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))

Rails Model scope chaining based on dynamic scope name list

Let's say I have some model
class MyModel < ApplicationRecord
scope :opened, -> { where(status: 'open') }
scope :closed, -> { where(status: 'closed') }
scope :colored, -> { where.not(color: nil) }
# etc
end
I can call scope chains like
MyModel.opened.colored
MyModel.send('opened').send('colored')
But how can I make scope chaining based on dynamic scope token list? I mean
scopes = ['opened', 'colored', ...]
The list may be very long and I need some general solution to do it as simple as possible, like MyModel.send_list(scopes).
More as result of scope, you can add like,
scope :send_list, -> (*scopes) { scopes.inject(self) { |out, scope| out.send(scope) } }
send this like YourModel.send_list(*scopes)

Rails scope with DateTime

I'm trying to create a set of results to display on a 'Summary' page, but am having trouble filtering for the DateTime in the scope called :can_be_shown_now
When I change the date in the database, my Poster still appears when it should not.
Model
class Poster < ActiveRecord::Base
scope :poster_for_summary, -> { self.where(show_on_summary_page: true) }
scope :can_be_shown_now, -> { poster_for_summary.where((:start_time..:end_time).cover?(DateTime.now)) }
scope :choose_for_summary, -> { can_be_shown_now.order("RANDOM()").first }
end
Application Controller
def fallback_poster
#fallback_poster = Poster.choose_for_summary if controller_name == 'summary'
end
Any help is greatly appreciated. Thanks!
In your code cover? method refers to given range, not a query. You should try something like this:
scope :can_be_shown_now, -> { poster_for_summary.where(["`posters`.start_time <= ? AND `posters`.end_time >= ?", t = DateTime.now, t]) }

Using another class scope in existing scope Activerecord

I want to use the scope of another class in the scope of the first class
so instead of
scope :active, -> {includes(:b).where(b: {column: 'ACTIVE'}).where(a: {column2: 'ACTIVE'})}
I want to be able to use a scope of b
scope :active, -> {includes(b.active).where(a: {column2: 'Active'})}
You can do this using merge:
scope :active, -> { includes(:b).merge(B.active)
.where(a: {column2: 'Active'}) }
Note: I used B to represent the model class for the b column or object.
Or, assuming you're in a's model already:
scope :active, -> { includes(:b).merge(B.active)
.where(column2: 'Active') }
Also, if you WANT eager loading then using includes is great. Otherwise, it's faster and less overhead to use joins, like this:
scope :active, -> { joins(:b).merge(B.active)
.where(column2: 'Active') }
I recommend to use scope on model, if it's admin specific, then can separate it to concern
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
module AdminUserScopes
extend ActiveSupport::Concern
included do
scope :admin_scope1, -> { includes(:b).where(b: {column: 'ACTIVE'}).where(a: {column2: 'ACTIVE'}) }
scope :admin_scope2, -> { admin_scope1.where(a: {column2: 'Active'}) }
end
end
# in your model
include AdminUserScopes
# in active_admin
scope :active, -> { admin_scope1 }
scope :active2, -> { admin_scope2 }
Upd:
If you want to use one condition to other model then can use merge
Dog.all.merge(User.males) # => select * from dogs where sex = 1;
If you want to use in association filtering, then:
Post.where(user: User.males) # => select * from posts where user_id in (select users.id from users where sex = 1)
In your case I guess you have A and B, and you want to get active A-records what connected to active B-records
# in A
scope :active, -> { where(column: 'ACTIVE') }
# in B
scope :active, -> { where(column2: 'ACTIVE', a: A.active) }
# in somewhere else
scope :active, -> { where(a: A.active) } # => have active A which have active B
p.s. it's much easier with more informative names, "A's" and "B's" are hard :)

How can I query my db for params by "and" instead of "or"

I have habtm relation between products and colors. When I preform a query on products that are "red" and "black" I want it to return product that have "red" AND "black" associations not "red" OR "black"
This is my scope for this query:
scope :items_design_filter_color, -> (colors) { joins(:colors).where('colors.id' => colors.to_i) unless colors.nil? }
colors params
params[:colors] = ["1", "2"]
colors table is just id and name columns
Calling my scopes:
#products =
Kaminari.paginate_array(ItemsDesign.items_design_by_category(#category.subtree_ids)
.items_design_filter_color(params[:colors])
.items_design_filter_sort(sort)
.items_design_filter_editors_pick(params[:editors_pick])
.items_design_filter_sold_out(params[:sold_out])
.items_design_filter_style(params[:style])
.items_design_filter_store(params[:store])
.items_design_filter_price(params[:low_end], params[:high_end]))
.page(params[:page]).per(48)
this is my attempt at using one of the answers bellows method within my scope:
scope :items_design_by_category, -> (category) { joins(:items_categories).where('items_categories.id' => category) }
scope :items_design_filter_color, -> (colors) { joins(:colors).where(colors: {id: colors}).each.select do |item|
(item.colors.map(&:id) & colors).size == colors.size
end unless colors.nil? }
scope :items_design_filter_style, -> (styles) { where('items_style_id' => styles) unless styles.nil? }
scope :items_design_filter_store, -> (stores) { where('store_id' => stores) unless stores.nil? }
scope :items_design_filter_editors_pick, -> (editors_pick) { where('editors_pick' => TRUE) unless editors_pick.nil? }
scope :items_design_filter_sold_out, -> (sold_out) { where('sold_out' => 'n') unless sold_out.nil? }
scope :items_design_filter_price, -> (lowend, highend) { where('price_as_decimal' => lowend..highend) unless lowend.nil? }
scope :items_design_filter_sort, -> (sort) { order(sort) unless sort.nil? }
OK, well I ended up getting this way based on the someones answer that was deleted. If they want to come and put the answer back I will give them credit. In the meantime this solved my problem.
scope :items_design_filter_color, -> (colors) { color_ids = colors.map(&:to_i) unless colors.nil?
includes(:colors).where(colors: {id: color_ids}).select do |item|
(item.colors.map(&:id) & color_ids).size == color_ids.size
end unless colors.nil? }

Resources