Multiple attachments with active_scaffold


admin

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_&lt;%= attachment.id %&gt;">&lt;%= attachment.filename %&gt;
&lt;%= link_to_remote "Remove", :url  =&gt; attachment_path(:id =&gt; attachment), :method =&gt; :delete, :html =&gt; { :title  =&gt; "Remove this attachment" } %&gt;</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 &lt;&lt; "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

&lt;% fields_for Attachment.new do |attachment| -%&gt;
 
	<label for="attachment_data">Attach Files:</label>
	&lt;%= attachment.file_field :data %&gt;
 
&lt;% end -%&gt;
<ul id="pending_files">
	&lt;% if @record.attachments.size &gt; 0 -%&gt;
	&lt;%= render :partial =&gt; "attachments/attachment", :collection =&gt; @record.attachments %&gt;
	&lt;% end -%&gt;</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 =&gt; true do |post|
  post.resources :attachments, :active_scaffold =&gt; 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)

 &lt;%= javascript_include_tag :defaults %&gt;
 &lt;%= active_scaffold_includes %&gt;

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

3 Responses

  1. Graveman Says:

    yop yop yop.. das hab ich auch immer gesagt

  2. Joshi Says:

    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?

  3. Alexwebmaster Says:

    Hello webmaster
    I would like to share with you a link to your site
    write me here preonrelt@mail.ru

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.