Rails 7.0.4- form_for - collection_select在编辑操作中具有多个选项

Rails 7.0.4- form_for - collection_select with multiple options in edit action

提问人:Michal 提问时间:2/1/2023 最后编辑:Michal 更新时间:2/2/2023 访问量:346

问:

我有三张表:

  • 员工
  • Staff_locations
  • 地点

商业案例:员工可以在多个地点工作。员工和位置之间的关联是通过staff_locations表完成的。在创建员工条目时,我正在选择他/她所属的位置。这工作正常。

但是我在编辑操作中正确显示collection_select有问题。它显示的条目数与staff_locations表中staff_id条目数相同。

我不知道如何解决这个问题,到目前为止,我在任何地方都没有找到任何好的提示。

模型

class Staff < ApplicationRecord
has_many :visits, dependent: :destroy
has_many :work_schedules
has_many :customers, through: :visits

    has_many :staff_locations, dependent: :destroy
    has_many :locations, through: :staff_locations
    
    accepts_nested_attributes_for :staff_locations, allow_destroy: true

def staff_locations_attributes=(staff_locations_attributes)

        staff_locations_attributes.values[0][:location_id].each do |loc_id| 
            if !loc_id.blank?
                staff_location_attribute_hash = {}; 
                staff_location_attribute_hash['location_id'] = loc_id;              
                            
                staff_location = StaffLocation.create(staff_location_attribute_hash)
                self.staff_locations << staff_location
            end
            
        end
    end

end

class StaffLocation < ApplicationRecord
belongs_to :staff
belongs_to :location

validates :staff_id, :location_id, uniqueness: true
end

class Location < ApplicationRecord
has_many :staff_locations
has_many :staffs, through: :staff_locations
end

staffs_controller

class StaffsController < ApplicationController
before_action :set_staff, only: %i [ show edit update destroy ]

def index
@staffs = Staff.all
end

def show
end

def new
@staff = Staff.new
@staff.staff_locations.build
end

def create
@staff = Staff.new(staff_params)

    if @staff.save
      redirect_to @staff
    else
      render :new, status: :unprocessable_entity
    end

end

def edit
end

def update
respond_to do |format|
if @staff.update(staff_params)
format.html { redirect_to @staff, notice: 'Staff was successfully updated.' }
format.json { render :show, status: :ok, staff: @staff }
else
format.html { render :edit }
format.json { render json: @staff.errors, status: :unprocessable_entity }
end
end
end

def destroy
end

private
    def staff_params
      params.require(:staff).permit(:first_name, :last_name, :status, :staff_type, staff_locations_attributes: [:location_id => [] ])
      #due to multiple select in the new staff form, staff_locations_attributes needs to contain Array of location_ids.
      #Moreover check Staff model's method: staff_locations_attributes. It converts staff_locations_attributes into hashes.
    end

    def set_staff
      @staff = Staff.find(params[:id])
    end

end

形式部分

<%= form_for(@staff) do |form| %>

    <div>
        <% if params["action"] != "edit" %>
            
            <%= form.fields_for :staff_locations do |staff_location_form| %>
                <%= staff_location_form.label :location_id, 'Associated Locations' %><br>
                <%= staff_location_form.collection_select :location_id, Location.all, :id, :loc_name, {include_blank: false}, {:multiple => true } %>
            <% end %>
    
        <% else %>
    
            <%= form.fields_for :staff_locations do |staff_location_form| %>
                <%= staff_location_form.label :location_id, 'Associated Locations' %><br>
                <%= staff_location_form.collection_select :location_id, Location.all, :id, :loc_name, {selected: @staff.locations.map(&:id).compact, include_blank: false}, {:multiple => true} %>
                <% #debugger %>
            <% end %>
    
        <% end %>
    </div>
    
    <div>
        <%= form.submit %>
    </div>

<% end %>

更新

在@Beartech建议的更改后,更新方法工作正常。但是,新操作停止工作。下面我粘贴了我在提交表单时捕获的内容,以在 Staff 表中创建一个条目,并在Staff_locations表中创建两个相关条目。

在将 objetct 保存到数据库之前,我在控制台中检查了:

  • @staff
  • @staff.location_ids
  • staff_params

在那之后,我确实保存了。我不明白为什么它最终处于 FALSE 状态。

   14|     #@staff.staff_locations.build
    15|   end
    16| 
    17|   def create
    18|     @staff = Staff.new(staff_params)
=>  19|     debugger
    20| 
    21|     respond_to do |format|
    22|       if @staff.save
    23|         format.html { redirect_to @staff, notice: 'Staff was successfully created.' }
=>#0    StaffsController#create at ~/rails_projects/dentysta/app/controllers/staffs_controller.rb:19
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/rails_projects/dentysta/vendor/bundle/ruby/3.0.0/gems/actionpack-7.0.4/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 75 frames (use `bt' command for all frames)

(ruby) @staff
#<Staff:0x00007f2400acb2e8 id: nil, first_name: "s", last_name: "dd", status: "Active", staff_type: "Doctor", created_at: nil, updated_at: nil>

(ruby) @staff.location_ids
[4, 5]

(ruby) staff_params
#<ActionController::Parameters {"first_name"=>"s", "last_name"=>"dd", "status"=>"Active", "staff_type"=>"Doctor", "location_ids"=>["", "4", "5"]} permitted: true>

(ruby) @staff.save
  TRANSACTION (0.1ms)  begin transaction
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
  StaffLocation Exists? (0.1ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."staff_id" IS NULL LIMIT ?  [["LIMIT", 1]]
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
  StaffLocation Exists? (0.1ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."location_id" = ? LIMIT ?  [["location_id", 4], ["LIMIT", 1]]
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
  CACHE StaffLocation Exists? (0.0ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."staff_id" IS NULL LIMIT ?  [["LIMIT", 1]]
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
  StaffLocation Exists? (0.3ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."location_id" = ? LIMIT ?  [["location_id", 5], ["LIMIT", 1]]
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
  TRANSACTION (0.1ms)  rollback transaction
  ↳ (rdbg)//home/mw/rails_projects/dentysta/app/controllers/staffs_controller.rb:1:in `create'
false

(rdbg) c    # continue command

  TRANSACTION (0.1ms)  begin transaction
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  StaffLocation Exists? (0.2ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."staff_id" IS NULL LIMIT ?  [["LIMIT", 1]]
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  StaffLocation Exists? (0.1ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."location_id" = ? LIMIT ?  [["location_id", 4], ["LIMIT", 1]]
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  CACHE StaffLocation Exists? (0.0ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."staff_id" IS NULL LIMIT ?  [["LIMIT", 1]]
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  StaffLocation Exists? (0.2ms)  SELECT 1 AS one FROM "staff_locations" WHERE "staff_locations"."location_id" = ? LIMIT ?  [["location_id", 5], ["LIMIT", 1]]
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  TRANSACTION (0.1ms)  rollback transaction
  ↳ app/controllers/staffs_controller.rb:22:in `block in create'
  Rendering layout layouts/application.html.erb
  Rendering staffs/new.html.erb within layouts/application
  Location Count (0.1ms)  SELECT COUNT(*) FROM "locations"
  ↳ app/views/staffs/_form.html.erb:36
  Location Load (0.1ms)  SELECT "locations".* FROM "locations"
  ↳ app/views/staffs/_form.html.erb:36
  Rendered staffs/_form.html.erb (Duration: 18.5ms | Allocations: 2989)
  Rendered staffs/new.html.erb within layouts/application (Duration: 21.7ms | Allocations: 3059)
  Rendered layout layouts/application.html.erb (Duration: 24.6ms | Allocations: 4054)
Completed 422 Unprocessable Entity in 2302301ms (Views: 30.1ms | ActiveRecord: 1.8ms | Allocations: 174939)
Ruby-on-Rails 嵌套表单集合 选择

评论

0赞 Beartech 2/1/2023
你是否因为遇到问题而使两个不同?我认为 rails 会优雅地处理 a 和 action 之间的区别。我不记得必须为集合选择做这种事情。.fields_forneweditif...else...
0赞 Beartech 2/1/2023
我认为在这种情况下你不需要。rails 控制器不关心表单,只要它在 因此,解决方案是创建一个构建正确参数格式的表单元素。fields_forstaff_locations_attributes: [...
0赞 Beartech 2/2/2023
如果将第 22 行更改为(注意感叹号),它将引发一个信息性错误,说明为什么会发生回滚。@staff.save!
0赞 Beartech 2/2/2023
此外,在 Rails 控制台中,您可以执行一个操作来查看正在发生的事情。@staff = Staff.new(...., location_ids: ['4', '5'])@staff.save!
0赞 Michal 2/2/2023
它现在可以:D我所做的唯一更改是使@staff.save!在第 22 行。

答:

0赞 Beartech 2/1/2023 #1

编辑重要提示:使用多选可能会出现意外的用户界面问题。当您使用下面的代码时,将加载现有记录的多选,并将现有关联的位置突出显示为选择。如果您不触摸该表单元素,然后保存表单,它们将保持关联状态。但整个多选列表可能不会立即显示。如果该人无法看到所有选定的元素,他们可以单击一个元素,这将取消选择所有其他元素,从而在记录保存时删除这些关联。我已经编辑了答案以添加到 HTML 属性中。这将显示所有选项,以便他们可以看到哪些选项被选中,以及当他们单击一个选项时会发生什么(取消选择所有其他需要 shfit/option 选择以重新选择它们)。我会考虑这是否是您正在做的事情的正确界面元素。您可能需要考虑将其视为正确的 UI 元素,因为他们必须故意取消选择任何他们想要删除的内容,并且不必在每次添加或删除一个位置时重新选择它们。size: collection_check_boxes

我花了一段时间才想起如何做到这一点。这是因为您专注于联接表。通常,当您需要多个表单字段时,您会这样做。但你实际上是在寻求利用这种关系。has_many

请记住,您为您提供了一种方法,该方法可让您仅通过传递 ID 来设置这些位置。Rails 将负责使用连接模型进行关联。accepts_nested_attributes_forlocation_ids=

在控制台中,尝试:

@staff = Staff.first
# returns a staff object
@staff.locations
#returns an array of location objects due to the has_many
@staff.location_ids
# [12, 32]
@staff.location_ids = [12, 44, 35]
#this will update the joined locations to those locations by id. If any current locations are not in that array, they get deleted from the join table.

将强参数从以下位置更改为:

  params.require(:staff).permit(:first_name, :last_name, :status,
  :staff_type, staff_locations_attributes: [:location_id => [] ])

自:

  params.require(:staff).permit(:first_name, :last_name, :status,
 :staff_type, :location_ids => [] )

在您的表单中,您只需要一个表单元素,使用 以下方法构建:@staff

<%= f.label :locations %><br />
<%= f.collection_select :location_ids, Location.all, :id, :name,{selected: @staff.location_ids, 
include_blank: false}, {:multiple => true, size: Location.all.count } %>

因此,这有效,因为 是 上的有效方法,返回所有位置的集合,则两个符号(:id 和 :name)都是单个位置对象的有效方法。然后,您只是使用相同的方法来抓取已经存在的那些,以将它们标记为已选择。.location_ids@staffLocation.allselected....location_ids

我忘记了该怎么做,已经有一段时间了。一旦我想起,它就很容易。

评论

0赞 Beartech 2/1/2023
不是真的相关,但总是可以让你以后用真正的嵌套形式等绊倒你的东西是 StrongParams “允许哈希,传递数组,允许数组,传递哈希”patshaughnessy.net/2014/6/16/......
0赞 Michal 2/1/2023
感谢您的详细解释和建议。现在“更新”似乎工作正常(即使表中存在多个条目collection_select也只使用一次表单但是“创建”操作停止工作staff_id staff_locations但“创建”操作停止了。我正在玩弄它,并试图弄清楚它背后的原因是什么。
0赞 Beartech 2/1/2023
也许是因为你还在行动?在保存新记录时检查您的 Web 服务器日志,并使用输出和任何其他可能与它未创建的原因相关的信息更新您的问题,我会看看。build.staffing_locations...new
0赞 Michal 2/2/2023 #2

对于那些将来会遇到类似情况的人,我正在粘贴现在对我有用的东西。@Beartech再次感谢您的帮助。它为我节省了很多时间。

模型

class Staff < ApplicationRecord
    has_many :visits, dependent: :destroy
    has_many :work_schedules
    has_many :customers, through: :visits

    has_many :staff_locations, dependent: :destroy
    has_many :locations, through: :staff_locations

    accepts_nested_attributes_for :staff_locations, allow_destroy: true
end

class StaffLocation < ApplicationRecord
  belongs_to :staff
  belongs_to :location
end

class Location < ApplicationRecord
    has_many :staff_locations
    has_many :staffs, through: :staff_locations
end

staffs_controller

class StaffsController < ApplicationController
  before_action :set_staff, only: %i[ show edit update destroy ]

  def index
    @staffs = Staff.all
  end

  def show
    #debugger
  end

  def new
    @staff = Staff.new
  end

  def create
    @staff = Staff.new(staff_params)
    debugger

    respond_to do |format|
      if @staff.save!
        format.html { redirect_to @staff, notice: 'Staff was successfully created.' }
        format.json { render :show, status: :ok, staff: @staff }
        #redirect_to @staff
      else
        format.html { render :new, status: :unprocessable_entity, notice: 'Somthing went wrong' }
        format.json { render json: @staff.errors, status: :unprocessable_entity }
        #render :new, status: :unprocessable_entity
      end
    end
  end

  def edit
  end

  def update
    respond_to do |format|
      if @staff.update(staff_params)
        format.html { redirect_to @staff, notice: 'Staff was successfully updated.' }
        format.json { render :show, status: :ok, staff: @staff }
      else
        format.html { render :edit }
        format.json { render json: @staff.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
  end

  private
    def staff_params
      params.require(:staff).permit(:first_name, :last_name, :status, :staff_type, :location_ids => [] )    
    end

    def set_staff
      @staff = Staff.find(params[:id])
    end

end

_form部分

<%= form_for(@staff) do |form| %>
    <div>
        <%= form.label :first_name %><br>
        <%= form.text_field :first_name %>
        <% @staff.errors.full_messages_for(:first_name).each do |message| %>
            <div><%= message %></div>
        <% end %>
    </div>

    <div>
        <%= form.label :last_name %><br>
        <%= form.text_field :last_name %>
        <% @staff.errors.full_messages_for(:last_name).each do |message| %>
            <div><%= message %></div>
        <% end %>
    </div>

    <div>
        <%= form.label :staff_type %><br>
        <%= form.collection_select :staff_type, Staff.valid_types, :to_s, :to_s, {include_blank: false}, {:multiple => false} %>
        <% @staff.errors.full_messages_for(:staff_type).each do |message| %>
            <div><%= message %></div>
        <% end %>
    </div>

    <div>
        <%= form.label :status %><br>
        <%= form.collection_select :status, Staff.valid_statuses, :to_s, :to_s, {include_blank: false}, {:multiple => false} %>
        <% @staff.errors.full_messages_for(:status).each do |message| %>
            <div><%= message %></div>
        <% end %>
    </div>

    <div>
        <%= form.label :locations %><br />
        <%= form.collection_select :location_ids, Location.all, :id, :loc_name,{selected: @staff.location_ids, include_blank: false}, {:multiple => true, size: Location.all.count } %>
    </div>

    <div>
        <%= form.submit %>
    </div>

<% end %>