Rails Model scope chaining based on dynamic scope name list - ruby-on-rails

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)

Related

How can I access a Rails scope lambda without calling it?

I would like to access the lamda defined in a rails scope as the lambda itself and assign it to a variable. Is this possible?
So if I have the following scope
scope :positive_amount, -> { where("amount > 0") }
I would like to be able to put this lambda into a variable, like "normal" lambda assignment:
positive_amount = -> { where("amount > 0") }
So something like this:
positive_amount = MyClass.get_scope_lambda(:positive_amount)
For clarification, I'm wanting the body of the method that I generally access with method_source gem via MyClass.instance_method(method).source.display. I'm wanting this for on-the-fly documentation of calculations that are taking place in our system.
Our invoicing calculations are combinations of smaller method and scopes. I'm trying to make a report that says how the calculations were reached, that uses the actual code. I've had luck with instance methods, but I'd like to show the scopes too:
Edit 1:
Following #mu's suggestion below, I tried:
Transaction.method(:positive_amount).source.display
But this returns:
singleton_class.send(:define_method, name) do |*args|
scope = all
scope = scope._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
And not the body of the method as I'd expect.
If you say:
class MyClass < ApplicationRecord
scope :positive_amount, -> { where("amount > 0") }
end
then you're really adding a class method called positive_amount to MyClass. So if you want to access the scope, you can use the method method:
positive_amount = MyClass.method(:positive_amount)
#<Method: MyClass(...)
That will give you a Method instance but you can get a proc if you really need one:
positive_amount = MyClass.method(:positive_amount).to_proc
#<Proc:0x... (lambda)>
If I get your idea right. Here is one approach to do this
class SampleModel < ApplicationRecord
class << self
##active = ->(klass) { klass.where(active: true) }
##by_names = ->(klass, name) { klass.where("name LIKE ?", "%#{name}%") }
def get_scope_lambda(method_name, *args)
method = class_variable_get("###{method_name}")
return method.call(self, *args) if args
method.call(self)
end
end
end
So after that you can access the scopes like this:
SampleModel.get_scope_lambda(:by_names, "harefx")
SampleModel.get_scope_lambda(:active)
Or you can define some more class methods above, the one extra klass argument might be not ideal. But I don't find a way to access the self from inside the lambda block yet, so this is my best shot now.
By the way, I don't think this is a good way to use scope. But I just express your idea and to point it out that it's possible :D
UPDATED:
Here I come with another approach, I think it could solve your problem :D
class SampleModel < ApplicationRecord
scope :active, -> { where(active: true) }
scope :more_complex, -> {
where(active: true)
.where("name LIKE ?", "%#{name}%")
}
class << self
def get_scope_lambda(method_name)
location, _ = self.method(:get_scope_lambda).source_location
content = File.read(location)
regex = /scope\s:#{method_name}, -> {[\\n\s\w\(\):\.\\",?%\#{}]+}/
content.match(regex).to_s.display
end
end
end
So now you can try this to get the source
SampleModel.get_scope_lambda(:active)
SampleModel.get_scope_lambda(:more_complex)

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.

How to get children records of a scoped parent in Rails 5

I need a controller to pass along children records of parents that match a certain scope. I'd like that scope to be on the parent record.
class Parent < ApplicationRecord
has_many :children
scope :not_blue, -> { where(blue:false) }
scope :blue, -> { where(blue:true) }
# Subjective, may change in the future
scope :funny, -> { where('funny_scale>=?',5) }
scope :genies, -> { blue.funny }
end
class Child < ApplicationRecord
belongs_to :parent, required: true
end
class ChildrenController < ApplicationController
def index
# Yeah, this breaks horribly (and should), but you get my gist
#children_of_genies = Parent.genies.children
end
end
I know the answer is probably staring me in the face, but the right combination of google searches is eluding me.
If you'd like your solution to still be an ActiveRecord::Associations::CollectionProxy try Children.where(parent_id: Parent.genies.ids) which you then could turn into a scope.
scope: children_of_genies, -> { where(parent_id: Parent.genies.ids)
Scopes return an ActiveRecord_Relation, to get children for each member of it you can use collect:
#children_of_genies = Parent.genies.collect { |p| p.children }

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

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

Resources