The title might not be very explanatory (I tried, sorry).
The scenario
I have a many-to-many relation, Computer
and Software
and thus a join model ComputerSoftware
. I would like to have the ability to connect a computer with many softwares from the list of softwares currently available while I am creating a new computer. Here's some code to explain:
class Computer < ApplicationRecord
has_many :computer_softwares
has_many :computers, through: :computer_softwares
accepts_nested_attributes_for :softwares
end
class Software < ApplicationRecord
has_many :computer_softwares
has_many :softwares, through: :computer_softwares
end
class ComputerSoftware < ApplicationRecord
belongs_to :software
belongs_to :computer
end
My controller looks like this:
class ComputersController < ApplicationController
before_action :set_computer, only: %i[show edit update destroy]
before_action :set_softwares, only: :new
def index
@computers = Computer.all
end
def show; end
def new; end
def edit; end
def create
@computer = Computer.new(computer_params)
# do something to save the associated softwares...
# params.require(:computer).permit(:softwares) <= unpermitted param
if @computer.save
redirect_to computers_path, notice: 'Successfully created.'
else
render :new
end
end
def update
if @computer.update(computer_params)
redirect_to @computer, notice: 'Successfully updated.'
else
render :edit
end
end
def destroy
@computer.destroy
redirect_to computers_url, notice: 'Successfully destroyed.'
end
private
def set_computer
@computer = Computer.find(params[:id])
end
def computer_params
params.require(:computer).permit(:name)
end
def set_softwares
@softwares = Software.all
end
end
The association works fine, I tested in rails console
, all good. But now I come to creating a form, say for Computer
:
app/views/computers/_form.html.erb
<%= form_with(model: computer, local: true) do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<%= form.collection_check_boxes :softwares, @softwares, :id, :name, include_hidden: false do |field| %>
<%= field.check_box %>
<%= field.text %>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
I have tried a few variations of this, but invariably I get some error in the view because of softwares
, or the param is not permitted in the controller, even with accepts_nested_attributes_for :softwares
is set in the Computer
model.
In desperation, I opted to try to have the checkboxes not part of the computer
at all, and create the join through association in the controller alone. This worked for new
, but then of course broke down for edit
. Any help greatly appreciated.
Edit 1 (at the request of jvillian)
The strong params I tried look like this:
def computer_params
required_params.permit(computer_softwares_attributes: [:software_id])
end
def required_params
params.require(:computer)
end
but this results in the unpermitted params
message in the server logs (and computer_params[:computer_softwares]
is nil
as a result). In the server logs it looks like the following:
Unpermitted parameters: :name, :computer_softwares
<ActionController::Parameters {} permitted: true>
Edit 2
Not sure why it didn't occur to me earlier - but remove the strong requirement and just doing params[:computer][:softwares]
has actually worked fine. The only question that remains really is how I can achieve the same result using strong parameters, as otherwise it would be vulnerable.
You need to permit those nested attributes inside computer_params
:
def computer_params
params.require(:computer).permit(:name, software: [:something, :something_else])
end
Whatever fields you want to permit for the nested software model need to go inside that software
key