Let’s tweak it a bit. Just like in the models, we can create what’s called a Nested Route:
Just like that! Now we can do the nested URLs as showed above. The first thing to understand is that when I type in this URL:
http://localhost:3000/posts/1/comments
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Rails will parse it like this:
* Load the CommentsController
* Set params[:post_id] = 1
* In this case, call the 'index' action
We have to make the CommentsController prepared to be nested. So that's what we are going to change:
--- ruby
class CommentsController < ApplicationController
before_filter :load_post
...
def load_post
@post = Post.find(params[:post_id])
end
end
|
This makes the @post already set for all the actions within the Comments controller. Now we have to make these changes:
| Before |
After |
| Comment.find |
@post.comments.find |
| Comment.new |
@post.comments.build |
| redirect_to(@comment) |
redirect_to([@post, @comment]) |
| redirect_to(comments_url) |
redirect_to(post_comments_url(@post)) |
That should make the Comments controller ready. Now let’s change the 4 views at app/views/comments. If you open up either new.html.erb or edit.html.erb you will notice the following new feature:
1
2
3
4
|
# new edit.html.erb and new.html.erb
form_for(@comment) do |f|
...
end
|
That’s the new way of doing this old statement in Rails 1.2:
1
2
3
4
|
# old new.rhtml
form_for(:comment, :url => comments_url) do |f|
...
end
|
1
2
3
4
5
|
# old edit.rhtml
form_for(:comment, :url => comment_url(@comment),
:html => { :method => :put }) do |f|
...
end
|
Notice how the same form_for statement suits both ‘new’ and ‘edit’ situations. That’s because Rails can infer what to do based on the Class name of the @comment model instance. But now, for the Nested Route, comments is dependent on the Post, so that’s what we have to do:
1
2
3
4
|
# new edit.html.erb and new.html.erb
form_for([@post, @comment]) do |f|
...
end
|
Rails will try to be smart enough to understand that this array represents a Nested Route, will check routes.rb and figure out and this is the post_comment_url(@post, @comment) named route.
Let’s explain named routes first. When we set a Resource Route in the routes.rb. We gain these named routes:
| route |
HTTP verb |
Controller Action |
| comments |
GET |
index |
| comments |
POST |
create |
| comment(:id) |
GET |
show |
| comment(:id) |
PUT |
update |
| comment(:id) |
DELETE |
destroy |
| new_comment |
GET |
new |
| edit_comment(:id) |
GET |
edit |
“7 Actions to Rule them all …” :-)
You can suffix them with both ‘path’ or ‘url’. The difference being:
| comments_url |
http://localhost:3000/comments |
| comments_path |
/comments |
Finally, you can prefix them with ‘formatted’, giving you:
| formatted_comments_url(:atom) |
http://localhost:3000/comments.atom |
| formatted_comment_path(@comment, :atom) |
/comments/1.atom |
Now, as Comments is nested within Post, we are obligated to add the prefix ‘post’. In Rails 1.2 this prefix was optional, it was able to tell the difference by the number or parameters passed to the named route helper, but this could lead to many ambiguities so it is now mandatory to have the prefix, like this:
| route |
HTTP verb |
URL |
| post_comments(@post) |
GET |
/posts/:post_id/comments |
| post_comments(@post) |
POST |
/posts/:post_id/comments |
| post_comment(@post, :id) |
GET |
/posts/:post_id/comments/:id |
| post_comment(@post, :id) |
PUT |
/posts/:post_id/comments/:id |
| post_comment(@post, :id) |
DELETE |
/posts/:post_id/comments/:id |
| new_post_comment(@post) |
GET |
/posts/:post_id/comments/new |
| edit_post_comment(@post, :id) |
GET |
/posts/:post_id/comments/edit |
So, to summarize, we have to make the Comments views to behave like they are nested within a Post. So we have to change the named routes within from the default scaffold generated code to the nested form:
1
2
3
4
5
6
7
8
9
10
11
|
<!-- app/views/comments/_comment.html.erb -->
<% form_for([@post, @comment]) do |f| %>
<p>
<b>Body</b><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit button_name %>
</p>
<% end %>
|
1
2
3
4
5
6
7
8
9
10
|
<!-- app/views/comments/edit.html.erb -->
<h1>Editing comment</h1>
<%= error_messages_for :comment %>
<%= render :partial => @comment,
:locals => { :button_name => "Update"} %>
<%= link_to 'Show', [@post, @comment] %> |
<%= link_to 'Back', post_comments_path(@post) %>
|
1
2
3
4
5
6
7
8
9
|
<!-- app/views/comments/new.html.erb -->
<h1>New comment</h1>
<%= error_messages_for :comment %>
<%= render :partial => @comment,
:locals => { :button_name => "Create"} %>
<%= link_to 'Back', post_comments_path(@post) %>
|
1
2
3
4
5
6
7
8
9
|
<!-- app/views/comments/show.html.erb -->
<p>
<b>Body:</b>
<%=h @comment.body %>
</p>
<%= link_to 'Edit', [:edit, @post, @comment] %> |
<%= link_to 'Back', post_comments_path(@post) %>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<!-- app/views/comments/index.html.erb -->
<h1>Listing comments</h1>
<table>
<tr>
<th>Post</th>
<th>Body</th>
</tr>
<% for comment in @comments %>
<tr>
<td><%=h comment.post_id %></td>
<td><%=h comment.body %></td>
<td><%= link_to 'Show', [@post, comment] %></td>
<td><%= link_to 'Edit', [:edit, @post, comment] %></td>
<td><%= link_to 'Destroy', [@post, comment],
:confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New comment',
new_post_comment_path(@post) %>
|
Some remarks:
- Notice that I created a partial to DRY up the new and edit forms. But pay attention that instead of :partial => ‘comment’, I did :partial => @comment. Then again it can infer the name of partial from the class name. If we passed a collection it would do the equivalent of the old ‘:partial, :collection’ statement.
- I can use both post_comment_path(
post, @comment), or simply [post, @comment]
- Pay close attention to not forget any named route behind.
Finally, it would be good to link the comments list to the post view. So let’s do it:
1
2
3
4
|
<!-- app/views/posts/show.html.erb -->
<%= link_to 'Comments', post_comments_path(@post) %>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
|
So, I just added a link there. Let’s see how it looks like:



Completing the Views
Ok, looks good, but that’s not how a Blog should behave! The Post’s show view should already have the Comments listing and New Comment Form as well! So let’s make some small adaptations. There’s nothing new here, just traditional Rails. Let’s start at the view:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<!-- app/views/posts/show.html.erb -->
<p>
<b>Title:</b>
<%=h @post.title %>
</p>
<p>
<b>Body:</b>
<%=h @post.body %>
</p>
<!-- #1 -->
<% unless @post.comments.empty? %>
<h3>Comments</h3>
<% @post.comments.each do |comment| %>
<p><%= h comment.body %></p>
<% end %>
<% end %>
<!-- #2 -->
<h3>New Comment</h3>
<%= render :partial => @comment = Comment.new,
:locals => { :button_name => 'Create'}%>
<%= link_to 'Comments', post_comments_path(@post) %>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
|
More remarks
- There is nothing new in the iterator, just listing all comments
- Again, we pass in the @comment variable to the partial statement
One final adjustment: whenever we create a new post, we would like to return to the same Posts’ show view, so we change the CommentsController to behave like this:
1
2
3
4
5
|
# app/controllers/comments_controller.rb
# old redirect:
redirect_to([@post, @comment])
# new redirect:
redirect_to(@post)
|
Namespaced Routes
Ok, now we have a bare bone mini-blog that kind of mimics the classic blog from the ‘15 minute’ screencast David did in 2005. Now let’s go one step further: Posts should not be publicly available to anyone to edit, we need an Administration section in our website. Let’s create a new controller for that:
./script/generate controller Admin::Posts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Rails 2.0 now supports namespaces. This will create a sub-directory called app/controllers/admin.
What we want to do is:
# Create a new routes
# Copy all actions from the old Posts controller to the new Admin::Posts
# Copy the old posts views to app/views/admin* Leave the old Posts controller with only the 'index' and 'show' actions, this means deleting the new and edit views as well
# Adapt the actions and views we just copied so it understands it is within the admin controller
First things first, let's edit config/routes.rb again:
--- ruby
map.namespace :admin do |admin|
admin.resources :posts
end
|
In practice this means that we now have names routes for Posts with the prefix ‘admin’. This will disambiguate the old posts routes from the newest admin posts routes, like this:
| posts_path |
/posts |
| post_path(@post) |
/posts/:post_id |
| admin_posts_path |
/admin/posts |
| admin_post_path(@post) |
/admin/posts/:post_id |
Now let’s copy the actions from the old Post controller and adapt the routes to fit the new namespace:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# app/controllers/admin/posts_controller.rb
...
def create
# old:
format.html { redirect_to(@post) }
# new:
format.html { redirect_to([:admin, @post]) }
end
def update
# old:
format.html { redirect_to(@post) }
# new:
format.html { redirect_to([:admin, @post]) }
end
def destroy
# old:
format.html { redirect_to(posts_url) }
# new:
format.html { redirect_to(admin_posts_url) }
end
...
|
Don’t forget to delete all the methods from the app/controllers/posts_controller.rb, leaving just the ‘index’ and ‘show’ methods.
Now, let’s copy the views (assuming your shell is already in the project’s root folder):
cp app/views/posts/*.erb app/views/admin/posts
rm app/views/posts/new.html.erb
rm app/views/posts/edit.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Now, let's edit the views from app/views/admin/posts:
--- html
<!-- app/views/admin/posts/edit.html.erb -->
<h1>Editing post</h1>
<%= error_messages_for :post %>
<% form_for([:admin, @post]) do |f| %>
...
<% end %>
<%= link_to 'Show', [:admin, @post] %> |
<%= link_to 'Back', admin_posts_path %>
|
1
2
3
4
5
6
7
8
9
10
|
<!-- app/views/admin/posts/new.html.erb -->
<h1>New post</h1>
<%= error_messages_for :post %>
<% form_for([:admin, @post]) do |f| %>
...
<% end %>
<%= link_to 'Back', admin_posts_path %>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!-- app/views/admin/posts/show.html.erb -->
<p>
<b>Title:</b>
<%=h @post.title %>
</p>
<p>
<b>Body:</b>
<%=h @post.body %>
</p>
<%= link_to 'Edit', edit_admin_post_path(@post) %> |
<%= link_to 'Back', admin_posts_path %>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<!-- app/views/admin/posts/index.html.erb -->
...
<% for post in @posts %>
<tr>
<td><%=h post.title %></td>
<td><%=h post.body %></td>
<td><%= link_to 'Show', [:admin, post] %></td>
<td><%= link_to 'Edit', edit_admin_post_path(post) %></td>
<td><%= link_to 'Destroy', [:admin, post],
:confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New post', new_admin_post_path %>
|
Almost done: if you test http://localhost:3000/admin/posts it should work properly now. But, it will look ugly, and that’s because we don’t have a global app layout. When we did the first scaffolds, Rails generated specific layouts for Post and Comment alone. So let’s delete them and create one that’s generic:
cp app/views/layouts/posts.html.erb \
app/views/layouts/application.html.erb
rm app/views/layouts/posts.html.erb
rm app/views/layouts/comments.html.erb
1
2
3
4
5
6
7
8
|
Then let's just change the title of it:
--- ruby
<!-- app/views/layouts/application.html.erb -->
...
<title>My Great Blog</title>
...
|
It only remain the old ‘index’ and ‘show’ pages from the previous Posts controllers. They still have links to the methods we deleted, so let’s rip them off from those links:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<!-- app/views/posts/index.html.erb -->
<h1>My Great Blog</h1>
<table>
<tr>
<th>Title</th>
<th>Body</th>
</tr>
<% for post in @posts %>
<tr>
<td><%=h post.title %></td>
<td><%=h post.body %></td>
<td><%= link_to 'Show', post %></td>
</tr>
<% end %>
</table>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<!-- app/views/posts/show.html.erb -->
<p>
<b>Title:</b>
<%=h @post.title %>
</p>
<p>
<b>Body:</b>
<%=h @post.body %>
</p>
<% unless @post.comments.empty? %>
<h3>Comments</h3>
<% @post.comments.each do |comment| %>
<p><%= h comment.body %></p>
<% end %>
<% end %>
<h3>New Comment</h3>
<%= render :partial => @comment = Comment.new,
:locals => { :button_name => 'Create'}%>
<%= link_to 'Back', posts_path %>
|
We can test everything from the browser, go in http://localhost:3000/admin/posts and see that everything is working properly now. But, we still have one thing missing: an administration section should not be publicly available. Right now you can just jump in and edit everything. We need authentication.
HTTP Basic Authentication
There are several ways of implementing authentication and authorization. One plugin that’s widely used for this is restful_authentication.
But, we don’t want to make anything fancy here. And for that Rails 2.0 gives us a great way of authenticating. The idea is: let’s use what HTTP already gives us: HTTP Basic Authentication. The drawback being: you will definitely want to use SSL when going into production. But, of course, you would do it anyway. HTML Form authentication is not protected without SSL either.
So, let’s edit our Admin::Posts controller to add authentication:
1
2
3
4
5
6
7
8
9
10
11
|
# app/controllers/admin/posts.rb
class Admin::PostsController < ApplicationController
before_filter :authenticate
...
def authenticate
authenticate_or_request_with_http_basic do |name, pass|
#User.authenticate(name, pass)
name == 'akita' && pass == 'akita'
end
end
end
|
You already know what ‘before_filter’ does: it runs the configured method before any action in the controller. If you set it in the ApplicationController class, then it runs before any action from any other controller. But we only want to protect Admin::Posts here.
Then, we implement this method and the secret sauce is the ‘authenticate_or_request_with_http_basic’ method that let’s us configure a block. It gives us the username and password that the user typed in the browser. We would usually have a User model of some kind to authenticate this data, but for our very very simple example I am hard coding the checking, but you get the idea.
