Drying up multiples scopes with simialar queries - ruby-on-rails

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

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)

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.

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)

How do I create a scope for an array?

The following code gives an empty scope. Category_ids is an array of categories.
scope :art, ->{ where(:category_ids => '1') }
How do I check to see if one of the categories exist in the array?
If you use Postgres you can use this approach: https://www.viget.com/articles/searching-serialized-fields-in-rails-using-postgres-arrays
get the categories
correct your where query
Example:
has_many :categories
scope :art, -> { required = [Category.first]; where(categories: required) }
I assume that in your model, you have categories association. In this case, you can just use categories: required in your where query. required should be set to an array of categories which you wanted
You say that category_ids is an array of categories(I'm assuming category id's). Are you trying to return all records with a category ID that is in that array? If so you're looking for:
scope :art, -> { where (:category_id => category_ids) }
Or with the new ruby syntax:
scope :art, -> { where(category_id: category_ids) }
If I've misunderstood and you're looking for any record with a category ID of 1, then you're looking for:
scope :art, -> { where(category_id: '1') }

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