Using Nested Attributes, Cocoon & Paperclip for Awesome File Uploads in Rails

by Luke Wright

June 23, 2015

Sometimes you’re presented with a need for a user to be able to upload a variety of files to be associated with some other data model. The best way to handle this in Rails is with Paperclip & nested attributes. Cocoon makes creating a great UX around it simple. This is a great combination for a variety of things including gallery images, file submission, CMS style admin panels, and any site where you need a user to upload a variety of files or photos.

The Problem

Recently, I was working on a site where teams of users needed to be able to upload photos, documents, and various other files. This presented a couple of challenges.


  1. Paperclip allows one file upload per model, our users would be uploading anywhere from one to dozens of files.

  2. The UX needed to be simple enough for users from dozens of countries to easily submit their documents in several categories.

The Solution

We used Paperclip to handle uploads in an elegant way, cocoon to allow users to easily vary the number of files they uploaded, and a new model for the uploads in conjunction with nested attributes to model the data properly and allow a variable number of uploads for each team.

We were already using Paperclip and S3 from AWS to handle team photos, so we wanted to continue using both for file submission. If you have not previously set up paperclip, check out their github for detailed instructions.

The Models

This method relies on two models, your main model to which you’re trying to attach the assets, and an AttachedAsset model, which will handle the assets. I’ll assume you’re already working with the first model, which we’ll call Team from here on.

Generate your asset model, team_id will be an integer column corresponding to whatever your parent class is, in our case Team.


rails g model attached_asset string:category integer:team_id

And then we need to have paperclip generate its fields for our assets.


rails g paperclip attached_asset asset

Now, migrate your database.

Creating an association between the models.

Our Teams will have many AttachedAssets and our AttachedAsset will belong to that team. We also need to tell our parent model in rails that it will be accepting nested attributes for the assets. Rails makes this kind of association a breeze.

In /models/attached_asset.rb


class AttachedAsset < ActiveRecord::Base
# important associates the asset with team
belongs_to :team
# Paperclip
has_attached_file :asset, :storage => :s3,
:s3_credentials => Proc.new{|a| a.instance.s3_credentials }, :s3_host_name => 's3-us-west-2.amazonaws.com'
# Validation of file size
validates_with AttachmentSizeValidator, :attributes => :asset, :less_than => 30.megabytes
# Allow users to upload any type of file, only use this if you need to!
do_not_validate_attachment_file_type :asset
def s3_credentials
{:bucket => "#{ ENV['S3_BUCKET'] }", :access_key_id => "#{ ENV['S3_ACCESS_ID']}", :secret_access_key => "#{ENV['S3_SECRET']}"}
end
end

In /models/team.rb


class Team < ActiveRecord::Base
# Our team can have many attached assets, destroy them if the team is destroyed
has_many :attached_assets, dependent: :destroy
# tell the model to accept the nested attributes for attached_assets
accepts_nested_attributes_for :attached_assets
end

The Forms (feat. Cocoon)

For the forms, we’re going to be using cocoon which handles the jQuery for dynamically adding additional form fields for assets or removing them. Cocoon can be a bit finicky in its class and file naming conventions, so be sure to pay attention to them and read the docs! They have numerous examples and it’s well documented.

Inside your Gemfile add the following:


gem "cocoon"

Add the following to your application.js


//= require cocoon

Our form will actually be split into two files, the parent form, and a partial containing the fields for the file upload.

Our parent form could be something simple like the following,


<%= form_for @team do |f| %>
<h3>Upload launch files</h3>
<div id="attached_assets">
<%= f.fields_for :attached_assets, AttachedAsset.new do |asset| %>
<%= render 'attached_asset_fields', :f => asset %>
<div class="links">
<%= link_to_add_association 'Add Another File', f, :attached_assets %>
</div>
<% end %>
</div>
<%= f.submit 'Upload Files'%>
<% end %>

Pay special attention to the AttachedAsset.new portion of the fields_for line, it isn’t documented widely, but we needed it in order to create the files.

Next, our partial _attached_asset_fields.html.erb


<div class="nested-fields" style="padding-bottom: 10px;">
<div class="field">
<%= f.label :category %>
<%= f.select :category, [
'Our Category',
'Another Category'
], :required => true %>
</div>
<div class="field">
<%= f.label :asset, "File to submit"%>
<%= f.file_field :asset, :required => true %>
</div>
<%= link_to_remove_association "Remove file", f %>
</div>

Finally, if you’re using Rails 4+ you’re probably using strong parameters. Getting the parameters right for nested attributes is hard. In addition to the file and any other fields you created, you need to include the id and destroy as permitted params.

So, for example, in our teams_controller.rb we had the following


def team_params
params.require(:team).permit(
:name,
attached_assets_attributes: [:id, :asset, :category, :_destroy]
)
end

Congrats! Your users should now be able to upload as many files as they please.

Viewing the files

You’ll want to create an instance variable and loop through the assets in your view.

So, for example, in our teams_controller.rb we could have the following if we wanted to view all uploads on the team index page.


def index
@uploads = AttachedAsset.order('team_id ASC')
end

And then in index.html.erb


<ul>
<% @uploads.each do |file| %>
<li>
<%=link_to file.team.name, file.team %> -
<%= link_to file.asset_file_name, file.asset.url, html_options = {download: ''} %>
</li>
<% end %>
</ul>

You can see that we can utilize the relationships we created. Rails knows what the AttachedAsset’s team is, and we use paperclip’s helpers to provide a convenient way to download the files.

Helpful Links