Search code examples
ruby-on-railsrubyruby-on-rails-5

In Rails 5 create many_to_many association along with saving either side of the relation


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.


Solution

  • 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