Rails 4 multiple image or file upload using carrie

2019-01-01 00:01发布

问题:

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?

回答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.



回答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.



回答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.



回答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


回答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


回答6:

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