module UploadProgress def self.append_features(base) #:nodoc: super base.extend(ClassMethods) base.helper_method :upload_progress, :next_upload_id, :last_upload_id, :current_upload_id end # == Action Pack Upload Progress for multipart uploads # # The UploadProgress module aids in the process of viewing an Ajax driven # upload status when working with multipart forms. It offers a macro that # will prepare an action for handling the cleanup of the Ajax updating including # passing the redirect URL and custom parameters to the Javascript finish handler. # # UploadProgress is available for all multipart uploads when the +upload_status_for+ # macro is called in one of your controllers. # # The progress is stored as an UploadProgress::Progress object in the session and # is accessible in the controller and view with the +upload_progress+ method. # # For help rendering the UploadProgress enabled form and supported elements, see # ActionView::Helpers::UploadProgressHelper. # # === Automatic updating on upload actions # # class DocumentController < ApplicationController # upload_status_for :create # # def create # # ... Your document creation action # end # end # # The +upload_status_for+ macro will override the rendering of the action passed # if +upload_id+ is found in the query string. This allows for default # behavior if Javascript is disabled. If you are tracking the upload progress # then +create+ will now return the cleanup scripts that will terminate the polling # of the upload status. # # === Customized status rendering # # class DocumentController < ApplicationController # upload_status_for :create, :status => :custom_status # # def create # # ... Your document creation action # end # # def custom_status # # ... Override this action to return content to be replaced in # # the status container # render :inline => "<%= upload_progress.completed_percent rescue 0 %> % complete", :layout => false # end # # The default status action is +upload_status+. The results of this action # are added used to replace the contents of the HTML elements defined in # +upload_status_tag+. Within +upload_status+, you can load the Progress # object from the session with the +upload_progress+ method and display your own # results. # # Completion of the upload status updating occurs automatically with an +after_filter+ call to # +finish_upload_status+. Because the upload must be posted into a hidden IFRAME to enable # Ajax updates during the upload, +finish_upload_status+ overwrites the results of any previous # +render+ or +redirect_to+ so it can render the necessary Javascript that will properly terminate # the status updating loop, trigger the completion callback or redirect to the appropriate URL. # # ==== Basic Example (View): # # <%= form_tag_with_upload_progress({:action => 'create'}, {:finish => 'alert("Document Uploaded")'}) %> # <%= upload_status_tag %> # <%= file_field 'document', 'file' %> # <%= end_form_tag %> # # ==== Basic Example (Controller): # # class DocumentController < ApplicationController # upload_status_for :create # # def create # @document = Document.create(params[:document]) # end # end # # ==== Extended Example (View): # # <%= form_tag_with_upload_progress({:action => 'create'}, {}, {:action => :custom_status}) %> # <%= upload_status_tag %> # <%= file_field 'document', 'file' %> # <%= submit_tag "Upload" %> # <%= end_form_tag %> # # <%= form_tag_with_upload_progress({:action => 'add_preview'}, {:finish => 'alert(arguments[0])'}, {:action => :custom_status}) %> # <%= upload_status_tag %> # <%= submit_tag "Upload" %> # <%= file_field 'preview', 'file' %> # <%= end_form_tag %> # # ==== Extended Example (Controller): # # class DocumentController < ApplicationController # upload_status_for :add_preview, :create, {:status => :custom_status} # # def add_preview # @document = Document.find(params[:id]) # @document.preview = Preview.create(params[:preview]) # if @document.save # finish_upload_status "'Preview added'" # else # finish_upload_status "'Preview not added'" # end # end # # def create # @document = Document.new(params[:document]) # # upload_progress.message = "Processing document..." # session.update # # @document.save # redirect_to :action => 'show', :id => @document.id # end # # def custom_status # render :inline => '<%= upload_progress_status %>
Updated at <%= Time.now %>
', :layout => false # end # # ==== Environment checklist # # This is an experimental feature that requires a specific webserver environment. Use the following checklist # to confirm that you have an environment that supports upload progress. # # ===== Ruby: # # * Running the command `ruby -v` should print "ruby 1.8.2 (2004-12-25)" or older # # ===== Web server: # # * Apache 1.3, Apache 2.0 or Lighttpd *1.4* (need to build lighttpd from CVS) # # ===== FastCGI bindings: # # * > 0.8.6 and must be the compiled C version of the bindings # * The command `ruby -e "p require('fcgi.so')"` should print "true" # # ===== Apache/Lighttpd FastCGI directives: # # * You must allow more than one FCGI server process to allow concurrent requests. # * If there is only a single FCGI process you will not get the upload status updates. # * You can check this by taking a look for running FCGI servers in your process list during a progress upload. # * Apache directive: FastCGIConfig -minProcesses 2 # * Lighttpd directives taken from config/lighttpd.conf (min-procs): # # fastcgi.server = ( # ".fcgi" => ( # "APP_NAME" => ( # "socket" => "/tmp/APP_NAME1.socket", # "bin-path" => "Rails.root/public/dispatch.fcgi", # "min-procs" => 2 # ) # ) # ) # # ===== config/environment.rb: # # * Add the following line to your config/environment.rb and restart your web server. # * ActionController::Base.enable_upload_progress # # ===== Development log: # # * When the upload progress is enabled by you will find something the following lines: # * "Multipart upload with progress (id: 1, size: 85464)" # * "Finished processing multipart upload in 0.363729s" # * If you are properly running multiple FCGI processes, then you will see multiple entries for rendering the "upload_status" action before the "Finish processing..." log entry. This is a *good thing* :) # module ClassMethods # Creates an +after_filter+ which will call +finish_upload_status+ # creating the document that will be loaded into the hidden IFRAME, terminating # the status polling forms created with +form_with_upload_progress+. # # Also defines an action +upload_status+ or a action name passed as # the :status option. This status action must match the one expected # in the +form_tag_with_upload_progress+ helper. # def upload_status_for(*actions) after_filter :finish_upload_status, :only => actions define_method(actions.last.is_a?(Hash) && actions.last[:status] || :upload_status) do render(:inline => '<%= upload_progress_status %>', :layout => false) end end end # Overwrites the body rendered if the upload comes from a form that tracks # the progress of the upload. After clearing the body and any redirects, this # method then renders the helper +finish_upload_status+ # # This method only needs to be called if you wish to pass a # javascript parameter to your finish event handler that you optionally # define in +form_with_upload_progress+ # # === Parameter: # # client_js_argument:: a string containing a Javascript expression that will # be evaluated and passed to your +finish+ handler of # +form_tag_with_upload_progress+. # # You can pass a String, Number or Boolean. # # === Strings # # Strings contain Javascript code that will be evaluated on the client. If you # wish to pass a string to the client finish callback, you will need to include # quotes in the +client_js_argument+ you pass to this method. # # ==== Example # # finish_upload_status("\"Finished\"") # finish_upload_status("'Finished #{@document.title}'") # finish_upload_status("{success: true, message: 'Done!'}") # finish_upload_status("function() { alert('Uploaded!'); }") # # === Numbers / Booleans # # Numbers and Booleans can either be passed as Number objects or string versions # of number objects as they are evaluated by Javascript the same way as in Ruby. # # ==== Example # # finish_upload_status(0) # finish_upload_status(@document.file.size) # finish_upload_status("10") # # === Nil # # To pass +nil+ to the finish callback, use a string "undefined" # # ==== Example # # finish_upload_status(@message || "undefined") # # == Redirection # # If you action performs a redirection then +finish_upload_status+ will recognize # the redirection and properly create the Javascript to perform the redirection in # the proper location. # # It is possible to redirect and pass a parameter to the finish callback. # # ==== Example # # redirect_to :action => 'show', :id => @document.id # finish_upload_status("'Redirecting you to your new file'") # # def finish_upload_status(client_js_argument='') if not @rendered_finish_upload_status and params[:upload_id] @rendered_finish_upload_status = true # erase_render_results # location = erase_redirect_results || '' ## TODO determine if #inspect is the appropriate way to marshall values ## in inline templates template = "<%= finish_upload_status({" template << ":client_js_argument => #{client_js_argument.inspect}, " template << ":redirect_to => #{location.to_s.inspect}, " template << "}) %>" render({ :inline => template, :layout => false }) end end # Returns and saves the next unique +upload_id+ in the instance variable # @upload_id def next_upload_id @upload_id = last_upload_id.succ end # Either returns the last saved +upload_id+ or looks in the session # for the last used +upload_id+ and saves it as the intance variable # @upload_id def last_upload_id @upload_id ||= ((session[:uploads] || {}).keys.map{|k| k.to_i}.sort.last || 0).to_s end # Returns the +upload_id+ from the query parameters or if it cannot be found # in the query parameters, then return the +last_upload_id+ def current_upload_id params[:upload_id] or last_upload_id end # Get the UploadProgress::Progress object for the supplied +upload_id+ from the # session. If no +upload_id+ is given, then use the +current_upload_id+ # # If an UploadProgress::Progress object cannot be found, a new instance will be # returned with total_bytes == 0, started? == false, # and finished? == true. def upload_progress(upload_id = nil) upload_id ||= current_upload_id session[:uploads] && session[:uploads][upload_id] || UploadProgress::Progress.new(0) end end