Rails 3.2 - Link to a File with an Additional AJAX action

March 19, 2012


Say you have a 'downloads' page with simple links to files -- but then you decide you want to update the database with some info about the file just downloaded (such as the name of the user who downloaded it, the number of times it's been downloaded, etc..).

There may be other ways of accomplishing this, but my thought was just to attach a jQuery $.post to each download link on the page. I was hoping to be able to do this via the Rails unobtrusive Javascript library, but it seems only to allow the link_to to be either :remote -- or not -- but not both.

In this example, 'attachments' are synonomous with files, and the AJAX call is replacing a row of a table listing the available downloads. This approach worked, but maybe someone can suggest a cleaner, more 'Railsy' approach...

Partial '_file_list.html.erb'
  <table class="table table-striped">
  <tr>
    <th>Name</th>
    <th>Uploaded by</th>
    <th>Folder</th>
    <th>Size</th>
    <th>Downloads</th>
    <th>Last Download</th>
    <th colspan="3" width="20%">Actions</th>
  </tr>

<% attachments.each do |a| %>
  <tr id="<%= "attachment_#{a.id}" %>" >
    <%= render  :partial => 'attachments/file_list_row', 
                :locals => {:a => a, :ret => ret, :rid => rid }  %>
  </tr>
<% end %>
</table>
Partial '_file_list_row.html.erb'
<%= link_to a.name, "/uploads/#{a.folder.directory}/#{a.path}", class: 'download_link', data: {id: a.id } %>
<%= a.uploader  %>
<%= a.folder.name %>
<%= a.size.nil? ? 'N/A' : number_to_human_size(a.size) %>
<%= a.download_ct %>
<%= a.downloader.nil? ? 'Never' : "by #{a.downloader} on #{a.last_downloaded_at.strftime("%m-%d-%Y")}" %>
<%= link_to 'Show', attachment_path(a, ret ? {:ret => ret, :rid => rid} : {}) %>
<% if a.uploader.id == current_user.id || admin? %>
      <%= link_to 'Edit', edit_attachment_path(a, ret ? {:ret => ret, :rid => rid} : {}) %>
    <% end %>

<% if a.uploader.id == current_user.id || admin? %>
       <%= link_to 'Destroy', attachment_path(a, ret ? {:ret => ret, :rid => rid} : {}), confirm: 'Are you sure?', method: :delete %>
    <% end %>

Note: the first column contains the link to the file. It has a class of "download_link" and a data-id of the file's id. Coffee script added to the layout
jQuery ->
  # handle downloads - update the file's download properties, then refresh the row
  $(".download_link").live "click", ->
    att_id = $(this).attr('data-id')
    myurl = "/attachments/" + att_id + "/handle_download"  
    settings =
      url: myurl
      type: 'POST'
      async: false
      processData: false
      dataType: 'html'
      contentType: 'text/javascript; charset=utf-8'
      beforeSend: (xhr, settings) ->
        xhr.setRequestHeader 'accept', '*/*;q=0.5,' + settings.accepts.script 
      success: (data) ->
        $("#attachment_" + att_id).html(data).effect("highlight", {}, 1500)  
      error: (textStatus) ->
        alert textStatus
    $.ajax settings
This just replaces the table row containing the downloaded file with updated HTML (and throws in a highlight effect). Note: The AJAX post worked fine on my localhost without the "async: false", but when I deployed to the live server, it failed until I set async to false.
'handle_download' action of 'Attachments' Controller
  # PUT /attachments/1/handle_download
  def handle_download
    @attachment.handle_download current_user
    render :partial => 'file_list_row', 
           :locals => { :a => @attachment, :ret => params[:ret], :rid => params[:rid] }
  end
In the attachment model...
  def handle_download(user)
    self.download_ct += 1
    self.last_downloaded_at = Time.now
    self.downloader = user
    self.save
  end

Edit

Do we really need AJAX? Well, if all we want to do is run some controller code before sending the download, the answer is no. We could accomplish the same thing by having our download links link to a controller action instead of to the files, then after running our other controller code, we could just redirect to the file. But if, as in the example above, we want to replace some content on the current page, I'm pretty sure we need AJAX. One other point to consider is that If the user has Javascript turned off, the stats won't get updated at all... For my purposes, that's not really a problem, but depending on the application, it could be a very strong reason to link to a controller action instead of using AJAX.

Feedback

Your feedback is welcome! If you find any errors in this post or have any additional pointers or insights, please take a moment to register and share your thoughts.



No Comments Yet

You must be logged in to comment

All Posts