Rails通过带有额外属性的表单进行has_many through关联

7
我正在尝试创建一个表单,允许用户添加/编辑/删除与活动相关的位置。我目前找到的所有示例都是针对HABTM表单(不允许编辑存在于has_many through配置中的其他属性)或仅列出现有的关系。
下面是一个显示我想要实现的内容的图片。
列表将显示每个可用的位置。通过campaign_locations模型具有关系的位置将被选中,并且其campaign_location特定属性可编辑。未选中的位置应该可以被选中,输入campaign_location特定数据,并在提交时创建一个新的关系。
以下是我目前实现的代码。我尝试使用collection_check_boxes,这非常接近我所需要的,但它不允许我编辑campaign_location属性。
我已成功地编辑/删除现有的campaign_locations,但我无法弄清楚如何将其合并以显示所有可用的位置(如附图)。

模型

campaign.rb

class Campaign < ActiveRecord::Base
  has_many :campaign_locations
  has_many :campaign_products
  has_many :products,  through: :campaign_products
  has_many :locations, through: :campaign_locations

  accepts_nested_attributes_for :campaign_locations, allow_destroy: true
end

campaign_location.rb

class CampaignLocation < ActiveRecord::Base
  belongs_to :campaign
  belongs_to :location
end

location.rb

class Location < ActiveRecord::Base
  has_many :campaign_locations
  has_many :campaigns, through: :campaign_locations
end

View

campaign/_form.html.haml

= form_for @campaign do |campaign_form|

  # this properly shows existing campaign_locations, and properly allows me
  # to edit the campaign_location attributes as well as destroy the relationship
  = campaign_form.fields_for :campaign_locations do |cl_f|
    = cl_f.check_box :_destroy, {:checked => cl_f.object.persisted?}, false, true
    = cl_f.label cl_f.object.location.title
    = cl_f.datetime_field :pickup_time_start
    = cl_f.datetime_field :pickup_time_end
    = cl_f.text_field :pickup_timezone

  # this properly lists all available locations as well as checks the ones
  # which have a current relationship to the campaign via campaign_locations
  = campaign_form.collection_check_boxes :location_ids, Location.all, :id, :title

表单HTML的部分内容

 <input name="campaign[campaign_locations_attributes][0][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_0__destroy" name="campaign[campaign_locations_attributes][0][_destroy]" type="checkbox" value="false" />
 <label for="campaign_campaign_locations_attributes_0_LOCATION 1">Location 1</label>
 <label for="campaign_campaign_locations_attributes_0_pickup_time_start">Pickup time start</label>
 <input id="campaign_campaign_locations_attributes_0_pickup_time_start" name="campaign[campaign_locations_attributes][0][pickup_time_start]" type="datetime" />
 <label for="campaign_campaign_locations_attributes_0_pickup_time_end">Pickup time end</label>
 <input id="campaign_campaign_locations_attributes_0_pickup_time_end" name="campaign[campaign_locations_attributes][0][pickup_time_end]" type="datetime" />
 <input id="campaign_campaign_locations_attributes_0_location_id" name="campaign[campaign_locations_attributes][0][location_id]" type="hidden" value="1" />
 <input id="campaign_campaign_locations_attributes_0_pickup_timezone" name="campaign[campaign_locations_attributes][0][pickup_timezone]" type="hidden" value="EST" />

 <input name="campaign[campaign_locations_attributes][1][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_1__destroy" name="campaign[campaign_locations_attributes][1][_destroy]" type="checkbox" value="false" />
 <label for="campaign_campaign_locations_attributes_1_LOCATION 2">Location 2</label>
 <label for="campaign_campaign_locations_attributes_1_pickup_time_start">Pickup time start</label>
 <input id="campaign_campaign_locations_attributes_1_pickup_time_start" name="campaign[campaign_locations_attributes][1][pickup_time_start]" type="datetime" />
 <label for="campaign_campaign_locations_attributes_1_pickup_time_end">Pickup time end</label>
 <input id="campaign_campaign_locations_attributes_1_pickup_time_end" name="campaign[campaign_locations_attributes][1][pickup_time_end]" type="datetime" />
 <input id="campaign_campaign_locations_attributes_1_location_id" name="campaign[campaign_locations_attributes][1][location_id]" type="hidden" value="2" />
 <input id="campaign_campaign_locations_attributes_1_pickup_timezone" name="campaign[campaign_locations_attributes][1][pickup_timezone]" type="hidden" value="EST" />

 <input name="campaign[campaign_locations_attributes][2][_destroy]" type="hidden" value="true" /><input id="campaign_campaign_locations_attributes_2__destroy" name="campaign[campaign_locations_attributes][2][_destroy]" type="checkbox" value="false" />
 <label for="campaign_campaign_locations_attributes_2_LOCATION 3">Location 3</label>
 <label for="campaign_campaign_locations_attributes_2_pickup_time_start">Pickup time start</label>
 <input id="campaign_campaign_locations_attributes_2_pickup_time_start" name="campaign[campaign_locations_attributes][2][pickup_time_start]" type="datetime" />
 <label for="campaign_campaign_locations_attributes_2_pickup_time_end">Pickup time end</label>
 <input id="campaign_campaign_locations_attributes_2_pickup_time_end" name="campaign[campaign_locations_attributes][2][pickup_time_end]" type="datetime" />
 <input id="campaign_campaign_locations_attributes_2_location_id" name="campaign[campaign_locations_attributes][2][location_id]" type="hidden" value="3" />
 <input id="campaign_campaign_locations_attributes_2_pickup_timezone" name="campaign[campaign_locations_attributes][2][pickup_timezone]" type="hidden" value="EST" />

我认为cocoon会为您节省很多时间。 - mtkcs
那么您的意思是,当您前往网页并单击新位置并向文本框中添加内容,然后点击保存时,表单上所有内容看起来都正确,但却没有任何反应?您的控制器长什么样子?同时,查看它生成的实际HTML有助于解决问题。我曾经制作过一些复杂的表单,根据下拉选择项使用AJAX更新复选框。当您想要包含即时生成的字段时,在您的 params{} 哈希中可能会变得棘手。 - Beartech
2个回答

2
您遇到的问题是空白位置没有被实例化,因此您的视图没有任何可构建表单元素。要解决此问题,您需要在控制器的newedit操作中构建空白位置。
class CampaignController < ApplicationController
  def new
    empty_locations = Location.where.not(id: @campaign.locations.pluck(:id))
    empty_locations.each { |l| @campaign.campaign_locations.build(location: l) }
  end

  def edit
    # do same thing as new
  end
end

接下来,在您的editupdate操作中,当用户提交表单时,您需要从params哈希中删除任何空位置。

class CampaignController < ApplicationController
  def create
    params[:campaign][:campaign_locations].reject! do |cl|
     cl[:pickup_time_start].blank? && cl[:pickup_time_end].blank? && cl[:pickup_timezone].blank?
    end
  end

  def update
    # do same thing as create
  end
end

此外,我认为你需要为location_id添加一个隐藏字段。

这似乎是正确的路径!我已经几乎把事情搞定了,但是遇到了一些问题。其中一个问题是,在提交时,CampaignLocation 尝试创建没有 campaign_id 的对象。可能这与使用嵌套属性有关?第二个问题是你提供的 create 和 update 方法的代码片段;这似乎不能正确地处理嵌套属性。我已更新原帖,包括我正在使用的 HTML 表单字段,希望能够解决这个问题。谢谢! - jamgood96

1
你应该在模型和表单中添加一个非模型属性复选框,表示是保存还是移除关系。向表单添加一个带有关系 ID 的隐藏字段,最后覆盖 accepts_nested_attributes_for 方法,根据复选框决定保存或销毁并调用 super。
class CampaignLocation < ActiveRecord::Base
  belongs_to :campaign
  belongs_to :location

  # Returns true if a saved record, used by form
  def option_included
    new_record? ? false : true
  end
end


class Campaign < ActiveRecord::Base
  ...

  accepts_nested_attributes_for :campaign_locations, allow_destroy: true

  def campaign_locations_attributes=(attributes)
    attributes.values.each do |attribute|
      attribute[:_destroy] = true if attribute[:option_included] != '1'
      attribute.delete(:option_included)
    end
    super
  end
end

表单:

= form_for @campaign do |campaign_form|
  - locations.each do |location|
    = campaign_form.fields_for, :campaign_locations, @campaign.campaign_locations.find_or_initialize_by(location_id: location.id) do |cf|
      = cf.check_box :option_included
      = location.name
      = cf.hidden_field :campaign_id
      = cf.datetime_field :pickup_time_start
      = cf.datetime_field :pickup_time_end
      = cf.text_field :pickup_timezone

option_included 如果存在保存的关系则返回 true,否则返回 false。


1
这个解决方案很好用。你需要将:disease_question_option_id更改为:campaign_id,并在locations_controller中添加到强参数中campaign_locations_attributes: [:id, :campaign_id, :_destroy, :option_included, :other_stuff_you_need] - Mauro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接