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 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.

Resources

Rails Nested Attributes API

Railscast: Nested Model Form

Railscast: Paperclip

This article has 34 comments

  1. Matthew

    Great post – helped me with exactly what I was trying to do – now I just have to figure out the “show” part of it!

  2. Dan

    This line is in correct!

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

    from models/tripe

  3. Monica Olinescu

    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

  4. Matthias

    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
    @page = Page.new
    4.times { @page.uploads.build }
    @choice = Category.all
    respond_to do |format|
    format.html
    format.xml { render :xml => @page }
    end
    end

    Thanks!

    Matthias

  5. Monica Olinescu

    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.

  6. Matthias

    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

  7. 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

  8. 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..

  9. Pingback: Rails multiple data uploader - Mattherick

  10. Juho Makkonen

    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!)

  11. Juho Makkonen

    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.

  12. Monica Olinescu

    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…

  13. Roman

    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??

  14. Monica Olinescu

    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.

  15. jon weber

    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. :(

  16. Pingback: Problem paperclip with ajax - Question Lounge

  17. rohan

    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

  18. Philipp P.

    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 :)

  19. Sylario

    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.

  20. kinopyo

    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

  21. Winston Kotzan

    @Monica’s validation problem

    The accepts_nested_attributes_for validator does not work if TripImage is nil or empty (trip.trip_images.blank?==true)

    You need an additional validator in your Trip model that looks like this:

    validates_each :trip_images do |record, attr, value|
    record.errors[:base] << "You must attach a valid photo." if record.trip_images.blank?
    end

  22. carlin

    script/generate paperclip history photo
    don´t generate four lines:

    def self.up
    change_table :history do |t|
    t.attachment :photo
    end

    please help me….

  23. Chris

    Kinopyo, that worked for me as well with Rails 3. The rest of the tutorial worked fine with the exception of needing to add an = sign for

    is now

    Without it the form fields dont render.

  24. Luis Ramos

    All seems to be working well, I have the form to upload several files, I click “submit” and it says everything is ok… but then, it writes to the “trip” table but not to the “trip_image”… anyone had this problem?

  25. Pingback: Paperclip: uninitialized constant Article::ArticleImageCopyQuery CopyQuery | Question & Answer Tool for your Technical Queries,CopyQuery, ejjuit, query, copyquery, copyquery.com, android doubt, ios question, sql query, sqlite query, nodejsquery, dns q

  26. Pingback: Paperclip: uninitialized constant Article::ArticleImage | QueryPost.com

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>