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 => 'form' %></div> |
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 f %>
<strong>Name:</strong> <%= f.text_field :name %>
<div id="attachments"><%= render :partial => "attachment", :collection =>
@batch.attachments%></div>
<%= add_attachment_link "Add a File" %>
Total Line Count: <%= @batch.prod_line_count %>
<input name="batch[user_id]" type="hidden" value="<%= @user.id %>" />
<%= submit_tag "Save Changes" %> or <%= link_to 'Cancel', batches_path %>
<% end %> |
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 af -%>
<strong>Attached File:</strong>
<% if attachment.new_record? %>
<%= af.file_field :uploaded_data %>
<% else %>
<%= af.hidden_field :id %>
<%= attachment.filename %>
<%= link_to_function "remove", "$(this).up('.attachment').remove()" unless params[:action] == "show"%>
<% end %>
<% end -%>
</div> |
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.