2007-01-22 14:43:50 +01:00
|
|
|
= Action Pack -- On rails from request to response
|
|
|
|
|
|
|
|
Action Pack splits the response to a web request into a controller part
|
|
|
|
(performing the logic) and a view part (rendering a template). This two-step
|
|
|
|
approach is known as an action, which will normally create, read, update, or
|
|
|
|
delete (CRUD for short) some sort of model part (often backed by a database)
|
|
|
|
before choosing either to render a template or redirecting to another action.
|
|
|
|
|
|
|
|
Action Pack implements these actions as public methods on Action Controllers
|
|
|
|
and uses Action Views to implement the template rendering. Action Controllers
|
|
|
|
are then responsible for handling all the actions relating to a certain part
|
|
|
|
of an application. This grouping usually consists of actions for lists and for
|
|
|
|
CRUDs revolving around a single (or a few) model objects. So ContactController
|
|
|
|
would be responsible for listing contacts, creating, deleting, and updating
|
|
|
|
contacts. A WeblogController could be responsible for both posts and comments.
|
|
|
|
|
|
|
|
Action View templates are written using embedded Ruby in tags mingled in with
|
|
|
|
the HTML. To avoid cluttering the templates with code, a bunch of helper
|
|
|
|
classes provide common behavior for forms, dates, and strings. And it's easy
|
|
|
|
to add specific helpers to keep the separation as the application evolves.
|
|
|
|
|
|
|
|
Note: Some of the features, such as scaffolding and form building, are tied to
|
|
|
|
ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational
|
|
|
|
mapping package), but that doesn't mean that Action Pack depends on Active
|
|
|
|
Record. Action Pack is an independent package that can be used with any sort
|
|
|
|
of backend (Instiki[http://www.instiki.org], which is based on an older version
|
|
|
|
of Action Pack, used Madeleine for example). Read more about the role Action
|
|
|
|
Pack can play when used together with Active Record on
|
|
|
|
http://www.rubyonrails.org.
|
|
|
|
|
|
|
|
A short rundown of the major features:
|
|
|
|
|
|
|
|
* Actions grouped in controller as methods instead of separate command objects
|
|
|
|
and can therefore share helper methods.
|
|
|
|
|
|
|
|
BlogController < ActionController::Base
|
2007-02-09 09:04:31 +01:00
|
|
|
def show
|
2007-01-22 14:43:50 +01:00
|
|
|
@customer = find_customer
|
|
|
|
end
|
|
|
|
|
|
|
|
def update
|
|
|
|
@customer = find_customer
|
|
|
|
@customer.attributes = params[:customer]
|
|
|
|
@customer.save ?
|
|
|
|
redirect_to(:action => "display") :
|
|
|
|
render(:action => "edit")
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def find_customer() Customer.find(params[:id]) end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Base.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Embedded Ruby for templates (no new "easy" template language)
|
|
|
|
|
|
|
|
<% for post in @posts %>
|
|
|
|
Title: <%= post.title %>
|
|
|
|
<% end %>
|
|
|
|
|
|
|
|
All post titles: <%= @post.collect{ |p| p.title }.join ", " %>
|
|
|
|
|
|
|
|
<% unless @person.is_client? %>
|
|
|
|
Not for clients to see...
|
|
|
|
<% end %>
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionView.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Builder-based templates (great for XML content, like RSS)
|
|
|
|
|
|
|
|
xml.rss("version" => "2.0") do
|
|
|
|
xml.channel do
|
|
|
|
xml.title(@feed_title)
|
|
|
|
xml.link(@url)
|
|
|
|
xml.description "Basecamp: Recent items"
|
|
|
|
xml.language "en-us"
|
|
|
|
xml.ttl "40"
|
|
|
|
|
|
|
|
for item in @recent_items
|
|
|
|
xml.item do
|
|
|
|
xml.title(item_title(item))
|
|
|
|
xml.description(item_description(item))
|
|
|
|
xml.pubDate(item_pubDate(item))
|
|
|
|
xml.guid(@recent_items.url(item))
|
|
|
|
xml.link(@recent_items.url(item))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionView/Base.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Filters for pre and post processing of the response (as methods, procs, and classes)
|
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
|
|
|
before_filter :authenticate, :cache, :audit
|
|
|
|
after_filter { |c| c.response.body = GZip::compress(c.response.body) }
|
|
|
|
after_filter LocalizeFilter
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
def index
|
2007-01-22 14:43:50 +01:00
|
|
|
# Before this action is run, the user will be authenticated, the cache
|
|
|
|
# will be examined to see if a valid copy of the results already
|
|
|
|
# exists, and the action will be logged for auditing.
|
|
|
|
|
|
|
|
# After this action has run, the output will first be localized then
|
|
|
|
# compressed to minimize bandwidth usage
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def authenticate
|
|
|
|
# Implement the filter with full access to both request and response
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Filters/ClassMethods.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Helpers for forms, dates, action links, and text
|
|
|
|
|
|
|
|
<%= text_field "post", "title", "size" => 30 %>
|
|
|
|
<%= html_date_select(Date.today) %>
|
|
|
|
<%= link_to "New post", :controller => "post", :action => "new" %>
|
|
|
|
<%= truncate(post.title, 25) %>
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionView/Helpers.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Layout sharing for template reuse (think simple version of Struts
|
|
|
|
Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html])
|
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
|
|
|
layout "weblog_layout"
|
|
|
|
|
|
|
|
def hello_world
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
Layout file (called weblog_layout):
|
2007-02-09 09:04:31 +01:00
|
|
|
<html><body><%= yield %></body></html>
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
Template for hello_world action:
|
|
|
|
<h1>Hello world</h1>
|
|
|
|
|
|
|
|
Result of running hello_world action:
|
|
|
|
<html><body><h1>Hello world</h1></body></html>
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Layout/ClassMethods.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Routing makes pretty urls incredibly easy
|
|
|
|
|
|
|
|
map.connect 'clients/:client_name/:project_name/:controller/:action'
|
|
|
|
|
|
|
|
Accessing /clients/37signals/basecamp/project/dash calls ProjectController#dash with
|
2007-02-09 09:04:31 +01:00
|
|
|
{ "client_name" => "37signals", "project_name" => "basecamp" } in params[:params]
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
From that URL, you can rewrite the redirect in a number of ways:
|
|
|
|
|
|
|
|
redirect_to(:action => "edit") =>
|
|
|
|
/clients/37signals/basecamp/project/dash
|
|
|
|
|
|
|
|
redirect_to(:client_name => "nextangle", :project_name => "rails") =>
|
|
|
|
/clients/nextangle/rails/project/dash
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Base.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Javascript and Ajax integration.
|
|
|
|
|
|
|
|
link_to_function "Greeting", "alert('Hello world!')"
|
|
|
|
link_to_remote "Delete this post", :update => "posts",
|
|
|
|
:url => { :action => "destroy", :id => post.id }
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Pagination for navigating lists of results.
|
|
|
|
|
|
|
|
# controller
|
|
|
|
def list
|
|
|
|
@pages, @people =
|
|
|
|
paginate :people, :order => 'last_name, first_name'
|
|
|
|
end
|
|
|
|
|
|
|
|
# view
|
|
|
|
<%= link_to "Previous page", { :page => @pages.current.previous } if @pages.current.previous %>
|
|
|
|
<%= link_to "Next page", { :page => @pages.current.next } if @pages.current.next %>
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Pagination.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Easy testing of both controller and template result through TestRequest/Response
|
|
|
|
|
|
|
|
class LoginControllerTest < Test::Unit::TestCase
|
|
|
|
def setup
|
|
|
|
@controller = LoginController.new
|
|
|
|
@request = ActionController::TestRequest.new
|
|
|
|
@response = ActionController::TestResponse.new
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_failing_authenticate
|
|
|
|
process :authenticate, :user_name => "nop", :password => ""
|
|
|
|
assert flash.has_key?(:alert)
|
|
|
|
assert_redirected_to :action => "index"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/TestRequest.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Automated benchmarking and integrated logging
|
|
|
|
|
|
|
|
Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55)
|
|
|
|
Parameters: {"action"=>"index", "controller"=>"weblog"}
|
|
|
|
Rendering weblog/index (200 OK)
|
|
|
|
Completed in 0.029281 (34 reqs/sec)
|
|
|
|
|
|
|
|
If Active Record is used as the model, you'll have the database debugging
|
|
|
|
as well:
|
|
|
|
|
|
|
|
Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
|
|
|
|
Params: {"controller"=>"weblog", "action"=>"create",
|
|
|
|
"post"=>{"title"=>"this is good"} }
|
|
|
|
SQL (0.000627) INSERT INTO posts (title) VALUES('this is good')
|
|
|
|
Redirected to http://test/weblog/display/5
|
|
|
|
Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%)
|
|
|
|
|
|
|
|
You specify a logger through a class method, such as:
|
|
|
|
|
|
|
|
ActionController::Base.logger = Logger.new("Application Log")
|
|
|
|
ActionController::Base.logger = Log4r::Logger.new("Application Log")
|
|
|
|
|
|
|
|
|
|
|
|
* Caching at three levels of granularity (page, action, fragment)
|
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
|
|
|
caches_page :show
|
|
|
|
caches_action :account
|
|
|
|
|
|
|
|
def show
|
|
|
|
# the output of the method will be cached as
|
|
|
|
# ActionController::Base.page_cache_directory + "/weblog/show/n.html"
|
|
|
|
# and the web server will pick it up without even hitting Rails
|
|
|
|
end
|
|
|
|
|
|
|
|
def account
|
|
|
|
# the output of the method will be cached in the fragment store
|
|
|
|
# but Rails is hit to retrieve it, so filters are run
|
|
|
|
end
|
|
|
|
|
|
|
|
def update
|
|
|
|
List.update(params[:list][:id], params[:list])
|
|
|
|
expire_page :action => "show", :id => params[:list][:id]
|
|
|
|
expire_action :action => "account"
|
|
|
|
redirect_to :action => "show", :id => params[:list][:id]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Caching.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Component requests from one controller to another
|
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
|
|
|
# Performs a method and then lets hello_world output its render
|
|
|
|
def delegate_action
|
|
|
|
do_other_stuff_before_hello_world
|
|
|
|
render_component :controller => "greeter", :action => "hello_world"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class GreeterController < ActionController::Base
|
|
|
|
def hello_world
|
|
|
|
render_text "Hello World!"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
The same can be done in a view to do a partial rendering:
|
|
|
|
|
|
|
|
Let's see a greeting:
|
|
|
|
<%= render_component :controller => "greeter", :action => "hello_world" %>
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Components.html]
|
|
|
|
|
|
|
|
|
|
|
|
* Powerful debugging mechanism for local requests
|
|
|
|
|
|
|
|
All exceptions raised on actions performed on the request of a local user
|
|
|
|
will be presented with a tailored debugging screen that includes exception
|
|
|
|
message, stack trace, request parameters, session contents, and the
|
|
|
|
half-finished response.
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionController/Rescue.html]
|
|
|
|
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
* Scaffolding for Active Record model objects
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
class AccountController < ActionController::Base
|
|
|
|
scaffold :account
|
|
|
|
end
|
|
|
|
|
|
|
|
The AccountController now has the full CRUD range of actions and default
|
|
|
|
templates: list, show, destroy, new, create, edit, update
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
{Learn more}[link:classes/ActionController/Scaffolding/ClassMethods.html]
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
* Form building for Active Record model objects
|
|
|
|
|
|
|
|
The post object has a title (varchar), content (text), and
|
|
|
|
written_on (date)
|
|
|
|
|
|
|
|
<%= form "post" %>
|
|
|
|
|
|
|
|
...will generate something like (the selects will have more options, of
|
|
|
|
course):
|
|
|
|
|
|
|
|
<form action="create" method="POST">
|
|
|
|
<p>
|
|
|
|
<b>Title:</b><br/>
|
|
|
|
<input type="text" name="post[title]" value="<%= @post.title %>" />
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
<b>Content:</b><br/>
|
|
|
|
<textarea name="post[content]"><%= @post.title %></textarea>
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
<b>Written on:</b><br/>
|
|
|
|
<select name='post[written_on(3i)]'><option>18</option></select>
|
|
|
|
<select name='post[written_on(2i)]'><option value='7'>July</option></select>
|
|
|
|
<select name='post[written_on(1i)]'><option>2004</option></select>
|
|
|
|
</p>
|
|
|
|
|
|
|
|
<input type="submit" value="Create">
|
|
|
|
</form>
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
This form generates a params[:post] array that can be used directly in a save action:
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
2007-02-09 09:04:31 +01:00
|
|
|
def create
|
2007-01-22 14:43:50 +01:00
|
|
|
post = Post.create(params[:post])
|
|
|
|
redirect_to :action => "display", :id => post.id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
{Learn more}[link:classes/ActionView/Helpers/ActiveRecordHelper.html]
|
|
|
|
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
* Runs on top of WEBrick, Mongrel, CGI, FCGI, and mod_ruby
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
== Simple example (from outside of Rails)
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
This example will implement a simple weblog system using inline templates and
|
|
|
|
an Active Record model. So let's build that WeblogController with just a few
|
|
|
|
methods:
|
|
|
|
|
|
|
|
require 'action_controller'
|
|
|
|
require 'post'
|
|
|
|
|
|
|
|
class WeblogController < ActionController::Base
|
|
|
|
layout "weblog/layout"
|
|
|
|
|
|
|
|
def index
|
2007-02-09 09:04:31 +01:00
|
|
|
@posts = Post.find(:all)
|
2007-01-22 14:43:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def display
|
2007-02-09 09:04:31 +01:00
|
|
|
@post = Post.find(params[:id])
|
2007-01-22 14:43:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def new
|
|
|
|
@post = Post.new
|
|
|
|
end
|
|
|
|
|
|
|
|
def create
|
|
|
|
@post = Post.create(params[:post])
|
|
|
|
redirect_to :action => "display", :id => @post.id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-12-21 08:48:59 +01:00
|
|
|
WeblogController::Base.view_paths = [ File.dirname(__FILE__) ]
|
2007-01-22 14:43:50 +01:00
|
|
|
WeblogController.process_cgi if $0 == __FILE__
|
|
|
|
|
|
|
|
The last two lines are responsible for telling ActionController where the
|
|
|
|
template files are located and actually running the controller on a new
|
|
|
|
request from the web-server (like to be Apache).
|
|
|
|
|
|
|
|
And the templates look like this:
|
|
|
|
|
2007-12-21 08:48:59 +01:00
|
|
|
weblog/layout.erb:
|
2007-01-22 14:43:50 +01:00
|
|
|
<html><body>
|
2007-02-09 09:04:31 +01:00
|
|
|
<%= yield %>
|
2007-01-22 14:43:50 +01:00
|
|
|
</body></html>
|
|
|
|
|
2007-12-21 08:48:59 +01:00
|
|
|
weblog/index.erb:
|
2007-01-22 14:43:50 +01:00
|
|
|
<% for post in @posts %>
|
|
|
|
<p><%= link_to(post.title, :action => "display", :id => post.id %></p>
|
|
|
|
<% end %>
|
|
|
|
|
2007-12-21 08:48:59 +01:00
|
|
|
weblog/display.erb:
|
2007-01-22 14:43:50 +01:00
|
|
|
<p>
|
|
|
|
<b><%= post.title %></b><br/>
|
|
|
|
<b><%= post.content %></b>
|
|
|
|
</p>
|
|
|
|
|
2007-12-21 08:48:59 +01:00
|
|
|
weblog/new.erb:
|
2007-01-22 14:43:50 +01:00
|
|
|
<%= form "post" %>
|
|
|
|
|
|
|
|
This simple setup will list all the posts in the system on the index page,
|
|
|
|
which is called by accessing /weblog/. It uses the form builder for the Active
|
|
|
|
Record model to make the new screen, which in turn hands everything over to
|
|
|
|
the create action (that's the default target for the form builder when given a
|
|
|
|
new model). After creating the post, it'll redirect to the display page using
|
|
|
|
an URL such as /weblog/display/5 (where 5 is the id of the post).
|
|
|
|
|
|
|
|
|
|
|
|
== Examples
|
|
|
|
|
|
|
|
Action Pack ships with three examples that all demonstrate an increasingly
|
|
|
|
detailed view of the possibilities. First is blog_controller that is just a
|
|
|
|
single file for the whole MVC (but still split into separate parts). Second is
|
|
|
|
the debate_controller that uses separate template files and multiple screens.
|
|
|
|
Third is the address_book_controller that uses the layout feature to separate
|
|
|
|
template casing from content.
|
|
|
|
|
|
|
|
Please note that you might need to change the "shebang" line to
|
|
|
|
#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
Also note that these examples are all for demonstrating using Action Pack on
|
|
|
|
its own. Not for when it's used inside of Rails.
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
== Download
|
|
|
|
|
|
|
|
The latest version of Action Pack can be found at
|
|
|
|
|
|
|
|
* http://rubyforge.org/project/showfiles.php?group_id=249
|
|
|
|
|
|
|
|
Documentation can be found at
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
* http://api.rubyonrails.com
|
2007-01-22 14:43:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
== Installation
|
|
|
|
|
|
|
|
You can install Action Pack with the following command.
|
|
|
|
|
|
|
|
% [sudo] ruby install.rb
|
|
|
|
|
|
|
|
from its distribution directory.
|
|
|
|
|
|
|
|
|
|
|
|
== License
|
|
|
|
|
|
|
|
Action Pack is released under the MIT license.
|
|
|
|
|
|
|
|
|
|
|
|
== Support
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
The Action Pack homepage is http://www.rubyonrails.org. You can find
|
2007-01-22 14:43:50 +01:00
|
|
|
the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack.
|
|
|
|
And as Jim from Rake says:
|
|
|
|
|
|
|
|
Feel free to submit commits or feature requests. If you send a patch,
|
|
|
|
remember to update the corresponding unit tests. If fact, I prefer
|
2007-02-09 09:04:31 +01:00
|
|
|
new feature to be submitted in the form of new unit tests.
|