Rails 4 przesyłanie wielu obrazów lub plików za pomocą carrierwave

86

Jak mogę przesłać wiele obrazów z okna wyboru plików przy użyciu Rails 4 i CarrierWave? Mam model post_controlleri post_attachments. Jak mogę to zrobić?

Czy ktoś może podać przykład? Czy jest na to proste podejście?

SSR
źródło

Odpowiedzi:

195

Jest to rozwiązanie umożliwiające przesyłanie od zera wielu obrazów za pomocą carrierwave w szynach 4

Lub możesz znaleźć działające demo: Wiele szyn mocujących 4

Aby to zrobić, wykonaj następujące kroki.

rails new multiple_image_upload_carrierwave

W pliku gem

gem 'carrierwave'
bundle install
rails generate uploader Avatar 

Utwórz rusztowanie słupkowe

rails generate scaffold post title:string

Utwórz rusztowanie post_attachment

rails generate scaffold post_attachment post_id:integer avatar:string

rake db:migrate

W post.rb

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

W post_attachment.rb

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

W 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

W 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 %>

Aby edytować załącznik i listę załączników do dowolnego posta. W 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 %>

Zaktualizuj formularz, aby edytować widoki załączników / 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 %>

Zmodyfikuj metodę aktualizacji w 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

W rails 3 nie ma potrzeby definiowania silnych parametrów, a ponieważ można zdefiniować atrybut_accessible zarówno w modelu, jak i accept_nested_attribute do modelu post, ponieważ atrybut access jest przestarzały w rails 4.

W celu edycji załącznika nie możemy modyfikować wszystkich załączników naraz. więc wymienimy załącznik jeden po drugim lub możesz zmodyfikować zgodnie z regułą, tutaj pokażę ci tylko, jak zaktualizować dowolny załącznik.

SSR
źródło
2
w akcji show kontrolera pocztowego myślę, że zapomniałeś @post = Post.find (params [: id])
wael
1
@SSR Dlaczego w createakcji przeglądasz załączniki każdego posta ? Railsy i carrierwave są wystarczająco inteligentne, aby automatycznie zapisywać kolekcje.
jastrząb
3
Chciałbym zobaczyć edycję (zwłaszcza :_destroyczęść dotyczącą obsługi )
Tun,
5
@SSR - Twoja odpowiedź jest bardzo pomocna. Czy mógłbyś zaktualizować swoją odpowiedź również za pomocą akcji edycji.
raj_on_rails
2
Kiedy dodam walidacje do modelu post_attachment, nie uniemożliwiają one zapisania modelu post. Zamiast tego wpis jest zapisywany, a następnie zgłaszany jest błąd nieprawidłowego rekordu ActiveRecord tylko dla modelu załącznika. Myślę, że to z powodu tworzenia! metoda. ale użycie Create zamiast tego po prostu cicho zawodzi. Masz pomysł, jak sprawić, by walidacja nastąpiła po dotarciu do załączników?
dchess
32

Jeśli spojrzymy na dokumentację CarrierWave, jest to teraz bardzo łatwe.

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

Będę używał produktu jako modelu, który chcę dodać zdjęcia, jako przykład.

  1. Pobierz główną gałąź Carrierwave i dodaj ją do swojego Gemfile:

    gem 'carrierwave', github:'carrierwaveuploader/carrierwave'
    
  2. Utwórz kolumnę w modelu docelowym, aby hostować tablicę obrazów:

    rails generate migration AddPicturesToProducts pictures:json
    
  3. Uruchom migrację

    bundle exec rake db:migrate
    
  4. Dodaj zdjęcia do modelu produktu

    app/models/product.rb
    
    class Product < ActiveRecord::Base
      validates :name, presence: true
      mount_uploaders :pictures, PictureUploader
    end
    
  5. Dodaj zdjęcia do silnych parametrów w ProductsController

    app/controllers/products_controller.rb
    
    def product_params
      params.require(:product).permit(:name, pictures: [])
    end
    
  6. Zezwól formularzowi na akceptowanie wielu zdjęć

    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. W swoich widokach możesz odwoływać się do obrazów analizujących tablicę pictures:

    @product.pictures[1].url
    

Jeśli wybierzesz kilka zdjęć z folderu, kolejność będzie dokładnie taka, jak robisz je od góry do dołu.

drjorgepolanco
źródło
9
Wzdrygam się przy rozwiązaniu tego problemu przez firmę CarrierWave. Polega na umieszczeniu wszystkich odniesień do plików w jednym polu w tablicy! Z pewnością nie byłoby to uważane za „sposób szynowy”. A co, jeśli chcesz usunąć część lub dodać dodatkowe pliki do posta? Nie mówię, że to nie byłoby możliwe, mówię tylko, że byłoby brzydkie. Stolik do łączenia to znacznie lepszy pomysł.
Toby 1 Kenobi
3
Nie mogłem się bardziej zgodzić, Toby. Czy byłbyś tak uprzejmy, aby zapewnić takie rozwiązanie?
drjorgepolanco
2
Takie rozwiązania zapewnia już SSR. Wprowadzany jest inny model do przechowywania przesłanego pliku, wtedy rzecz, która wymaga przesłania wielu plików, jest powiązana w relacji jeden do wielu lub wiele do wielu z tym innym modelem. (stół do łączenia, o którym wspomniałem w moim wcześniejszym komentarzu, byłby w przypadku relacji wiele do wielu)
Toby 1 Kenobi
Dzięki @ Toby1Kenobi, zastanawiałem się, jak metoda tablic kolumnowych mogłaby uwzględniać wersje obrazów (nie widzę, jak to może). Twoja strategia jest wykonalna.
chaostheory
Zaimplementowałem tę funkcję Carrierwave z Railsami 5.xx, github.com/carrierwaveuploader/carrierwave/blob/master/… Ale nie mogę jej pomyślnie uruchomić i generuje błąd, UndefinedConversionError ("\x89" from ASCII-8BIT to UTF-8) W przypadku rozwiązania SSR działa dobrze z Rails 4.xx, ale stoję przed wyzwaniami (z Railsami 5.xx), tj. Przechowywanie ActionDispatch::Http::UploadedFilew bazie danych zamiast nazwy pliku. Nie przechowuje również plików w folderach publicznych dla podanej ścieżki w programie do przesyłania.
Mansi Shah
8

Kilka drobnych dodatków do odpowiedzi SSR :

accepts_nested_attributes_for nie wymaga zmiany kontrolera obiektu nadrzędnego. Więc jeśli poprawiać

name: "post_attachments[avatar][]"

do

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

wtedy wszystkie te zmiany kontrolera, takie jak te, stają się zbędne:

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

Należy również dodać PostAttachment.newdo formularza obiektu nadrzędnego:

W 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 %>

Spowodowałoby to zbędną zmianę w kontrolerze rodzica:

@post_attachment = @post.post_attachments.build

Aby uzyskać więcej informacji, zobacz Rails fields_for formularz, który się nie wyświetla, zagnieżdżony formularz

Jeśli używasz Rails 5, zmień Rails.application.config.active_record.belongs_to_required_by_defaultwartość z truena false(w config / initializers / new_framework_defaults.rb) z powodu błędu wewnątrz accepts_nested_attributes_for (w przeciwnym razie accepts_nested_attributes_for nie będzie działać w Rails 5).

EDYCJA 1:

Aby dodać o zniszczeniu :

W modelach / post.rb

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

W 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 %>

W ten sposób w ogóle nie musisz mieć kontrolera obiektu podrzędnego! To znaczy, nic PostAttachmentsControllerjuż nie jest potrzebne. Jeśli chodzi o kontroler obiektu nadrzędnego ( PostController), prawie go nie zmieniasz - jedyną rzeczą, którą tam zmieniasz, jest lista parametrów na białej liście (w celu uwzględnienia parametrów związanych z obiektem potomnym) w następujący sposób:

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

Dlatego accepts_nested_attributes_forjest tak niesamowity.

prograils
źródło
To są właściwie główne dodatki do odpowiedzi @SSR, a nie drugorzędne :) accept_nested_attributes_for to całkiem coś. W rzeczywistości w ogóle nie jest potrzebny kontroler podrzędny. Postępując zgodnie z Twoim podejściem, jedyną rzeczą, której nie jestem w stanie zrobić, jest wyświetlanie dziecku komunikatów o błędach, gdy coś pójdzie nie tak z przesyłaniem.
Luis Fernando Alen
Dzięki za wkład. Przesyłanie działa, ale zastanawiam się, jak mogę dodać dodatkowe atrybuty do pola formularza post_attachments w views / posts / _form.html.erb? <%= d.text_field :copyright, name: "album[diapos_attributes][][copyright]", class: 'form-field' %>zapisuje prawa autorskie tylko do ostatniej płyty, a nie do wszystkich.
SEJU
6

Dowiedziałem się również, jak zaktualizować przesyłanie wielu plików, a także trochę go zreformowałem. Ten kod jest mój, ale rozumiesz.

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
źródło
1
Dzięki za udostępnienie kodu. kiedy będziesz miał czas, zaktualizuj kod w moim repozytorium gihub i nie zapomnij skomentować każdej metody, aby każdy mógł łatwo zrozumieć kod.
SSR,
1
Sklonowałem repozytoria, czy dasz mi pozwolenie na wykonanie PR?
Chris Habgood,
Tak, oczywiście. Jaka jest twoja nazwa użytkownika na githubie
SSR,
Czy miałeś okazję dać mi dostęp?
Chris Habgood,
2

Oto mój drugi refaktor do modelu:

  1. Przenieś metody prywatne do modelu.
  2. Zastąp @motherboard własnym.

Kontroler:

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

W modelu z płytą główną:

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
źródło
2

Podczas korzystania z asocjacji @post.post_attachmentsnie musisz ustawiać pliku post_id.

Chris Habgood
źródło