Ruby on Rail, Carrierwave - File Upload fields disappear on form validation failure

Viewed 494

I have been beating my head into the wall for the last week trying to figure this problem out.

I have a Ruby on Rails application that has a form submission that has many file uploads as well as input fields. Upon successful submission the form info is saved to a Postgres DB and an email is sent out with the submitted info and the attached files. This works correct if either I disable validations or if the form doesn't fail an of the validations, however if a validation fails a problem crops up.

Fields on LoadDocuments SelectedMissing/Disabled Fields

If a file is selected and then the submit button is clicked and the form fails validations some of the file fields will either by disabled or completely deleted from the html. It will also rearrange the field fields from the original. For example Additional Document(s) is upload49, but after failed validation the file field is now upload25.

I have tried every Google search I can think of to track down the problem. The closest that I have found is the problem with AJAX and file uploads not being allowed by default for security reasons, which is why Carrierwave is required. I noticed that when the error 'ajax:aborted:file' was occurring this was what was deleting the file fields. The other thing I found is that when the submit button is pushed and the form is being processed that all file fields are temporarily disabled and I can only assume that somewhere something was breaking that was preventing them being re-enabled when validations failed.

If I added a listener for 'ajax:aborted:file' and had it return false it would stop the file fields from being messed up, but it would cause the form submission and validations to stop working.

Any help would be extremely appreciated. Let me know if you have any questions or need more code.

Here are some code snippets.

# uploaders/AttachmentsUpload.rb
class AttachmentsUploader < CarrierWave::Uploader::Base
  # Choose what kind of storage to use for this uploader:
  # storage :fog
  storage :file # for local testing, saving to S3 in production

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
end

# models/buyer_under_contract.rb
class BuyerUnderContract < ApplicationRecord
  include AttributeOption
  belongs_to :user
  belongs_to :team
  mount_uploader :upload1, AttachmentsUploader
  mount_uploader :upload2, AttachmentsUploader
  mount_uploader :upload3, AttachmentsUploader
  mount_uploader :upload4, AttachmentsUploader
  mount_uploader :upload5, AttachmentsUploader
  mount_uploader :upload6, AttachmentsUploader
  ...
  ...
  mount_uploaders :upload49, AttachmentsUploader

  after_create :send_notification

  # Agent Info Validations
  validates :location, :agent_fname, :agent_lname,
            :agent_email,
            presence: true
  validates :prior_contract,
            presence: true,
            if: :prior_contract_status?
  validates :previous_transaction_coordinator,
            presence: true,
            if: :previous_contract?
  ....
  ....
  ....

  def send_notification
    NotificationMailer.buyer_under_contract_form(self).deliver
  end
end

# controllers/buyer_under_contracts.rb
class BuyerUnderContractsController < ApplicationController
  include ListItemsController
  responders :ajax_modal, :collection
  respond_to :js, only: %i[index new create edit update]
  respond_to :html, only: [:index]
  load_and_authorize_resource

  before_action :restrict_users, only: %i[new create edit update]

  def new
    list_item.user = current_user
    respond_with(list_item)
  end

  def create
    puts '@create' + list_item.inspect
    created = list_item.save

    if created
      redirect_to URI(request.referer).path
      return
    end
    puts '@errors' + list_item.errors.inspect
    respond_with(list_item)
  end

  def update
    updated = list_item.update_attributes(resource_params)

    return if resource_params[:_update_type] == 'inline'

    if updated
      target = URI(request.referer).to_s
      redirect_to target
      return
    end
    respond_with(list_item)
  end

  def buyer_under_contract_params
    numeric = %i[location zip mls purchase_price broker_commission_percentage
                 broker_commission_fee buyer_transaction_fee]

    params[:buyer_under_contract].each do |param, value|
      if param.to_sym == :communication_pref
        value.delete_if { |x| x == '' }
        params[:buyer_under_contract][param] = value.join(', ')
      end
      if param.to_sym == :documents_present
        value.delete_if { |x| x == '' }

        params[:buyer_under_contract][param] = value.join(', ')
      end
      next unless numeric.include?(param.to_sym)
      params[:buyer_under_contract][param] = value.gsub(/[^0-9.]/, '')
    end

    # I know this is bad practice, was having problems with 
    # unpermitted parameters even though I permitted them,
    # just for testing now.
    params.require(:buyer_under_contract).permit!
  end


# views/_form.html.haml
= simple_form_for(@buyer_under_contract, remote: true) do |form|
  -if @buyer_under_contract.errors.any?
    .modal-header.error-container
      -@buyer_under_contract.errors.messages.each do |attr, _msg|
        .row
          =I18n.t(:"simple_form.labels.buyer_under_contract.#{attr}") + " is required"

  .modal-body.container
    %div.hidden.container
      = form.input :user_id, value: User
    %div.agent_info.container
      %h2 Agent Information
      %div.container
        %h3.form *Location
        = form.input :location, placeholder:'Location', label:false, collection: Team.all, prompt: 'Location', wrapper_html: {class: 'col-sm-5'}, input_html: { data: { placeholder: 'Location', 'allow-clear': false } }
        Select the city your office is located in
      %div.container
        %h3.form Agent Name (Your Name)
        = form.input :agent_fname, label:'First', wrapper_html: { class: 'col-sm-5' }
        = form.input :agent_lname, label:'Last', wrapper_html: { class: 'col-sm-5' }
     ...
     ...
     ...
     %div.documents.container(id = 'doc_uploads')
        %h3.form Documents Upload(s)
        %div.container.hidden(id = 'doc_addendum_purchase_agree_arbitration')
          %h5 Addendum to Purchase Agreement/Arbitration
          = form.file_field :upload1, multiple: false, wrapper_html: { class: 'col-sm-6'}
        %div.container.hidden(id = 'doc_addendum_purchase_agree_cic')
          %h5 Addendum to Purchase Agreement/CIC (if applies)
          = form.file_field :upload2, multiple: false, wrapper_html: { class: 'col-sm-6'}
        %div.container.hidden(id = 'doc_addendum_purchase_agree_inspect')
          %h5 Addendum to Purchase Agreement/Inspections
          = form.file_field :upload3, multiple: false, wrapper_html: { class: 'col-sm-6'}
        ...
        ...
        %div.container.hidden(id = 'doc_additional')
          %h5 Additional Documents
          = form.file_field :upload49, multiple: true, wrapper_html: { class: 'col-sm-6'}


  .modal-footer
    = form.button :submit, class: "btn btn-success contract_submit_button", data: {disable_with: "Please wait…"}, value: 'Submit'
    = link_to "Cancel", "#", class: "btn btn-danger", data: {dismiss: "modal"}, type: "button"

# assets/buyer_under_contract.js.coffee
# Some complicated JS to hide and unhide fields, but no 
# disabling of fields, I can attach if it's needed

# mailers/notification_mailer.rb
class NotificationMailer < ApplicationMailer
  default from: "info@test.com"

  def buyer_under_contract_form(submission)
    @submission = submission
    @user = User.find_by_id(@submission.user_id)
    @address = @submission.address
    @location = Team.find_by_id(@submission.location)
    puts 'address: ' + @address.to_s
    puts 'user: ' + @user.to_s
    puts 'location: ' + @location.to_s

    attachments.inline['white-logo.png'] = File.read('app/assets/images/white-logo.png')
    (1..49).to_a.each do |value|
      begin
        if @submission.send("upload#{value}").is_a?(Array)
          @submission.send("upload#{value}").each do |attachment|
            file_url = attachment.to_s
            next if file_url == ''
            puts 'file url: ' + file_url
            puts "#{Rails.root}/public/#{file_url}"
            attachments.inline[attachment.filename] = File.read("#{Rails.root}/public/#{file_url}")
          end
        else
          file_url = @submission.send("upload#{value}").to_s
          next if file_url == ''
          puts 'file url: ' + file_url
          puts "#{Rails.root}/public/#{file_url}"
          attachments.inline[@submission.send("upload#{value}").filename] = File.read("#{Rails.root}/public/#{file_url}")
        end
      rescue
        puts 'Mailer/Attachment Failure'
        puts $!.message
        next
      end
    end
    mail(to: "testuser@example.com",
         subject: "New Buyer Closing File at #{@address} - #{@location}",
         reply_to: @user.email
    )
  end
end
1 Answers

Had the same problem and spent days to fix it. But when i found out the solution i was disappointed a bit.

If you're using jquery_ujs instead of rails-ujs try changing it to rails-ujs in your application.js.

And probably its too late to answer but there would be someone else having the same issue.

Related