Create dynamic scopes based on other model - ruby-on-rails

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

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)

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 }

ActiveRecord scope "default" result

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

How to unscope joins in RoR 4

Trying to unscope joins.
Example:
class MyModel < ActiveRecord::Base
default_scope -> { joins("JOIN other_table ON other_table.this_table_id = this_table.id") }
scope :without_relation -> { unscope(:joins) }
end
The problem is that it unscope ALL joins, even those that are automatically constructed by AR relations.
Here's what I did:
module UnscopeJoins
extend ActiveSupport::Concern
def unscope_joins(joins_string)
joins_values.reject! {|val| val == joins_string}
self
end
end
ActiveRecord::Relation.send :include, UnscopeJoins
Usage:
scope :without_joins, -> { unscope_joins("JOIN table on table2.id = table1.table2_id") }

Rails: ActiveRecord interdependent attributes setters

In activerecord, attribute setters seems to be called in order of the param hash.
Therefore, in the following sample, "par_prio" will be empty in "par1" setter.
class MyModel < ActiveRecord::Base
def par1=(value)
Rails.logger.info("second param: #{self.par_prio}")
super(value)
end
end
MyModel.new({ :par1 => 'bla', :par_prio => 'bouh' })
Is there any way to simply define an order on attributes in the model ?
NOTE: I have a solution, but not "generic", by overriding the initialize method on "MyModel":
def initialize(attributes = {}, options = {})
if attributes[:par_prio]
value = attributes.delete(:par_prio)
attributes = { :par_prio => value }.merge(attributes)
end
super(attributes, options)
end
Moreover, it does not works if par_prio is another model that has a relation on, and is used to build MyModel:
class ParPrio < ActiveRecord::Base
has_many my_models
end
par_prio = ParPrio.create
par_prio.my_models.build(:par1 => 'blah')
The par_prio param will not be available in the initialize override.
Override assign_attributes on the specific model where you need the assignments to happen in a specific order:
attr_accessor :first_attr # Attr that needs to be assigned first
attr_accessor :second_attr # Attr that needs to be assigned second
def assign_attributes(new_attributes, options = {})
sorted_new_attributes = new_attributes.with_indifferent_access
if sorted_new_attributes.has_key?(:second_attr)
first_attr_val = sorted_new_attributes.delete :first_attr
raise ArgumentError.new('YourModel#assign_attributes :: second_attr assigned without first_attr') unless first_attr_val.present?
new_attributes = Hash[:first_attr, first_attr_val].merge(sorted_new_attributes)
end
super(new_attributes, options = {})
end

Resources