ruby-on-railsrubynested-attributesruby-on-rails-6.1accepts-nested-attributes

nested attributes with has many through


I am getting unpermitted params when I am passing values from UI. The association is many to many between models.

class User < ApplicationRecord
    has_many :user_posts
    has_many :posts, through: :user_posts
end


class Post < ApplicationRecord
    has_many :user_posts
    has_many :users, through: :user_posts

    accepts_nested_attributes_for :user_posts
end

class UserPost < ApplicationRecord
  belongs_to :user
  belongs_to :post

  accepts_nested_attributes_for :user
end

posts/_form.html.erb

<%= form_with(model: post) do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%#= form.label :name %>
    <%#= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.fields_for :user_posts do |f| %>
      <%= f.collection_select :user_id, User.all, :id, :username,  {include_blank: false, include_hidden: false }, {:multiple => true, :class=>""} %> 
    <% end %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  # GET /posts or /posts.json
  def index
    @posts = Post.all
  end

  # GET /posts/1 or /posts/1.json
  def show
  end

  # GET /posts/new
  def new
    @post = Post.new
    @post.user_posts.build 
  end

  # GET /posts/1/edit
  def edit
  end

  # POST /posts or /posts.json
  def create
    @post = Post.new(post_params)
    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: "Post was successfully created." }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /posts/1 or /posts/1.json
  def update
    respond_to do |format|
      if @post.update(post_params)
        format.html { redirect_to @post, notice: "Post was successfully updated." }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/1 or /posts/1.json
  def destroy
    @post.destroy
    respond_to do |format|
      format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def post_params
      params.require(:post).permit(:username, user_posts_attributes: [:user_id])
    end
end

When I run the below line it gives unpermitted params

@post = Post.new(post_params)
Unpermitted parameter: :user_id

Schema

\d users
                                          Table "public.users"
   Column   |              Type              | Collation | Nullable |              Default              
------------+--------------------------------+-----------+----------+-----------------------------------
 id         | bigint                         |           | not null | nextval('users_id_seq'::regclass)
 username   | character varying              |           |          | 
 created_at | timestamp(6) without time zone |           | not null | 
 updated_at | timestamp(6) without time zone |           | not null | 


 \d posts
                                          Table "public.posts"
   Column   |              Type              | Collation | Nullable |              Default              
------------+--------------------------------+-----------+----------+-----------------------------------
 id         | bigint                         |           | not null | nextval('posts_id_seq'::regclass)
 name       | character varying              |           |          | 
 created_at | timestamp(6) without time zone |           | not null | 
 updated_at | timestamp(6) without time zone |           | not null | 


\d user_posts
                                          Table "public.user_posts"
   Column   |              Type              | Collation | Nullable |                Default                 
------------+--------------------------------+-----------+----------+----------------------------------------
 id         | bigint                         |           | not null | nextval('user_posts_id_seq'::regclass)
 user_id    | bigint                         |           | not null | 
 post_id    | bigint                         |           | not null | 
 created_at | timestamp(6) without time zone |           | not null | 
 updated_at | timestamp(6) without time zone |           | not null | 

Am I missing something crucial? Any help is appreciated

rails - 6.1
ruby - 3.0


Solution

  • You do not need or even want to use nested attributes here.

    If you just want to associate existing records you just need a select for the user_ids attribute:

    <%= form_with(model: post) do |form| %>  
      ...
      <div class="field">
        <%= form.label :user_ids, 'Select the users'  %>
        <%= f.collection_select :user_ids, User.all, :id, :username, { include_blank: false, include_hidden: false }, { multiple: true, class: ""} %> 
      </div>
      ...
    <% end %>
    

    These setters and getters are created by has_many :users, through: :user_posts.

    And to whitelist the post[user_ids] parameter as an array:

    def post_params
      params.require(:post).permit(
        :foo, 
        :bar,
        :baz,
        user_ids: []
      )
    end
    

    As you can see you don't need to explitly deal with the join table either as user_ids= will do all the work for you.

    Creating join table entities with nested attributes is only necissary if the join table actually contains additional data that must be input by the user.