ActiveRecord::Concern and base class scope - ruby-on-rails

I have Filterable module that creates scopes usesd by various models,
module Filterable
extend ActiveSupport::Concern
included do
# References
scope :filter_eq_reference, ->(val){ where(reference: val) }
scope :filter_eq_description, ->(val){ where(description: val) }
scope :filter_eq_ref1, ->(val){ where(ref1: val) }
scope :filter_eq_ref2, ->(val){ where(ref2: val) }
scope :filter_eq_ref3, ->(val){ where(ref3: val) }
scope :filter_eq_ref4, ->(val){ where(ref4: val) }
scope :filter_eq_ref5, ->(val){ where(ref5: val) }
scope :filter_eq_ref6, ->(val){ where(ref6: val) }
# Weights
scope :filter_eq_weight, ->(val){ where(weight: val) }
scope :filter_lt_weight, ->(val){ where('weight < ?', val) }
scope :filter_lte_weight,->(val){ where('weight <= ?', val) }
scope :filter_gt_weight, ->(val){ where('weight > ?', val) }
scope :filter_gte_weight,->(val){ where('weight >= ?', val) }
# ...
end
class_methods do
end
end
I want to refactor it for several reasons
#1. it's getting large
#2. All models don't share the same attributes
I came to this
module Filterable
extend ActiveSupport::Concern
FILTER_ATTRIBUTES = ['reference', 'description', 'weight', 'created_at']
included do |base|
base.const_get(:FILTER_ATTRIBUTES).each do |filter|
class_eval %Q?
def self.filter_eq_#{filter}(value)
where(#{filter}: value)
end
?
end
end
It works, but I want to have the attributes list in the model class, As issue #2, I think it more their responsability So I moved FILTER_ATTRIBUTES in each class including this module The problem when doiing that, I get an error when calling Article.filter_eq_weight 0.5
NameError: uninitialized constant #Class:0x000055655ed90f80::FILTER_ATTRIBUTES
Did you mean? Article::FILTER_ATTRIBUTES
How can I access the base class - having 'base' injected or not doesn't change anything
Or maybe better ideas of implementation ? Thanks

I would suggest a slightly different approach. You already see that you are going to be declaring a list of filters in each class that wants to filter based on some value. Stepping back a bit, you may also realize that you are going to create different types of filters depending on the type of value of the field. With that in mind, I would consider stealing a page from the Rails playbook and create class methods that create the filters for you.
module Filterable
extend ActiveSupport::Concern
class_methods do
def create_equality_filters_for(*filters)
filters.each do |filter|
filter_name = "filter_eq_#{filter}".to_sym
next if respond_to?(filter_name)
class_eval %Q?
def self.filter_eq_#{filter}(value)
where(#{filter}: value)
end
?
end
end
def create_comparison_filters_for(*filters)
filters.each do |filter|
{ lt: '<', lte: '<=', gt: '>', gte: '>=' }.each_pair do |filter_type, comparison|
filter_name = "filter_#{filter_type}_#{filter}".to_sym
puts filter_name
next if respond_to?(filter_name)
class_eval %Q?
def self.filter_#{filter_type}_#{filter}(value)
where('#{filter} #{comparison} \?', value)
end
?
end
end
end
end
end
Then you would use it like this:
class Something < ApplicationRecord
include Filterable
create_equality_filters_for :reference, :description, :weight
create_comparison_filters_for :weight
end
Something.methods.grep(/filter_/)
[:filter_eq_reference, :filter_eq_description, :filter_eq_weight, :filter_lt_weight, :filter_lte_weight, :filter_gt_weight, :filter_gte_weight]
This approach makes your code significantly more declarative -- another developer (even yourself in a year!) won't wonder what the FILTER_ATTRIBUTES constant does or why (if?) it's required (a potential problem when the consuming code is not part of the class you are reading). Though future-you may not remember where the create_xyz methods are defined or exactly how they do what they do, the method names make fairly clear what is being done.

You could just create a simple module that defines a method to construct all of these filters and then call it with a list of filterable attributes.
module Filterable
def add_filters(*filters)
filters.each do |filter|
define_singleton_method("filter_eq_#{filter}"){ |val| where(filter => val) }
end
end
end
Then
class MyClass < ApplicationRecord
extend Filterable
add_filters :reference, :description, :weight, :created_at
end
Simple Example
You could make this more comprehensive as (this solution utilizes Arel::Predications to build query filters so you can expand based on that list)
module Filterable
FILTERS = [:eq,:gt,:lt,:gteq,:lteq]
def add_filters(*fields, filters: Filterable::FILTERS)
raise ArgumentError, "filter must be one of #{Filterable::FILTERS}" if filters.any? {|filter| !Filterable::FILTERS.include?(filter)}
filters.each do |predicate|
fields.each do |field|
_add_filter(predicate,field)
end
end
end
def _add_filter(predicate,field)
define_singleton_method("filter_#{predicate}_#{field}") do |val|
where(arel_table[field].public_send(predicate,val))
end
end
end
Then call as
class MyClass < ApplicationRecord
extend Filterable
# add equality only filters
add_filters :description, :ref1, :ref2, :ref3, filters: [:eq]
# add all filters
add_filters :weight, :height, :created_at
end

Related

Single Table Inheritance or Type Table

I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end

Define dynamic class methods

I know this question has been asked several times, but I think never in this conditions.
I try to create an "helper" for my models (an acts_as), for automatically set a status from a status_id.
acts_as_statusable :status, [ :in_progress, closed ]
This helper creates methods status, status=(sym), in_progress?, closed? and named scopes in_progress and closed.
My helper work, this is its code put in lib/
class ActiveRecord::Base
def self.acts_as_statusable(*args)
field_name = args.first
field_name_id = :"#{field_name}_id"
statuses = args.second
# Define getter
define_method(field_name) do
return nil if self.send(field_name_id).nil?
return statuses[self.send(field_name_id)]
end
# Define setter
define_method(:"#{field_name}=") do |v|
return self.send(:"#{field_name_id}=", statuses.index(v))
end
# Define a status? for each status,
# and a named scope .status
statuses.each do |status|
define_method(:"#{status}?") do
return self.send(field_name) == status
end
scope status, -> { where(["#{table_name}.#{field_name_id} = ?", statuses.index(status)]) }
end
validates field_name_id, :inclusion => { :in => (0...statuses.length).to_a }
end
end
Now, the problem
I need to have a Class method, for every class using acts_as_statusable, named status_index(sym) which returns the status_id from a status.
The problem is that every solutions I found for defining a class models are using Module, extend or intend, but I can not insert the statuses variable of the acts_as_statusable line into this modules...
How can I do this ? I use Rails 4.
you can use class_eval, and class_variable_set
class_variable_set :"###{field_name}_options", statuses
class_eval <<-RUBY
def self.status_index(sym)
###{field_name}_options.index(sym)
end
RUBY
which in your case will give a
##status_options class variable
and a
status_index(sym)
method that returns the offset of the given status
xlembouras - your solution works great! I have two models:
class User < ActiveRecord::Base
acts_as_statusable :status, [ :little, :big ]
end
class Price < ActiveRecord::Base
acts_as_statusable :status, [ :in_progress, :closed ]
end
'rails c' execution:
> User.status_index(:little)
=> 0
User.status_index(:big)
=> 1
> Price.status_index(:closed)
=> 1
> Price.status_index(:in_progress)
=> 0
I modified the structure, and used code written by ForgetTheNorm.
Rails 4, ActiveSupport::Concern used
lib/active_record_extension.rb :
module ActiveRecordExtension
extend ActiveSupport::Concern
end
ActiveRecord::Base.send(:include, ActiveRecordExtension)
config/initializers/extensions.rb :
require "active_record_extension"
config/initializers/active_record_monkey_patch.rb
class ActiveRecord::Base
def self.acts_as_statusable(*args)
...
# ForgetTheNorm's CODE..
...
# xlembouras CODE:
class_variable_set :"###{field_name}_options", statuses
class_eval <<-RUBY
def self.status_index(sym)
###{field_name}_options.index(sym)
end
RUBY
end
end

Single Table Inheritance (STI) parent ActiveRecord .subclasses .descendants returns empty

I have a STI in place for 10 models inheriting from one ActiveRecord::Base model.
class Listing::Numeric < ActiveRecord::Base
end
class Listing::AverageDuration < Listing::Numeric
end
class Listing::TotalViews < Listing::Numeric
end
There are 10 such models those inherit from Listing::Numeric
In rails console, when I try for .descendants or .subclasses it returns an empty array.
Listing::Numeric.descendants
=> []
Listing::Numeric.subclasses
=> []
Ideally this should work.
Any ideas why its not returning the expected subclasses ?
This will work only if all the inherited classes are referenced in some running code as rails will load the classes when required then only it will be added as descendants
For eg:
Listing::Numeric.descendants.count
=> 0
Listing::AverageDuration
Listing::TotalViews
Listing::Numeric.descendants.count
=> 2
Old Q, but for anyone else like me who get confused with empty lists for subclasses MyClass.subclasses => []
You need to explicitly set the dependency to your MySubclass class.
class MyClass < ApplicationRecord
end
require_dependency 'my_subclass'
$ MyClass.subclasses
=> ['MySubclass']
https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-and-sti
Execute Rails.application.eager_load! before calling the .descendants method.
This helped me
config.autoload_paths += %W( #{config.root}/app/models/listings )
Taken from here - http://hakunin.com/rails3-load-paths
Here is a solution for Rails 7. Originally sourced from RailsGuides, I made modifications to include results even if no data is stored in the table:
module StiPreload
unless Rails.application.config.eager_load
extend ActiveSupport::Concern
included do
cattr_accessor :preloaded, instance_accessor: false
end
class_methods do
def descendants
preload_sti unless preloaded
super
end
def preload_sti
types_from_db = base_class
.unscoped
.select(inheritance_column)
.distinct
.pluck(inheritance_column)
.compact
(types_from_db.present? || types_from_file).each do |type|
type.constantize
end
self.preloaded = true
end
def types_from_file
Dir::each_child("#{Rails.root}/app/models").reduce([]) do |acc, filename|
if filename =~ /^(#{base_class.to_s.split(/(?=[A-Z])/).first.downcase})_(\w+)_datum.rb$/
acc << "#{$1.capitalize}#{$2.classify}"
end
acc
end
end
end
end
end
class YoursTruly < ApplicationRecord
include StiPreload
end
YoursTruly.descendants # => [...]

Rails Modules: How to define instance methods inside a class method?

I'd like to create a module called StatusesExtension that defines a has_statuses method. When a class extends StatusesExtension, it will have validations, scopes, and accessors for on those statuses. Here's the module:
module StatusesExtension
def has_statuses(*status_names)
validates :status, presence: true, inclusion: { in: status_names }
# Scopes
status_names.each do |status_name|
scope "#{status_name}", where(status: status_name)
end
# Accessors
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
Here's an example of a class that extends this module:
def Question < ActiveRecord::Base
extend StatusesExtension
has_statuses :unanswered, :answered, :ignored
end
The problem I'm encountering is that while scopes are being defined, the instance methods (answered?, unanswered?, and ignored?) are not. For example:
> Question.answered
=> [#<Question id: 1, ...>]
> Question.answered.first.answered?
=> false # Should be true
How can I use modules to define both class methods (scopes, validations) and instance methods (accessors) within the context of a single class method (has_statuses) of a module?
Thank you!
As the comments have said, the method is being defined, just not working as expected. I suspect this is because you are comparing a string with a symbol within the method (status_names is an array of symbols, and status will be a string). Try the following:
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name.to_s
end
end

Defining a virtual attribute in Rails and making it available as a symbol

I've defined a couple of virtual attributes, defining both the setter and the getters methods:
class Post < ActiveRecord::Base
def shared_to_all
# evaluates some expression of the attribute privacy
end
def shared_to_friends
# evaluates some other expression of the attribute privacy
end
def shared_to_all=(bool)
# write_attribute( :privacy, ... )
end
def shared_to_friends=(bool)
# write_attribute( :privacy, ... )
end
end
So far so good, but I want also to make this virtual attributes available using symbols, so I can do something like #post= Post.first; #post[:shared_to_all]= true
[Edit:]
A Ruby approach would be to override [] and []= operators, like in:
def [](shared_to_all)
shared_to_all
end
def []=(shared_to_all, bool)
self.shared_to_all= (bool)
end
But this seems to break Rails relationship methods (those brought by has_one - has_many - belongs_to - has_and_belongs_to_many directives): e.g. now Post.first.author => nil and Author.first.posts => []
Seems like this should do it:
def [](attr)
attr
end
def []=(attr, value)
self.attr = value
end

Resources