Rob Hughes
Rob Hughes

Reputation: 906

Ruby on rails. Form for a nested model in a different view

I've been battling this for a while now and I can't figure it out. I have a user model using devise. Users can upload songs, and add youtube videos etc..

I'm trying to let users add/delete songs and videos from the devise edit registrations view.

Videos upload fine, but as songs are a nested resource of playlists, which belongs to user, I think I'm getting muddle up.

Music uploads with the same form on it's corresponding page, but not from the devise registration edit view.

routes:

  devise_for :users, controllers: { registrations: "users/registrations", sessions: "users/sessions" }

  resources :videos

  resources :playlists do
    resources :songs
  end

Devise registrations controller:

   def edit
     @song = Song.new
     @video = Video.new
   end

Form in devise edit registrations:

<div id="user-music-box">
  <p class="p-details-title"> Upload Music </p>
<%= simple_form_for [@user.playlist, @song] do |f| %>
<%= f.file_field :audio %>
<%= f.button :submit %>
<% end %>
</div>

<div id="user-video-box">
  <p class="p-details-title"> Add videos </p>
<%= simple_form_for @video do |f| %>
<%= f.input :youtubeurl %>
<%= f.button :submit %>
<% end %>
</div>

As I said, videos (Which is a youtube url string) create and save no problem. The exact same form for songs, basically seems to just update the user registration. The song information is shown in the server logs, but no playlist_id is present and nothing gets saved.

Songs controller:

def new
      if user_signed_in?
      @song = Song.new
      if current_user.playlist.songs.count >= 5
        redirect_to user_path(current_user)
        flash[:danger] = "You can only upload 5 songs."
      end
      else
     redirect_to(root_url)
     flash[:danger] = "You must sign in to upload songs"
   end
  end

  def create
    @song = current_user.playlist.songs.new song_params
    @song.playlist_id = @playlist.id
    if @song.save
        respond_to do |format|
          format.html {redirect_to user_path(current_user)}
          format.js
        end
    else
      render 'new'
    end
  end

Playlist.rb

class Playlist < ActiveRecord::Base
  belongs_to :user
  has_many :songs, :dependent => :destroy
  accepts_nested_attributes_for :songs
end

song.rb

class Song < ActiveRecord::Base
  belongs_to :user
  belongs_to :playlist
  has_attached_file :audio
  validates_attachment_presence :audio
  validates_attachment_content_type :audio, :content_type => ['audio/mp3','audio/mpeg']
end

Upvotes: 1

Views: 232

Answers (1)

Richard Peck
Richard Peck

Reputation: 76774

Unless you're passing songs/playlists through accepts_nested_attributes_for you shouldn't be using registrations#edit. I'll detail both ways to achieve what you want below:


Nested Attributes

#app/models/user.rb
class User < ActiveRecord::Base
  has_many :videos

  has_many :playlists
  has_many :songs, through: :playlists

  accepts_nested_attributes_for :videos
end

#app/models/playlist.rb
class PlayList < ActiveRecord::Base
  belongs_to :user
  has_and_belongs_to_many :songs
end

#app/models/song.rb
class Song < ActiveRecord::Base
  has_and_belongs_to_many :playlists
end

The importance of this is that to use it properly, you're able to edit the @user object directly, passing the nested attributes through the fields_for helper:

#config/routes.rb
devise_for :users, controllers: { registrations: "users/registrations", sessions: "users/sessions" }

#app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < ApplicationController
  before_action :authenticate_user!, only: [:edit, :update]

  def edit
    @user = current_user
    @user.playlists.build.build_song
    @user.videos.build
  end

  def update
    @user = current_user.update user_params
  end

  private

  def user_params
    params.require(:user).permit(:user, :attributes, videos_attributes: [:youtubeurl], playlists_attributes: [song_ids: [], song_attributes: [:title, :artist, :etc]])
  end
end

This will allow you to use:

#app/views/users/registrations/edit.html.erb
<%= form_for @user do |f| %>
  <%= f.fields_for :videos do |v| %>
    <%= v.text_field :youtubeurl %>
  <% end %>
  <%= f.fields_for :playlists do |p| %>
    <%= p.collection_select :song_ids, Song.all, :id, :name %>
    <%= p.fields_for :song do |s| %>
      <%= f.text_field :title %>
    <% end %>
  <% end %> 
  <%= f.submit %>
<% end %>

This will give you a single form, from which you'll be able to create videos, playlists and songs for the @user.


Separate

The other option is to create the object separately.

There is no technical reason for preferring this way over nested attributes; you'd do it to make sure you have the routes in the correct order etc.

As a note, you need to remember that routes != model structure. You can have any routes you want, so long as they define a good pattern for your models:

# config/routes.rb
authenticated :user do #-> user has to be logged in
  resources :videos, :playlists, :songs #-> url.com/videos/new
end

# app/controllers/videos_controller.rb
class VideosController < ApplicationController
  def new
    @video = current_user.videos.new
  end

  def create
    @video = current_user.videos.new video_params
    @video.save
  end

  private

  def video_params
    params.require(:video).permit(:youtubeurl)
  end
end

# app/views/videos/new.html.erb
<%= form_for @video do |f| %>
  <%= f.text_field :youtubeurl %>
  <%= f.submit %>
<% end %>

The above will require the duplication of the VideosController for Playlists and Songs

Upvotes: 1

Related Questions