Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

Chapter 21

Action Controller and Rails

In the previous chapter we worked out how Action Controller routes an incoming request to the appropriate code in your application. Now let’s see what happens inside that code.

21.1Action Methods

When a controller object processes a request, it looks for a public instance method with the same name as the incoming action. If it finds one, that method is invoked. If not, but the controller implements method_missing, that method is called, passing in the action name as the first parameter and an empty argument list as the second. If no method can be called, the controller looks for a template named after the current controller and action. If found, this template is rendered directly. If none of these things happen, an “Unknown Action” error is generated.

By default, any public method in a controller may be invoked as an action method. You can prevent particular methods from being accessible as actions by making them protected or private. If for some reason you must make a method in a controller public but don’t want it to be accessible as an action, hide it using hide_action.

class OrderController < ApplicationController

def create_order

order = Order.new(params[:order]) if check_credit(order)

order.save else

# ...

end end

hide_action :check_credit

ACTION METHODS 425

def check_credit(order)

# ...

end end

If you find yourself using hide_action because you want to share the nonaction methods in one controller with another, consider moving these methods into separate libraries—your controllers may contain too much application logic.

Controller Environment

The controller sets up the environment for actions (and, by extension, for the views that they invoke). In the old days, this environment was established in instance variables (@params, @request, and so on). This has now been officially deprecated—you should use the accessor methods listed here.

action_name

The name of the action currently being processed.

cookies

The cookies associated with the request. Setting values into this object stores cookies on the browser when the response is sent. We discuss cookies on page 435.

headers

A hash of HTTP headers that will be used in the response. By default,

Cache-Control is set to no-cache. You might want to set Content-Type headers for special-purpose applications. Note that you shouldn’t set cookie values in the header directly—use the cookie API to do this.

params

A hash-like object containing request parameters (along with pseudoparameters generated during routing). It’s hash-like because you can index entries using either a symbol or a string—params[:id] and params[’id’] return the same value. Idiomatic Rails applications use the symbol form.

request

The incoming request object. It includes the attributes

domain, which returns the last two components of the domain name of the request.

remote_ip, which returns the remote IP address as a string. The string may have more than one address in it if the client is behind a proxy.

env, the environment of the request. You can use this to access values set by the browser, such as

request.env['HTTP_ACCEPT_LANGUAGE' ]

Report erratum

ACTION METHODS 426

method returns the request method, one of :delete, :get, :head, :post, or :put.

delete?, get?, head?, post?, and put? return true or false based on the request method.

xml_http_request? and xhr? return true if this request was issued by one of the AJAX helpers. Note that this parameter is independent of the method parameter.

class BlogController < ApplicationController def add_user

if request.get? @user = User.new

else

@user = User.new(params[:user]) @user.created_from_ip = request.env["REMOTE_HOST"] if @user.save

redirect_to_index("User #{@user.name} created") end

end end

end

See the documentation of ActionController::AbstractRequest for full details.

response

The response object, filled in during the handling of the request. Normally, this object is managed for you by Rails. As we’ll see when we look at filters on page 449, we sometimes access the internals for specialized processing.

session

A hash-like object representing the current session data. We describe this on page 437.

In addition, a logger is available throughout Action Pack. We describe this on page 243.

Responding to the User

Part of the controller’s job is to respond to the user. There are basically four ways of doing this.

The most common way is to render a template. In terms of the MVC paradigm, the template is the view, taking information provided by the controller and using it to generate a response to the browser.

The controller can return a string directly to the browser without invoking a view. This is fairly rare but can be used to send error notifications.

Report erratum

ACTION METHODS 427

The controller can return nothing to the browser.1 This is sometimes used when responding to an AJAX request.

The controller can send other data to the client (something other than HTML). This is typically a download of some kind (perhaps a PDF document or a file’s contents).

We’ll look at these in more detail shortly.

A controller always responds to the user exactly one time per request. This means that you should have just one call to a render, redirect_to, or send_xxx method in the processing of any request. (A DoubleRenderError exception is thrown on the second render.) The undocumented method erase_render_results discards the effect of a previous render in the current request, permitting a second render to take place. Use at your own risk.

Because the controller must respond exactly once, it checks to see whether a response has been generated just before it finishes handling a request. If not, the controller looks for a template named after the controller and action and automatically renders it. This is the most common way that rendering takes place. You may have noticed that in most of the actions in our shopping cart tutorial we never explicitly rendered anything. Instead, our action methods set up the context for the view and return. The controller notices that no rendering has taken place and automatically invokes the appropriate template.

You can have multiple templates with the same name but with different extensions (.rhtml, .rxml, and .rjs). If you don’t specify an extension in a render request (or if Rails issues a render request on your behalf), it searches for the templates in the order given here (so if you have an .rhtml template and an .rjs template, a render call will find the .rhtml version unless you explicitly say render(:file => "xxx.rjs").2

Rendering Templates

A template is a file that defines the content of a response for our application. Rails supports three template formats out of the box: rhtml, which is HTML with embedded Ruby code; builder, a more programmatic way of constructing XML content; and rjs, which generates JavaScript. We’ll talk about the contents of these files starting on page 465.

By convention, the template for action action of controller control will be in the file app/views/control/action.xxx (where xxx is one of rhtml, rxml, or rjs). The

1.In fact, the controller returns a set of HTTP headers, because some kind of response is expected.

2.There’s an obscure exception to this. Once Rails finds a template, it caches it. If you’re in

development mode and you change the type of a template, Rails may not find it, because it will give preference to the previously cached name. You’ll have to restart your application to get the new template invoked.

Report erratum

ACTION METHODS 428

app/views part of the name is the default. It may be overridden for an entire application by setting

ActionController::Base.template_root =dir_path

The render method is the heart of all rendering in Rails. It takes a hash of options that tell it what to render and how to render it.

It is tempting to write code in our controllers that looks like this.

# DO NOT DO THIS def update

@user = User.find(params[:id])

if @user.update_attributes(params[:user]) render :action => show

end

render :template => "fix_user_errors" end

It seems somehow natural that the act of calling render (and redirect_to) should somehow terminate the processing of an action. This is not the case. The previous code will generate an error (because render is called twice) in the case where update_attributes succeeds.

Let’s look at the render options used in the controller here (we’ll look separately at rendering in the view starting on page 509).

render()

With no overriding parameter, the render method renders the default template for the current controller and action. The following code will render the template app/views/blog/index.

class BlogController < ApplicationController def index

render end

end

So will the following (as the default action of a controller is to call render if the action doesn’t).

class BlogController < ApplicationController def index

end end

And so will this (as the controller will call a template directly if no action method is defined).

class BlogController < ApplicationController end

Report erratum

ACTION METHODS 429

render(:text =>string)

Sends the given string to the client. No template interpretation or HTML escaping is performed.

class HappyController < ApplicationController def index

render(:text => "Hello there!") end

end

render(:inline =>string, [ :type =>"rhtml"|"rxml"|"rjs" ], [ :locals =>hash] )

Interprets string as the source to a template of the given type, rendering the results back to the client. If the :locals hash is given, the contents are used to set the values of local variables in the template.

The following code adds method_missing to a controller if the application is running in development mode. If the controller is called with an invalid action, this renders an inline template to display the action’s name and a formatted version of the request parameters.

class SomeController < ApplicationController

if RAILS_ENV == "development"

def method_missing(name, *args) render(:inline => %{

<h2>Unknown action: #{name}</h2>

Here are the request parameters:<br/> <%= debug(params) %> })

end end

end

render(:action =>action_name)

Renders the template for a given action in this controller. Sometimes folks use the :action form of render when they should use redirects—see the discussion starting on page 432 for why this is a bad idea.

def display_cart if @cart.empty?

render(:action => :index) else

# ...

end end

Note that calling render(:action...) does not call the action method; it simply displays the template. If the template needs instance variables, these must be set up by the method that calls the render.

Let’s repeat this, because this is a mistake that beginners often make: calling render(:action...) does not invoke the action method—it simply renders that action’s default template.

Report erratum

ACTION METHODS 430

render(:file =>path, [ :use_full_path =>true|false], [:locals =>hash)

Renders the template in the given path (which must include a file extension). By default this should be an absolute path to the template, but if the :use_full_path option is true, the view will prepend the value of the template base path to the path you pass in. The template base path is set in the configuration for your application (described on page 237). If specified, the values in the :locals hash are used to set local variables in the template.

render(:template =>name)

Renders a template and arranges for the resulting text to be sent back to the client. The :template value must contain both the controller and action parts of the new name, separated by a forward slash. The following code will render the template app/views/blog/short_list.

class BlogController < ApplicationController def index

render(:template => "blog/short_list") end

end

render(:partial =>name, ...)

Renders a partial template. We talk about partial templates in depth on page 509.

render(:nothing => true)

Returns nothing—sends an empty body to the browser.

render(:xml =>stuff )

Renders stuff as text, forcing the content type to be application/xml.

render(:update) do |page| ... end

Renders the block as an rjs template, passing in the page object.

render(:update) do |page|

page[:cart].replace_html :partial => 'cart', :object => @cart page[:cart].visual_effect :blind_down if @cart.total_items == 1

end

All forms of render take optional :status, :layout, and :content_type parameters. The :status parameter is used to set the status header in the HTTP response. It defaults to "200 OK". Do not use render with a 3xx status to do redirects; Rails has a redirect method for this purpose.

The :layout parameter determines whether the result of the rendering will be wrapped by a layout (we first came across layouts on page 98, and we’ll look at them in depth starting on page 505). If the parameter is false, no layout will be applied. If set to nil or true, a layout will be applied only if there is one associated with the current action. If the :layout parameter has a string as a

Report erratum

ACTION METHODS 431

value, it will be taken as the name of the layout to use when rendering. A layout is never applied when the :nothing option is in effect.

The :content_type parameter lets you specify a value that will be passed to the browser in the Content-Type HTTP header.

Sometimes it is useful to be able to capture what would otherwise be sent to the browser in a string. The render_to_string method takes the same parameters as render but returns the result of rendering as a string—the rendering is not stored in the response object and so will not be sent to the user unless you take some additional steps. Calling render_to_string does not count as a real render: you can invoke the real render method later without getting a DoubleRender error.

Sending Files and Other Data

We’ve looked at rendering templates and sending strings in the controller. The third type of response is to send data (typically, but not necessarily, file contents) to the client.

send_data

Send a string containing binary data to the client.

send_data(data, options...)

Sends a data stream to the client. Typically the browser will use a combination of the content type and the disposition, both set in the options, to determine what to do with this data.

def sales_graph

png_data = Sales.plot_for(Date.today.month)

send_data(png_data, :type => "image/png", :disposition => "inline") end

Options:

 

:disposition

string Suggests to the browser that the file should be displayed inline (option inline)

 

or downloaded and saved (option attachment, the default)

:filename

string A suggestion to the browser of the default filename to use when saving this

 

data

:status

string The status code (defaults to "200 OK")

:type

string The content type, defaulting to application/octet-stream

Report erratum

ACTION METHODS 432

send_file

Send the contents of a file to the client.

send_file(path, options...)

Sends the given file to the client. The method sets the Content-Length, Content-Type,

Content-Disposition, and Content-Transfer-Encoding headers.

Options:

 

 

:buffer_size

number

The amount sent to the browser in each write if streaming is enabled

 

 

(:stream is true).

:disposition

string

Suggests to the browser that the file should be displayed inline (option

 

 

inline) or downloaded and saved (option attachment, the default).

:filename

string

A suggestion to the browser of the default filename to use when saving

 

 

the file. If not set, defaults to the filename part of path.

:status

string

The status code (defaults to "200 OK").

:stream

true or false

If false, the entire file is read into server memory and sent to the

 

 

client. Otherwise, the file is read and written to the client in :buffer_size

 

 

chunks.

:type

string

The content type, defaulting to application/octet-stream.

You can set additional headers for either send_ method using the headers attribute in the controller.

def send_secret_file send_file("/files/secret_list") headers["Content-Description" ] = "Top secret"

end

We show how to upload files starting on page 501.

Redirects

An HTTP redirect is sent from a server to a client in response to a request. In effect it says, “I can’t handle this request, but here’s some URL that can.” The redirect response includes a URL that the client should try next along with some status information saying whether this redirection is permanent (status code 301) or temporary (307). Redirects are sometimes used when web pages are reorganized; clients accessing pages in the old locations will get referred to the page’s new home. More commonly, Rails applications use redirects to pass the processing of a request off to some other action.

Redirects are handled behind the scenes by web browsers. Normally, the only way you’ll know that you’ve been redirected is a slight delay and the fact that the URL of the page you’re viewing will have changed from the one you requested. This last point is important—as far as the browser is concerned, a redirect from a server acts pretty much the same as having an end user enter the new destination URL manually.

Redirects turn out to be important when writing well-behaved web applications.

Report erratum

ACTION METHODS 433

Let’s look at a simple blogging application that supports comment posting. After a user has posted a comment, our application should redisplay the article, presumably with the new comment at the end. It’s tempting to code this using logic such as the following.

class BlogController def display

@article = Article.find(params[:id]) end

def add_comment

@article = Article.find(params[:id]) comment = Comment.new(params[:comment]) @article.comments << comment

if @article.save

flash[:note] = "Thank you for your valuable comment" else

flash[:note] = "We threw your worthless comment away" end

# DON'T DO THIS render(:action => 'display')

end end

The intent here was clearly to display the article after a comment has been posted. To do this, the developer ended the add_comment method with a call to render(:action=>'display'). This renders the display view, showing the updated article to the end user. But think of this from the browser’s point of view. It sends a URL ending in blog/add_comment and gets back an index listing. As far as the browser is concerned, the current URL is still the one that ends blog/add_comment. This means that if the user hits Refresh or Reload (perhaps to see whether anyone else has posted a comment), the add_comment URL will be sent again to the application. The user intended to refresh the display, but the application sees a request to add another comment. In a blog application this kind of unintentional double entry is inconvenient. In an online store it can get expensive.

In these circumstances, the correct way to show the added comment in the index listing is to redirect the browser to the display action. We do this using the Rails redirect_to method. If the user subsequently hits Refresh, it will simply reinvoke the display action and not add another comment.

def add_comment

@article = Article.find(params[:id]) comment = Comment.new(params[:comment]) @article.comments << comment

if @article.save

flash[:note] = "Thank you for your valuable comment" else

flash[:note] = "We threw your worthless comment away"

Report erratum

ACTION METHODS 434

end

redirect_to(:action => 'display') end

Rails has a simple yet powerful redirection mechanism. It can redirect to an action in a given controller (passing parameters), to a URL (on or off the current server), or to the previous page. Let’s look at these three forms in turn.

redirect_to

Redirects to an action

redirect_to(:action => ..., options...)

Sends a temporary redirection to the browser based on the values in the options hash. The target URL is generated using url_for, so this form of redirect_to has all the smarts of Rails routing code behind it. See Section 20.2, Routing Requests, on page 393 for a description.

redirect_to

Redirect to a URL.

redirect_to(path)

Redirects to the given path. If the path does not start with a protocol (such as http://), the protocol and port of the current request will be prepended. This method does not perform any rewriting on the URL, so it should not be used to create paths that are intended to link to actions in the application (unless you generate the path using url_for or a named route URL generator).

def save

order = Order.new(params[:order]) if order.save

redirect_to :action => "display" else

session[:error_count] ||= 0 session[:error_count] += 1 if session[:error_count] < 4

flash[:notice] = "Please try again" else

# Give up -- user is clearly struggling redirect_to("/help/order_entry.html")

end end

end

Report erratum