Add callback to dynamically created ActiveRecord class - ruby-on-rails

I am creating an ActiveRecord class dynamically, and have code along the lines of the following:
new_klass = Class.new ActiveRecord::Base do
cattr_accessor :model_name
self.abstract_class = false
self.table_name = "foo"
# do more stuff...
end
Object.const_set "Foo", new_klass
How can I add a callback to this class, for example, a before_save callback?

Don't see your problem you don't need anything since you are in the class scope, just call the callback methods you want to register ...
Below i've added a validation callback to prevent instance validation :
new_klass = Class.new ActiveRecord::Base do
cattr_accessor :model_name
self.abstract_class = false
self.table_name = "items"
validate do
errors.add :base, "not good"
end
end
> instance = new_klass.new
> instance.valid?
=> false
> instance.errors.full_messages
=> ["not good"]

try including a concern to your dynamic class, and then handle it there.
new_klass = Class.new ActiveRecord::Base do
#...
include MyConcern
#...
end
# app/models/concerns/my_concern.rb
module MyConcern
extend ActiveSupport::Concern
included do
before_save ...
end
end

Related

Rails, issue with auto loading

So im having this weird issue for some reason i dont know why it is happening.
So, i have this extension in conifg/initializers/has_friendly_token.rb
ActiveRecord::Base.define_singleton_method :has_friendly_token do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
And I'm using it in two different models
class Business < ApplicationRecord
include Colorful, Channels, Rails.application.routes.url_helpers
has_friendly_token
end
and
class Conversation < ApplicationRecord
has_friendly_token
include AASM, Colorful, Rails.application.routes.url_helpers
end
the thing is, whenver i try to to run i get this error
`method_missing': undefined local variable or method `has_friendly_token' for Business (call 'Business.connection' to establish a
connection):Class (NameError)
Did you mean? has_secure_token
but it works in conversation. Any ideas why this happenes?
I can't see a good reason to use an initializer to monkeypatch ActiveRecord::Base here in the first place. Instead create a module and extend your ApplicationRecord class with it as this is simply a "macro" method.
module FriendlyToken
def has_friendly_token
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
end
class ApplicationRecord < ActiveRecord::Base
extend FriendlyToken
end
Unlike a monkeypatch its easy to test this code and it will be picked up by documentation tools like rdoc or yard as well as coverage tools.
You could also just simply write it as a concern since your macro method does not take any arguments anyways:
module FriendlyToken
extend ActiveSupport::Concern
included do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
end
def set_token?
new_record? || saved_change_to_token?
end
def set_unique_token?
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
class Business < ApplicationRecord
include Colorful,
Channels,
Rails.application.routes.url_helpers,
FriendlyToken
end
The advantage here is easier debugging since the methods are not defined dynamically.
See 4 Ways To Avoid Monkey Patching.

Rails: How to retrieve the polymorphic '_type' column name for a polymorphic model dynamically?

I basically want to create a concern which will be included in all the polymorphic models. This concern needs to have a dynamic setter method which which sets the value for the '_type' column.
module StiPolymorphable
extend ActiveSupport::Concern
included do
define_method "#{magic_method_to_get_type_column}=" do |type_field|
super(type_field.to_s.classify.constantize.base_class.to_s)
end
end
end
I basically want to access all the addresses of a Parent instance instead of a Person instance.
Example -
Suppose I have the following classes
class Person < ActiveRecord::Base
end
class Parent < Person end
class Teacher < Person end
class Address < ActiveRecord::Base
include StiPolymorphable
belongs_to :addressable, polymorphic: true
end
Right now if I try to access the addresses of a Parent it gives me zero records since the addressable_type field contains the value 'Person'.
Parent.first.addresses => #<ActiveRecord::Associations::CollectionProxy []>
Person.first.addresses => #<ActiveRecord::Associations::CollectionProxy [#<Address id: .....>]>
You might be interested on looking at Modularity gem so you could pass variables when you're including the Module. Haven't really tried it though. Hope it helps.
We do something like this:
module Shared::PolymorphicAnnotator
extend ActiveSupport::Concern
class_methods do
# #return [String]
# the polymorphic _id column
def annotator_id
reflections[annotator_reflection].foreign_key.to_s
end
# #return [String]
# the polymorphic _type column
def annotator_type
reflections[annotator_reflection].foreign_type
end
end
included do
# Concern implementation macro
def self.polymorphic_annotates(polymorphic_belongs, foreign_key = nil)
belongs_to polymorphic_belongs.to_sym, polymorphic: true, foreign_key: (foreign_key.nil? ? (polymorphic_belongs.to_s + '_id').to_s : polymorphic_belongs.to_s)
alias_attribute :annotated_object, polymorphic_belongs.to_sym
define_singleton_method(:annotator_reflection){polymorphic_belongs.to_s}
end
attr_accessor :annotated_global_entity
# #return [String]
# the global_id of the annotated object
def annotated_global_entity
annotated_object.to_global_id if annotated_object.present?
end
# #return [True]
# set the object when passed a global_id String
def annotated_global_entity=(entity)
o = GlobalID::Locator.locate entity
write_attribute(self.class.annotator_id, o.id)
write_attribute(self.class.annotator_type, o.class.base_class)
true
end
end
end
In your model:
class Foo
include Shared::PolymorphicAnnotator
polymorphic_annotates('belongs_to_name', 'foreign_key')
end

How do a custom method applicable to various models

I have the following method called capitalizeEachWord. Inside this method there is an attribute called company
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord
self.company=self.company.downcase.split.map(&:capitalize).join(' ')
end
end
I would like that this method not use the attribute company directly, but receives this attribute as a parameter for doesn't do it dependent of the model BusCompany. Something as the following. The problem is that this method I going to use in various models and don't want to write it in each model but use the inheritance
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord(self.company)
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord(attribute)
self.attribute=self.attribute.downcase.split.map(&:capitalize).join(' ')
end
end
Add the following code into config/initializers/capitalizer.rb
module Capitalizer
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def capitalize(*attributes)
#attributes_to_capitalize = attributes
before_save :capitalize_each_word
end
def attributes_to_capitalize
Array.new(#attributes_to_capitalize)
end
end
def capitalize_each_word
self.class.attributes_to_capitalize.each do |attr|
if value = send(attr)
self.send("#{attr}=", value.strip.titleize)
end
end
end
end
And then in your class:
class BusCompany < ActiveRecord::Base
include Capitalizer
capitalize :company
...
end
First, I'd recommend you override the setter for company instead of using error prone callbacks, like this:
class BusCompany < ActiveRecord::Base
# you can also use #titleize instead of capitalize each word
# also use try, in case `arg` is nil
def company=(arg)
super arg.try(:titleize)
end
end
Then you can use modules to wrap this functionality into a reusable unit. Throw this in a file in your concerns folder, or just in to the models folder:
module CapitalizedSetter
def capitalize_setter(*attr_names)
# for each attr name, redifine the setter so it supers the titleized argument instead
attr_names.each do |attr|
define_method(:"#{attr}=") { |arg| super arg.try(:titleize) }
end
end
end
Finally extend it into the desired models:
class BusCompany
extend CapitalizedSetter
capitalized_setter :company
end

How to make helpers for models

I have this private method in my User model
class User < ActiveRecord::Base
attr_accessible :email, :fullname, :password, :username
has_secure_password
before_create { generate_token(:remember_token) }
.
.
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while User.exists?(col => self[col])
end
end
How can i make generate_token available In other models?
Thanks
many options here are some:
use a plain, simple ruby module.
module TokenRememberable
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while User.exists?(col => self[col])
end
end
class User < ActiveRecord::Base
include TokenRememberable
before_create { generate_token(:remember_token) }
end
for more complex functionnality, use ActiveSupport::Concern :
module TokenRememberable
extend ActiveSupport::Concern
# thanks to Concern, this block wil be evaluated
# in the context of the including class
included do
before_create { generate_token(:remember_token) }
end
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while self.class.exists?(col => self[col])
end
end
class User < ActiveRecord::Base
include TokenRememberable
end
extract the functionnality in a dedicated class, and possibly use composition
# we only need a class method here, but you can also build
# full-fledged objects as you need
class TokenGenerator
def self.generate_token
# your generation logic here
end
end
class User
def after_initialize
#token_generator = TokenGenerator # or TokenGenerator.new( self ), for instance
end
attr_reader :token_generator
delegate :generate_token, to: :token_generator # optionnaly use delegation
end
NOTE :
this is not really a rails issue, more a Ruby one. You should document yourself more on the language you are using... Modules are a very common idiom. I can give you some good reference books if you need
a few approaches:
slightly dependent:
do:
class User < ActiveRecord::Base
before_create { User::Helpers.new(self).generate_token(:remember_token) }
class Helpers < Struct.new(:user)
def generate_token(col)
begin
user.send("#{col}=", SecureRandom.urlsafe_base64)
end while User.exists?(col => user.send(col))
end
end
end
independent: don't pass the user object to the class, just make the method render a random token and assign it from the model.

Rails: How to effectively use self.inherited

I have an AbstractRecord model from which some of the concrete models (which have their own tables) descend. Following is the inheritance.
AbstractRecord < ActiveRecord::Base
Blog < AbstractRecord
Post < AbstractRecord
....
....
In order for Rails to look for the proper tables if there is inheritance, API docs say to define a class method abstract_class? that returns true so that rails won't look for its table. In my case, in order for rails to look for blogs table (instead of abstract_records table, which is typically the case as in STI) I defined method abstract_class? in AbstractRecord that returns true. All the queries seems to work fine. But I see whenever I instantiate Blog, rails shows it as Blog (abstract) in the console as its parent class returns true. In order to avoid this, I could again define abstract_class? that returns false in Blog class.
But I was thinking instead of defining abstract_class? in all the child models, if I could somehow make use of self.inherited and define that method in AbstractClass itself. I tried to use several approaches (following) none seems to work.
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.instance_eval do
define_method(:abstract_class?) { false }
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.class_eval do
define_method(:abstract_class?) { false }
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.instance_eval do
def abstract_class?
false
end
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.class_eval do
def abstract_class?
false
end
end
end
end
Any advise on what I am doing wrong is appreciated?
Try this:
def self.inherited(subclass)
super
def subclass.abstract_class?
false
end
end
Or:
def self.inherited(subclass)
super
subclass.class_eval do
def self.abstract_class?
# You lost the 'self' part, so you had just defined an instant method for the subclass
false
end
end
end
class Account < Foo::Bar::Base
end
module Foo
module Bar
class Base < ::ActiveRecord::Base
def self.abstract_class?
true
end
end
end
end
That works fine for me. It results in table name "accounts" as it have to be.

Resources