Simples assim! Agora podemos fazer URLs aninhadas como mostrado acima. A primeira coisa a entender é que quando escreve-se esta 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 entenderá como:
* Carregar the CommentsController
* Configurar o params[:post_id] = 1
* Neste caso, chamar o 'index' action
Nós temos que preparar o CommentsController para este aninhamento. Então, o que mudaremos:
--- ruby
class CommentsController < ApplicationController
before_filter :load_post
...
def load_post
@post = Post.find(params[:post_id])
end
end
|
Isso deixará o @post já configurado para todas as acões no controller Comments. Agora temos que fazer estas alterações:
| Antes |
Depois |
| 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)) |
Isto deveria deixar o controller Comments preparado. Agora vamos alterar as 4 views em app/views/comments. Se você abrir o new.html.erb ou o edit.html.erb, verá a nova característica abaixo:
1
2
3
4
|
# novos edit.html.erb e new.html.erb
form_for(@comment) do |f|
...
end
|
Este é o novo jeito de fazer esta antiga instrução do Rails 1.2:
1
2
3
4
|
# antigo new.rhtml
form_for(:comment, :url => comments_url) do |f|
...
end
|
1
2
3
4
5
|
# antigo edit.rhtml
form_for(:comment, :url => comment_url(@comment),
:html => { :method => :put }) do |f|
...
end
|
Repare como a instrução form_for adapta ambas as situações ‘new’ e ‘edit’. Isto porque o Rails pode deduzir o que fazer baseado no nome da classe do objeto @comment. Mas agora, para as Rotas Aninhadas, comentários são dependentes do Post, então isto é o que temos que fazer:
1
2
3
4
|
# novos edit.html.erb e new.html.erb
form_for([@post, @comment]) do |f|
...
end
|
Rails tentará ser esperto o suficiente para entender que este array representa uma Rota Aninhada. Checará routes.rb, calculará e esta é a rota nomeada post_comment_url(@post, @comment).
Vamos explicar primeiro a rota nomeada. Quando configuramos um Resource Route no arquivo routes.rb, ganhamos estas rotas nomeadas:
| rota |
verbo HTTP |
Action do Controller |
| 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 Ações para dominar tudo …” :-)
“Lord of the Rings”, para quem não entendeu a piada ;-)
Você pode acrescentar os sufixo ‘path’ ou ‘url’. A diferença é:
| comments_url |
http://localhost:3000/comments |
| comments_path |
/comments |
Finalmente você pode acrescentar o prefixo ‘formatted’, dando a você:
| formatted_comments_url(:atom) |
http://localhost:3000/comments.atom |
| formatted_comment_path(@comment, :atom) |
/comments/1.atom |
Agora, como Comments está aninhado com Post, nós somos obrigados a adicionar o prefixo ‘post’. No Rails 1.2 este prefixo era opcional. Era possível dizer a diferença através do número de parâmetros passados para o helper de rota nomeada, mas isto poderia levar a muitas ambigüidades. Então agora é obrigatório ter o prefixo, como:
| rota |
verbo HTTP |
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 |
Logo, para resumir, temos que fazer as views de Comments se comportarem aninhados no Post. Assim temos que mudar as rotas nomeadas no código padrão gerado pelo scaffold para a forma aninhada:
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) %>
|
Algumas observações:
- Repare que eu criei uma partial para ser DRY nos formulários new e edit. Mas preste atenção que ao invés de :partial => ‘comment’, Eu fiz :partial => @comment. Então ele pode deduzir o nome do partial através do nome da classe. Se passamos uma coleção isto será equivalente à antiga instrução ‘:partial, :collection’.
- Eu posso usar post_comment_path(
post, @comment), ou simplesmente [post, @comment].
- Preste bem atenção para não esquecer qualquer rota nomeada.
Finalmente, seria bom unir a lista de comentários ao post view. Logo, vamos fazer isso:
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 %>
|
Assim, eu já adicionei um link lá. Vejamos como se parece:



Completando as views
Ok, parece bom, mas não é como um Blog deveria se comportar! A view “show” do Post já deveria ter a lista de comentários e um formulário para postar um novo comentário também! Então vamos fazer algumas pequenas adaptações. Nada de novo aqui, só o Rails tradicional. Vamos começar com a 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 %>
|
Mais observações
- Nada novo no iterador, apenas listando todos os comentários
- Novamente, nós passamos a variável para a partial
Um ajuste final: sempre que nós criarmos um novo post, gostaríamos de retornar ao mesmo view “show” do Post, então mudamos o CommentsController para se comportar assim:
1
2
3
4
5
|
# app/controllers/comments_controller.rb
# old redirect:
redirect_to(@post, @comment)
# new redirect:
redirect_to(@post)
|
Rotas em Namespace
Ok, temos um simples mini-blog que de certo modo imita o clássico blog do screencast ‘15 minutes’ do David em 2005. Agora vamos um passo a frente: Posts não deveriam estar disponíveis publicamente para qualquer um editar. Precisamos de uma área de administração em nosso website. Criaremos um novo controle para isso:
./script/generate controller Admin::Posts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Rails 2.0 agora suporta namespaces. Isto criará um subdiretório chamado app/controllers/admin.
O que queremos fazer com isso:
# Criar uma nova rota
# Copiar todas as actions do antigo controller Posts par o novo Admin::Posts
# Copiar as views de posts para app/views/admin*
# Deixar o antigo controller Posts somente com as actions 'index' e 'show', isso significa deletar as views new e edit também
# Adaptar as actions e views que há pouco copiamos para que eles entendam que estão no controller admin
Antes de mais nada, vamos editar config/routes.rb novamente:
--- ruby
map.namespace :admin do |admin|
admin.resources :posts
end
|
Na prática isso significa que temos rotas nomeadas para Posts com o prefixo ‘admin’. Isto tornará claro as antigas rotas de posts para as novas rotas de posts do admin, semelhante a isso:
| posts_path |
/posts |
| post_path(@post) |
/posts/:post_id |
| admin_posts_path |
/admin/posts |
| admin_post_path(@post) |
/admin/posts/:post_id |
Vamos copiar as actions do antigo controller Posts e adaptar as rotas para o novo 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
...
|
Não esqueça de deletar todos os métodos do app/controllers/posts_controller.rb, deixando apenas os métodos ‘index’ e ‘show’.
Vamos copiar as views (assumindo que seu shell já está na pasta principal do projeto):
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
|
Agora vamos editar as views de 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 %>
|
Quase pronto: se você testar http://localhost:3000/admin/posts deverá funcionar corretamente agora. Porém estará feio, e isso porque não temos um layout global da aplicação. Quando fizemos o primeiro scaffolds, Rails gerou um layout específico para Post e Comment somente. Vamos apagá-lo e criar um genérico:
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
|
Logo, vamos mudar o título para:
--- ruby
<!-- app/views/layouts/application.html.erb -->
...
<title>My Great Blog</title>
...
|
Só permanecem as antigas páginas ‘index’ e ‘show’ do controller Posts. Eles também têm links para os métodos que deletamos, então vamos retirá-los:
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 %>
|
Nós podemos testar tudo do browser, indo em http://localhost:3000/admin/posts e vendo que tudo está funcionando corretamente agora. Mas, nós também temos uma coisa errada: uma área administrativa não deveria estar disponível publicamente. No momento você pode entrar e editar qualquer coisa. Precisamos de autenticação.
Autenticação HTTP Básica
Há várias maneiras de implementar autenticação e autorização. Um plugin que é muito usado para isto é restful_authentication.
Porém nós não queremos nada demais aqui. E para isto o Rails 2.0 nos dá um ótima maneira de fazer autenticação. A idéia é: vamos usar o que HTTP já nos dá: Autenticação HTTP Básica. A desvantagem é: nós definitivamente precisamos usar SSL quando utilizarmos em produção. Mas, claro, você deveria fazer isso de qualquer maneira. Formulário de autenticação HTML não é protegido sem SSL também.
Assim, vamos editar nosso controller Admin::Posts para adicionar autenticação:
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
|
Você já sabe o que o ‘before_filter’ faz: ele executa o método configurado antes de qualquer ação no controller. Se você configurar na classe ApplicationController então ele vai executar antes de qualquer ação de qualquer outro controller. Porém nós queremos somente proteger Admin::Posts aqui
Logo, implementamos este método e o segredo é o método ‘authenticate_or_request_with_http_basic’ que vamos bloquear. Isto nos dará o username e senha que o usuário digitou no browser. Nós normalmente teríamos um modelo de Usuário de algum tipo para autenticar estes dados, mas para nosso exemplo muito, muito simples eu estou fazendo a verificação diretamente, mas você entendeu a idéia.
