Silly rails question: undefined method within class declaration - ruby-on-rails

I have a user class, where I am trying to attach a profile created by a factory. Here is the class:
class User < ActiveRecord::Base
acts_as_authentic
has_one :profile
after_create {self.profile = ProfileFactory.create_profile(self.role)}
end
and the factory looks like this
class ProfileFactory
def self.create_profile(role)
String s = "#{role}#{"Profile"}"
Object.const_get(s).new
end
end
For some reason it doesnt recognize self as a User. This is the error I get on making the ProfileFactory.create_profile call
undefined method 'role' for
#<Class:0x2304218>
The user object has a role: String declared in its migration.
Any help is appreciated thanks.

Duncan's got the correct answer in terms of using your factory as a callback. But it might help you to understand what was going wrong.
Class methods receive the class as self, instance methods receive the instance as self. When you provide any method a block, the scope of the calling method is used for the block.
after_create is a class method that adds a call back to the block provided or methods listed as arguments. Blocks provided to callbacks (after_create, before_save, etc) are interpreted in the context of class methods. So self refers not, to the object being created, but the Class of the object being created.
In this snippet:
after_create {self.profile = ProfileFactory.create_profile(self.role)}
self is the User class, not an instance of the User class as you expect.
Compared to the more traditional after_create syntax that Matt was hinting at, where an instance method is added to the callback chain. In this context self refers to the instance.
class User < ActiveRecord::Base
has_one :profile
after_create :add_profile
protected
def add_profile
self.profile = ProfileFactory.create_profile(role)
end
end
EmFi, This makes a lot of sense. So
just to clarify, when invoking methods
that are in the class from the
callback methods but NOT actually in
one of the callback methods, allows us
to get around this class method
problem, and use the current instance?
Yes, but not for the reasons you think. Callbacks only looks for instance methods when passed a symbol.
Instead what you've found a way to get around the instance method problem. You can't give a callback a class method, but you can provide it a block in which it calls one. I guess you could also define an instance method that calls the class method, but that seems a little backwards.

The User object in question is passed to the after_create block as a parameter.
class User < ActiveRecord::Base
after_create do |user|
user.profile = ProfileFactory.create_profile(user.role)
user.save
end
end

Why not do something more simplistic? Something like:
class User < ActiveRecord::Base
has_one :profile
after_create :add_profile
protected
def add_profile
self.create_profile(:role => self.role)
end
end
class Profile < ActiveRecord::Base
belongs_to :user
end
Have you come from a Java background perchance?

Related

How do you refer an instance of a model in model class in Ruby on Rails?

I'm trying to refer an instance of User class in a Rails for geocoding
class User < ActiveRecord::Base
geocoded_by :address
after_validation GeocodeJob.perform_later(self), if: :address_changed?
end
What I'm trying to pass on is the current instance of user. However as it is obvious, what I'm ending up passing is the class and not the instance, failing starting my job queue.
How can I refer and pass the user instance on a model callback? I know I can use instance variables using a controller, wondering if I can queue directly as a model callback.
With what you have currently written, GeocodeJob.perform_later(self) will be called when the class is loaded and it's return value will be used as the parameter passed to the call to after_validation. As you say, that's not what you want.
Instead, you can pass a symbol for a method to call like so:
class User < ActiveRecord::Base
geocoded_by :address
after_validation :setup_geocode_job, if: :address_changed?
def setup_gecode_job
GeocodeJob.perform_later(self)
end
end
This will do what you want by calling the instance method of the model and self will be the model instance.
See: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
It is definitely bad idea to use ActiveRecord instance directly as a job argument. The main problem here in serialization of Ruby object into some kind of a writeable format (JSON, YAML etc). Links to read more:
Read comment from ActiveJob source code
The first point from Sidekiq Best Practices
To access ActiveRecord instance in a job worker context it would be better to find the record by given id value:
class GeocodeJob < ActiveJob::Base
def perform(user_id)
user = User.find user_id
# do hard geocode work here
end
end
Start the job from callback like:
class User < ActiveRecord::Base
geocoded_by :address
after_validation if: :address_changed? do |user|
GeocodeJob.perform_later(user.id)
end
end
PS. I would strongly recommend to use after_commit callback instead of after_validation because sometime record saving process might be cancelled in other callbacks or if some problem with database will be raised. One more thing here - if a user has not being saved before (like User.create(...)), its instance does not have id value.

Passing in a hash to a class without using an initialization method

I was under the, apparently incorrect, impression that when I pass a hash into a class the class requires an initialization method like this:
class Dog
attr_reader :sound
def initialize(params = {})
#sound = params[:sound]
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
But I've run into a bit of code (for creating a password digest) that works within a rails application that doesn't use an initialization method and seems to work just fine and it's kind of confuses me because when I try this anywhere else it doesn't seem to work. Here is the sample code that works (allows me to pass in a hash and initializes without an initialization method):
class User < ActiveRecord::Base
attr_reader :password
validates :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
def password=(pwd)
#password = pwd
self.password_digest = BCrypt::Password.create(pwd)
end
end
NOTE: In the create action I pass in a hash via strong params from a form that, at the end of the day, looks something like this {"email"=>"joeblow#gmail.com", "password"=>"holymolycanoli”}
In this bit of code there is no initialization method. When I try something like this (passing in a hash without an initialization method) in pry or in a repl it doesn't seem to work (for instance the following code does not work):
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
The error I get is:
wrong number of arguments (1 for 0)
Is it rails that allows me to pass in hashes like this or ActiveRecord? I'm confused as to why it works within rails within this context but generates an error outside of rails. Why does this work in rails?
If you look at the top you have this:
class Dog < ActiveRecord::Base
This causes your class Dog to inherit from ActiveRecord::Base
when it does so it gains a bunch of methods that allows you to set things up.
Now when you call for example:
Dog.create(password: 'some_password', username: 'some_username')
Your calling a method on the class object that then returns an instance of the class.
so taking your example
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
def self.create data_hash
new_dog = self.new #create new instance of dog class
new_dog.sound = data_hash[:sound] #set instance of dog classes sound
new_dog # return instance of dog class
end
end
It's essentially what we would term a factory method, a method that takes in data and returns an object based on that data.
Now I have no doubt that ActiveRecord::Base is doing something more complicated than that but that is essentially what it's doing at the most basic of levels.
I'd also like to point out that when inheriting from ActiveRecord::Base your also inheriting its 'initialize' method so you don't have to set one yourself.
The class knows what attribute methods to create based on the schema you set when you did the DB migrations for a table that matches (through rail's conventions) the class.
A lot of things happen when you subclass ActiveRecord::Base. Before looking at other issues I'm guessing that Dog is a rails ActiveRecord model and you just forgot to add
class Dog < ActiveRecord::Base

how to call a class, after_create method from either a spec or rails console

I have the following
class Item < ActiveRecord::Base
after_create TellMe
end
class TellMe
def self.after_create(model)
end
and would like to be able to do something analogous to this:
i=Item.new :name => 'my test name'
i.send(:TellMe.after_create)
similar to how I could call send on a public method? It looks like I can do
i.instance_eval 'TellMe.after_create(self)'
but feels a little ugly (amongst other things)
The only way to trigger a callback is to do a qualifying event, in this case, creating an item.
As a workaround to what you want, you could just create another method that will do exactly what the callback would do and you would be able to access it like normal
Class Item < ActiveRecord::Base
def tellme(params)
TellMe.function(params)
end
end

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

Subclassing ActiveRecord with permalink_fu in a rails engine

This question is related to extending class methods in Ruby, perhaps more specifically in the way that permalink_fu does so.
It appears that has_permalink on a model will not be available in a derived model. Certainly I would expect anything defined in a class to be inherited by its derived classes.
class MyScope::MyClass < ActiveRecord::Base
unloadable
self.abstract_class = true
has_permalink :name
end
class MyClass < MyScope::MyClass
unloadable
#has_permalink :name # This seems to be required
end
Is there something in the way permalink_fu mixes itself in that causes this issue?
I'm using the permalink-v.1.0.0 gem http://github.com/goncalossilva/permalink_fu
After investigating this, I can now see that the problem is related to how permalink_fu verifies it it should create a permalink or not. It verifies this by checking if the permalink_field of the class is blank or not.
What's the permalink_field? When you do
class Parent < ActiveRecord::Base
has_permalink :name
end
class Child < Parent
end
you can access the permalink by writing Parent.new.permalink or Child.new.permalink. This method name can be changed by writing
class Parent < ActiveRecord::Base
has_permalink :name 'custom_permalink_name'
end
If so, the permalink will be accessible by writing Parent.new.custom_permalink_name (or Child.new.custom_permalink_name).
What's the problem with this? The permalink_field accessor methods are defined on Parent's metaclass:
class << self
attr_accessor :permalink_field
end
When you run the has_permalink method, it calls Parent.permalink_field = 'permalink'.
The problem is that although the permalink_field method is available on all subclasses, its value is stored on the class it was called. This means that the value is not propagated to the subclasses.
So, as the permalink_field is stored on the Parent class, the Child does not inherit the value, although it inherits the accessor methods. As Child.permalink_field is blank, the should_create_permalink? returns false, and Child.create :name => 'something' does not create a permalink.
A possible solution would be to replace the attr_acessors on the metaclass with cattr_accessors on the class (lines 57 to 61 on the permalink_fu.rb file).
Replace
class << base
attr_accessor :permalink_options
attr_accessor :permalink_attributes
attr_accessor :permalink_field
end
with
base.cattr_accessor :permalink_options
base.cattr_accessor :permalink_attributes
base.cattr_accessor :permalink_field
Note that this will invalidate any possible customization on the subclass. You will no longer be able to specify different options for the subclasses, as these three attributes are shared by Parent and all its subclasses (and subsubclasses).

Resources