Search code examples
ruby-on-railscontrollercontroller-actions

How do Rails' Controllers work?


1.) Is it possible to create a controller action that does not directly interact with a model? (i.e. upload a file to be parsed, then added to a DB model)

2.) What is the controller's order of operation? I don't understand how the controller action both instantiates the view, and reacts to params variables inputted by the user.

Could somebody please explain this, thank you.

PART II - Routing a Model-less form

So for my current upload form, I have 2 actions, an upload action (takes a file from a user) that I would like routed to the parse_upload action (manipulates the file uploaded in the upload.html.erb view):

routes.rb:

::Application.routes.draw do
    devise_for :users
    resources :revenue_models do
        get 'upload', :on => :collection
        put 'parse_upload',:on => :collection
    end
    root :to => "home#index"
 end

actions:

# UPLOAD multiple files from an Exel Doc; integrate them accordingly
def upload
    @uploaded_doc = { :workbook => RubyXL::Parser.new }     
end
# Parse the uploaded file
def parse_upload
@worksheet = RubyXL::Parser.parse(params[:uploaded_doc]       
                  [:workbook]).worksheets[0].extract_data
end

upload.html.erb (I want this upload form to push its params to the parse_upload action)

<%= form_tag(:url => {:controller => "revenue_models", :action => "parse_upload"}, :html => {:method => "put", :multipart => true}) do %>
    <%= file_field(:uploaded_doc, :workbook) %>
<%= submit_tag("Upload") %>     
<% end %> 

Currently I am getting a routing error when I submit the file: No route matches [POST] "/revenue_models/upload"

I am assuming everything is working fine up to the point of routing from upload form to the [parse_upload] action. I tried following your MORE answer below, but due to the fact that in my case, I am not using a form centered around an existing model, I am a bit lost. Any clue what the issue is? Thanks in advance.


Solution

  • 1) Yes absolutely, a controller 'action' does NOT have to deal with a model, i.e.

    ThingController < ApplicationController
      def status
        @status = system("#{Rails.root}/lib/mystatusscript");
      end
    end  
    

    Actions are called when a URL hits the server, and the routing table is consulted, and a controller and action are determined. So if you put this in your routes.rb:

    match "/whatever" => "things#status"
    

    and type

    http://localhost:3000/whatever
    

    The status action in the ThingsController (app/controllers/things_controller.rb) will get called.

    What happens next, by default, because you've not told it to do anything else, rails will look for app/views/things/status.html.erb, and render it, i.e.:

    The stats is <%= @status %>
    

    But you can prevent that, and make rails do something else, possible examples:

    ThingController < ApplicationController
      def status
        @status = system("#{Rails.root}/lib/mystatusscript");
        render :js=>"$('#status_retreived').show();"
      end
    end  
    
    ThingController < ApplicationController
      def status
        system("#{Rails.root}/lib/do_something_server_side");
        render :nothing=>true
      end
    end  
    
    ThingController < ApplicationController
      def status
        @status = system("#{Rails.root}/lib/mystatusscript");
        render action=>:edit
      end
    end  
    

    ADDITIONAL

    Let's make a form and see what happens

    Say you have this in app/views/things/edit.html.erb:

    <%= form_for @thing do |f| %>
      <%= f.input :name %>
      <%= f.submit %>
    <% end %>
    

    Say you have these routes in routes.rb:

    get '/things/:id/edit' => 'things#edit'
    put '/things/:id/update' => 'things#update'
    

    And your controller has:

    def update
      @thing = Thing.find(params[:id])
      @thing.attributes = params[:thing]
      @thing.save
    end
    def edit
      @thing = Thing.find(params[:id])
    end
    

    So here is the flow, you hit your app with '/things/100/edit'

    The edit action is called , the instance variable @thing is set to the record who's id is 100. Then the edit.html.erb view is rendered, presenting you with an edit screen for the name field and a submit button.

    When you click 'submit', you will PUT to '/things/100/update'

    Because of the way the route was defined '/things/:id/update', when you get inside the update action, params[:id] will contain 100, AND params[:thing] will contain what was posted by the form, i.e. your params could contain:

    params[:thing][:name]
    params[:thing][:city]
    ....
    params[:thing][:zip]
    

    The ID is abstracted out into params[:id], and the form data is in params[:thing]

    MORE

    rails does a lot of automatic url generation for you, it's very smart about it, for example, in edit.html.erb, you have this:

    <%= form_for @thing do |f| %>
      <%= f.input :name %>
      <%= f.submit %>
    <% end %>   
    

    If you look at the HTML generated you'll see something like:

    <form id="edit_thing_100" method="put" action="/things/100/update"> 
    

    How did rails KNOW to do an update instead of a create? Because it checked @thing and noticed it had already been saved to the database prior, it's NOT a new record, so it must be an update.

    So in your view you typically create various URI's that get sent to the server via links , submit buttons, etc. When they are looked up in routes.rb, the appropriate action in the appropriate controller is called.

    FILE UPLOAD

    Is easier than you may think, first you need to add the file upload field AND change the form slightly:

    <%= form_for @thing do ,:html=>{:multipart=>true} |f| %>
      <%= f.input :name %>
      <%= f.file_field :upload %>
      <%= f.submit %>
    <% end %>   
    

    Now, when inside the update action you can do this:

    def update
      filename = params[:thing][:upload].original_filename
      filetype = params[:thing][:upload].content_type
      filedata = params[:thing][:upload].read
    
      File.open("#{Rails.root}/filestorage/#{filename}","wb") { |f| f.write(filedata) } 
    
      @thing = Thing.find(params[:id])
    
      @thing.attributes = params[:thing]
      @thing.uploadstoredin = "#{Rails.root}/filestorage/#{filename}"
      @thing.save
    end
    

    Because you made the form multipart, and you declared an attribute :upload as a file_field, when the params are posted, the :upload param has three extra methods (original_filename, content_type and read), Rails MAGIC!