84

How can I upload multiple images from a file selection window using Rails 4 and CarrierWave? I have a post_controller and post_attachments model. How can I do this?

Can someone provide an example? Is there a simple approach to this?

SeanWM
  • 16,789
  • 7
  • 51
  • 83
SSR
  • 6,398
  • 4
  • 34
  • 50

6 Answers6

193

This is solution to upload multiple images using carrierwave in rails 4 from scratch

Or you can find working demo : Multiple Attachment Rails 4

To do just follow these steps.

rails new multiple_image_upload_carrierwave

In gem file

gem 'carrierwave'
bundle install
rails generate uploader Avatar 

Create post scaffold

rails generate scaffold post title:string

Create post_attachment scaffold

rails generate scaffold post_attachment post_id:integer avatar:string

rake db:migrate

In post.rb

class Post < ActiveRecord::Base
   has_many :post_attachments
   accepts_nested_attributes_for :post_attachments
end

In post_attachment.rb

class PostAttachment < ActiveRecord::Base
   mount_uploader :avatar, AvatarUploader
   belongs_to :post
end

In post_controller.rb

def show
   @post_attachments = @post.post_attachments.all
end

def new
   @post = Post.new
   @post_attachment = @post.post_attachments.build
end

def create
   @post = Post.new(post_params)

   respond_to do |format|
     if @post.save
       params[:post_attachments]['avatar'].each do |a|
          @post_attachment = @post.post_attachments.create!(:avatar => a)
       end
       format.html { redirect_to @post, notice: 'Post was successfully created.' }
     else
       format.html { render action: 'new' }
     end
   end
 end

 private
   def post_params
      params.require(:post).permit(:title, post_attachments_attributes: [:id, :post_id, :avatar])
   end

In views/posts/_form.html.erb

<%= form_for(@post, :html => { :multipart => true }) do |f| %>
   <div class="field">
     <%= f.label :title %><br>
     <%= f.text_field :title %>
   </div>

   <%= f.fields_for :post_attachments do |p| %>
     <div class="field">
       <%= p.label :avatar %><br>
       <%= p.file_field :avatar, :multiple => true, name: "post_attachments[avatar][]" %>
     </div>
   <% end %>

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

To edit an attachment and list of attachment for any post. In views/posts/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @post.title %>
</p>

<% @post_attachments.each do |p| %>
  <%= image_tag p.avatar_url %>
  <%= link_to "Edit Attachment", edit_post_attachment_path(p) %>
<% end %>

<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>

Update form to edit an attachment views/post_attachments/_form.html.erb

<%= image_tag @post_attachment.avatar %>
<%= form_for(@post_attachment) do |f| %>
  <div class="field">
    <%= f.label :avatar %><br>
    <%= f.file_field :avatar %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Modify update method in post_attachment_controller.rb

def update
  respond_to do |format|
    if @post_attachment.update(post_attachment_params)
      format.html { redirect_to @post_attachment.post, notice: 'Post attachment was successfully updated.' }
    end 
  end
end

In rails 3 no need to define strong parameters and as you can define attribute_accessible in both the model and accept_nested_attribute to post model because attribute accessible is deprecated in rails 4.

For edit an attachment we cant modify all the attachments at a time. so we will replace attachment one by one, or you can modify as per your rule, Here I just show you how to update any attachment.

SSR
  • 6,398
  • 4
  • 34
  • 50
  • 2
    in the show action of the post controller i think you've forgot @post =Post.find(params[:id]) – wael Apr 23 '14 at 15:50
  • Yes thanks, but In rails 4 by default there is one method called "set_post" when you create scaffold. it finds parameter for show, edit, update method. – SSR Apr 24 '14 at 04:53
  • 2
    @SSR Why you looping through each post attachments in `create` action? Rails and carrierwave are smart enough to save collections automatically. – hawk Jun 11 '14 at 13:42
  • It's not possible to create a Post then go back and change the image? @SSR – Chris Jul 15 '14 at 20:30
  • @Chris: Means you want to create a post first and then attach a Image? – SSR Jul 20 '14 at 05:45
  • @hawk : Yea we can do so. but when you cant find nested attributes params, then you can do so. – SSR Jul 20 '14 at 05:47
  • @SSR :I have used the code, in my index method in post_controller `code`@posts = Posts.all @post_attachment = @posts.post_attachments.all`code` I have undefined method post_attachments for – user1876128 Jul 29 '14 at 14:49
  • 1
    This works great on a new post but does not look very good on an edit. Anyway to avoid showing all attachments in the edit form? – Jepzen Aug 07 '14 at 10:57
  • 1
    @Jepzen : Will update my ans with edit view very soon. Actually I missed that part. I didn't work on that yet but I will do now. – SSR Sep 09 '14 at 05:34
  • 3
    Would love to see edit (especially handling `:_destroy` part) – Tun Sep 21 '14 at 18:06
  • I am not getting the above answer you have posted for edit, how we will edit multiple files if we are using accepts_nested_attributes_for? – Deepti Kakade Dec 18 '14 at 07:28
  • @DeeptiKakade : You can add multiple items at a time but can't change or update multiple items at time. because how you define that which item will replace with existing one. so you can only edit a single item. – SSR Dec 18 '14 at 09:19
  • This has been very helpful! Do you know how to use cache to sort-of save the files to the uploaded? I've been having trouble using the cache feature for multiple file uploads. – Ji Mun Mar 19 '15 at 22:37
  • 5
    @SSR - Your answer is very helpful. Could you please update your answer with edit action too. – raj_on_rails Mar 30 '15 at 16:30
  • 1
    Why do you need to include the post_attachments in the post_params as these are not being mass assigned as a normal nested attribute is? These are being manually added within the create action referencing the params hash directly. I removed the post_attachments from the strong params and it still works. – Dercni Aug 09 '15 at 12:09
  • 1
    Upon further testing it doesn't work with the nested item removed from the strong params. – Dercni Aug 09 '15 at 20:13
  • Hi @SSR, if I want just ONE (maybe the first) image to show on the index page while all images showing on the show, what do I need to do on the index page to just have the first image showing (not every image) – Loi Huynh Aug 24 '15 at 19:23
  • 1
    Does anyone know how to make the images persist in a form that has been submitted and failed due to validation errors? There's documentation for this in the carrierwave gem but only for individual files, and I can't get it working for this scenario. Thanks! – Dennis Oct 06 '15 at 00:07
  • In the create controller shouldn't it be `params[:post][:post_attachments]['avatar'].each`? `params[:post_attachments]` is nil – user4584963 Feb 23 '16 at 13:25
  • 2
    When I add validations to the post_attachment model, they do not prevent the post model from saving. Instead the post is saved, and then the ActiveRecord invalid error is thrown for the attachment model only. I think this is because of the create! method. but using create instead just fails silently. Any idea how to have the validation happen on the post reach into the attachments? – dchess Jul 26 '16 at 23:51
  • Since this is making a gallery, is there a way to make this work with carrierwave versions i.e. generating and using thumbnails – chaostheory Aug 23 '16 at 02:34
32

If we take a look at CarrierWave's documentation, this is actually very easy now.

https://github.com/carrierwaveuploader/carrierwave/blob/master/README.md#multiple-file-uploads

I will use Product as the model I want to add the pictures, as an example.

  1. Get the master branch Carrierwave and add it to your Gemfile:

    gem 'carrierwave', github:'carrierwaveuploader/carrierwave'
    
  2. Create a column in the intended model to host an array of images:

    rails generate migration AddPicturesToProducts pictures:json
    
  3. Run the migration

    bundle exec rake db:migrate
    
  4. Add pictures to model Product

    app/models/product.rb
    
    class Product < ActiveRecord::Base
      validates :name, presence: true
      mount_uploaders :pictures, PictureUploader
    end
    
  5. Add pictures to strong params in ProductsController

    app/controllers/products_controller.rb
    
    def product_params
      params.require(:product).permit(:name, pictures: [])
    end
    
  6. Allow your form to accept multiple pictures

    app/views/products/new.html.erb
    
    # notice 'html: { multipart: true }'
    <%= form_for @product, html: { multipart: true } do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>
    
      # notice 'multiple: true'
      <%= f.label :pictures %>
      <%= f.file_field :pictures, multiple: true, accept: "image/jpeg, image/jpg, image/gif, image/png" %>
    
      <%= f.submit "Submit" %>
    <% end %>
    
  7. In your views, you can reference the images parsing the pictures array:

    @product.pictures[1].url
    

If you choose several images from a folder, the order will be the exact order you are taking them from top to bottom.

Matthias
  • 1,884
  • 2
  • 18
  • 35
drjorgepolanco
  • 7,479
  • 5
  • 46
  • 47
  • 11
    CarrierWave's solution to this problem makes me cringe. It involves putting all the references to the files into one field in an array! It certainly wouldn't be considered the "rails way". What if you then want to remove some, or add extra files to the post? I'm not saying it wouldn't be possible, I'm just saying it would be ugly. A join table is a much better idea. – Toby 1 Kenobi Mar 11 '16 at 01:12
  • 3
    I couldn't agree more Toby. Would you be so kind to provide that solution? – drjorgepolanco Mar 12 '16 at 22:49
  • 2
    That solutions is already provided by SSR. Another model is put in place to hold the uploaded file, then the thing that needs many files uploaded relates in a one-to-many or many-to-many relationship with that other model. (the join table I mentioned in my earlier comment would be in the case of a many-to-many relationship) – Toby 1 Kenobi Mar 14 '16 at 03:29
  • Thanks @Toby1Kenobi, I was wondering how the column array method would account for image versions (I don't see how it can). Your strategy is doable. – chaostheory Aug 23 '16 at 02:37
  • I have implemented this feature of Carrierwave with Rails 5.x.x, https://github.com/carrierwaveuploader/carrierwave/blob/master/README.md#multiple-file-uploads But I am not able to run it successfully, and it is generating error, `UndefinedConversionError ("\x89" from ASCII-8BIT to UTF-8)` For SSR solution, it works fine with Rails 4.x.x, but I am facing challenges (with Rails 5.x.x.) i.e. its storing `ActionDispatch::Http::UploadedFile` in database instead filename. Its also not storing files in public folders for given path in uploader. – Mansi Shah Jun 08 '17 at 10:28
  • Putting multiple images of an entity inside one JSON/text field makes it also impossible to assign `title` and `alt` attributes for those images, unless it is possible to do so using nested JSON. Any idea? – W.M. Sep 01 '17 at 19:12
  • Multiple file upload has several issues with carrierwave with mount_uploaders :pictures, PictureUploader – Nitin Jain Dec 28 '20 at 09:30
8

Some minor additions to the SSR answer:

accepts_nested_attributes_for does not require you to change the parent object's controller. So if to correct

name: "post_attachments[avatar][]"

to

name: "post[post_attachments_attributes][][avatar]"

then all these controller changes like these become redundant:

params[:post_attachments]['avatar'].each do |a|
  @post_attachment = @post.post_attachments.create!(:avatar => a)
end

Also you should add PostAttachment.new to the parent object form:

In views/posts/_form.html.erb

  <%= f.fields_for :post_attachments, PostAttachment.new do |ff| %>
    <div class="field">
      <%= ff.label :avatar %><br>
      <%= ff.file_field :avatar, :multiple => true, name: "post[post_attachments_attributes][][avatar]" %>
    </div>
  <% end %>

This would make redundant this change in the parent's controller:

@post_attachment = @post.post_attachments.build

For more info see Rails fields_for form not showing up, nested form

If you use Rails 5, then change Rails.application.config.active_record.belongs_to_required_by_default value from true to false (in config/initializers/new_framework_defaults.rb) due to a bug inside accepts_nested_attributes_for (otherwise accepts_nested_attributes_for won't generally work under Rails 5).

EDIT 1:

To add about destroy:

In models/post.rb

class Post < ApplicationRecord
    ...
    accepts_nested_attributes_for :post_attachments, allow_destroy: true
end

In views/posts/_form.html.erb

 <% f.object.post_attachments.each do |post_attachment| %>
    <% if post_attachment.id %>

      <%

      post_attachments_delete_params =
      {
      post:
        {              
          post_attachments_attributes: { id: post_attachment.id, _destroy: true }
        }
      }

      %>

      <%= link_to "Delete", post_path(f.object.id, post_attachments_delete_params), method: :patch, data: { confirm: 'Are you sure?' } %>

      <br><br>
    <% end %>
  <% end %>

This way you simply do not need to have a child object's controller at all! I mean no any PostAttachmentsController is needed anymore. As for parent object's controller (PostController), you also almost don't change it - the only thing you change in there is the list of the whitelisted params (to include the child object-related params) like this:

def post_params
  params.require(:post).permit(:title, :text, 
    post_attachments_attributes: ["avatar", "@original_filename", "@content_type", "@headers", "_destroy", "id"])
end

That's why the accepts_nested_attributes_for is so amazing.

Community
  • 1
  • 1
prograils
  • 2,248
  • 1
  • 28
  • 45
  • Those are actually major additions to @SSR answer, not minor :) accept_nested_attributes_for is quite something. Indeed there's no need for a child controller at all. By following your approach, the only thing I'm unable to do is to display form error messages for the child when something goes wrong with the upload. – Luis Fernando Alen Jun 13 '17 at 15:55
  • Thanks for your input. I got the upload working, but I am wondering how I could add additional attributes to the post_attachments form field in views/posts/_form.html.erb? `<%= d.text_field :copyright, name: "album[diapos_attributes][][copyright]", class: 'form-field' %>` writes the copyright only to the last record and not to all of them. – SEJU Dec 19 '19 at 14:54
6

Also I figured out how to update the multiple file upload and I also refactored it a bit. This code is mine but you get the drift.

def create
  @motherboard = Motherboard.new(motherboard_params)
  if @motherboard.save
    save_attachments if params[:motherboard_attachments]
    redirect_to @motherboard, notice: 'Motherboard was successfully created.'
  else
    render :new
  end
end


def update
  update_attachments if params[:motherboard_attachments]
  if @motherboard.update(motherboard_params)
    redirect_to @motherboard, notice: 'Motherboard was successfully updated.'
  else
   render :edit
  end
end

private
def save_attachments
  params[:motherboard_attachments]['photo'].each do |photo|
    @motherboard_attachment = @motherboard.motherboard_attachments.create!(:photo => photo)
  end
end

 def update_attachments
   @motherboard.motherboard_attachments.each(&:destroy) if @motherboard.motherboard_attachments.present?
   params[:motherboard_attachments]['photo'].each do |photo|
     @motherboard_attachment = @motherboard.motherboard_attachments.create!(:photo => photo)
   end
 end
Chris Habgood
  • 400
  • 3
  • 11
2

When using the association @post.post_attachments you do not need to set the post_id.

SeanWM
  • 16,789
  • 7
  • 51
  • 83
Chris Habgood
  • 400
  • 3
  • 11
2

Here is my second refactor into the model:

  1. Move private methods to model.
  2. Replace @motherboard with self.

Controller:

def create
  @motherboard = Motherboard.new(motherboard_params)

  if @motherboard.save
    @motherboard.save_attachments(params) if params[:motherboard_attachments]
  redirect_to @motherboard, notice: 'Motherboard was successfully created.'
  else
    render :new
  end
end

def update
  @motherboard.update_attachments(params) if params[:motherboard_attachments]
  if @motherboard.update(motherboard_params)
    redirect_to @motherboard, notice: 'Motherboard was successfully updated.'
  else
    render :edit
  end
end

In motherboard model:

def save_attachments(params)
  params[:motherboard_attachments]['photo'].each do |photo|
    self.motherboard_attachments.create!(:photo => photo)
  end
end

def update_attachments(params)
  self.motherboard_attachments.each(&:destroy) if self.motherboard_attachments.present?
  params[:motherboard_attachments]['photo'].each do |photo|
    self.motherboard_attachments.create!(:photo => photo)
  end
end
Chris Habgood
  • 400
  • 3
  • 11