8
votes

I set up a story model with an image attachment handled by Paperclip, that looks like:

class Story < ActiveRecord::Base  
  has_attached_file :image # [...]
  attr_accessible :user_id, :title, :image, :image_file_name
  belongs_to: user

  validates_presence_of :user_id
  validates :title,     :length => { :maximum => 50 }
  validates_attachment_size :image, :less_than => 2.megabytes, :unless => Proc.new { |story| story[:image].nil? }
  # [...] 
end

When I fill my story form, that looks like:

<%= form_for @story, html: { multipart: true } do |f| %>
<% if @story.errors.any? %>
<div id="error-explanation">
    <ul>
        <% @story.errors.full_messages.each do |msg| %>
        <li class="error-mess">Error: <%= msg.downcase %></li>
        <% end %>
    </ul>
</div>
<% end %>

<%= f.text_field :title %></td>
<%= f.file_field :image %>
<%= f.submit t('.send') %>

<% end %>

If validation fails for a story.title too long the form is redisplayed correctly along with the proper error message and the invalid title already filled in, but the file_field is now blank and I have to click again on it in order to re-select the file I want to upload.

And here is how my stories_controller.rb looks like:

def create
  @story = @current_user.stories.new(params[:story]) 
  if @story.save                                                                                                                   
    redirect_to thanks_path    
  else
    # [email protected] so I render action 'new' again just to
    # bang my head against this 'anomaly' 
    render action: "new"
  end              
end

How can I avoid users having to re-select the file to upload after a validation error?

4
I'm facing the same problem, did you find a workaround?? Thanks!yorch
Only a graphic one: when a picture is selected I give a UI feedback via jQuery. Given that my image field is generated by <%= f.file_field :image %> I can use the following jQuery to trigger it: $("#story_image").change(function() { $("#image_selected").show(300); }); Then in my view I have an hidden div "image_selected" which contains an eloquent gif... If you find a real solution, please, let me know!Darme

4 Answers

5
votes

The way HTTP file uploads work in browsers, the file has been uploaded to your app already on first submit – so you should store it somewhere, so that you still have access to it later on a second form submit. (At least in PHP an uploaded file get’s deleted after the script has run, if it’s not explicitly been moved somewhere else – I don’t know if that applies for RoR as well.)

You can’t pre-fill an input type=file field in HTML – for security reasons. And even if the user picks the file again, they would have to send it a second time – waste of user’s and your bandwidth.

So either store it somewhere on first submit, or try to do your validations on the client-side too with JavaScript before allowing submit (as far as possible), so that you minimize the form submits that actually fail validation on the server side.

2
votes

May be using https://github.com/bcardarella/client_side_validations will be suitable for you. It allows to perform form validating accordingly to rules defined in your model without reloading the page.

1
votes

CBroe is right, the best solution is to store the file temporarily. What I would do to do this is: - Move the file to a temp directory, and name it with the id of the user that was trying to upload it. - When the form is posted and there is no file uploaded, try to use the temp file for that user (if it exists). - If the Story is succesfully saved, delete any temp files for that user.

I think that should do the trick.

1
votes

I had to fix this on a recent project. 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: ''}