Combine many scopes - ruby-on-rails

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.

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)

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.

ROR: Creating a method with a parameter

I have an admins dashboard which displays posts created in the last 24 hours, 7 days, 28 days etc.
def index
#1DayPosts = Post.where(created_at: 1.days.ago..DateTime.now).count
#7DaysPosts = Post.where(created_at: 7.days.ago..DateTime.now).count
#28DaysPosts = Post.where(created_at: 28.days.ago..DateTime.now).count
end
How could I make this into one line? Something like the below:
def index
#calculatePosts(a) = Post.where(created_at: a.days.ago..DateTime.now).count
end
Then in the view I could do:
=#calculatePosts(1)
Or would I need to create a new method?
def calculatePosts(a)
#calculatePost = Post.where(created_at: a.days.ago..DateTime.now).count
end
How would I then call this in the index view?
Your best bet would be to create a scope on the Post model.
class Post ...
scope :last_x_days, -> (x) { where(created_at: x.days.ago..Time.zone.now) }
end
Then you can call that anywhere really, in your view or controller like this.
#last_10_days = Post.last_x_days(10).count
EDIT:
You could do this also, but scopes are meant to be chain-able, so this is discouraged, though not wrong.
scope :last_x_days_count, -> (x) { where(created_at: x.days.ago..Time.zone.now).count }

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

Rails - Wondering if there's a more efficient way to do this?

I have a Properties model which is using a field to sort by property type(properties.kind).
class Property < ActiveRecord::Base
scope :house, -> { where(kind: "House") }
scope :apartment, -> { where(kind: "Apartment") }
scope :commercial, -> { where(kind: "Commercial") }
end
Then in my Properties controller I'm combining them into an array so that I can group them by type in the view.
class PropertiesController < ApplicationController
def index
#house = Property.house.all
#apartment = Property.apartment.all
#commercial = Property.commercial.all
#listing = [#house, #apartment, #commercial]
end
The problem that stands out to me is that the model must perform at least 3 SQL queries on the same table for every index action. Obviously this is super inefficient, and ill be lucky if I can serve more than one user at a time.
My question is this: is there a better way to group an array's contents by a string value?
You can combine these into a single query and keep them scoped as follows:
scope :listing, -> { where(kind: ["House","Apartment","Commercial"]) }
Passing an array to Where is an alias in SQL for IN, which will cover all 3 options in one query. You could then split them out with a group_by for assigning them to instance variables.

Resources