Rails setting macros in module - ruby-on-rails

I am trying to setup belongs_to, validates, and default scopes in a module.
module MultiTenancy
class TenantNotSetError < StandardError ; end
def self.included(model)
class << model
belongs_to :tenant
validates :tenant_id, presence: true
default_scope -> {
raise TenantNotSetError.new unless Tenant.current_tenant
where(tenant_id: Tenant.current_tenant.id)
}
def multi_tenanted?
true
end
end
end
end
I keep getting a
NoMethodError: undefined method `belongs_to' for #<Class:User>
error.
What am I doing wrong?

This should work:
def self.included(base)
base.class_eval do
# your code goes here
end
end
The reason it doesn't work is you try to call belongs_to on metaclass of User, not on User.

Related

Sequel validations in concerns

I have a Sequel model like this:
class User < Sequel::Model
include Notificatable
def validate
super
validates_presence [:email]
end
end
# concerns/notificatable.rb
module Notificatable
extend ActiveSupport::Concern
included do
def validate
super
validates_presence [:phone]
end
end
end
And here I got a problem: Notificatable validate method overrides the same method in the User model. So there is no :name validations.
How can I fix it? Thanks!
Why use a concern? Simple ruby module inclusion works for what you want:
class User < Sequel::Model
include Notificatable
def validate
super
validates_presence [:email]
end
end
# concerns/notificatable.rb
module Notificatable
def validate
super
validates_presence [:phone]
end
end

Define Custom Object Lifecycle Hooks In Ruby

So I have a object and I want to define a lifecycle hook such as before_create, after_create, etc.
I want to call this after_retire and have it setup so I can do the following:
class User < ActiveRecord::Base
include Active
after_retire :method
def method
#do stuff
end
end
So far I have a module setup like so but I keep getting a method undefined after_retire error on my User model.
module Active
extend ActiveSupport::Concern
included do
define_callbacks :retire
set_callback :retire, :after, :after_retire
default_scope { where(:retired => false) }
scope :retired, where(:retired => true)
end
def retire!
run_callbacks :retire do
update_attribute :retired, true
update_attribute :retired_at, Time.now.to_datetime
end
end
end
How should I be setting this up?
The before/after_callback syntax is handled in ActiveModel::Callbacks through #define_model_callbacks. The raw ActiveSupport::Callbacks will require you to use #set_callback without any syntactic sugar:
module Active
extend ActiveSupport::Concern
included do
define_callbacks :retire
default_scope { where(:retired => false) }
scope :retired, where(:retired => true)
end
def retire!
run_callbacks :retire do
update_attribute :retired, true
update_attribute :retired_at, Time.now.to_datetime
end
end
end
class User < ActiveRecord::Base
include Active
set_callback :retire, :after, :method
def method
#do stuff
end
end
If you want to have the after/before syntax, since you are working with an ActiveRecord (and thus ActiveModel) class, you can use:
module Active
extend ActiveSupport::Concern
included do
define_model_callbacks :retire
default_scope { where(:retired => false) }
scope :retired, where(:retired => true)
end
def retire!
run_callbacks :retire do
update_attribute :retired, true
update_attribute :retired_at, Time.now.to_datetime
end
end
end
class User < ActiveRecord::Base
include Active
after_retire :method
def method
#do stuff
end
end

Setter methods can't see child built in parents after_initialize callback

An Email has many Variants (for ab testing purposes) and always has one set as the master
I want to ensure an email always has a master variant built on initialization.
I also want to delegate attr accessors 'subject' and 'body' to the master variant.
I originally tried using
delegate :subject, :body, to: :master
but rails complained master was nil.
So I tried hand rolling my own subject= setter method and via pry I found that whilst my master is being set in the after_initialize callback, the subsequent call to subject= complains master is nil. I dont understand why.
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # Rails says master is nil here
end
#
# Callbacks
#-----------------------------------------
after_initialize :ensure_master
def ensure_master
return unless new_record?
self.master ||= build_master
end
end
Email.new(:subject => 'yah') # undefined method `subject=' for nil:NilClass
when your email instance is initialized your master is nil, you need to trigger build_master before you set anything on it
how about:
# app/models/email.rb
class Email < ActiveRecord::Base
def subject=(str)
master.subject = str # Rails says master is nil here
end
def master
super || build_master
end
end
Don't know much when and why you need after_initialize callback in your project, but if you sure you need this functionality, I would consider to use custom service class to achieve this
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # this way Rails won't says master is nil here
end
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args
def self.build(args={})
new(args).build
end
def initialize(args)
#args = args
end
def build
email = Email.new
email.build_master
email.attributes = args
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email
...and another variation of this for persisted record
Update
or even better
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args, :subject
def self.build(args={})
new(args).build
end
def initialize(args)
#subject = args.fetch(:subject)
#args = args
end
def build
email = Email.new args
email.build_master
email.master.subject = subject
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email

Undefined method error when I add method to model in Rails

I have the error
undefined method events_and_repeats' for #<Class:0x429c840>
app/controllers/events_controller.rb:11:in `index'
my app/models/event.rb is
class Event < ActiveRecord::Base
belongs_to :user
validates :title, :presence => true,
:length => { :minimum => 5 }
validates :shedule, :presence => true
require 'ice_cube'
include IceCube
def events_and_repeats(date)
#events = self.where(shedule:date.beginning_of_month..date.end_of_month)
return #events
end
end
app/controllers/events_controller.rb
def index
#date = params[:month] ? Date.parse(params[:month]) : Date.today
#repeats = Event.events_and_repeats(#date)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #events }
end
end
What is wrong?
Like Swards said, you called a instance method on a class. Rename it:
def self.events_and_repeats(date)
I am only writting this in an answer because it's too long for a comment,
checkout the ice-cube github page, it strictly says:
Include IceCube inside and at the top of your ActiveRecord model file to use the IceCube classes easily.
Also i think it you don't need the require in your model.
You can do it both ways:
class Event < ActiveRecord::Base
...
class << self
def events_and_repeats(date)
where(shedule:date.beginning_of_month..date.end_of_month)
end
end
end
or
class Event < ActiveRecord::Base
...
def self.events_and_repeats(date)
where(shedule:date.beginning_of_month..date.end_of_month)
end
end
Just for more clarity:
class Foo
def self.bar
puts 'class method'
end
def baz
puts 'instance method'
end
end
Foo.bar # => "class method"
Foo.baz # => NoMethodError: undefined method ‘baz’ for Foo:Class
Foo.new.baz # => instance method
Foo.new.bar # => NoMethodError: undefined method ‘bar’ for #<Foo:0x1e820>
Class method and Instance method

Modularize an ActiveRecord class

If I have classes like this,
class A < ActiveRecord::Base
include ExampleModule
end
class B < ActiveRecord::Base
include ExampleModule
end
module ExampleModule
module ClassMethods
...
end
def included(base)
...
end
end
how do I get the a reference to class A or B inside of ExampleModule upon referencing including this module into either one of those classes? I'm asking this question because I wanted to do something like adding has_one :association or after_create :do_something to class A or B via including ExampleModule such as below.
class A < ActiveRecord::Base
include ExampleModule
end
class B < ActiveRecord::Base
include ExampleModule
end
module ExampleModule
has_one :association
after_create :do_something
module ClassMethods
...
end
def included(base)
...
end
end
Is there a better way to do this as well? Thanks!
If you extend ActiveSupport::Concern, you should be able to do it when the module is included:
module ExampleModule
extend ActiveSupport::Concern
def do_something
# ...
end
included do
has_one :association
after_create :do_something
end
end
If what you're wanting to do is call has_one or after_create depending on which class is including the module you can do this
module Extender
def self.included(base)
if base.name == A.name
# do stuff for A
has_one :association
elsif base.name == B.name
# do stuff for B
after_create :do_something
end
end
end

Resources