Adding multiple Images to a Rails Model with Paperclip

The Goal

I have been working on improving the admin section of a ski club website. The club sells ski trips online and they need to be able to create new trips from the admin panel. A trip has a title, description, date and price. For marketing reasons when the trip information is displayed on the website a number of pictures related to the trip are also displayed in a slideshow. The goal was to allow admins to add a new trip to the website (including the images) through a single form. Up until now, admins had to create the trip text data and then add the images separately through another form.

The Solution

The solution makes use of Paperclip (the awesome file attachment ruby gem) and the nested attribute functionality that has been added in rails 2.3. If you are new to these I strongly suggest watching these two screencasts from Railscasts: http://railscasts.com/episodes/196-nested-model-form-part-1 and http://railscasts.com/episodes/134-paperclip

To install paperclip add the following line to the environment.rb file and run rake gems:install.

config.gem "thoughtbot-paperclip", :lib => "paperclip"

The Models

Let’s start by generating the two models. I like to use the nifty_scaffold because it’s a quick way to get going. I will generate the model, controller and RESTful views. A trip has many images so we will need two models, but we don’t actually need the controller for the trip_image model:

script/generate nifty_scaffold trip title:string description:text
departure:date price:decimal

script/generate model trip_image caption:string trip_id:integer

Run the paperclip generator to add the fields to the trip_image model that will actually store the photo.

script/generate paperclip trip_image photo

This will create a migration file that adds the fields photo_file_name, photo_content_type and photo_file_size to the trip_images table.

Now let’s define the relationships between these two models. Here’s what trip.rb and trip_image.rb look like.

class Trip < ActiveRecord::Base

  has_many :trip_images, :dependent => :destroy

end

class TripImage < ActiveRecrod::Base

  belongs_to :trip
  has_attached_file :photo, :styles => { :small => "150x150>", :large => "320x240>" }
  validates_attachment_presence : photo
  validates_attachment_size : photo, :less_than => 5.megabytes

end

The new trip form looks something like this:

<% form_for @trip do |f| %>

  <%= f.error_messages %>

  <%= f.label :title %> <%= f.text_field :title %>

  <%= f.label :departure, "Departure Date"%> <%= f.date_select :departure %>

  <%= f.label :description %> <%= f.text_area :description %>

  <%= f.label :price %> <%= f.text_field :price %>
  
  <%= f.submit "Submit" %>

<% end %>

Up to now it’s been pretty standard stuff.

Nested Attributes

What we need to do is add some fields to the trip form that will let the user upload trip images. But trip_image and trip are two different models, so how do we make it all work in one form? That’s where nested attributes come in.

class Trip < ActiveRecord::Base

  has_many :trip_images, :dependent => :destroy

  accepts_nested_attributes_for :trip_images, :reject_if => lambda { |t| t['trip_image'].nil? }

end

This allows us to access the trip images straight from the trip object like this: @trip.trip_images. The :reject_if call makes sure that we do not store empty TripImage records – sometimes users will not add an image for the trip right away.

In the new and edit actions of the TripsController let’s initialize three new trip images. This will let the user upload up to three images for a trip at once. No other changes are needed in the controller! As you can see the create and update actions are completely standard.

def new
  @trip = Trip.new
  3.times {@trip.trip_images.build} # added this
end

def create
  @trip = Trip.new(params[:trip])
  if @trip.save
    flash[:notice] = "Successfully created trip."
    redirect_to @trip
  else
    render :action => 'new'
  end
end

def edit
  @trip = Trip.find(params[:id])
  3.times { @trip.trip_images.build } # ... and this
end

def update
  @trip = Trip.find(params[:id])
  if @trip.update_attributes(params[:trip])
    flash[:notice] = "Successfully updated trip."
    redirect_to @trip
  else
    render :action => 'edit'
  end
end

fields_for in the form

Finally, in the form add the actual form fields that will be used to upload the images using fields_for. Also edit the form_for call and mark the form as multipart so it can accept file uploads (this is an important step, because otherwise the images just won’t upload).

<% form_for @trip, :html => {:multipart => true} do |f| %>
  ...
  
  <% f.fields_for :trip_images do |builder| %>
  
  <% if builder.object.new_record? %>
    <%= builder.label :caption, "Image Caption" %> <%= builder.text_field :caption %>
    <%= builder.label :photo, "Image File" %> <%= builder.file_field :photo %> 
  <% end %>

<% end %>

The if builder.object.new_record? call allows us to use this same form for the edit action. It makes sure that if the trip already has some images stored the builder will not display fields for them.

Final Notes

My goal was to keep this example as simple as possible. The code above let’s a user add images to a trip, BUT:

1. The user can only add up to three images at once.

If she wants to add 5 images she will have to add three and then edit the trip to add another two. This could be solved by dynamically generating fields using javascript – see the Nested Attributes Railscast for more details.

2. The user cannot delete images.

Again, this is a minimal example. See the railscasts for inspiration on how to do this.

Resources

Rails Nested Attributes API

Railscast: Nested Model Form

Railscast: Paperclip