Rails 4 multiple image or file upload using carrierwave

74,167

Solution 1

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.

Solution 2

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.

Solution 3

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.

Solution 4

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

Solution 5

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
Share:
74,167

Related videos on Youtube

SSR
Author by

SSR

Ping me on Skype : savan.raval4 Drop me an Email : [email protected]

Updated on May 07, 2020

Comments

  • SSR
    SSR almost 4 years

    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?

  • wael
    wael about 10 years
    in the show action of the post controller i think you've forgot @post =Post.find(params[:id])
  • SSR
    SSR about 10 years
    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.
  • hawk
    hawk almost 10 years
    @SSR Why you looping through each post attachments in create action? Rails and carrierwave are smart enough to save collections automatically.
  • Chris
    Chris almost 10 years
    It's not possible to create a Post then go back and change the image? @SSR
  • SSR
    SSR almost 10 years
    @Chris: Means you want to create a post first and then attach a Image?
  • SSR
    SSR almost 10 years
    @hawk : Yea we can do so. but when you cant find nested attributes params, then you can do so.
  • user1876128
    user1876128 over 9 years
    @SSR :I have used the code, in my index method in post_controller code@posts = Posts.all @post_attachment = @posts.post_attachments.allcode I have undefined method post_attachments for <Post::ActiveRecord_Relation:0x007fa6087482a8>
  • Jepzen
    Jepzen over 9 years
    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?
  • SSR
    SSR over 9 years
    @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.
  • Tun
    Tun over 9 years
    Would love to see edit (especially handling :_destroy part)
  • Deepti Kakade
    Deepti Kakade over 9 years
    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?
  • SSR
    SSR over 9 years
    @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.
  • Ji Mun
    Ji Mun about 9 years
    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.
  • raj_on_rails
    raj_on_rails about 9 years
    @SSR - Your answer is very helpful. Could you please update your answer with edit action too.
  • SSR
    SSR almost 9 years
    Thanks for sharing your code. when you get time please update code at my gihub repo and do not forget to comment for each method so everyone can easily understand code.
  • Chris Habgood
    Chris Habgood almost 9 years
    I cloned the repos, will you give me permission to do a PR?
  • SSR
    SSR almost 9 years
    Yes sure. What is your github username
  • Chris Habgood
    Chris Habgood almost 9 years
    Have you had a chance to give me access?
  • Dercni
    Dercni over 8 years
    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
    Dercni over 8 years
    Upon further testing it doesn't work with the nested item removed from the strong params.
  • Loi Huynh
    Loi Huynh over 8 years
    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)
  • Dennis
    Dennis over 8 years
    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!
  • user4584963
    user4584963 about 8 years
    In the create controller shouldn't it be params[:post][:post_attachments]['avatar'].each? params[:post_attachments] is nil
  • Toby 1 Kenobi
    Toby 1 Kenobi about 8 years
    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.
  • epicrato
    epicrato about 8 years
    I couldn't agree more Toby. Would you be so kind to provide that solution?
  • Toby 1 Kenobi
    Toby 1 Kenobi about 8 years
    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)
  • dchess
    dchess almost 8 years
    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?
  • chaostheory
    chaostheory over 7 years
    Since this is making a gallery, is there a way to make this work with carrierwave versions i.e. generating and using thumbnails
  • chaostheory
    chaostheory over 7 years
    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.
  • Mansi Shah
    Mansi Shah almost 7 years
    I have implemented this feature of Carrierwave with Rails 5.x.x, github.com/carrierwaveuploader/carrierwave/blob/master/… 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.
  • Luis Fernando Alen
    Luis Fernando Alen almost 7 years
    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.
  • W.M.
    W.M. over 6 years
    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?
  • SEJU
    SEJU over 4 years
    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.
  • Nitin Jain
    Nitin Jain over 3 years
    Multiple file upload has several issues with carrierwave with mount_uploaders :pictures, PictureUploader