Topic: Observer oddity

I have lost nearly half my day trying to work this odd problem out.

I am using an observer to trigger an ActionMailer notification

class BmiContactObserver < ActiveRecord::Observer
  def after_create(bmi_contact)
    BmiContactMailer.deliver_new_bmi_contact_notification(bmi_contact)
  end
end

class BmiContactMailer < ActionMailer::Base
 
  def new_bmi_contact_notification(bmi_contact)
    setup_email(bmi_contact)
    @subject    += "To view your BMI again visit"
    @body[:url]  = "http://#{Diagnosis::Config.site_url}/bmi/#{bmi_contact.body.id}"
  end
 
  protected
    def setup_email(bmi_contact)
      @recipients  = "#{bmi_contact.email}"
      @from        = "#{Diagnosis::Config.admin_email}"
      @subject     = "[#{Diagnosis::Config.site_url}] "
      @sent_on     = Time.now
      @body[:bmi_contact] = bmi_contact
    end

end


The problem is I can't access the body association, it comes up blank. However if I call it directly from the controller it works fine (same code).

BmiContactMailer.deliver_new_bmi_contact_notification(@bmi_contact)

I have tried all sorts of different ways to load the body too and all return nil.

Does anyone know if it is possible to access the associations when triggered through an observer?

Re: Observer oddity

You should be able to access an association in an observer.

should this:

@body[:bmi_contact] = bmi_contact

be this:

@body[:bmi_contact] = bmi_contact.body

or why would you be sending an AR object in an email? I've never used ActionMailer, so maybe I'm missing something.

Re: Observer oddity

This doesn't seem to be the problem because the code as it is works fine when I run it manually from my controller. It doesn't work when run from an observed event.

Also my understanding is

@body[:bmi_contact] = bmi_contact

makes @bmi_contact available to the view for my email and that works fine. I can use @bmi_contact.first_name in the template. Although to be really clean I should probably set

@body[:first_name] = bmi_contact.first_name

Anyway, when triggered from the observer all attributes of my BmiContact class are available. What is not available is the associated Body class. It doesn't seem to load

Because it works without modification from my controller I have to believe it is something inherent in the observer.

Re: Observer oddity

Depending on way the association is made, the observed object might have to be saved first for the observer to access the associated object. That's definitely the case if you are creating the associated object off of the observed object.

my_new_and_observed_object.create_associated_object(some params)

Changing the "after_create" to "after_save" might preclude this timing problem.

Last edited by Matt Garland (2008-10-09 05:16:03)

Re: Observer oddity

I was wondering about the whole 'after_save' event but was concerned that if I ever ran save on the object again then the observer would trigger and a "you're registered" email would fire off again.

Perhaps I should put the observer on the associated model instead, then I could just run 'after_create'

I had a feeling it had to do with saving and not being given an id yet. What tricked me though was that bmi_contact.id is valid which I thought would not be until it was saved either.

Cheers

Re: Observer oddity

You can try:

def after_save
     if new_record?
          #send mail
     end
end

Re: Observer oddity

Sweet. Of course I could. Still very new to this.

Awesome. I'll give that a go.

Re: Observer oddity

OK, so I tried this and I think the problem is that after_save means the object no longer is a new_record.

So I'm still looking for a way to send an email once on creation but it needs to be after_save because I need to make sure the associated models are also created and saved.

I don't want to have to resort to keeping track sent emails ('Y'/'N') in the database and neither do I want to clog up my controllers with 'deliver' messages.

Any thoughts?

Thanks

Re: Observer oddity

you can try using this regular callback--or others--in the model itself, if you can stomach cluttering up the model:

def after_initialize
  if new_record?
  #email
end

I believe that observers are called BEFORE regular in-model callbacks. (Obie says so in the Rails Way). So if you are creating the association in a regular callback of the model, it won;t exist yet for the observer.

Where are you creating the association? Let's see some code.

Re: Observer oddity

Thanks Matt,

here is some code with what I am doing

class BMIContactController < AC
  def create
    logout_keeping_session!
    begin
      Protector.check_authenticity(params[:token],params[:ts])
      @bmi_contact = BmiContact.new(params[:user])
      @body = @bmi_contact.body = Body.new(params[:body])
      success = @bmi_contact && @bmi_contact.body && @bmi_contact.save
      #works fine if I manually call BmiContactMailer:: here
    ...

class BodyContact < Contact
  has_one :body
  validates_associated  :body
end

class BmiContact < BodyContact
  attr_accessible :bmia
end


class BmiContactObserver < ActiveRecord::Observer
  def after_save(bmi_contact)
    if bmi_contact.new_record?
      bmi_contact.logger.info("found body with id #{bmi_contact.body.id}")
      BmiContactMailer.deliver_new_bmi_contact_notification(bmi_contact)
    end
  end
end

class Body < ActiveRecord::Base
  belongs_to :body_contact
  validates_presence_of :kgs, :cms, :gender, :age

As I was ssying I tried this last observer and it never send because it was already saved. The base problem is after_create(bmi_contact) doesn't have access to the Body association if I go through the observer.

Thanks

Re: Observer oddity

      @bmi_contact = BmiContact.new(params[:user])
      @body = @bmi_contact.body = Body.new(params[:body])
      success = @bmi_contact && @bmi_contact.body && @bmi_contact.save

You are assuming that saving the bmi_contact ALSO saves bmi.contact.body...I'm don't think that's the case, because of the way the body is created.

Do you check the db to see if it was saved?

It probably works in the controller because you have class variable @body that you probably refer to below the fold.

You can add: @body.save. (But BEFORE the contact is saved, so when the contact observer kicks in, it's there.)

But there is a better way.

Usually you would use:

@bmi_contact.body.create(params[:body]) (or whatever the "body" or "bodies" association is.)

Then @bmi_contact.save saves both of them, I believe (or perhaps create just handles the foreign key and save the body right off.)