Using the form_for Method with multipart in Rails

If you’re going to upload files via an HTML form, the form’s encoding must be set to multipart/form-data. In Rails, you use a helper method to generate the form tags, so you can’t directly edit them. The preferred helper is form_for; that’s what scaffold generates.

Normally, a form_for call would look like this:

<% form_for(@entry) do |f| %>
  <!-- Form fields -->
<% end %>

To make this form be multipart, change the call to the following:

<% form_for(@entry, :html => {:multipart => true}) do |f| %>
  <!-- Form fields -->
<% end %>

Passing Arguments to before_filter in a Rails Controller

I’ve been working with Ruby on Rails a lot recently. It’s a great platform for creating both old-fashioned web applications as well as services for RIAs. I especially like the fact that Rails encourages you to think about the right way to do something, before writing any code. For all you know, that complex thing you wanted to add could involve nothing more than one shell command and a tweaked line of Ruby code.

There was one seemingly ubiquitous thing that I had to think about for long time however, so I’m posing it here for posterity: How do you pass arguments to an ActionController method from a filter, while also using filter conditions?

It seems like such a common problem. You buy the Agile Web Development with Rails book, like everybody else, read half the first chapter, then start writing your own application. At some point you need to add user authentication to your application. If you were working with any other language/framework, this would have taken waaayyyy longer than it did to copy-and-paste the authentication example in chapter 11. Here’s the example I’m referring to:

my_app/app/controllers/application.rb
class ApplicationController < ActionController::Base
session :session_key = "_my_session_id"
private
def authorize
unless User.find_by_id(session[:user_id])
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end

This method gets called automatically by specifying so on a filter in the individual controller, e.g.

my_app/app/controllers/item_controller.rb
class ItemController < ApplicationController
before_filter :authorize, :except => [:show, :search]

# ... various methods ...
end

Then, several features later, you realize that you need to have different levels of user (administrator, moderator, gnat, etc.).  The authorize method now needs to take an argument of what type of user is required.  You would have to specify this when you write the filter (in item_controller.rb), but you need to retain the filter conditions (:except). This took me a long time to find. The reference page on filters didn’t help.

Filtes can pass arguments to the methods they call, but the syntax is not obvious:

  • The authorize method you call must be public.
  • Your filter conditions have to be the first argument to the filter.
  • Instead of referencing the authorize method, you will need to call it, but you must do so from a code block, and that must be the last argument to the filter. Note the do … end in the example below.
  •  You can pass as many arguments as you like to the method inside this code block, since you’re actually calling it here.
  • To call the method, you need to define a block variable inside your code block (it can be named anything you like). This will be a reference to the controller. Note the two pipes in the example below.

So here’s the controller class with filter:

my_app/app/controllers/item_controller.rb
class ItemController < ApplicationController
before_filter :except => [:show, :search] do |controller| controller.authorize({"required_user_level" => "administrator"})
end
# ... various methods ...
end

Don’t forget to add an argument to your method declaration in the ActionController, and to make it public:

my_app/app/controllers/application.rb
class ApplicationController < ActionController::Base
session :session_key = "_my_session_id"

def authorize(vars)
puts "Required User Level is " + vars["required_user_level"]
unless User.find_by_id(session[:user_id])
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end