0

I'm using Paperclip in a Rails app to validate file attachments and load them into AWS S3. The desired behavior is for users to be able to upload multiple "RecordAttachments" (file attachments) when they submit a Record form.

The catch is that I want to start uploading the RecordAttachments as soon as the user selects their files to upload with the file selector, regardless of whether the user has submitted the Record form. I also want to ensure each RecordAttachment is mapped to the Record to which it belongs. This must be possible, but I'm not yet sure how to build out the logic.

Currently, I can allow users to create multiple RecordAttachments when they fill out and submit the Record form, but I'd like to start uploading the files immediately so that I can show users the progress of each upload on the form page. Here's the code that lets users submit a Record with multiple RecordAttachments:

#app/models/record.rb
class Record < ActiveRecord::Base
  has_many :record_attachments
  accepts_nested_attributes_for :record_attachments

  # Ensure user has provided the required fields
  validates :title, presence: true
  validates :description, presence: true
  validates :metadata, presence: true
  validates :hashtag, presence: true
  validates :release_checked, presence: true

end

And the child element:

#app/models/record_attachment.rb  
class RecordAttachment < ActiveRecord::Base
  belongs_to :record
  validates :file_upload, presence: true

  # Before saving the record to the database, manually add a new
  # field to the record that exposes the url where the file is uploaded
  after_save :set_record_upload_url 

  # Use the has_attached_file method to add a file_upload property to the Record
  # class. 
  has_attached_file :file_upload

  # Validate that we accept the type of file the user is uploading
  # by explicitly listing the mimetypes we are willing to accept
  validates_attachment_content_type :file_upload,
    :content_type => [
      "video/mp4", 
      "video/quicktime"
  ]
end

Record Controller:

class RecordsController < ApplicationController
  # GET /records/new
  def new
    @record = Record.new
  end

  def create
    # retrieve the current cas user name from the session hash
    @form_params = record_params()
    @form_params[:cas_user_name] = session[:cas_user]
    @record = Record.create( @form_params )

    respond_to do |format|
      if @record.save
        if params[:record_attachments]
          params[:record_attachments].each do |file_upload|
            @record.record_attachments.create(file_upload: file_upload)
          end
        end

        flash[:success] = "<strong>CONFIRMATION</strong>".html_safe + 
          ": Thank you for your contribution to the archive."
        format.html { redirect_to @record }
        format.json { render action: 'show', 
          status: :created, location: @record }
      else
        format.html { render action: 'new' }
        format.json { render json: @record.errors, 
          status: :unprocessable_entity }
      end
    end
  end
end

My form looks like this:

<%= form_for @record, html: { multipart: true } do |f| %>
  <div class="field">
    <%= f.label :title, "Title of Material<sup>*</sup>".html_safe %>
    <%= f.text_field :title %>
  </div>
  <!-- bunch of required fields followed by multiple file uploader: -->
  <div id="file-upload-container" class="field">
    <%= f.label :file_upload, "Upload File<sup>*</sup>".html_safe %>
    <div id="placeholder-box">File name...</div>
    <%= f.file_field :file_upload, 
      multiple: true,
      type: :file, 
      name: 'record_attachments[]', 
      id: "custom-file-upload",
      style: "display:none"
    %>
    <span class="btn btn-small btn-default btn-inverse" id="file-upload-button" onclick="$(this).parent().find('input[type=file]').click();">Browse</span>
  </div>

  <!-- bunch of fields followed by submit button -->
  <%= button_tag(type: 'submit', class: "btn btn-primary blue-button submit-button") do %>
    <i class="icon-ok icon-white"></i> SUBMIT
  <% end %>
<% end %>

Given this setup, are others aware of any approaches that would allow me to start uploading the RecordAttachments as soon as the user selects them, rather than when they submit the Record form?

After talking this out with @ShamSUP below, here's what I'm thinking: On page load, check if user has any RecordAttachments for which the record_id is nil. If so, delete them. Then user selects one or more files. For each, save a row in the RecordAttachment table with the record_id set to nil. Then, if/when the user successfully submits their Record form, find all of the user’s RecordAttachments that have record_id == nil, and set the record_id of each to the current Record’s id.

Does this seem like a reasonable solution? Is there a better/easier method? I would be very grateful for any help others can offer on this question!

duhaime
  • 25,611
  • 17
  • 169
  • 224

2 Answers2

0

Javascript is unable to actually read the contents of files in the file input field, so they unfortunately can't be submitted via ajax while the user is trying to fill out the rest of the form. That doesn't mean that you can't perform some roundabout methods, though.

Something you could try is generating the record id when the form page loads, then including the id in a hidden input in the main form, and either put the file input in a separate form, or move them into a separate form with javascript after page load. In the same form as the file input, also include the record id that was generated.

<form id="main-form" enctype="multipart/form-data">
    <input type="hidden" name="recordid" value="123">
    <input type="text" name="title">

    .... etc fields
</form>
<form id="file-form" enctype="multipart/form-data" target="file_frame">
    <div id="placeholder-box">Select a file</div>
    <input type="file" multiple name="record_attachments">
    <input type="hidden" name="recordid" value="123">
</form> 

After you have the two forms, you can use javascript to submit the file-form to an iframe after the value changes, preventing page reload in submit.

<iframe name="file_frame" id="file_frame" src="path_to_upload_script" />

Some example javascript: More in-depth script here

$("input.[name=record_attachments]").change(function () {
    $("#file_form').submit();
});

Because you're including the hiden recordid field, you can use this to associate the two forms after the user completes the other portion.

Community
  • 1
  • 1
shamsup
  • 1,952
  • 14
  • 18
  • please note, I don't have much info for ruby-specifics, but this is how the client portion should work. The actual upload script I will be no help with, but hopefully this points you in the right direction. – shamsup Apr 28 '16 at 22:48
  • Thanks ShamSUP! Is it going to be threadsafe to assign record ids to Records on pageload? Also, won't malicious users be able to edit the recordid in the hidden field with something like Chrome devtools, thereby making RecordAttachments point to the wrong Record ids? Is it crazy to think I could save the RecordAttachments, pass their returned ids to the session hash for the current user, then upon completion of the Record form, update the record_id field of each RecordAttachment the user just uploaded? – duhaime Apr 28 '16 at 23:00
  • Session variables would definitely be the way to go, I completely spaced that for some reason. – shamsup Apr 28 '16 at 23:13
0

I ended up using ng-file-upload, an awesome file upload library for Angular.js, for this task. The full source is available in case others end up needing to save children before saving parent records, but the long and the short is that you'll want to write children to the db from Javascript, then once the parent is saved, update the parent_id element of each child.

duhaime
  • 25,611
  • 17
  • 169
  • 224