Forum: Ruby on Rails belongs_to causing NoMethodError exceptions ... ?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Ced8e05c3093ae2526cda6311a697e28?d=identicon&s=25 Marshall Pierce (Guest)
on 2005-12-29 10:31
(Received via mailing list)
I've got a really strange problem using belongs_to. I apologize in
advance for the length... this is going to take a while to explain.

Basic idea: Creating a User requires that the user enter an email
address and activation key that matches an existing PendingUser.
After creating the user successfully, that pending user should be
marked as "used".

The problem:
When I add a belongs_to :pending_user declaration to the User class,
attempting to save a User yields this exception (taken here from the
output of a simple test case -- code below.) You'll note some lines
in the User class with two hashes '##' preceding them -- those are
alternate statements that avoid using the .pending_user attribute. If
you comment out the belongs_to :pending_user and use the alternate
statements (which just work with the pending_user_id field),
everything seems to work fine.

   1) Error:
test_create_with_valid_pending_user(UserTest):
NoMethodError: undefined method `updated?' for #<PendingUser:0x8d09310>
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/base.rb:1498:in `method_missing'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/callbacks.rb:341:in `callback'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/callbacks.rb:335:in `callback'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/callbacks.rb:330:in `each'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/callbacks.rb:330:in `callback'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/callbacks.rb:248:in `create_or_update'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/base.rb:1226:in `save_without_validation'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/validations.rb:698:in `save_without_transactions'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/transactions.rb:126:in `save'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/transactions.rb:126:in `transaction'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/transactions.rb:91:in `transaction'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/transactions.rb:118:in `transaction'
     /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.13.2/lib/
active_record/transactions.rb:126:in `save'
     test/unit/user_test.rb:62:in `test_create_with_valid_pending_user'

I'm not calling the 'updated?' method anywhere in my code, but
looking in ${PREFIX}/lib/ruby/gems/1.8/doc/activerecord-1.13.2/rdoc/
classes/ActiveRecord/Associations/ClassMethods.src, it seems that
belongs_to calls the .updated? method.

Any light you can shed would be greatly appreciated -- I've been
banging my head against this for a few days, and no one on
#rubyonrails on freenode has been able to figure out what's going on.

-Marshall Pierce


Code follows...

Test case:

require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase
   fixtures :accounts, :pending_users
   # we don't need the users fixture -- all we're testing here is
creating a new user
   # and the corresponding validation of the associated pending user
and account
   def setup
     @new = User.new
     # give it a valid email address
     @new.email_address = pending_users(:ok_req).email_address
     @new.password = @new.password_confirmation = 'doesntmatter'
     @new.activation_key = 'activationkey'
     @new.account_id = pending_users(:ok_req).account_id
   end

   def test_create_with_valid_pending_user
     assert @new.save
   end
end

SQL definitions for the tables for the classes involved (postgresql):

DROP TABLE generic_users CASCADE;
CREATE TABLE generic_users (
     password_hash   text NOT NULL
,   email_address   text UNIQUE NOT NULL
,   created_at      timestamp with time zone NOT NULL
,   updated_at      timestamp with time zone NOT NULL
);

DROP TABLE users CASCADE;
CREATE TABLE users (
     LIKE generic_users      INCLUDING DEFAULTS
,   id                      SERIAL PRIMARY KEY
,   account_id              int NOT NULL REFERENCES accounts(id) ON
UPDATE CASCADE ON DELETE CASCADE
,   is_account_admin        bool NOT NULL DEFAULT false
,   email_validation_key    text NOT NULL
,   email_validated         bool NOT NULL DEFAULT false
,   pending_user_id         int NOT NULL REFERENCES pending_users(id)
ON UPDATE CASCADE ON DELETE CASCADE
);

DROP TABLE pending_users CASCADE;
CREATE TABLE pending_users (
     id                      SERIAL PRIMARY KEY
,   account_id              int NOT NULL REFERENCES accounts(id) ON
UPDATE CASCADE ON DELETE CASCADE
,   email_address           text UNIQUE NOT NULL
,   activation_key_hash     text NOT NULL
,   created_at              timestamp with time zone NOT NULL
,   updated_at              timestamp with time zone NOT NULL
,   used                    bool NOT NULL DEFAULT false
);


The class definitions: (yeah, I know -- re-implementing login logic
yet again is dumb, but this isn't near completion yet, so don't give
me too much grief about it. :)

class GenericUser < ActiveRecord::Base

validates_presence_of :password, :password_confirmation, :email_address
   validates_uniqueness_of :email_address
   validates_confirmation_of :password

   # password must be at least 8 characters long
   validates_format_of :password, :with => %r{^.{8,}$}, :message =>
"must be at least 8 characters"

   validates_format_of :email_address, :with => MiscUtils::EMAIL_REGEX

   # user can only supply these params
   attr_accessible :password, :password_confirmation, :email_address

   # class variable -- see koz's post http://www.koziarski.net/
archives/2005/07/16/environment
   cattr_accessor :current_user

   # non-db variables
   attr_accessor :password

   def before_save
     #breakpoint("generic user before_save")
     # save the new hash if the password is set
     # XXX - only set password_hash during methods that can change
password
     self.password_hash = MiscUtils.hexdigest(@password) unless
@password.nil?
   end

   def after_save
     @password = nil
     @password_confirmation = nil
   end

   def try_to_login
     self.class.lookup(self.email_address, @password)
   end

   # Class methods

   def self.lookup(email_address, password, additional_conds = '')
     hashed_password = MiscUtils.hexdigest(password)
     begin
       find( :first,
             :conditions => ["email_address = ? and password_hash
= ?" + additional_conds,
                           email_address, hashed_password])
     rescue
       return nil
     end
   end

end

--------------------

class User < GenericUser
   set_table_name "users"

   belongs_to :account
   belongs_to :pending_user

   validates_presence_of :activation_key

   attr_accessible :activation_key

   # non-db variable
   attr_accessor :activation_key

   # XXX: need to make sure that hypothetical user-editing tools
don't allow malicious queries
   # to change their account_id or anything like that
   def validate_on_create
     # we use an artificial, non db-backed parameter to hold the
activation key,
     # then perform lookup here so we have access to errors.add, etc.
     @pending_user = PendingUser.lookup(self.email_address,
@activation_key)
     if @pending_user.nil?
       errors.add_to_base("Invalid email address or activation key")
       return false;
     elsif @pending_user.used == true
       # XXX : only for debug. TMI.
       errors.add_to_base("User request already used")
     end

     begin
       self.account = Account.find(@pending_user.account_id)
     rescue
       errors.add(:account_id, "is invalid")
     end
   end

   def before_create
     self.email_address = @pending_user.email_address
     self.account = @pending_user.account

     ##self.pending_user = @pending_user
     self.pending_user_id = @pending_user.id

     # XXX: send an email with the activation key here...
     self.email_validation_key = MiscUtils.random_string(80)

     # XXX: since we're not sending email, manually set the
validation flag
     self.email_validated = true
     #breakpoint("user before_create finish")
   end

   def after_create
     # mark the pending user as used
     # XXX - handle failure better

     ##self.pending_user.used = true
     @pending_user.used = true

     ##unless self.pending_user.save
     unless @pending_user.save
       ##raise "couldn't save pending user #{self.pending_user.id}"
       raise "couldn't save pending user #{@pending_user.id}"
     end
   end

   def self.lookup(email_address, password)
     super(email_address, password, "and email_validated = true")
   end

end

-----------

class PendingUser < ActiveRecord::Base
   validates_presence_of :email_address, :account_id
   validates_uniqueness_of :email_address
   validates_format_of :email_address, :with => MiscUtils::EMAIL_REGEX

   attr_accessible :email_address, :account_id

   attr :activation_key

   belongs_to :account
   has_one :user

   # XXX: eventually we'll want to programmatically set which account
the pending user will
   # be tied to, and remove the accessible, etc symbols. (that is,
pull it from the current
   # user's info.)

   def validate
     begin
       self.account = Account.find(self.account_id)
     rescue
       errors.add(:account_id)
     end
   end

   # need to create an activation key and store its hash
   def before_create
     #randkey = MiscUtils.random_string(20)
     # XXX: right now we're not sending emails, so we won't know what
the un-hashed key was...
     randkey = "foo"
     self.activation_key_hash = MiscUtils.hexdigest(randkey)
   end

   # simple wrapper to do the digest'ing of the key
   def self.lookup(email_address, key)
     hashed_key = MiscUtils.hexdigest(key)
     begin
       find( :first,
             :conditions => ["email_address = ? and
activation_key_hash = ? and used = false",
                             email_address, hashed_key])
     rescue
       return nil
     end
   end
end
C64e63b70be7dfed8b0742540b8b27e5?d=identicon&s=25 Mark Reginald James (Guest)
on 2005-12-29 13:44
(Received via mailing list)
Marshall, try renaming before_create in User to
after_validation_on_create
to ensure the association is assigned before Rails tries to save it.
Also rename before_save in GenericUser to set_password_hash, and add
"before_save :set_password_hash" to ensure it's also called.

--
We develop, watch us RoR, in numbers too big to ignore.
Ced8e05c3093ae2526cda6311a697e28?d=identicon&s=25 Marshall Pierce (Guest)
on 2005-12-29 20:57
(Received via mailing list)
On Dec 29, 2005, at 4:41, Mark Reginald James wrote:

> Marshall, try renaming before_create in User to
> after_validation_on_create
> to ensure the association is assigned before Rails tries to save it.
> Also rename before_save in GenericUser to set_password_hash, and add
> "before_save :set_password_hash" to ensure it's also called.
>
> --
> We develop, watch us RoR, in numbers too big to ignore.
>

Mark,
Unfortunately, making those changes didn't help any. Also, changing
before_create to after_validation_on_create caused errors with using
@pending_user -- after_validation_on_create gets called even when
validation failed, so sometimes @pending_user was nil. So, I changed
it to "set_user_attributes" and used before_create :set_user_attributes,
just in case that makes any difference. In any case, before_create is,
well, supposed to happen before the db interaction, so I don't see why
that would cause problems. It seems to me that the problem lies more
with
belongs_to not mixing in the appropriate method to the PendingUser
class.

Those changes don't help, though -- the test still fails in exactly the
same way in exactly the same place.

-Marshall
C64e63b70be7dfed8b0742540b8b27e5?d=identicon&s=25 Mark Reginald James (Guest)
on 2005-12-30 02:05
(Received via mailing list)
Marshall Pierce wrote:

> Those changes don't help, though -- the test still fails in exactly the
> same way in exactly the same place.

OK, maybe it's related to the AR sbuclassing you're doing.

Or perhaps you can change the order of declarations.  There's
this from http://api.rubyonrails.com/classes/ActiveRecord/Ca...
:

*IMPORTANT:* In order for inheritance to work for the callback queues,
you must specify the callbacks before specifying the associations.
Otherwise, you might trigger the loading of a child before the parent
has registered the callbacks and they won?t be inherited.


--
We develop, watch us RoR, in numbers too big to ignore.
Ced8e05c3093ae2526cda6311a697e28?d=identicon&s=25 Marshall Pierce (Guest)
on 2005-12-30 02:56
(Received via mailing list)
On Dec 29, 2005, at 17:02, Mark Reginald James wrote:

> Callbacks.html :
>
> *IMPORTANT:* In order for inheritance to work for the callback queues,
> you must specify the callbacks before specifying the associations.
> Otherwise, you might trigger the loading of a child before the parent
> has registered the callbacks and they wonâ??t be inherited.

I re-ordered the top of the User class:
class User < GenericUser
   set_table_name "users"

   before_create :set_user_attributes

   validates_presence_of :activation_key

   attr_accessible :activation_key

   # non-db variable
   attr_accessor :activation_key

   belongs_to :account
   #belongs_to :pending_user

Still no dice if I uncomment that belongs_to. I can't imagine what
would fail just because I'm subclassing a subclass of AR, though...
any idea where I should look?

-Marshall
C64e63b70be7dfed8b0742540b8b27e5?d=identicon&s=25 Mark Reginald James (Guest)
on 2006-01-02 11:33
(Received via mailing list)
Marshall Pierce wrote:

> Still no dice if I uncomment that belongs_to. I can't imagine what
> would fail just because I'm subclassing a subclass of AR, though...  any
> idea where I should look?

Did you find a solution to this Marshall?

--
We develop, watch us RoR, in numbers too big to ignore.
Ab4c5cd5d9cc028fcba7a5eec8e1bf30?d=identicon&s=25 Alain Pilon (Guest)
on 2006-05-09 13:43
I am having a similar problem, any updates?!?
Ba5816b2a9c23d87324e3b98beabba8c?d=identicon&s=25 Alison Rowland (Guest)
on 2006-05-10 20:02
(Received via mailing list)
I just experienced this error, and I was also using belongs_to and a
callback. Perhaps it was related to that. I banged my head against
this all day today, with no change I could make to my code affecting
the error.

Finally, I did a "rake tmp:clear", restarted my development web server
(in Locomotive) and shut down my browser to clear out any
cache/sessions. The error magically disappeared, so you might want to
try clearing out all your caches as well. Doing so has fixed similarly
inexplicable errors for me in the past.

Come to think of it, I did re-order my associations and callbacks in
the head of my model to put the callbacks first, as Mark suggested.
Perhaps that was what actually fixed it, but I couldn't see the change
until doing a total refresh.

Good luck.

-- Alison
This topic is locked and can not be replied to.