Posted by
flyerhzm
on
August 18, 2010
This is a flexible and reusable solution for multiple uploads, using STI model to save all the uploaded assets in one "assets" table and using polymorphic model to reuse "Asset" model in different uploadable models.
This is extracted from my answer of How do you design your model for multiple upload?
I have built several rails applications, most of them allow user to upload assets, such as images and videos. It's easy if there is only one or two assets to upload, but it makes your code in a mess to deals with many uploading assets.
Here I give you a flexible and reusable solution for multiple uploads by using STI and polymorphic model, the solution use paperclip for uploading, it's just an example, you can use other ways for uploading.
Image you are dealing with a system that allow user to upload different kinds of assets, for example, user can upload many images and a video for a Post, upload a logo for a Site, and upload many images for a Question. How do you design the models for such system?
I always use STI and polymorphic model because I save all the uploaded assets in assets table and reuse the Asset model.
For this case, I will define the Asset models as follows
class Asset < ActiveRecord::Base
belongs_to :assetable, :polymorphic => true
delegate :url, :to => :attachment
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 different uploadable models, here are Post, Site and Question.
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_video
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.url(:small) %>
<% end %>
This is a flexible and reusable solution.
Updated: thanks @reu for suggesting me to add a delegate url to attachment in Asset model.

Comments
Good article! Thanks.
If your write the edit action, or reuse the form in a partial, you cannot be sure that the logo is build, or the images are correctly built 4 times like you did in the new action.
So, IMHO, I prefer to build the relations (if not any) in the view, like this :
And also a single controller for all uploads that delegated specific extras to the Upload model wich implemented them diferently on each subtype.
It saved me a lot of work and feeled very DRY.
So your models simply become:
...and you get to do any extras you defined in the module for free:
Thanks
If you want to use the rails counter_cache, tt depends on what counter do you need. For example:
If you want to get the counter of all the assets, you can put the counter_cache in the model Asset
If you want get the counter for each asset, you can put the counter_cache in the subclass for model Asset with different counter name. To do this way, you have to change some codes, such as move the belongs_to from Asset to the subclass of Asset and others.
So now you can use @post.image.url(:small) in place of @post.image.attachment.url(:small).
Looking at the logs it seems to NULL out the original attachment's attachable_id and then insert the new one.
If you still have problem, feel free to contact me, we can build a practical example.
On rails 2.3.8, ruby 1.8.7.
I kept getting this error:
WARNING: Can't mass-assign these protected attributes: attachmentI changed to models to:
has_one :images, :as => :assetable, :class_name => "PostVideo", :dependent => :destroyI need help, somehow is not working for me. When I want to create a new Site I get
I checked that the class Site::Logo exists in assets.rb and that I defined that class_name at site.rb
Any idea what could be wrong?
i create for a model "work" this method in asset model:
and i have followed all others steps
but when i try to call http://localhost:3000/works/new i got this error:
but now i have another problem when i try to view the images rails retrive this error :
undefined method `to_f'
the code is:
24:
25: <%- @work.images.each do |image| %>
26: <%= image_tag image.url(:small) %>
27: <% end %>
28:
some helps??
Thanks
if in a form some of the attachment field is empty now the function "create" save a NULL record in the assets db table:
How can i avoid the system save an empty attachment record in db??
thanks
Davide
accepts_nested_attributes_for :images, :reject_if => proc { |attributes| attributes['name'].blank? }
which is the best method in your example to delete each single image when you are in edit view of the record?
thanks again!
Any thoughts as to why a file upload might be saved twice? Weird.
Commits to Assets twice; back to back. Uploads to S3 twice.
Thank's for the tip!
Say, I created a post instance at first. Later I try to update the post and upload the image. There is always a mass assignment issue when saving, "Cannot mass assign protected attribute: type".
Is anyone has the same issue as mine?
I use similar STI classes, assets with different styles in each subclasses.
(Note: I should change the name of the model, as my assets_controller index is conflicting with new assets pipelines)
I'm running into trouble in production mode, with cache_class set to true, my subclasses don't use correct styles... I tried to put styles inside a Proc, to get it run at instance level, but then again, it does not select the right Proc.
STI polymorphic assets are cool, but when it comes in production...
Thanks you! The site is really helpful.