File uploads is something that is used in Web Applications all of the time. It is becoming somewhat of a “standard” feature in many applications. When using rails, there are a number of ways that this feature can be achieved. Originally, I used attachment_fu and followed a tutorial (which I have since lost) that described how to perform multi-file uploads. This worked wonders. However, attachment_fu has somewhat fallen behind, or perhaps is no longer even supported. I have since started using Paperclip, as it seems to be the most common file upload plugin used for Rails and is very well supported.
The second element of this tutorial is jQuery. jQuery is one of the most popular javascript library’s that is used on the web. Furthermore, it is even easier to use with rails since the upgrade to Rails 3 and the release of the jquery-ujs gem. It only seems natural that someone would want to use Paperclip and jQuery together, and this tutorial shows you how.
Requirements
- Rails 3
- Intermediate knowledge of Ruby on Rails
- Intermediate knowledge of jQuery and Javascript
Paperclip
The setup of Paperclip is almost standard, however we are going to modifying one or two things to allow for multiple file uploading. Firstly, install the gem.
#gemfile gem "paperclip" #console bundle install
We are going to call our Paperclip attachments “Assets”. Here is our setup process.
#generate scaffold rails g scaffold asset data_file_name:string data_content_type:string data_file_size:integer attachable:references #create_assets.rb migration #change t.references :attachable #to t.references :attachable, :polymorphic => true #add index after create_table add_index :assets, [:attachable_id, :attachable_type]
#asset.rb model
class Asset < ActiveRecord::Base
#path is generally set by default, but i had to set mine
has_attached_file :data, :url => "/assets/:id", :path => ":rails_root/public/system/assets/:id/:basename.:extension"
belongs_to :attachable, :polymorphic => true
#Set number to the Max Attachments allowed for owner
Max_Attachments = 5
Max_Attachment_Size = 2.megabyte
def url(*args)
data.url(*args)
end
def name
data_file_name
end
def content_type
data_content_type
end
def file_size
data_file_size
end
end
#in the model that is going to have the attachments added to it.
has_many :assets, :as => :attachable, :dependent => :destroy
accepts_nested_attributes_for :assets
validate :validate_attachments
def validate_attachments
errors.add_to_base("Too many attachments - maximum is #{Asset::Max_Attachments}") if assets.length > Asset::Max_Attachments
assets.each {|a| errors.add_to_base("#{a.name} is over #{Asset::Max_Attachment_Size/1.megabyte}MB") if a.file_size > Asset::Max_Attachment_Size}
end
class AssetsController < ApplicationController def show asset = Asset.find(params[:id]) #do security check here send_file asset.data.path, :type => asset.data_content_type end #this will be called via ajax/remote def destroy asset = Asset.find(params[:id]) @allowed = Asset::Max_Attachments - asset.attachable.assets.count @attachable = asset.attachable if asset.destroy respond_to do |format| format.html do if request.xhr? #get attachable item again to ensure we get the new asset list render :partial => "attachments", :collection => Attachable.find(@attachable.id).assets end end end else respond_to do |format| format.html do if request.xhr? render :json => asset.errors end end end end end end
In the forms:
#the file field to add a new one. Disabled if there are already the max number of attachments <div id="attachment_fields"> <% if @attachable.assets.count >= Asset::Max_Attachments %> <input id="newfile_data" type="file" disabled /> <% else %> <input id="newfile_data" type="file" /> <% end %> </div> #the list of pending attachments <p>Pending Attachments: (Max of <%= Asset::Max_Attachments %> each under <%= Asset::Max_Attachment_Size/1.megabyte%>MB)</p> <div id="attachment_list"> <ul id="pending_files"></ul> </div> #the list of already attached files (required for edit form only) <p>Attached Files:</p> <div id="attachment_list"> <%= render :partial => "attachment", :collection => @attachable.assets %> </div>
The attachment partial:
<% if !attachment.id.nil? %> <li id="attachment_<%=attachment.id %>"> <a href="<%=attachment.url %>"> <%=attachment.name %> </a> (<%=attachment.file_size/1.kilobyte %>KB) #only needed if in edit action <% if action_name == "edit" %> <%= link_to "Remove", asset_path(:id => attachment), :method => :delete, :confirm => "Are you sure you want to delete " + attachment.name + "?", :remote => true, :class => 'remove_file' %> <% end %> </li> <% end %>
Okay so that almost it. The last thing we need to add is the javascript to remove a Asset via ajax, handle the file field change event and also remove a non-uploaded file.
$(document).ready(function(){
//change form button text on submit. Handy for general usage.
$('form').submit(function(){
$('input[name="commit"]', this).attr('value', "Submitting...");
$('input[name="commit"]', this).attr('disabled', 'disabled');
});
//ajax for removing the file from the list of attachments
$('.remove_file')
.live("ajax:beforeSend", function(evt, xhr, settings){
var $form = $('.attachable_form');
var $submitButton = $form.find('input[name="commit"]');
$submitButton.data('origText', $submitButton.attr('value'));
$submitButton.attr('value', "Waiting...");
$submitButton.attr("disabled", true);
})
.live("ajax:success", function(evt, data, status, xhr){
$('#attached_list').html(xhr.responseText);
})
.live("ajax:complete", function(evt, xhr, status){
//always set form class to 'attachable_form' when they have an asset
var $form = $('.attachable_form');
var $submitButton = $form.find('input[name="commit"]');
$submitButton.attr('value', $submitButton.data('origText'));
$submitButton.attr("disabled", false);
})
.live("ajax:error", function(evt, xhr, status, error){
var $form = $('.attachable_form'),
errors,
errorText;
try {
// Populate errorText with the comment errors
errors = $.parseJSON(xhr.responseText);
} catch(err) {
// If the responseText is not valid JSON (like if a 500 exception was thrown), populate errors with a generic error message.
errors = {message: "Please reload the page and try again"};
}
// Build an unordered list from the list of errors
errorText = "There were errors with the submission: \n<ul>";
for ( error in errors ) {
errorText += "<li>" + error + ': ' + errors[error] + "</li> ";
}
errorText += "</ul>";
// Insert error list into form
$('div#errorExplanation').html(errorText);
});
$('input:file').live('change', function(){
var index = $('#pending_files').children().size();
var totalAssets = index + $('#attachment_list').children().size() + 1;
if (totalAssets < 5)
{
$('#attachment_fields').prepend("<input type='file' id='newfile_data' />");
}
else
{
$('#attachment_fields').prepend("<input type='file' id='newfile_data' disabled />");
}
$(this).css('position', 'absolute');
$(this).css('left', '-1000px');
$(this).attr('name', 'attachment[file_' + index + ']');
var fileText = "<li>" + $(this).val() + " <a href='#' class='remove_pending' title='Remove this attachment'>Remove</a></li>";
$('#pending_files').prepend(fileText);
});
$('.remove_pending').live('click', function(){
var thisIndex = $('#pending_files').index($(this).parent());
var position = thisIndex++;
$('#attachment_fields').children().eq(position).remove();
$(this).parent().remove();
var totalAssets = $('#pending_files').children().size() + $('#attachment_list').children().size();
if (totalAssets < 5)
{
$('#attachment_fields input:first-child').removeAttr('disabled');
}
return false;
});
});
To create this, I read a bunch of tutorials on the many different aspects of rails including the following:
- Multiple attachments with Validations In Rails with Paperclip
- Rails 3 Remote Links and Forms: A Definitive Guide
I hope that this post has helped you out!
Thanks,
Cameron