What I’m showing here is how to make a multiple file attachment for one or more models (it’s DRY) that integrate with active scaffold. I build on the great contribution http://www.practicalecommerce.com/blogs/post/432-Multiple-Attachments-in-Rails by Brian Getting.
What I did is this:
- integrated the multiple attachment handling with active scaffold
- DRYed up the code
- removed the post limit
And it looks something like this:
Which is quite a change in the end. Be sure to have attachment_fu and active_scaffold in your plugins-forlder. Let’s go through the parts:
First some obvious prerequisites: generate the models and get the db going. Remember to configure your config/database.yml or your rake will chocke. ;-)
script/generate model post title:string description:text script/generate model attachment size:integer height:integer width:integer parent_id:integer attachable_id:integer position:integer content_type:string filename:string thumbnail:string attachable_type:string rake db:create rake db:migrate
Since I’m using local filestorage here, make sure there’s an uploads directory in public.
Now for the models themselves, set up the relations as follows
app/models/post.rb
has_many :attachments, :as => :attachable, :dependent => :destroy
app/models/attachment.rb
belongs_to :attachable, :polymorphic => true has_attachment :storage => :file_system, :path_prefix => 'public/uploads', :max_size => 1.megabyte
app/controllers/application.rb
protected def process_file_uploads(owner) params[:attachment].each do |key, value| # only process files that got a size (and thus are not nil or empty etc) next unless value.size > 0 attachment = Attachment.create({:uploaded_data => params[:attachment][key]}) owner.attachments << attachment end owner.save end
I moved process_file_uploads into the ApplicationController, so that it’s accessible from every model and controller that needs access to it. The checks for the valid params have been a bit bogus, and actually it did not work for the update in some cases. So I just test for size. If there’s a size, there’s a file, easy as pie. In the end I save the owner, something rails can’t live without, it seems.
app/controllers/posts_controller.rb I created this by hand (no need for a rails scaffold here)
active_scaffold :post do |config| config.columns = [:title, :description, :attachments] config.create.multipart = true config.update.multipart = true end def before_create_save(record) process_file_uploads(record) end def before_update_save(record) process_file_uploads(record) end
Well this code got pretty concise. I basically removed 30 lines of code and wrote it so active scaffold can take care of almost everything. I just used before_create_save and before_update_save to hook into the saving process and take care of the attachments. Like before, I had to tell the forms that they are multipart/formdata.
app/controllers/attachments_controller.rb
active_scaffold :attachments do |config| config.action_links.add 'send_myfile', :label => 'Download', :type => :record, :popup => true end def send_myfile attachment = Attachment.find(params[:id]) send_file(attachment.full_filename, :type => attachment.content_type, :filename => attachment.filename) end def destroy @attachment = Attachment.find(params[:id]) @attachment.destroy asset = @attachment.attachable end
Here the destroy is important. It’s the old way to do it, but works pretty swell.
app/views/attachments/_attachment.html.erb
<li id="attachment_<%= attachment.id %>"><%= attachment.filename %> <%= link_to_remote "Remove", :url => attachment_path(:id => attachment), :method => :delete, :html => { :title => "Remove this attachment" } %></li>
So here I have a file with again DRYed up content. Actually it’s mostly Brian’s code, but here it doesn’t need to be copied to each and every view that uses the multiple attachment functionality.
app/views/attachments/destroy.rjs
page.hide "attachment_#{@attachment.id.to_s}" page.remove "attachment_#{@attachment.id.to_s}" page << "if ($('attachment_data').disabled) { $('attachment_data').disabled = false };"
This also got a bit smaller, mainly because I don’t check for a max. amount of allowed uploads.
app/views/active_scaffold_overrides/_attachments_form_column.rhtml
<% fields_for Attachment.new do |attachment| -%> <label for="attachment_data">Attach Files:</label> <%= attachment.file_field :data %> <% end -%> <ul id="pending_files"> <% if @record.attachments.size > 0 -%> <%= render :partial => "attachments/attachment", :collection => @record.attachments %> <% end -%></ul> <script type="text/javascript"><!--mce:0--></script>
I created the active_scaffold_overrides folder, just like it’s recommended in the neat documentation of active scaffold. Here I provide the code (again, it’s mostly Brian’s, I just DRYd it up and moved it to a DRYer place) that active scaffold needs to display the upload functionality. I also just copied the MultSelector code, so there’s this “max” parameter left, I just feed it a high enough number, like 100.
Well that’s basically all, remember to put in the config/routes.rb:
map.resources :posts, :active_scaffold => true do |post| post.resources :attachments, :active_scaffold => true end map.resources :attachments
I use this funky new nested routing stuff here. :-)
remember to load the ajax and active_scaffold javascripts in app/views/layouts/application.html.erb (delete index.html from public)
<%= javascript_include_tag :defaults %> <%= active_scaffold_includes %>
and into public/javascripts/application.js append the following:
// -------------------------
// Multiple File Upload
// -------------------------
function MultiSelector(list_target, max) {
this.list_target = list_target;this.count = 0;this.id = 0;if( max ){this.max = max;} else {this.max = -1;};this.addElement = function( element ){if( element.tagName == 'INPUT' && element.type == 'file' ){element.name = 'attachment[file_' + (this.id++) + ']';element.multi_selector = this;element.onchange = function(){var new_element = document.createElement( 'input' );new_element.type = 'file';this.parentNode.insertBefore( new_element, this );this.multi_selector.addElement( new_element );this.multi_selector.addListRow( this );this.style.position = 'absolute';this.style.left = '-1000px';};if( this.max != -1 && this.count >= this.max ){element.disabled = true;};this.count++;this.current_element = element;} else {alert( 'Error: not a file input element' );};};this.addListRow = function( element ){var new_row = document.createElement('li');var new_row_button = document.createElement( 'a' );new_row_button.title = 'Remove This Image';new_row_button.href = '#';new_row_button.innerHTML = 'Remove';new_row.element = element;new_row_button.onclick= function(){this.parentNode.element.parentNode.removeChild( this.parentNode.element );this.parentNode.parentNode.removeChild( this.parentNode );this.parentNode.element.multi_selector.count--;this.parentNode.element.multi_selector.current_element.disabled = false;return false;};new_row.innerHTML = element.value.split('/')[element.value.split('/').length - 1];new_row.appendChild( new_row_button );this.list_target.appendChild( new_row );};
}
I have a WORKING example for download :-)


November 7th, 2008 at 16:33
yop yop yop.. das hab ich auch immer gesagt
Dezember 12th, 2008 at 08:11
Thanks a lot for sharing your wonderful contribution!
I followed all the steps but when I tried to access the main page, I just got blank screen.
By trial and error, I found that if I create posts.html.erb file in views directory and add the following it works fine. Is it supposed to work this way in your scheme or am I missing something?
März 3rd, 2009 at 13:11
Hello webmaster
I would like to share with you a link to your site
write me here preonrelt@mail.ru