Adding multiple images to a Rails model with paperclip
Posted on Feb 13 in General, Ruby on Rails, Tutorials | (26) Comments
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 %> <p> <%= f.label :title %> <%= f.text_field :title %> </p> <p> <%= f.label :departure, "Departure Date"%> <%= f.date_select :departure %><br/> </p> <p> <%= f.label :description %> <%= f.text_area :description %> </p> <p> <%= f.label :price %> <%= f.text_field :price %> </p> <p><%= f.submit "Submit" %> </p> <% 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? %>
<p>
<%= builder.label :caption, "Image Caption" %>
<%= builder.text_field :caption %>
</p>
<p>
<%= builder.label :photo, "Image File" %>
<%= builder.file_field :photo %>
</p>
<% 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 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.
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!
This line is in correct!
accepts_nested_attributes_for :trip_images, :reject_if => lambda { |t| t['trip_image'].nil? }
from models/tripe
hi Monica, thanks for this great post
@mathew and @anderson Thanks guys
@Dan the code works for me in the actual application… could you expand on why it is incorrect?
@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?
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
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
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.
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
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
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..
[...] http://sleekd.com/general/adding-multiple-images-to-a-rails-model-with-paperclip/ [...]
Hey, thanks, this was exactly what I needed, and it works like a charm!
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!)
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.
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…
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??
the validations now works was a blank space between “:” en “photo”… but not save yet
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
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.
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.
@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.
[...] 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 [...]
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
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
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.
Leave a Comment