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)
Related
In a Rails (5.0) app, I have the following
class Batch < ApplicationRecord
belongs_to :zone, optional: false
end
class Zone < ApplicationRecord
scope :lines, -> { where(kind: 'line') }
end
Now I need to define in Batch a scope for each Zone which is a line. Something like the code below works.
Zone.lines.map(&:name).each do |name|
scope "manufactured_on_#{name}".to_sym, -> { joins(:zone).where("zones.name = '#{name}'") }
end
The issue is that the code above is evaluated when the app boots and at that time the scopes are created. If I add a newZone of kind line, the scope is not created.
Is there a way to solve this issue?
You could pass the zone's name as a scope param
scope :manufactured_on, -> (name) { joins(:zone).where(zones: { name: name } ) }
You can look into documentation and search for method_missing.
But this does not seem a good approach to me.
Instead, define scope in following way:
class Batch < ApplicationRecord
scope :manufactured_on, ->(line) { joins(:zone).where("zones.name = '#{name}'") }
end
Then simply use
Batch.manufactured_on(zone.name)
If you really need the scope name to be dynamic you can use method_missing as below:
class Batch < ApplicationRecord
belongs_to :zone, optional: false
def self.method_missing(name, *args)
method_name = name.to_s
if method_name.start_with?('manufactured_on_')
zone_name = method_name.split('manufactured_on_')[1]
joins(:zone).where(zones: { name: zone_name } )
else
super
end
end
def self.respond_to_missing?(name, include_private = false)
name.to_s.start_with?('manufactured_on_') || super
end
end
Now, you can call this scope as normal:
Batch.manufactured_on_zone_a
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.
So let's say I want to check for nils in an ActiveRecord scope:
class Person < ActiveRecord::Base
scope :closest, ->(point) {
return nil unless point # How can I return the ActiveSupport::Relation here?
# other code goes below
}
end
You can just return self for it to return the default scope:
class Person < ActiveRecord::Base
def self.closest(point = nil)
point.nil? ? self : where(point: point)
end
end
Edit/Solution
As mentioned in the comments above, and as #ahmacleod pointed out, all is what we're looking for
scope :closest, ->(point) {
point.nil? ? all : where(point: point)
}
End edit
I think I have found what I am looking for and it's unscoped
scope :closest, ->(point) {
point.nil? ? unscoped : where(point: point)
}
The problem is that if I chain this, I would lose prior scopes if I use this after them.
You can set point parameter as optional. Something like this:
scope :closest, -> (point = '') {
where(point: point)
}
This way, the scope will return a ActiveRecord::Relation every time.
Hope this help :)
I have an available attribute on a Product model which is a boolean.
I'm defining a helper method that takes an argument, for example:
def family_products(available: true)
Product.where(available: available)
end
This is fine for true or false -- but what I would like is a default of all.
Is it possible without creating a conditional wrapper?
I would define a scope in your Product model like this.
class Product < ActiveRecord::Base
scope :all_family_products, -> { where('available = ? or available = ?', true, false) }
end
Now calling Product.all_family_products returns both available products(available = true) and unavailable products(available = false)
Try this :
def family_products(available: [true, false])
Product.where(available: [available])
end
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 } }