How to persist file upload fields after a rails va

2019-01-13 20:14发布

I have form with multiple file uploads, The issue is when i am submitting the form and an validation error occurs, the file input field gets reset.

I basically wanted to persist those files inside the file input field for the complete process.

I have also gone through few links

How can I "keep" the uploaded image on a form validation error?

Please let me know what are the various options in such cases that one can follow.

6条回答
疯言疯语
2楼-- · 2019-01-13 20:44

Well - I thought of taking a different approach to this; Instead of temporarily storing the file on the server, why not serve it back to the client to be resubmitted when the user fixes the validation issues.

This might still need a bit of refinement but it's the general concept:

# in the controller - save the file and its attributes to params

def create
  # ...
  if params[:doc] # a regular file uploaded through the file form element
    # when the form re-renders, it will have those additional params available to it
    params[:uploaded_file] = params[:doc].read # File contents
    params[:uploaded_file_original_filename] = params[:doc].original_filename
    params[:uploaded_file_headers] = params[:doc].headers
    params[:uploaded_file_content_type] = params[:doc].content_type
  elsif params[:uploaded_file] # a file coming through the form-resubmit
    # generate an ActionDispatch::Http::UploadedFile
    tempfile = Tempfile.new("#{params[:uploaded_file_original_filename]}-#{Time.now}")
    tempfile.binmode
    tempfile.write CGI.unescape(params[:uploaded_file]) #content of the file / unescaped
    tempfile.close

    # merge into the params
    params.merge!(doc: 
       ActionDispatch::Http::UploadedFile.new(
                                :tempfile => tempfile,
                                :filename => params[:uploaded_file_original_filename],
                                :head => params[:uploaded_file_headers],
                                :type => params[:uploaded_file_content_type]
                           )
                 )

  end
  #...
  # params (including the UploadedFile) can be used to generate and save the model object
end


# in the form (haml)
- if !params[:uploaded_file].blank?
  # file contents in hidden textarea element
  = text_area_tag(:uploaded_file, CGI.escape(params[:uploaded_file]), style: 'display: none;') #escape the file content
  = hidden_field_tag :uploaded_file_headers, params[:uploaded_file_headers]
  = hidden_field_tag :uploaded_file_content_type, params[:uploaded_file_content_type]
  = hidden_field_tag :uploaded_file_original_filename, params[:uploaded_file_original_filename]
查看更多
贪生不怕死
3楼-- · 2019-01-13 20:59

I took a completely different approach to the other solutions on offer here, as I didn't fancy switching to CarrierWave or using yet another gem to implement a hack to get around this.

Basically, I define placeholders for validation error messages and then make an AJAX call to the relevant controller. should it fail validation I simply populate the error message placeholders - this leaves everything in place client side including the file input ready for resubmission.

Example follows, demonstrating an organisation with nested address model and a nested logo model (that has a file attachment) - this has been cut for brevity :

organisations/_form.html.erb

<%= form_for @organisation, html: {class: 'form-horizontal', role: 'form', multipart: true}, remote: true do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <p class='name error_explanation'></p>

  <%= f.fields_for :operational_address do |fa| %>
    <%= fa.label :postcode %>
    <%= fa.text_field :postcode %>
    <p class='operational_address postcode error_explanation'></p>
  <% end %>

  <%= f.fields_for :logo do |fl| %>
    <%= fl.file_field :image %>
    <p class='logo image error_explanation'></p>
  <% end %>
<% end %>

organisations_controller.rb

def create   
  if @organisation.save
    render :js => "window.location = '#{organisations_path}'"
  else
    render :validation_errors
  end
end

organisations/validation_errors.js.erb

$('.error_explanation').html('');
<% @organisation.errors.messages.each do |attribute, messages| %>
  $('.<%= attribute %>.error_explanation').html("<%= messages.map{|message| "'#{message}'"}.join(', ') %>");
<% end %>
查看更多
时光不老,我们不散
4楼-- · 2019-01-13 21:01

Carrierwave is a great tool for handling file uploads and can handle this for you

https://github.com/jnicklas/carrierwave#making-uploads-work-across-form-redisplays

查看更多
Lonely孤独者°
5楼-- · 2019-01-13 21:02

I had to fix this on a recent project using the Paperclip Gem. It's a bit hacky but it works. I've tried calling cache_images() using after_validation and before_save in the model but it fails on create for some reason that I can't determine so I just call it from the controller instead. Hopefully this saves someone else some time!

model:

class Shop < ActiveRecord::Base    
  attr_accessor :logo_cache

  has_attached_file :logo

  def cache_images
    if logo.staged?
      if invalid?
        FileUtils.cp(logo.queued_for_write[:original].path, logo.path(:original))
        @logo_cache = encrypt(logo.path(:original))
      end
    else
      if @logo_cache.present?
        File.open(decrypt(@logo_cache)) {|f| assign_attributes(logo: f)}
      end
    end
  end

  private

  def decrypt(data)
    return '' unless data.present?
    cipher = build_cipher(:decrypt, 'mypassword')
    cipher.update(Base64.urlsafe_decode64(data).unpack('m')[0]) + cipher.final
  end

  def encrypt(data)
    return '' unless data.present?
    cipher = build_cipher(:encrypt, 'mypassword')
    Base64.urlsafe_encode64([cipher.update(data) + cipher.final].pack('m'))
  end

  def build_cipher(type, password)
    cipher = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC').send(type)
    cipher.pkcs5_keyivgen(password)
    cipher
  end

end

controller:

def create
  @shop = Shop.new(shop_params)
  @shop.user = current_user
  @shop.cache_images

  if @shop.save
    redirect_to account_path, notice: 'Shop created!'
  else
    render :new
  end
end

def update
  @shop = current_user.shop
  @shop.assign_attributes(shop_params)
  @shop.cache_images

  if @shop.save
    redirect_to account_path, notice: 'Shop updated.'
  else
    render :edit
  end
end

view:

= f.file_field :logo
= f.hidden_field :logo_cache

- if @shop.logo.file?
  %img{src: @shop.logo.url, alt: ''}
查看更多
爱情/是我丢掉的垃圾
6楼-- · 2019-01-13 21:03

Created a repo with a example of using Paperclip on rails and mainting your files when validation error occurs

https://github.com/mariohmol/paperclip-keeponvalidation

查看更多
三岁会撩人
7楼-- · 2019-01-13 21:07

A workaround for this rather than an outright solution is to use client side validation so that the file isn't lost because the whole form persists.

The few users that don't have JavaScript enabled will lose the files between requests, but perhaps this % is so low for you as to make it an acceptable compromise. If this is the route you decide to go down I'd recommend this gem

https://github.com/bcardarella/client_side_validations

Which makes the whole process really simple and means you don't have to rewrite your validation in JavaScript

查看更多
登录 后发表回答