AttachmentFu is a great plugin. It’s heavily used all over the place to upload images to a Rails app. It’s also good with other file types.
I had a requirement to provide users with the ability to set up an entity (call it a Batch), then upload a number of CSV files to that batch. I used AttachmentFu, FasterCSV, and some Multi-Model editing forms to make the trickery work.
Before starting, I set up the AttachmentFu plugin. If you have never done this before, Mike Clark’s Tutorial is a good place to start. Install the plugin and take note of how you have to set up your Attachment model.
Then, I had to set up a Multi-model editing form. If you haven’t done this before, then you should read Ryan Bates’ tutorial on setting up multiple models in one view. In this situation, I had to modify some of the steps because we’re dealing with File Attachments, not Task objects. Here is what I did:
First I created my “Batch” object.
1 | script/generate migration Batch user_id:integer name:string |
Then I created my “Attachment” object.
1 | script/generate migration Attachment |
I use Oracle. In order to make AttachmentFu work, I renamed “size” to “filesize”
1 2 3 4 5 6 7 | ... In 002_create_attachment.rb def self.up create_table :attachments do |t| t.integer :parent_id # foreign key to :batches t.string :content_type, :filename, :thumbnail t.integer :filesize, :width, :height end |
Then, in my Batch model, I added the Multi-Model handling code. My method differs from the Rails Recipe because you cannot update an attachment once you upload it, only delete it. In the Update action, for each attachment on the model, we check to see where there is an attribute containing the ID of the attachment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # If there is not an ID in an attribute, we delete the attachment. # ... In batch.rb has_many :attachments, :foreign_key => "parent_id", :dependent => :destroy validates_associated :attachments def new_attachment_attributes=(attrs) attrs.each { |attr| attachments.build(attr) } end def existing_attachment_attributes=(attrs) attachments.reject(&:new_record?).each do |attach| attachments.delete(attach) unless attrs[attach.id.to_s] end end def save_attachments attachments.each { |attach| attach.save(false) } end |
Then I set up the Attachment model to handle the AttachmentFu-ery.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Attachment < ActiveRecord::Base belongs_to :batch has_attachment :storage => :file_system # Or S3, or DB, or whatever validates_as_attachment ## Patch to make this AttachmentFu Model object work with Oracle. def size self.filesize end def size=(bytes) self.filesize = bytes end end |
Now I needed to build the view and controller support. To keep the app mostly RESTful, we let the standard “Attachment” controller be built by the scaffolding system. Since we will be primarily working with Attachments in the “Batch” view, though, I’ve omitted those details.
Here is the additional controller code in batch_controller.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # GET /batches/new def new @batch = Batch.new @batch.attachments.build end def update params[:batch][:existing_attachment_attributes] ||= {} @batch = Batch.find(params[:id]) respond_to do |format| if @batch.update_attributes(params[:batch]) flash[:notice] = 'Your Batch was updated successfully.' format.html { redirect_to batch_url(@batch) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @batch.errors.to_xml } end end end |
What’s left? Setting up the views. We leave the scaffolded “index” action for the Batch view in place. Attachments aren’t relevant in that view. But in the “Edit” and “New” views, we need to create a custom form that can handle attaching files in addition to setting up the Batch model object.
Step 1, make the Edit and New actions point to a shared form:
1 2 3 4 | <!-- Contents of views/batches/new.html.erb and views/batches/edit.html.erb --> <div class="greybox"><%= render :partial => |
Then, we set up the shared form in a partial like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <h1>Schedule A Batch</h1>
<!-- views/batches/_form.html.erb: error display code omitted -->
<% form_for(@batch, :html => { :multipart => true }) do |
The code above renders the primary Model object, and then renders the associated attachments using the “_attachment” partial. Let’s take a look at that code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <div id="attachments" class="attachment"><% new_or_existing = attachment.new_record? ? 'new' : 'existing' %>
<% prefix = "batch[#{new_or_existing}_attachment_attributes][]" %>
<% fields_for prefix, attachment do |
This sets the front end up to show the attached files, and provide a link to delete each on on the front end using javascript (unless we are being rendered in the “show” action. We’ll refactor that piece of bad juju later).
The last piece of work to do is to set up the “show” action to display the child records. This isn’t hard. There is no need to modify the default scaffolded view. Just add the partial to show the attachments:
1 2 | <-- Add to views/batches/show.html.erb --> <div id="attachments"><%= render :partial => "attachment", :collection => @batch.attachments%></div> |
That’s it. You still have an application that responds RESTfully to Batch and Attachment requests, but when you are a person in front of a browser, you can attach multiple files to your Batch object.
