views:

608

answers:

1

I have a rails application that models a house. house contains rooms and rooms have nested attributes for light and small_appliance. I have a calculator controller, which is how end users will access the application.

My problem is that I can't get the partial for adding rooms to render and submit correctly from calculator. The initial page lets the user enter house information, which is saved using save_house when submit is clicked. This also redirects the user to the add_rooms page, where they can add rooms to the house.

add_rooms displays correctly, but when I click submit, I get this error:

RuntimeError in Calculator#add_room

Showing app/views/calculator/add_rooms.html.erb where line #2 raised:

Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id

Extracted source (around line #2):

1: <div id="addRooms">
2:   <p>House id is <%= @house.id %></p>
3:   
4:   <h3>Your rooms:</h3>
5:   <% if @house.rooms %>

RAILS_ROOT: C:/Users/ryan/Downloads/react
Application Trace | Framework Trace | Full Trace

C:/Users/ryan/Downloads/react/app/views/calculator/add_rooms.html.erb:2:in `_run_erb_app47views47calculator47add_rooms46html46erb'
C:/Users/ryan/Downloads/react/app/controllers/calculator_controller.rb:36:in `add_room'
C:/Users/ryan/Downloads/react/app/controllers/calculator_controller.rb:33:in `add_room'

This is odd to me, because when add_rooms first renders, it shows the house_id. I don't understand why it isn't passed after the form is submitted.

Here's the code:

app/models/room.rb

class Room < ActiveRecord::Base
  # schema { name:string, house_id:integer }
  belongs_to :house
  has_many :lights, :dependent => :destroy
  has_many :small_appliances, :dependent => :destroy
  validates_presence_of :name
  accepts_nested_attributes_for :lights, :reject_if => lambda { |a| a.values.all?(&:blank?) }, :allow_destroy => true
  accepts_nested_attributes_for :small_appliances, :reject_if => lambda { |a| a.values.all?(&:blank?) }, :allow_destroy => true         
end

app/models/house.rb

class House < ActiveRecord::Base
  has_many :rooms

  # validation code not included

  def add_room(room)
    rooms << room
  end

end

app/controllers/calculator_controller.rb

class CalculatorController < ApplicationController
  def index
  end

  def save_house
    @house = House.new(params[:house])
    respond_to do |format|
      if @house.save
        format.html { render :action => 'add_rooms', :id => @house }
        format.xml { render :xml => @house, :status => :created, :location => @house }
      else
        format.html { render :action => 'index' }
        format.xml  { render :xml => @house.errors, :status => :unprocessable_entity }
      end
    end
  end

  def add_rooms
    @house = House.find(params[:id])
    @rooms = Room.find_by_house_id(@house.id)

  rescue ActiveRecord::RecordNotFound
    logger.error("Attempt to access invalid house #{params[:id]}")
    flash[:notice] = "You must create a house before adding rooms"
    redirect_to :action => 'index'
  end

  def add_room
    @room = Room.new(params[:room])
    @house = @room.house

    respond_to do |format|
      if @room.save
        flash[:notice] = "Room \"#[email protected]}\" was successfully added."
        format.html { render :action => 'add_rooms' }
        format.xml { render :xml => @room, :status => :created, :location => @room }
      else
        format.html { render :action => 'add_rooms' }
        format.xml  { render :xml => @room.errors, :status => :unprocessable_entity }
      end
    end
  rescue ActiveRecord::RecordNotFound
    logger.error("Attempt to access invalid house #{params[:id]}")
    flash[:notice] = "You must create a house before adding a room"
    redirect_to :action => 'index'
  end

  def report
    flash[:notice] = nil
    @house = House.find(params[:id])
    @rooms = Room.find_by_house_id(@house.id)
  rescue ActiveRecord::RecordNotFound
    logger.error("Attempt to access invalid house #{params[:id]}")
    flash[:notice] = "You must create a house before generating a report"
    redirect_to :action => 'index'
  end

end

app/views/calculator/add_rooms.html.erb

<div id="addRooms">
  <p>House id is <%= @house.id %></p>

  <h3>Your rooms:</h3>
  <% if @house.rooms %>
  <ul>
    <% for room in @house.rooms %>
    <li>
      <%= h room.name %> has <%= h room.number_of_bulbs %> 
      <%= h room.wattage_of_bulbs %> watt bulbs, in use for 
      <%= h room.usage_hours %> hours per day.
    </li> 
    <% end %>
  </ul>
  <% else %>
  <p>You have not added any rooms yet</p>
  <% end %>  

  <%= render :partial => 'rooms/room_form' %>

  <br />
</div>

<%= button_to "Continue to report", :action => "report", :id => @house %>

app/views/rooms/_room_form.html.erb

<% form_for :room, @house.rooms.build, :url => { :action => :add_room } do |form| %>
  <%= form.error_messages %>
  <p>
    <%= form.label :name %><br />
    <%= form.text_field :name %>
  </p>

  <h3>Lights</h3>
  <% form.object.lights.build if form.object.lights.empty? %>
  <% form.fields_for :lights do |light_form| %>
    <%= render :partial => "light", :locals => { :form => light_form } %>
  <% end %>
  <p class="addLink"><%= add_child_link "[+] Add new light", form, :lights %></p>

  <h3>Small Appliances</h3>
  <% form.object.small_appliances.build if form.object.small_appliances.empty? %>
  <% form.fields_for :small_appliances do |sm_appl_form| %>
    <%= render :partial => "small_appliance", :locals => { :form => sm_appl_form } %>
  <% end %>
  <p class="addLink"><%= add_child_link "[+] Add new small appliance", form, :small_appliances %></p>

  <p><%= form.submit "Submit" %></p>
<% end %>

application_helper.rb

module ApplicationHelper
  def remove_child_link(name, form)
    form.hidden_field(:_delete) + link_to_function(name, "remove_fields(this)")
  end

  def add_child_link(name, form, method)
    fields = new_child_fields(form, method)
    link_to_function(name, h("insert_fields(this, \"#{method}\", \"#{escape_javascript(fields)}\")"))
  end

  def new_child_fields(form_builder, method, options = {})
    options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new
    options[:partial] ||= method.to_s.singularize
    options[:form_builder_local] ||= :form
    form_builder.fields_for(method, options[:object], :child_index => "new_#{method}") do |form|
      render(:partial => options[:partial], :locals => { options[:form_builder_local] => form })
    end
  end
end

Thanks,
Ryan

+1  A: 

Out of curiosity why not have house accept nested attributes for rooms. This would make your controller code simpler, as adding many rooms, lights and small appliances is as simple as just doing @house.update_attributes(params[:house]). However this is not an answer that helps, as you would still have your current problems if you made the change.

Your first error comes from the first line of app/views/calculator/_room_form.html.erb

<% form_for :room, :url => { :action => :add_room, :id => @house } do |form| %>

You're not giving form_for an object so the new_child_fields method called by add_child _link is trying to call reflect_on_association on the Nil class.

The solution is change the line to

<% form_for :room, @house.rooms.build, :url => { :action => :add_room } do |form| %>

This lets you simplify your controller, because a room associated with a house is already being passed to it.

def add_room
    @room = Room.new(params[:room])
    @house = @room.house
    respond_to do |format|
      if @room.save
        flash[:notice] = "Room \"#[email protected]}\" was successfully added."
        format.html { render :action => 'add_rooms' }
        format.xml { render :xml => @room, :status => :created, :location => @room }
      else
        format.html { render :action => 'add_rooms' }
        format.xml  { render :xml => @room.errors, :status => :unprocessable_entity }
      end
    end
  rescue ActiveRecord::RecordNotFound
    logger.error("Attempt to access invalid house #{params[:id]}")
    flash[:notice] = "You must create a house before adding a room"
    redirect_to :action => 'index'
  end

I believe your second error is the same problem. However because you're calling a has_many accessor instead of getting a nil being generated, you're passing an empty array, which explains the difference in error messages. Again the solution is to build a light and small appliance before rendering if none exist yet.

  <h3>Lights</h3>
  <% form.object.lights.build if form.object.lights.empty? %>
  <% form.fields_for :lights do |light_form| %>
    <%= render :partial => 'rooms/light', :locals => { :form => light_form } %>
  <% end %>
  <p class="addLink"><%= add_child_link "[+] Add new light", form, :lights %></p>

  <h3>Small Appliances</h3>
  <% form.object.small_appliances.build if form.object.small_appliances.empty? %>
  <% form.fields_for :small_appliances do |sm_appl_form| %>
    <%= render :partial => 'rooms/small_appliance', :locals => { :form => sm_appl_form } %>
  <% end %>

Your new error comes from this:

 def new_child_fields(form_builder, method, options = {})
    options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new

    # specifically this line. 
    options[:partial] ||= method.to_s.singularize

    options[:form_builder_local] ||= :form
    form_builder.fields_for(method, options[:object], :child_index => "new_#{method}") do |form|
      render(:partial => options[:partial], :locals => { options[:form_builder_local] => form })
    end
  end

new_child_fields is assuming that the _light partial is in the app/views/calculators folder

The solution is to either move the light and small_appliances partials to this folder or modify your helper methods to accept a partial option.

EmFi
Thank you for your response. I thought about making `room` a nested attribute for `house` at one point, but didn't because I wanted them to be created on separate pages. I made those changes, but now I get a new error, which I've added to the original post.
Ryan
Solved your new error. The error messages you get are your best indication of what's going wrong. If it's not a syntax error, 90% of the time, it's Rails making an incorrect assumption. Use the error message to figure out what doesn't mesh with the way you expect things to work.
EmFi
Thanks. I moved the partials and now everything displays correctly. However, once I click the submit for the `add_rooms` page, it gives a new error. `Showing app/views/calculator/add_rooms.html.erb where line #2 raised:Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id` on line `<p>House id is <%= @house.id %></p>`. I don't understand why it passes nil as the `house` id, because it shows the `house` id when it first loads `add_rooms`.
Ryan
I've updated the original post to the current problem and current code.
Ryan
Got it fixed. Thanks for all the help.
Ryan