Search code examples
ruby-on-railsparametersnested-forms

Rails Nested Form submits but does not persist the data


I'm trying to get my nested form to work. It's a form for a New Album with a space to write a review underneath. The form submits and the Album displays on the page, but the Review does not, it just appears blank. I only am getting one error in the log "Unpermitted parameter: reviews_attributes"

Log:

Started POST "/albums" for ::1 at 2020-04-19 12:10:58 -0400
Processing by AlbumsController#create as HTML
  Parameters: {"authenticity_token"=>"jYHM+yeExcTJtENvjQBDsOMo8Ig1g5bRa+hYZ9kCkiI4NO3KP3xdV7SpSZ2IeIOp0wC+5WLxflu22NTIXtoibg==", "album"=>{"artist"=>"Blink 182", "title"=>"California", "avatar"=>#<ActionDispatch::Http::UploadedFile:0x00007fc372d8d5f0 @tempfile=#<Tempfile:/var/folders/26/p006tryd6yb9sp9rq446p07c0000gn/T/RackMultipart20200419-64975-1bp8ang.jpg>, @original_filename="71GfPCWJHXL._SL1500_.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"album[avatar]\"; filename=\"71GfPCWJHXL._SL1500_.jpg\"\r\nContent-Type: image/jpeg\r\n">, "reviews_attributes"=>{"0"=>{"title"=>"Blink 182 review", "date"=>"2020-04-19", "content"=>"NOT THE BLINK182 I KNOW AND LOVE WHERE IS ToM BRING BACK TOM"}}}, "commit"=>"Create Album"}
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 11], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:10:in `current_user'
Unpermitted parameter: :reviews_attributes
   (0.1ms)  begin transaction
  ↳ app/controllers/albums_controller.rb:29:in `create'
  Album Create (0.4ms)  INSERT INTO "albums" ("artist", "title", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["artist", "Blink 182"], ["title", "California"], ["created_at", "2020-04-19 16:10:58.838672"], ["updated_at", "2020-04-19 16:10:58.838672"]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
  ActiveStorage::Blob Load (0.3ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" INNER JOIN "active_storage_attachments" ON "active_storage_blobs"."id" = "active_storage_attachments"."blob_id" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 6], ["record_type", "Album"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
  ActiveStorage::Attachment Load (0.2ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 6], ["record_type", "Album"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
  ActiveStorage::Blob Create (0.3ms)  INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "metadata", "byte_size", "checksum", "created_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["key", "l22w59ulprgmmqrs025woawhi1h6"], ["filename", "71GfPCWJHXL._SL1500_.jpg"], ["content_type", "image/jpeg"], ["metadata", "{\"identified\":true}"], ["byte_size", 137504], ["checksum", "IXxJAt318tAPkwmaLBUW/A=="], ["created_at", "2020-04-19 16:10:58.852999"]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
  ActiveStorage::Attachment Create (0.4ms)  INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES (?, ?, ?, ?, ?)  [["name", "avatar"], ["record_type", "Album"], ["record_id", 6], ["blob_id", 11], ["created_at", "2020-04-19 16:10:58.856948"]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
  Album Update (0.2ms)  UPDATE "albums" SET "updated_at" = ? WHERE "albums"."id" = ?  [["updated_at", "2020-04-19 16:10:58.862445"], ["id", 6]]
  ↳ app/controllers/albums_controller.rb:29:in `create'
   (3.3ms)  commit transaction
  ↳ app/controllers/albums_controller.rb:29:in `create'
  Disk Storage (2.5ms) Uploaded file to key: l22w59ulprgmmqrs025woawhi1h6 (checksum: IXxJAt318tAPkwmaLBUW/A==)
[ActiveJob] Enqueued ActiveStorage::AnalyzeJob (Job ID: 9114fadd-5897-4939-9cd6-1d0751f129b2) to Async(active_storage_analysis) with arguments: #<GlobalID:0x00007fc372ddf5d0 @uri=#<URI::GID gid://review-project/ActiveStorage::Blob/11>>
Redirected to http://localhost:3000/albums/6
Completed 302 Found in 63ms (ActiveRecord: 5.5ms | Allocations: 15980)

Nested form (albums/_form.html.erb)

<%= form_for(@album) do |f| %> 
<% if @album.errors.any? %>
<ul>
  <% @album.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
  <% end %>
</ul>
<% end %>
      <%= f.label :artist %>
      <%= f.text_field :artist %>
            <br><br>
      <%= f.label :title %>
      <%= f.text_field :title %>
            <br><br>
      <%= f.label "Album Image:" %><br>
      <%= f.file_field :avatar %>
           <br><br>
            <h2>Write your review of the album</h2>
            <%= f.fields_for :reviews do |ff| %>
            <%= ff.label :title %>
            <%= ff.text_field :title %>
            <br>
            <%= ff.label :date %>
            <%= ff.date_field :date %>
            <br>
            <%= ff.label :content %>
            <%= ff.text_area :content %>
            <% end %>
            <br>
      <%= f.submit %>
<% end %>
<br><br><br>
<%= link_to "Back to Album", albums_path(@album) %>

albums controller:

class AlbumsController < ApplicationController
    before_action :set_album, only: [:show, :edit, :update, :destroy]
    before_action :must_login, only: [:new, :show, :create, :edit, :update, :destroy]

    def index
        @albums = Album.all
        @user = current_user
    end

    def show
        @review = @album.reviews.build
        @review.user = current_user

        @review.save
        @reviews = Review.recent #scope
    end

    def new
        @album = Album.new
        @review = @album.reviews.build
        @user = current_user
    end

    def create
        #@user = User.find(current_user.id)
        @album = current_user.albums.build(album_params)
        #@album.user_id = current_user.id
        @album.reviews.each { |r| r.user ||= current_user } # I'm using ||= so i can use the same code on update without changing reviews that already have a user
        if @album.save
            redirect_to album_path(@album)
        else
            render :new
        end
    end

    def edit
        @user = current_user
    end

    def update
        #@album = current_user.albums.build(album_params)
        @album.user_id = current_user.id
        if @album.update(album_params)
            redirect_to album_path(@album), notice: "Your album has been updated."
        else
            render 'edit'
        end
    end

    def destroy
        @album.delete
        @album.avatar.purge
        redirect_to albums_path
    end

    private

    def set_album
        @album = Album.find(params[:id])
    end

    def album_params
        params.require(:album).permit(:artist, :title, :avatar, :user_id, review_attributes:[:title, :date, :content])
    end
end

Reviews controller

class ReviewsController < ApplicationController
    before_action :set_review, only: [:show, :edit, :update, :destroy]
    before_action :set_current_user, only: [:index, :show, :new, :edit, :destroy]
    before_action :find_album, only: [:show, :create, :edit, :update, :destroy]
    before_action :must_login, only: [:index, :show, :new, :create, :edit, :update, :destroy]

    def index
        @albums = Album.with_recent_reviews
    end

    def show

        #@reviews = Review.where("album_id = ?", params[:album_id])
    end

    def new
        if params[:album_id] && @album = Album.find_by(id: params[:client_id])
            @review = @album.reviews.build
        else
            redirect_to albums_path
        end
    end

    def create
        @review = current_user.reviews.build(review_params)
        @review.album = @album
        if @review.save
            redirect_to album_path(@album)
        else
            @album = @review.album
            render :new
        end
    end

    def edit
    end

    def update
        if @review.update(review_params)
            redirect_to album_path(params[:album_id])
        else
            render 'edit'
        end
    end

    def destroy
        if current_user.id == @review.user_id
          @album.reviews.find(params[:id]).destroy
          redirect_to album_path(params[:album_id])
        else
           flash[:error] = "Unable to delete your review. Please try again."
           redirect_to album_reviews_path(@review)
        end
      end

    private

    def set_review
        @review = Review.find(params[:id])
    end

    def set_current_user
        @user = current_user
    end

    def find_album
        @album = Album.find(params[:album_id])
    end

    def review_params
        params.require(:review).permit(:title, :date, :content, album_attributes:[:artist, :title, :user_id])
    end

end

Album model:

class Album < ApplicationRecord
    has_many :reviews
    has_many :users, through: :reviews
    has_one_attached :avatar
    accepts_nested_attributes_for :reviews
    validates_presence_of :artist
    validates_presence_of :title
    scope :with_recent_reviews, -> { includes(:reviews).where(reviews: { date: [(Date.today - 7.days)..Date.tomorrow] }) } #scope relies on include method and custom query on related model (reviews)
end

Review model:

class Review < ApplicationRecord
    belongs_to :album, optional: true
    belongs_to :user
    validates_presence_of :content
    validates :title, presence: true, uniqueness: true
    validates :date, presence: true
    accepts_nested_attributes_for :album
    scope :recent, -> { where("date(date) >= ?", Date.today - 7.days) } #scope
end

Routes.rb

Rails.application.routes.draw do
  get '/auth/:provider/callback' => 'sessions#omniauth'
  get 'auth/failure', to: redirect('/')
  get '/signup' => 'users#new', as: 'signup'
  post '/signup' => 'users#create'
  get '/signin' => 'sessions#new'
  post '/signin' => 'sessions#create'
  get '/signout' => 'sessions#destroy'
  post '/logout', to: "sessions#destroy"

  resources :albums do
    resources :reviews, except: [:index]
  end
  resources :users, only: [:show, :destroy]
  resources :reviews, only: [:index]

  root to: "albums#index"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end


Solution

  • The reviews association is a has_many so fields_for should use the plural form.

    = f.fields_for :reviews do |ff|
    

    So that way rails creates that parameter reviews_attributes you are permitting.

    If you are still having problems with that change show the new error and stacktrace.

    EDIT: If you want to set the current user id as the user of the review (similar to what you did to assign the current user as the album creator), you can assign that before saving the record:

    def create
        # @user = User.find(current_user.id) you don need this, you already have the user at current_user, no need to find it again
        @album = current_user.albums.build(album_params)
        # @album.user = current_user you don't need this, current_user.albums.build already sets this
        @album.reviews.each { |r| r.user ||= current_user } # I'm using ||= so you can use the same code on update without changing reviews that already have a user
        if @album.save
            redirect_to album_path(@album)
        else
            render :new
        end
    end
    

    And also, remove the :user_id and :album_id from the permitted parameters for reviews_attributes, you don't want users to exploit that assignation adding those parameters that you are actually not using