Uploading multiple images with paperclip

Firstly, I recommend not using the model name in the field name. Instead do file_etc. Also I recommend against using Attachment as your model name. I ran into this problem:

undefined method `quoted_table_name' for Paperclip::Attachment:Class

Also see, Advanced Rails Recipes, recipe #13 for help on this.

Ok let’s go.

Create the model.

1
script/generate scaffold Photo file_file_name:string  file_content_type:string file_file_size:integer

Migrate the database in terminal

rake db:migrate

Add the following to app/model/photo.rb

1
has_attached_file :file, :styles => { :medium => "300x300>", :thumb => "100x100>" }

Aside: This assumes you are setting this up under the admin folder for app administration purposes.

Create admin folder with photos controller inside

script/generate controller admin::photos

(If you get a warning like helpers/photos_helper.rb already exists then just delete that file, and run the generator again.)

Set up controllers/admin/photos_controller.rb to look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Admin::PhotosController < ApplicationController
  layout 'admin'
  before_filter :login_required
 
  # GET /photos
  # GET /photos.xml
  def index
    @photos = Photo.find(:all)
 
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @photos }
    end
  end
 
  # GET /photos/1
  # GET /photos/1.xml
  def show
    @photo = Photo.find(params[:id])
 
    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @photo }
    end
  end
 
  # GET /photos/new
  # GET /photos/new.xml
  def new
    @photo = Photo.new
 
    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @photo }
    end
  end
 
  # GET /photos/1/edit
  def edit
    @photo = Photo.find(params[:id])
  end
 
  # POST /photos
  # POST /photos.xml
  def create
    @photo = Photo.new(params[:photo])
 
    respond_to do |format|
      if @photo.save
        flash[:notice] = 'Photo was successfully created.'
        format.html { redirect_to(admin_photos_path) }
        format.xml  { render :xml => @photo, :status => :created, :location => @photo }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @photo.errors, :status => :unprocessable_entity }
      end
    end
  end
 
  # PUT /photos/1
  # PUT /photos/1.xml
  def update
    @photo = Photo.find(params[:id])
 
    respond_to do |format|
      if @photo.update_attributes(params[:photo])
        flash[:notice] = 'Photo was successfully updated.'
        format.html { redirect_to(admin_photos_path) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @photo.errors, :status => :unprocessable_entity }
      end
    end
  end
 
  # DELETE /photos/1
  # DELETE /photos/1.xml
  def destroy
    @photo = Photo.find(params[:id])
    @photo.destroy
 
    respond_to do |format|
      format.html { redirect_to(admin_photos_url) }
      format.xml  { head :ok }
    end
  end
end

Create the following view files under views/admin/photos/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# _form.html.erb
<%= f.error_messages %>
 
<p>
	<%= f.label :file %><br />
	<%= f.file_field :file %>
</p>
<p>
  <%= f.submit %> or <%= link_to 'Cancel', admin_photos_path %>
</p>
 
# edit.html.erb
<h2>Editing photo</h2>
 
<% form_for([:admin, @photo], :html => { :multipart => true }) do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
<% end %>
 
# index.html.erb
<h2>Photos <small>(<%= link_to 'Create new photo', new_admin_photo_path %>)</small></h2>
 
<table class="styledtable">
  <tr>
    <th>Photo</th>
		<th>Sizes</th>
    <th>Manage</th>
  </tr>
<% for photo in @photos %>
  <tr class="<%= cycle('even','odd') %>">
    <td><%= image_tag(photo.file.url(:thumb)) %></td>
		<td><%= link_to 'thumb', photo.file.url(:thumb) %>, <%= link_to 'medium', photo.file.url(:medium) %>, <%= link_to 'original', photo.file.url(:original) %></td>
    <td><%= link_to 'edit', edit_admin_photo_path(photo) %> | <%= link_to 'delete', [:admin, photo], :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>
 
 
# new.html.erb
<h2>New photo</h2>
 
<% form_for([:admin, @photo], :html => { :multipart => true }) do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
<% end %>
 
# show.html.erb
<p>
  <b>photo file name:</b>
  <%=h @photo.file_file_name %>
</p>
 
<p>
  <b>photo content type:</b>
  <%=h @photo.file_content_type %>
</p>
 
<p>
  <b>photo file size:</b>
  <%=h @photo.file_file_size %>
</p>
 
<%= link_to 'Back', admin_photos_path %>

Edit your config/routes.rb file so that photos can route to apps/controllers/admin/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  map.resources :photos
  map.resources :categories
  map.resources :manufacturers
  map.resources :users
  map.resource :session
  map.resources :line_items
  map.resources :orders
  map.resources :products
 
  map.namespace :admin do |admin|
    admin.resources :products
    admin.resources :orders
    admin.resources :manufacturers
    admin.resources :categories
    admin.resources :users
    admin.resource :session
    admin.resources :photos
  end

Add paperclip’s plugin functionality to the model photo.rb

1
2
3
class Photo < ActiveRecord::Base
	has_attached_file :file, :styles => { :medium => "300x300>", :thumb => "100x100>" }
end

Restart your server and test that you can add photos by going to http://localhost:3000/admin/photos/new

Did that work. Good. Now we can finally, move onto adding multiple images to another model. I am going to use the product.rb model and products_controller.rb to add these multiple images.

Add the belongs_to and has_many to the models.

1
2
# app/models/product.rb
has_many :photos
1
2
3
4
5
# app/models/photo.rb
  belongs_to :product
 
# paperclip
has_attached_file :file, :styles => { :medium => "300x300>", :thumb => "100x100>" }

Add references to the photos model

script/generate migration add_product_references_to_photos

And populate that file like so

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddProductReferencesToPhotos < ActiveRecord::Migration
  def self.up
    change_table :photos do |t|
      t.references :product
    end
  end
 
  def self.down
    change_table :photos do |t|
      t.remove_references :product
    end
  end
end
rake db:migrate

Then I am going to follow Railscast Episode 73: complex forms to add multiple images.
————————
UPDATE:
Use advanced Rails Recipes # 13

DON’T FORGET THE MULTIPART FOR UPLOADING IN YOUR FORM!!! Recipe #13 fails to mention this since it’s not dealing with images.

1
2
3
<% form_for([:admin, @product], :html => { :multipart => true }) do |f| %>
	<%= render :partial => 'form', :locals => { :f => f } %>
<% end %>

————————

Add photos.build to new action of apps/controllers/admin/products_controller.rb

1
2
3
4
5
6
7
8
9
10
# admin/products_controller.rb
def new
    @product = Product.new
    3.times { @product.photos.build }
 
    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @product }
    end
  end

Create _photo.html.erb partial under /app/views/admin/products/

1
2
3
4
5
6
7
# _photo.html.erb
<% fields_for "product[photo_attributes][]", photo do |photo_form| %>
	<p>
		<%= photo_form.label :file %><br />
		<%= photo_form.file_field :file %>
	</p>
<% end %>

Add collection partial to /app/views/admin/products/_form.html.erb

1
	<%= render :partial => 'photo', :collection => @product.photos %>

And that’s it. That will work.

Though there are still some unresolved issues. Take a look at Ryan’s Railscasts (except for episode 75. See Advanced Rails Recipe #13 instead.) on complex forms to add ajax, and also make sure you add dependent destroy to your model. Otherwise, when you delete a product the photos won’t be deleted.

Comments

  1. Andrew Herbert | August 31, 2008

    Great tutorial so far, but I’m getting one error when I try to edit a photo (before adding the photo to the products model). Any ideas on why this is popping up? Also, the label line is commented out because it was also throwing an error…

    `@admin#’ is not allowed as an instance variable name
    Extracted source (around line #5):

    2:
    3:
    4:
    5:
    6:

  2. Andrew Herbert | August 31, 2008

    In my last post, it stripped out the text inside the gt and lt.

    The error message should read
    @admin#<Photo:0×26c9470> is not allowed as an instance variable name

  3. Andrew Herbert | August 31, 2008

    Ignore those last two posts, I figured out the solution. Following recipe 13 now. Thanks.