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
Related
In my model, I'm trying to dynamically expose objects that are inside an array as a top level attribute. Here's the code snippet:
class Widget < ActiveRecord::Base
# attr_accessor :name
end
class MyModel < ActiveRecord::Base
has_many :widgets
#attr_accessor :widgets
after_initialize :init_widgets
def init_widgets
widgets
widgets.each_with_index do |widget, index|
define_method(widget.name) do
widgets[index]
end
end
end
end
Is there any way for me to define the value of index into the new method I am creating so that it will be associated with the proper index?
I might create an accessor / assignment methods that overload the [] operator:
class BracketOperator
def initialize
#values = (1..100).to_a
end
def [](index)
#values[index]
end
def []=(index, value)
#values[index] = value
end
end
bo = BracketOperator.new
bo[3] # => 4
bo[3] = 17
bo[3] # => 17
So for reference, here's the answer I came up with. Apparently I don't understand scoping for ruby too well. The variable n somehow remains referenced to the n within the each loop. Hence for my original question, I can just use the index variable within the method and it will be mapped to what I am expecting it to be mapped to.
class Test
def setup
names = [ "foo","bar" ]
names.each do |n|
self.class.send :define_method, n do
puts "method is called #{n}!"
end
end
end
end
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 # => [...]
I'm sure this has to do with my lack of understanding of Ruby internals, but here goes:
Essentially I want to be able to set an 'attachments directory' within my assets folder by simply calling has_attachments_folder "videos" near the top of the model. Don't worry about what the method's purpose is, I have that part of it working. Example usage below.
Video.rb
class Video < ActiveRecord::Base
has_attachment_assets_folder "videos"
end
Email.rb
class Email < ActiveRecord::Base
has_attachment_assets_folder "attachments/email"
end
activerecord_base.rb
def self.has_attachment_assets_folder(physical_path, arguments={})
ActiveRecord::Base.send :define_method, :global_assets_path do |args={}|
# Default to using the absolute system file path unless the argument
# :asset_path => true is supplied
p = "#{ Rails.root }/app/assets/#{ physical_path }"
if args[:asset_path]
p = "/assets/#{ physical_path.include?("/") ? physical_path.partition("/")[2..-1].join("/") : physical_path }"
end
p.squeeze!("/")
logger.info "Defining global_assets_path...class name: #{ self.class.name }, physical_path: #{ physical_path }, p: #{ p }"
p
end
end
So essentially the method global_assets_path should return the full or relative path to the attachments directory specified by the call to has_attachment_assets_folder.
has_attachment_assets_folder "videos", :asset_path => true = "/assets/videos"
has_attachment_assets_folder "attachments/email", :asset_path => true = "/assets/attachments/email"
The thing is, if I use it in a regular context of a Rails app (one model being accessed at a time) it works just fine. However I'm running a major migration which requires me to use multiple models at once in the migration file. It seems that each model shares the method global_assets_path because the following happens:
Example 1
Email.all.each do |e|
puts e.global_assets_path(:asset_path => true) # Correct output of "/assets/attachments/email"
end
Video.all.each do |v|
puts v.global_assets_path(:asset_path => true) # Correct output of "/assets/video"
end
Example 2
test = Video.pluck(:category)
Email.all.each do |e|
puts e.global_assets_path(:asset_path => true) # Correct output of "/assets/attachments/email"
end
Video.all.each do |v|
puts v.global_assets_path(:asset_path => true) # Incorrect output of "/assets/attachments/email" because the Video model was instantiated for the first time on the 1st line.
end
You're calling define_method on the wrong thing. This:
ActiveRecord::Base.send :define_method, :global_assets_path
is sending define_method to ActiveRecord::Base so that exact global_assets_path method will be added to ActiveRecord::Base. When you say this:
class Email < ActiveRecord::Base
has_attachment_assets_folder "attachments/email"
end
you want to add your global_assets_path method to Email, not ActiveRecord::Base. self will be Email inside that has_attachment_assets_folder call so you want to say:
def self.has_attachment_assets_folder(physical_path, arguments = {})
define_method :global_assets_path do |args={}|
...
end
end
to define global_assets_path on self (the ActiveRecord::Base subclass) and leave ActiveRecord::Base itself alone.
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
I have a function that does this:
def blank_to_negative(value)
value.is_number? ? value : -1
end
If the value passed is not a number, it converts the value to -1.
I mainly created this function for a certain model, but it doesn't seem appropriate to define this function in any certain model because the scope of applications of this function could obviously extend beyond any one particular model. I'll almost certainly need this function in other models, and probably in views.
What's the most "Rails Way" way to define this function and then use it everywhere, especially in models?
I tried to define it in ApplicationHelper, but it didn't work:
class UserSkill < ActiveRecord::Base
include ApplicationHelper
belongs_to :user
belongs_to :skill
def self.splice_levels(current_proficiency_levels, interest_levels)
Skill.all.reject { |skill| !current_proficiency_levels[skill.id.to_s].is_number? and !interest_levels[skill.id.to_s].is_number? }.collect { |skill| {
:skill_id => skill.id,
:current_proficiency_level => blank_to_negative(current_proficiency_levels[skill.id.to_s]),
:interest_level => blank_to_negative(interest_levels[skill.id.to_s]) }}
end
end
That told me
undefined method `blank_to_negative' for #
I've read that you're "never" supposed to do that kind of thing, anyway, so I'm kind of confused.
if you want to have such a helper method in every class in your project, than you are free to add this as a method to Object or whatever you see fits:
module MyApp
module CoreExtensions
module Object
def blank_to_negative
self.is_number? ? self : -1
end
end
end
end
Object.send :include, MyApp::CoreExtensions::Object
There are a few options:
Monkey-patch the method into ActiveRecord and it will be available across all of your models:
class ActiveRecord::Base
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
Add a "concern" module which you then mix into selected models:
# app/concerns/blank_to_negate.rb
module BlankToNegate
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
# app/models/user_skill.rb
class UserSkill < ActiveRecord::Base
include BlankToNegate
# ...
end
Ruby Datatypes functionality can be extended. They are not sealed. Since you wan to use it in all places why not extend FIXNUM functionality and add a method blank_to_negative to it.
Here's what I ended up doing. I put this code in config/initializers/string_extensions.rb.
class String
def is_number?
true if Float(self) rescue false
end
def negative_if_not_numeric
self.is_number? ? self : -1
end
end
Also, I renamed blank_to_negative to negative_if_not_numeric, since some_string.negative_if_not_numeric makes more sense than some_string.blank_to_negative.