Adding multiple images to a Rails model with paperclip
by Monica Olinescu on February 13, 2010
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
[sourcecode language='ruby']
class Trip < ActiveRecord::Base
has_many :trip_images, :dependent => :destroy
end
class TripImage < ActiveRecrod::Base
belongs_to :trip
has_attached_file :photo, :styles => { :small => “150×150>”, :large => “320×240>” }
validates_attachment_presence : photo
validates_attachment_size : photo, :less_than => 5.megabytes
end
[/sourcecode]
The new trip form looks something like this:
[sourcecode language='ruby']
<% 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 %>
[/sourcecode]
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.
[sourcecode language='ruby']
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
[/sourcecode]
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.
[sourcecode language='ruby']
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
[/sourcecode]
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).
[sourcecode language='ruby']
<% 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 %>
[/sourcecode]
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 he wants to add 5 images he 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.
27 comments
Great post – helped me with exactly what I was trying to do – now I just have to figure out the “show” part of it!
by Matthew on February 23, 2010 at 2:58 pm. #
This line is in correct!
accepts_nested_attributes_for :trip_images, :reject_if => lambda { |t| t['trip_image'].nil? }
from models/tripe
by Dan on March 11, 2010 at 10:58 am. #
hi Monica, thanks for this great post
by Anderson Marques on March 12, 2010 at 11:36 am. #
@mathew and @anderson Thanks guys
@Dan the code works for me in the actual application… could you expand on why it is incorrect?
by Monica Olinescu on March 12, 2010 at 12:32 pm. #
@Monica Olinescu
not sure y really, sorry, it just wouldn’t save the nested attribute to the database!
I did it this way:
}
accepts_nested_attributes_for :style_images,
:reject_if => proc { |a| a.all? { |k, v| v.blank?} #found this here http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
Can you display the image in the index view?
by Dan on March 15, 2010 at 10:08 pm. #
accepts_nested_attributes_for takes the code block provided through reject_if and passes in the attributes hash of the nested attribute (in our case the trip image). The code block is then executed for every nested attribute and if it returns true the record will be saved.
Here’s the call in accepts_nested_attributes_for that actually does the saving.
self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
I didn’t actually verify this, but I suspect that the trip_image['trip_image'] attribute is an object (a Paperclip object of sorts). That’s why I was calling .nil? instead of .blank? on it. Calling blank? on an object will also work, but it just doesn’t feel right.
The code you are pasting here checks that ALL the parameters of the trip image are not blank. But what if we want the user to be able to submit a trip image without the caption?
The code in the post works. One thing to keep in mind is that passing in a symbol for the attribute name will not work:
:reject_if => lambda { |t| t['trip_image'].nil? } # works
:reject_if => lambda { |t| t[:trip_image].nil? } # does not work
by Monica Olinescu on March 23, 2010 at 11:02 am. #
hi!
very nice tutorial! but I have a problem…
I have categories, pages and uploads to the pages. If I create a new page I can select a cagegory and upload data files (images, documents, ..). So I followed your tutorial step-by-step and now if I would like to add data files, they won´t be save in the database. Do you have an idea what could be the problem?
In the form I already have set the html to multipart true.
My new in pages_controller look like this:
def new
ml => @page }
@page = Page.new
4.times { @page.uploads.build }
@choice = Category.all
respond_to do |format|
format.html
format.xml { render
end
end
Thanks!
Matthias
by Matthias on April 14, 2010 at 9:00 pm. #
That’s a cool setup. I’m assuming it’s for a cms.
So if I understand this correctly you have
page has_many data_files
data_file belongs_to category
category has_many data_files
This means you would have a category_id field in the data_file model. This field is populated through the category drop-down.
This should work. Are you seeing any errors in the logs? Check the parameters that are being submitted and make sure the category_id is picked up.
by Monica Olinescu on April 15, 2010 at 9:49 am. #
Hi!
Yes its for a cms.
I have:
Page
belongs_to :category
has_many :uploads, :dependent => :destroy
Upload
belongs_to :page
has_attached_file :data
Category
has_many :pages
In the upload model I have a page_id
In the pages model I have a category_id
In the category model I have nothing.
Any idea?
Matthias
by Matthias on April 15, 2010 at 12:42 pm. #
ok, now i can upload data_files now
but, if I just give the data_file a name, and no upload, the name is saved in the database..
and if I just add a upload-file, it also works, and the file is not saved, but there should be an error with following for example “please enter data_file name first, than create” or something like that.
if data_file_name and upload-file is given it works good. nothing also works.
I have following validations:
validates_attachment_presence :data
and here I put proc instead of lambda (lambda didn´t work)
accepts_nested_attributes_for :uploads, :reject_if => proc { |attributes| attributes['data_file_name'].blank? }
Any idea for my errors? Sry for asking and asking over and over again
matthias
by Matthias on April 15, 2010 at 2:17 pm. #
sorry, I am to fast with posting
now I think it works.
I just added another validation to the upload model:
validates_attachment_presence :data
Do you have an idea, how I can arrange that all with pdf-documents? I think they can´t have thumbs or different sizes..
by Matthias on April 15, 2010 at 2:45 pm. #
[...] http://sleekd.com/general/adding-multiple-images-to-a-rails-model-with-paperclip/ [...]
by Rails multiple data uploader - Mattherick on April 26, 2010 at 12:57 pm. #
Hey, thanks, this was exactly what I needed, and it works like a charm!
by Juho Makkonen on July 29, 2010 at 10:54 am. #
A question related to this post and your previous post about jquery validation: is it possible to use AJAX validation with images? I’d like to check with AJAX if the image that the user is about to send is correct using paperclip validators (but the image should not be saved at this point if it passes the validation, only validated!)
by Juho Makkonen on July 30, 2010 at 3:59 am. #
Another question: have you tested this code with cucumber? For me it seems that attaching a file works fine, but when i try to create a new entry without an image it fails to paperclip validations. Thus, it seems that the reject_if thing is not working in tests, even though it works just fine in my application.
by Juho Makkonen on July 30, 2010 at 6:41 am. #
Hi Juho! Regarding your first question I don’t know if it’s possible to validate file attachments on the client side..
As for your second question – the reject_if call works in my application just fine, but it seems that a lot of people are having trouble with it… Perhaps the .nil? call should be replaced with .blank? Let me know if this works for you…
by Monica Olinescu on July 31, 2010 at 12:59 pm. #
Hi Monica, thanks for this tuto is great like all the tutos that you make… i run my site but not saved anything and a have to comments the lines of validates_attachment_presence and validates_attachment_size because fails with the next error: syntax error, unexpected ‘:’ any help??
by Roman on August 9, 2010 at 8:54 pm. #
the validations now works was a blank space between “:” en “photo”… but not save yet
by Roman on August 10, 2010 at 1:01 am. #
Good post but you shouldn’t constrain yourself to 3 images. The form can be made to handle as many or as few images as you need.
Check this example out: http://openmonkey.com/articles/2009/10/complex-nested-forms-with-rails-unobtrusive-jquery
and for rails3
http://stackoverflow.com/questions/1704142/unobtrusive-dynamic-form-fields-in-rails-with-jquery
by David Henner on August 17, 2010 at 12:55 am. #
Thanks for the links David! I’ll add them as further reading. As I explained in the conclusion, this example limits the number of images for simplicity – the point is to demonstrate how nested attributes work with multipart fields.
by Monica Olinescu on September 14, 2010 at 12:09 pm. #
Hi, excellent post. I’m using your code to make my User model accept multiple uploads, but I also want to do the processing the background like in this blog post: http://ezror.com/blog/index.shtml. Can the two things work together? Sorry if this is a newbie question, just starting out with Rails. I’ve been having a very hard trying to get anyone to answer this question.
by jon weber on October 15, 2010 at 4:21 am. #
@jon Thanks! That setup you are linking to should work with the code above because it’s just standard paperclip stuff. It’s a complicated setup though.
by Monica Olinescu on October 16, 2010 at 12:33 am. #
[...] I am reading this tutorial : http://sleekd.com/general/adding-multiple-images-to-a-rails-model-with-paperclip/ because i need Save the product images in a independent model. However when Product.create execute [...]
by Problem paperclip with ajax - Question Lounge on February 3, 2011 at 12:09 pm. #
hey so im developing this application now, and is it possible to replace images? so that when I edit a model using the web browser it changes the image instead of adding it? Thanks
by rohan on April 25, 2011 at 12:14 pm. #
You forgot to put an equal sign before fields_for. So
should be
Took me an hour or so to figure out why the fields won’t be printed
by Philipp P. on September 23, 2011 at 11:06 am. #
I had the same problem on reject_if. blank? does not work either, i had to test on photo.
I also tried to add a delete checkbox, in that case, i have not been able to use reject_if, i think it is impossible to keep it if you want to be able to delete your paperclip.
by Sylario on November 25, 2011 at 6:20 am. #
The snippet of accepts_nested_attributes_for didn’t work.
I found :all_blank from the api document, it’s short and works!
accepts_nested_attributes_for :classroom_images, reject_if: :all_blank
by kinopyo on January 12, 2012 at 2:47 am. #