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