How do you design your model for multiple upload?

questioner Posted by questioner on August 16, 2010

I am designing a system that contains a lot of upload assets, such as model Post has_one Video and/or has_many Images, model Site has_one Logo and model Question has_many Images.

How do you design the models for such system? I don't care which upload plugin you use.

Answers

eric Answered by eric on August 16, 2010

You should use the nested form functionality that Rails provides. Then you can use whatever plugin you want (I prefer paperclip myself).

flyerhzm Answered by flyerhzm on August 17, 2010

I always use STI + polymorphic model for multiple uploads with paperclip.

I use STI and polymorphic model because I save all the uploaded assets in assets table and reuse the Asset model.

For your case, I will define the Asset models as follows

class Asset < ActiveRecord::Base
  belongs_to :assetable, :polymorphic => true
end

class Post::Video < Asset
  has_attached_file :attachment, :processors => [:flash], :styles => {:default => ["400x300>", "flv"]}
end

class Post::Image < Asset
  has_attached_file :attachment, :styles => { :small => "200x150>", :large => "400x300>" }
end

class Site::Logo < Asset
  has_attached_file :attachment, :styles => { :default => "64x64>" }
end

class Question::Image < Asset
  has_attached_file :attachment, :styles => { :small => "200x150>", :large => "400x300>" }
end

As you seen, all the uploaded assets (including video and images of post, logo of site and images of question) are saved in assets table by STI model. The video upload is not supported by paperclip, you should define your own processor to process the video. The assets table definition is like

create_table :assets, :force => true do |t|
  t.string   :type
  t.integer  :assetable_id
  t.string   :assetable_type
  t.string   :attachment_file_name
  t.string   :attachment_content_type
  t.integer  :attachment_file_size
  t.datetime :attachment_updated_at
end

The column type is for STI, so you can save Post::Video, Post::Image, Site::Logo and Question::Image in one assets table, and the assetable_id and assetable_type are for polymorphic, so you can reuse the Asset model in Post, Site and Question models.

So the relationships between asset and post, site, question are polymorphic as follow

class Post < ActiveRecord::Base
  has_one :video, :as => :assetable, :class_name => "Post::Video", :dependent => :destroy
  has_many :images, :as => :assetable, :class_name => "Post::Image", :dependent => :destroy

  accepts_nested_attributes_for :video, :images
end

class Site < ActiveRecord::Base
  has_one :logo, :as => :assetable, :class_name => "Site::Logo", :dependent => :destroy

  accepts_nested_attributes_for :logo
end

class Question < ActiveRecord::Base
  has_many :images, :as => :assetable, :class_name => "Question::Image", :dependent => :destroy

  accepts_nested_attributes_for :images
end

Be attention that I add the accepts_nested_attributes_for for models who needs to upload assets so that I can easily to create or update object and assets with nested form.

<%= form_for @post, :html => {:multipart => true} do |form| %>
  <p>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </p>
  <%= form.fields_for :video do |video_form| %>
    <p>
      <%= logo_form.label :video %>
      <%= logo_form.file_field :attachment %>
    </p>
  <% end %>
  <%= form.fields_for :images do |image_form| %>
    <p>
      <%= image_form.label :image %>
      <%= image_form.file_field :attachment %>
    </p>
  <% end %>
  <p>
    <%= form.submit %>
  </p>
<% end %>

Before you handle the form, be sure you have build assets objects, such as build a logo for post object and build 4 images for post object

def new
  @post = Post.new
  @post.build_logo
  4.times do
    @post.images.build
  end
end

Then you can save the post object with uploaded assets as normal

def create
  @post = Post.new(params[:post])
  if @post.save
    redirect_to post_path(@post)
  else
    render :action => :new
  end
end

Here is the way to display the images for post

<%- @post.images.each do |image| %>
  <%= image_tag image.attachment.url(:small) %>
<% end %>

This is a flexible and reusable solution.