Tuesday, 22 July 2008

The Controller Part 3 - Render and Filter

Ruby on Rails uses the MVC archetecture for web applications. This post is the last of three posts about the controller part, focusing on the rendering and filters.

The render directive is used to generate a web page, typically a view.

Render has several options. Render defaults to the view with the method name, in the folder with the name of the current controller, using the layout with the controller name if it exists, or the application layout otherwise. In the Edit method of PostsController , these two are equivalent:
render :action => 'edit', :layout => 'posts'

Indeed, even the word render is unnecessary; Rails will default to the above even if no render directive is present, as seen in the Edit method of PostsController (behind the scenes Rails has a variable that notes when a render has been done, to ensure one gets done even if not specified, and to throw an error if you try to do it twice).

Render has several options.
:action  # Render using the view app/views/[controller]/[action].html.erb
# (note that the action named need not exist in any controller)

:template # As :action, but using the file app/views/[template].html.erb.
# Use this for consistent views between controllers.

:file # Render the given file (needs the absolute path, which seems a
# bit of a drawback to me). Layout not used.

:text # Render the given text as the web page. Strangely, you can use
# this to return an image (render :text => image.to_blob).

:inline # As text, but ERb is parsed.

:partial # Render as a partial web page (can be updated while the rest
# of the web page remains).

:nothing # Return a web page with no content.

:status # The HTTP code to be returned (returning :status => :success
# with :text causes an error, and it seems to be ignored for :action).

:content_type # For an image, this mght be :content_type => 'image/gif'

# My attempts to change the controller failed; it was just ignored.
# I had hoped to reuse the view of another controller; use template
# to do that instead!

More on render:

Filters are directives to perform certain methods before or after every action. A before_filter might be used to ensure a user has been authenticated, or for logging purposes, for instance.

Filters should appear at the start of the class, in the form:
before_filter :my_filter

Set up my_filter like any normal method. If it returns a Boolean false, the action will be aborted. Alternatively, your filter could initiate a redirect (say, pointing the user to the login page).

There are several different flavours of filters:
prepend_before_filter # Do this filter before
other before filters
append_before_filter # Do this filter after other
before filters
around_filter # use the yield keyword in the
filter when the output would go. Useful for
capturing exceptions
skip_filter # Want to skip filters in a superclass?

Filters apply to all actions in your controller (or all actions in your application if they are in ApplicationController). However, you can limit them:
:only => :edit # Apply this filter to the edit action only
:except => [:new, :delete] # Do not apply to the :new or :delete actions


This site uses a filter to avoid repetitive loading of objects

Using a Filter to Test if a Record Exists
The find method of ActiveRecord will throw an exception if no record is found with that ID. This can happen even if your web app is perfect, for example when the user makes a mistake typing in a URL directly (as Rails interprates any unknown as being an ID). How do you handle that?

You have three choices. All the examples I have come across just ignore it. Let Rails throw the exception, and deal with it itself. In the development environment this means giving a web page with a stack trace; in the production environment a page is displayed telling the user that the administrator has been informed of the problem. A better solution is to catch the exception and handle it yourself, giving the user a more helpful result that does not leave the impression that your software is buggy!

However, my prefered solution is to avoid the exception altogether, by testing whether the record exists before trying to retrieve. And the best place to do that is in a before_filter. You might set the filter up like this (note that these are just the typical methods that retrieve a specific record by ID):
before_filter :check_id, :only =>
[ :edit, :update, :destroy, :show]

Then you include the method (which should be protected, so it cannot be called as an action):
def check_id
return if Post.exists? params[:id]
@ref = params[:id]
render :action => 'no_show'

This simply checks the record exists, and if not renders an alternative page.

Struggling with Ruby: Contents Page

No comments: