Частичный рендеринг для модели с вложенными атрибутами в другой модели

У меня есть приложение для рельсов, которое моделирует дом. house содержит rooms, а в комнатах есть вложенные атрибуты для light и small_appliance. У меня есть calculator контроллер, с помощью которого конечные пользователи будут получать доступ к приложению.

Моя проблема в том, что я не могу получить партиал для добавления rooms для правильного рендеринга и отправки из calculator. Начальная страница позволяет пользователю вводить house информацию, которая сохраняется с использованием save_house при нажатии кнопки «Отправить». Это также перенаправляет пользователя на страницу add_rooms, где он может добавить комнаты в дом.

add_rooms отображается правильно, но когда я нажимаю "Отправить", я получаю эту ошибку:

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'

Для меня это странно, потому что при первом рендеринге add_rooms отображается house_id. Я не понимаю, почему он не передается после отправки формы.

Вот код:

приложение / модели / 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

приложение / модели / house.rb

class House < ActiveRecord::Base
  has_many :rooms

  # validation code not included

  def add_room(room)
    rooms << room
  end

end

приложение / контроллеры / 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

приложение / просмотров / калькулятор / 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

Спасибо,
Райан


person Ryan    schedule 15.10.2009    source источник


Ответы (1)


Из любопытства, почему бы не сделать так, чтобы дом принимал вложенные атрибуты для комнат. Это упростит код вашего контроллера, так как добавить много комнат, источников света и небольших приборов так же просто, как просто сделать @ house.update_attributes (params [: house]). Однако это не тот ответ, который поможет, поскольку, если вы внесете изменение, у вас все равно будут проблемы.

Ваша первая ошибка возникает из первой строки файла app / views / calculator / _room_form.html.erb.

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

Вы не указываете form_for для объекта, поэтому метод new_child_fields, вызываемый add_child _link, пытается вызвать Reflection_on_association в классе Nil.

Решение - изменить строку на

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

Это позволяет упростить контроллер, поскольку ему уже передается комната, связанная с домом.

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

Я считаю, что ваша вторая ошибка - та же проблема. Однако, поскольку вы вызываете метод доступа has_many вместо генерации nil, вы передаете пустой массив, что объясняет разницу в сообщениях об ошибках. Опять же, решение состоит в том, чтобы построить легкое и маленькое устройство перед рендерингом, если оно еще не существует.

  <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 %>

Ваша новая ошибка возникает из-за этого:

 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 предполагает, что партиал _light находится в папке app / views / calculators

Решение состоит в том, чтобы либо переместить части light и small_appliances в эту папку, либо изменить вспомогательные методы, чтобы они принимали частичный вариант.

person EmFi    schedule 15.10.2009
comment
Благодарю за ваш ответ. Я думал о том, чтобы сделать room вложенным атрибутом для house в какой-то момент, но не сделал этого, потому что хотел, чтобы они были созданы на отдельных страницах. Я внес эти изменения, но теперь получаю новую ошибку, которую добавил в исходное сообщение. - person Ryan; 15.10.2009
comment
Решил вашу новую ошибку. Сообщения об ошибках, которые вы получаете, - лучшее указание на то, что идет не так. Если это не синтаксическая ошибка, в 90% случаев это Rails делает неверное предположение. Используйте сообщение об ошибке, чтобы выяснить, что не соответствует тому, как вы ожидаете, что все будет работать. - person EmFi; 15.10.2009
comment
Спасибо. Я переместил частичные файлы, и теперь все отображается правильно. Однако, как только я нажимаю кнопку отправки для страницы add_rooms, появляется новая ошибка. 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 в строке <p>House id is <%= @house.id %></p>. Я не понимаю, почему он передает nil как house id, потому что он показывает house id при первой загрузке add_rooms. - person Ryan; 16.10.2009
comment
Я обновил исходное сообщение, указав текущую проблему и текущий код. - person Ryan; 17.10.2009
comment
Получил это исправлено. Спасибо за помощь. - person Ryan; 19.10.2009