T0ny lombardi
T0ny lombardi

Reputation: 1920

Rails Multiple Uploads with Shrine and nested attachments

I got an issue with uploading multiple files to AWS. I have two models Lessons && Attachments. Lessons has_many :attachments and I'm setting up a form that has lesson fields and I want to upload multiple attachments to that lesson. Everything uploads correctly except when I upload multiple files, it creates a new lesson and attachment. I'm working off:

https://gorails.com/episodes/multiple-file-uploads-with-shrine?autoplay=1

And

https://u.osu.edu/hasnan.1/2014/03/30/rails-4-multiple-file-upload-with-carrierwave-nested-form-and-jquery-file-upload/

Models

Lessons Model

class Lesson < ApplicationRecord
  belongs_to :user
  has_many :attachments, dependent: :destroy
  accepts_nested_attributes_for :attachments
end

Attachment Model

class Attachment < ApplicationRecord
  belongs_to :lesson, optional: true
  include AttachmentUploader::Attachment.new(:media)
end

Controllers

Lessons controller

Class LessonsController < ApplicationController
  # truncated for brevity.

  def new
    @lesson = current_user.lessons.build
  end


  def create
    @lesson = current_user.lessons.build(lesson_params)

    respond_to do |format|
      if @lesson.save

        if params[:media]
          params[:media].each { |media|
            @lesson.attachments.create(media_data: media)
          }
        end

        format.html { redirect_to @lesson, notice: 'Lesson was successfully created.' }
        format.json { render :show, status: :created, location: @lesson }
      else
        puts "\n\n\n#{@lesson.errors.full_messages.to_sentence}\n\n\n"
        format.html { render :new, notice: @lesson.errors }
      end
    end

  end



  private

    def set_lesson
      @lesson = Lesson.find(params[:id])
    end

    def lesson_params
      params.require(:lesson).permit(
        :title, 
        :content, 
        :document, 
        :category_id,
        :pinned, 
        :bootsy_image_gallery_id, 
        attachments_attributes: {media: []},
      )
    end

end

Attachment Controller

class AttachmentsController < ApplicationController
  before_action :set_attachment, only: [:edit, :update, :destroy]

  def create
    @attachment = Attachment.new(attachment_params)
    @attachment.save
  end


  private

    def set_attachment
      @attachment = Attachment.find(params[:id])
    end

    def attachment_params
      params.fetch(:attachment, {})
    end
end

ERB

_form.html.erb

<%= @lesson.errors.full_messages.first if @lesson.errors.any? %>
<%= form_for @lesson do |f| %>
<div class="control-group">
  <%= f.label :title %>
  <div class="controls">
    <%= f.text_field :title, required: true %>
  </div>

  <div>
    <%= f.label :content %>
    <%= f.bootsy_area :content, editor_options: { html: false }, rows: "20", cols: "100" %>
  </div>

  <div>
    <%= f.label 'File under at least one class' %>
    <%= f.collection_select :category_id, Subject.all, :id, :name, { promt: "Choose a Class" } %>
  </div>

  <div>
    <%= f.label :pinned %>
    <%= f.label :pinned, "Yes", value: "Yes"  %>
    <%= f.radio_button :pinned, true%>
    <%= f.label :pinned, "No", value: "No" %>
    <%= f.radio_button :pinned, false, checked: true %>
  </div>
  <hr>


    <div>
      <%= f.label 'Or Upoad a file' %>

      <%
        ######################
        # This is where I have the attachment file field. 
        ######################      
      %>

      <%= file_field_tag "media[]", type: :file, multiple: true %>

    </div>

  <br>
    <%= f.submit nil %>
    <%= link_to 'Cancel', lessons_path%>

  <div class="form-actions btn-a">
    <%= link_to 'Cancel', lessons_path, class: "btn btn-default" %>
  </div>

Routes

Rails.application.routes.draw do
  mount AttachmentUploader::UploadEndpoint => "/attachments/upload"


  resources :lessons do
    member do
      put "like",     to: "lessons#upvote"
      put "dislike",  to: "lessons#downvote"
    end
    resources :comments
    resources :attachments
  end

  root 'static#index'

end

JS

$(document).on("turbolinks:load", function () {
  $("[type=file]").fileupload({
    add: function (e, data) {
      data.progressBar = $('<div class="progress" style="width: 300px"><div class="progress-bar"></div></dov>').insertAfter("#file-upload");
      var options = {
        extension: data.files[0].name.match(/(\.\w+)?$/)[0],
        _: Date.now() // revent caching
      }

      $.getJSON("/attachments/upload/cache/presign", options, function (result) {
        data.formData = result['fields'];
        data.url = result['url'];
        data.paramName = "file";
        data.submit();
      });
    },
    progress: function (e, data) {
      var progress = parseInt(data.loaded / data.total * 100, 10);
      var percentage = progress.toString() + '%';
      data.progressBar.find(".progress-bar").css("width", percentage).html(percentage);
    },
    done: function (e, data) {
      console.log("done", data);
      data.progressBar.remove();

      var document = {
        id: data.formData.key.match(/cache\/(.+)/)[1],
        storage: 'cache',
        metadata: {
          size: data.files[0].size,
          filename: data.files[0].name.match(/[^\/\\]+$/)[0],
          mime_type: data.files[0].type
        }
      }

      form = $(this).closest("form");
      form_data = new FormData(form[0]);
      form_data.append($(this).attr("name"), JSON.stringify(document))

      $.ajax(form.attr("action"), {
        contentType: false,
        processData: false,
        data: form_data,
        method: form.attr("method"),
        dataType: "json",
        success: function(response) {
          var $img = $("<img/>", { src: response.image_url, width: 400 });
          var $div = $("<div/>").append($img);
          $("#photos").append($div);
        }
      });
    }
  });
});

Upvotes: 1

Views: 1262

Answers (1)

excid3
excid3

Reputation: 1667

The reason this is happening is because the Javascript looks for the URL of the file field's parent form. You have the form for creating a new Lesson, which means every time you upload a file, it will create a new lesson.

Some options to consider are:

  1. Place the upload form in a place that's only available after the Lesson has been created. This lets you create a form_for [@lesson, Attachment.new] which will point to the right URL and let you upload files really quickly, but the Lesson must be created first.

  2. You could adjust the JS not to submit an AJAX request right away and instead attach hidden fields with the image data.

My screencast covered this method because you had to create the Album first before uploading on the Album's show page. I think this ends up being a better user experience so they aren't worrying about file uploads failing and causing their other form data to possibly get lost or not saved.

Upvotes: 2

Related Questions