Modularize an ActiveRecord class - ruby-on-rails

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

Related

confusion over validates_with custom validations syntax shown in rails guide

I am attempting to use the validates_with custom validations helper with Rails 4.
The following code is working in my application:
class Photo
validates_with CleanValidator
include ActiveModel::Validations
end
class CleanValidator < ActiveModel::Validator
def validate(record)
if record.title.include? "foo"
record.errors[:title] << "Photo failed! restricted word"
end
end
end
However I want to pass this helper to multiple attributes in multiple models, not just :title.
There is an example in validates_with section of guide that contains the following example:
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any?{|field| record.send(field) == "Evil" }
record.errors[:base] << "This person is evil"
end
end
end
class Person < ApplicationRecord
validates_with GoodnessValidator, fields: [:first_name, :last_name]
end
This is what I want to achieve, substituting [:fields] for [:title] in my code example so that I can use CleanValidator for multiple models and multiple attributes (User.name, Photo.title etc).
I think you want the other example from the guides, each validator. You should be able to do
class CleanValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless ["Evil", "Other", "Restricted", "Words"].include?(value)
record.errors[attribute] << (options[:message] || "is a restricted word")
end
end
end
class Photo
include ActiveModel::Validations
attr_accessor :title
validates :title, clean: true
end

Using the created nested attribute object after parent is saved in Rails 4

I'm trying to access a created object that is created using nested attributes when the parent item is saved, and I'm not sure how to do that. My current setup is such:
foo-model.rb
class Foo < ActiveRecord::Base
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars
end
bar-model.rb
class Bar < ActiveRecord::Base
belongs_to :foo
end
foos-controller.rb
class FoosController < ApplicationController
def create
#foo = Foo.create(foo_params)
if #foo.save
# What I want to do is essentially this
bar.do_something
# Redirect
redirect_to path
end
end
private
def foo_params
params.require(:foo).permit(:attribute, bar_attributes: [:id, :attribute1])
end
end
First of all, use ::new, not ::create on Foo.
def create
#foo = Foo.new(foo_params)
if #foo.save
After the #save call, you are then free to do any action on the associated Bars:
if #foo.save
#foo.bars.each do |bar|
bar.do_something
end
Foo.create will trigger the 'save' event. Instead, do this
class FoosController < ApplicationController
def create
#foo = Foo.new(foo_params)
if #foo.save
redirect_to path
end
end
end
In the model, add
class Foo < ActiveRecord::Base
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars
after_save :bars_do_something
def bars_do_something
bars.each{|b| b.do_something}
end
end
Instead of updating the foo.bars in an each (not a good idea, according to law of Demeter), you could update bar after it's created
class Bar < ActiveRecord::Base
belongs_to :foo
after_create :do_something
def do_something
do_something
end
end

Rails setting macros in module

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.

Namespacing issue in rails

I have a class A in module M like below
Module M
class A
def method1
# how to instantiate a model having same name as A
#like A.first
end
end
end
In my models I have a class A
class A < ActiveRecord::Base
end
You can access the global scope using the :: operator, e.g:
Module M
class A
def method1
::A.first
end
end
end

Rails 3 Including Nested Module Inside Controller

I have a controller that I'd like to include some standard methods.
class Main::UsersController < Main::BaseController
include MyModule::ControllerMethods
end
uninitialized constanct MyModule::ClassMethods::InstanceMethods
My module looks like this, which is also wrong, and was originally meant for a model. What's the best way to do it so that I can use it with a controller as well?
module MyModule
def self.included(base)
base.has_one :example, :autosave => true
base.before_create :make_awesome
base.extend ClassMethods
end
module ClassMethods
...
include InstanceMethods
end
module InstanceMethods
...
end
module ControllerMethods
...
# I want to include these in my controller
def hello; end
def world; end
end
end
Use extend instead of include for your ClassMethods. You should also split your model and controller modules:
module MyModule
module ModelMethods
def acts_as_something
send :has_one, :example, :autosave => true
send :before_create, :make_awesome
send :include, InstanceMethods
end
module InstanceMethods
...
end
end
module ControllerMethods
...
# I want to include these in my controller
def hello; end
def world; end
end
end
ActiveRecord::Base.extend MyModule::ModelMethods
Your Model would then look like this:
class Model < ActiveRecord::Base
acts_as_something
end

Resources