views:

1646

answers:

3

Please see UPDATES at the bottom of the question...

For reference, this problem evolved out of some fixes I made based on a previous problem I was having here: http://stackoverflow.com/questions/1298846/associating-two-models-in-rails-user-and-profile

I'm building an app that has a user model and a profile model.

I want to associate these models such that:
- After the user creates an account, he is automatically sent to the "create profile" page, and the profile he creates is connected to only that particular user.
- Only the user who owns the profile can edit it.

I generated the user model using nifty_generators. When the user hits submit for the account creation, I redirect him to the "new profile" view to create a profile. I did this by editing the redirect path in the user controller. The user controller looks like this:

def new
  @user = User.new
end

def create
  @user = User.new(params[:user])
  if @user.save
    session[:user_id] = @user.id
    flash[:notice] = "Thank you for signing up! You are now logged in."
    redirect_to new_user_profile_path(:user_id => @user)
  else
    render :action => 'new'
  end
end

When I click 'submit' to create a new user, it now sends me to the following url, which seems right: localhost:3000/users/6/profile/new. But it throws the following exception:

NoMethodError in ProfilesController#new
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.build

The trace indicates that the problem is in the profiles controller, in the New method. The profiles controller looks like this:

def index
  @user = User.find(params[:user_id])
  @profile = @user.profile(:order => "created_at DESC")
end

def show
  @user = User.find(params[:user_id])
  @profile = @user.profile.find(params[:id])
end

def new
  @user = User.find(params[:user_id])
  @profile = @user.profile.build
end

def edit
  @user = User.find(params[:user_id])
  @profile = @user.profile.find(params[:id])
end

def create
  @user = User.find(params[:user_id])
  @profile = @user.profile.build(params[:profile])
    if @profile.save
      flash[:notice] = 'Profile was successfully created.'
      redirect_to(@profile)
    else
      flash[:notice] = 'Error.  Something went wrong.'
      render :action => "new"
    end
end

Additionally, the app also throws an exception when I try to view the index page of the profiles (there are currently no profiles because I can't get past the user creation step to create one). This is the exception:
ActiveRecord::RecordNotFound in ProfilesController#index
Couldn't find User without an ID

This is what the log is telling me:
Processing ProfilesController#index [GET]
Parameters: {"action"=>"index", "controller"=>"profiles"}

ActiveRecord::RecordNotFound (Couldn't find User without an ID):
app/controllers/profiles_controller.rb:5:in `index'

To give you the rest of the details on the app, the models have the following associations:
Profile belongs _to :user
User has _one :profile

I have this in the routes.rb file: map.resources :users, :has_one => :profile

In the view for the new profile page that's throwing the first exception listed above, I have this:

<% form_for([@user, @profile]) do |f| %>
  <%= f.error_messages %>
....
<% end %>

In the view for the profile index that's throwing the second exception explained above, I have this:

<% @profiles.each do |profile| %>
<div class="post">
 <div class="left">
  <p>Store: </p>
  <p>Category: </p>
 </div>
 <div class="right">
  <p><%=h profile.name %></p>
  <p><%=h profile.category %></p>
 </div>
 <div class="bottom">
  <p><%= link_to 'Go to profile', user_profile_path(@user, profile) %></p>
  <p><%= link_to 'Edit', edit_user_profile_path(@user, profile) %></p>
  <p><%= link_to 'Destroy', user_profile_path(@user, profile), :confirm => 'Are you sure?', :method => :delete %></p>
 </div>

<% end %>

I've spent hours trying to track down the problem myself as a learning exercise, but at this point I have no idea how to fix this. Appreciate the help!

UPDATE:

jdl, per your request:
profiles/new.html.erb:

<% form_for([@user, @profile]) do |f| %>
  <%= f.error_messages %>

<%= f.label :name %>
<%= f.text_field :name %>required

<%= f.label :category %>
<%= f.text_field :category %>required

<%= f.label :address1 %>
<%= f.text_field :address1 %>

<%= f.label :address2 %>
<%= f.text_field :address2 %>

<%= f.label :city %>
<%= f.text_field :city %>

<%= f.label :state %>
<%= f.text_field :state %>

<%= f.label :zip %>
<%= f.text_field :zip %>required

<%= f.label :phone %>
<%= f.text_field :phone %>

<%= f.label :email %>
<%= f.text_field :email %>

<%= f.label :website %>
<%= f.text_field :website %>

<%= f.label :description %>
<%= f.text_area :description %>

<%= f.submit 'Create' %>

<% end %>

routes.rb:
ActionController::Routing::Routes.draw do |map|
map.signup 'signup', :controller => 'users', :action => 'new'
map.logout 'logout', :controller => 'sessions', :action => 'destroy'
map.login 'login', :controller => 'sessions', :action => 'new'
map.resources :sessions

  map.resources :users, :has_one => :profile  

  map.root :controller => "home"   
  map.connect ':controller/:action/:id'  
  map.connect ':controller/:action/:id.:format'  
end

PROFILES CONTROLLER (as of 8/20/09, 8pm EST)
class ProfilesController < ApplicationController

  def index
    @users = User.all(:order => "created_at DESC")
  end

  def show
    @user = User.find(params[:user_id])
  end

  def new
    @user.profile = Profile.new
  end

  def edit
    @user = User.find(params[:user_id])
    @profile = @user.profile.find(params[:id])
  end

  def create
    @user = User.find(params[:user_id])
    @profile = @user.profile.build(params[:profile])
      if @profile.save
        flash[:notice] = 'Profile was successfully created.'
        redirect_to(@profile)
      else
        flash[:notice] = 'Error.  Something went wrong.'
        render :action => "new"
      end
  end

  def update
    @profile = Profile.find(params[:id])
      if @profile.update_attributes(params[:profile])
        flash[:notice] = 'Profile was successfully updated.'
        redirect_to(@profile)
      else
        render :action => "edit"
      end
  end

  def destroy
    @profile = Profile.find(params[:id])
    @profile.destroy
      redirect_to(profiles_url)
  end
end

Cody, the index page below is throwing the following exception:
NoMethodError in Profiles#index
Showing app/views/profiles/index.html.erb where line #14 raised:
undefined method `name' for #

<div id="posts">

<% @users.each do |profile| %>

Store:

Category:

<%=h profile.name %>

<%=h profile.category %>

<%= link_to 'Go to profile', user_profile_path(@user, profile) %>

<%= link_to 'Edit', edit_user_profile_path(@user, profile) %>

<%= link_to 'Destroy', user_profile_path(@user, profile), :confirm => 'Are you sure?', :method => :delete %>

+3  A: 

The error is telling you that in this line:

@profile = @user.profile.build

@user.profile is nil.

Since you haven't created the profile yet, this makes sense. Instead, go for something like this.

@profile = Profile.new(:user_id => @user.id)


Re: The index page throwing an exception.

You are defining @users in your controller, and then referencing @user in your path helpers. Also, iterating over @users should give you User objects, not Profile objects.

It looks like you're trying to use the Profile#index action to show a list of users. That's kind-of OK, in a not-quite-pure-REST way. However, I would expect to see something more like this.

<% @users.each do |user| -%>
  <% unless user.profile.blank? -%>
    <%= h user.profile.name %>
    <%= h user.profile.category %>
    <%= link_to 'Go to profile', user_profile_path(user, user.profile) %>
  <% end -%>
<% end -%>
jdl
Well whoever downvoted this should explain why ... I thing its a reasonable guess
Sam Saffron
jdl, when I make your change, I get the following exception:ActionView::TemplateError (undefined method `user_profiles_path' for #<ActionView::Base:0x2651eac>) on line #3 of app/views/profiles/new.html.erb: <br />This is what's causing the problem, but I don't know how to fix it: <% form_for([@user, @profile]) do |f| %><br />I tried removing the @user, but that didn't work. Suggestions?
MikeH
Can you post new.html.erb and your routes.rb file? This should be pretty easy to track down, but it's just guessing unless we can see the code in question.
jdl
A: 

@jdl was correct in that there is no @user.profile object in ProfilesController#new so calling @user.profile.build would throw a nil exception.

You can fix this by instantiating a new Profile object by

@user.profile = Profile.new

Later if @user gets saved then it will trigger a @user.profile and the foreign keys will be set appropriately.

Also in your ProfilesController, the #index and #show actions are kind of weird. Conventionally, the #index action returns a "list of objects" and #show is to display just one object, a specific one given an ID. However, your #index returns a very specific object because it does a User.find with a specific ID. Furthermore, since a user only has one Profile object it doesnt make sense to also load up its Profile object with an ORDER BY. There should only be one so no order is necessary. On top of that, its debatable whether you need to explicitly load the profile object as you can just access it via @user.profile and ActiveRecord will load it on demand. So your new #index looks something like

def index
  @users = User.paginate(:all, :order = "created_at desc", :page => 1, :per_page => 10)
end

This assumes you have the WillPaginate plugin, but the gist is that you are loading a LIST of objects, not just one. In your view, if you are iterating over @users and call .profile on an element of that list then ActiveRecord will load the associated Profile on the fly.

Pretty same thing for #show - no need to explicitly load the profile.

def show
  @user = User.find(params[:user_id])
end
Cody Caughlan
Cody, in your explanation, where exactly does this line go? @user.profile = Profile.new
MikeH
+1  A: 

hmm.. I thought when User has many profiles, you can then use:

@user.profiles.build
@user.profiles.create

when User has one profile, you have to use:

@user.build_profile
@user.create_profile

Can't be sure, check API

penger
Yes, as far as I understand, this is correct.
MikeH