Apr 30

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 =&gt; :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 =&gt; "edit" }
       format.xml  { render :xml =&gt; @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" %&gt or &lt%= 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.

Apr 30

This is an extension of an article I wrote on my Livejournal a couple years ago. Since many Rails developers work for small startups or are themselves starting up a business, I thought I’d update it with some reflections from the past couple years I’ve spent working in a scrappy startup.

Top 10 Startup Lessons

1. There is no better way to ensure attendance at a 7:00 AM conference call than to schedule it yourself, invite your employees, and discuss something important. At my startups, we held a daily briefing. It lasted 15 minutes. We got a lot done in 15 minutes.

2. Skype, GotoMeeting, Vonage, VirtualPBX, and other services are incredibly cheap ways to get big company communications services quickly. And they tend to just "work."

3. Accountants, Lawyers, and Benefits Administrators cost so much because they are worth it. Outsource everything that is not related to growing your business. Corollary: In the startup days, don’t buy anything you don’t absolutely need unless it makes strategic sense to do so. You generally don’t need recruiters, expensive server software, top of the line new machines, or a country club membership to woo clients. You do, however, need the best people you can get, tooling, basic processes, and the facility to encourage innovation.

4. Hire slowly, but fire quickly.

5. Get an Advisory Board Get one. Make sure they are successful people you can trust. You don’t have to know them terribly closely. FOAF is adequate.

6. You can start a business working half days. Whichever 12 hours you want.

7. Small business professional services is about babysitting. Employees, clients, partners, vendors, etc. They all need babysitting from time to time.

8. Everything you learned in B school was wrong, and it was also right. Because you see, it depends. I often found myself asked by my partners for the magic bullet they taught us in B school that would solve problem X. The magic bullet is a myth. They simply made sure we know how to think in B school. Or, at least, I think that was the point.

9. Don’t underestimate the power of your network. But don’t overestimate it either. Or, put another way, start a business, and you will quickly find out who you can count on. Don’t hold it against your friends if they can’t help you. After all, business is business. Friendship ought not be business (even though a version of it is the oldest business model around). And, don’t be suprised if you get a 6-figure contract from someone you had on your 5-th round call list. If that does happen, move that person up in your CRM system. Oh, and use a CRM system. Preferably a free one.

10. There is no reason to shower and change out of your pajamas for a 7AM conference call. Unless it’s a video iChat and you forgot.