Artigo WebMobile 26 - Mini-curso de Ruby on Rails – Parte 7

Neste artigo veremos como estabelecer relacionamentos entre os usuários – criados no último artigo – e os posts de nosso aplicativo. Além disso, criaremos uma entidade para categorizar nossas publicações.

De que trata o artigo: Nessa edição, veremos como estabelecer relacionamentos entre os usuários – criados no último artigo – e os posts de nosso aplicativo. Além disso, criaremos uma entidade para categorizar nossas publicações.

Para que serve: Com esse artigo, aprenderemos como integrar o sistema de autenticação da última edição com os posts do blog. Além dessa relevante ligação, também veremos como interligar outra entidade aos posts, mostrando o quanto é simples o funcionamento do framework.

Em que situação o tema útil: A interligação de diversas entidades de um banco de dados, conforme mostrado no artigo, está sempre presente no desenvolvimento de um aplicativo. Com ela, aprimoramos a qualidade da interface exibida ao usuário de nossa aplicação.

Na última edição da WebMobile conhecemos o Authlogic, um plugin (ou gem) que fornece todos os recursos necessários para autenticação de usuários. No artigo dessa edição continuaremos a integração em nosso blog, criando recursos que facilitarão a navegabilidade dos usuários no aplicativo. Além disso, veremos nesse artigo como organizar os posts por categoria, onde o usuário do site poderá utilizar filtros para encontrar assuntos pelo seu interesse.

Integração da Autenticação

No último artigo, instalamos e configuramos o Authlogic no projeto. Com ele, já criamos em nosso projeto toda a estrutura necessária para a criação e autenticação de usuários. Baseado na implantação do Authlogic, vamos definir algumas mudanças no nosso blog: a partir de agora os posts só poderão ser feitos por usuários registrados. Para isso, algumas mudanças deverão ser feitas:

  1. adicionar na tabela Posts a coluna user_id, onde essa coluna será responsável por identificar o usuário dono do post;
  2. criar o relacionamento entre posts e users;
  3. definir um filtro para que a página de novo post só possa ser acessada por usuários cadastrados.

Para que realizemos a primeira mudança – adição de uma coluna user_id na tabela Posts – é necessário criarmos uma migration que será responsável por adicionar o campo no banco. Confira mais sobre migrations na Nota DevMan 1. No terminal, digite:

./script/generate migration add_user_id_column

O comando criará um arquivo de migração na pasta db/migrate. O nome do arquivo é composto pela data atual e pelo nome informado no terminal, no caso, “add_user_id_column”. Esse arquivo será responsável pela adição da coluna user_id; seu conteúdo é apresentado na Listagem 1.

Listagem 1. Migration add_user_id_column


class AddUserIdColumn < ActiveRecord::Migration
  def self.up
    add_column :posts, :user_id, :integer
  end

  def self.down
    remove_column :posts, :user_id
  end
end

Os arquivos de migration possuem dois métodos: o método self.up determina o que será feito caso a migration seja processada, e o método self.down determina o que será refeito caso a migration seja revertida. Para que a alteração seja efetivada no banco, é necessária a execução do comando:

rake db:migrate

O parâmetro db:migrate executará as alterações do banco de dados criadas pelos arquivos de migration que ainda não foram executadas.

Nota DevMan 1. Por que Migrations?

Os arquivos de migração são usados para que as alterações no banco de dados fiquem registradas em ordem cronológica. Isso facilita a integração do projeto em novos computadores e faz com que não fiquemos presos a uma confusão de alterações de banco de dados.

Nosso próximo passo é determinar o relacionamento entre Posts e Users: um post pertence a um usuário e um usuário possui muitos posts. Então, abra o model post.rb e acrescente o relacionamento ‘belongs_to :user’, de acordo com a Listagem 2. Em seguida acrescente o relacionamento ‘has_many :posts’ no model user.rb, conforme indicado na Listagem 3.

Listagem 2. Alteração no model post.rb


class Post < ActiveRecord::Base
  has_many :comments

  belongs_to :user

Listagem 3. Alteração no model user.rb


class User < ActiveRecord::Base
  acts_as_authentic
  
  has_many :posts
end

Determinado o relacionamento entre as duas entidades e criada a chave estrangeira que indica o usuário que criou o post, nosso próximo passo é indicar no controller que a coluna user_id da entidade Post seja preenchida com o usuário atualmente logado no sistema. A Listagem 4 mostra como faremos isso.

Listagem 4. Alteração no controller posts_controller.rb


def create
  @post = Post.new(params[:post])
  @post.user_id = current_user.id

Na linha 3 da Listagem 4 é definido, portanto, que o user_id corresponda ao usuário atualmente logado. O método current_user foi definido no artigo da edição anterior e criado com a ajuda do Authlogic.

A outra alteração necessária em nosso controller é impedir que a página de novo post seja aberta por usuários não logados. Para isso, vamos adicionar um filtro nesse controller, como pode ser visto na Listagem 5.

Listagem 5. Alteração no model user.rb


class PostsController < ApplicationController
  before_filter :require_user, :only => [:new, :edit, :create, :update] 

Além da página que exibe um novo post, as páginas de edição, atualização e criação de um post também serão restritas a usuários cadastrados. Assim, quando alguém clicar em “novo post” na homepage do aplicativo, terá uma mensagem informando-o que ele precisa estar cadastrado no sistema para poder entrar na página, como apresentado na Figura 1.

Figura 1. Tela de novo post caso o usuário não esteja logado.

Nossa quarta modificação refere-se ao indicar nas views do nosso sistema de blog os dados do usuário que escreveu a publicação.

Listagem 6. A view posts/index.html.erb


<% for post in @posts %> 
  <h2><%= link_to post.title, post %></h2> 

  <h3><%= post.created_at.to_s(:short) %> - <%= time_ago_in_words(post.created_at) %> - <%= post.user.login %></h3>

Na linha 4 da Listagem 6, o login do usuário é exibido, mas até agora, nenhum dos nossos posts tinha dono! E agora? Ao processar essa página, teremos a Figura 2.

Figura 2. Erro de usuário nulo.

Como não temos a informação ‘user_id’ preenchida para os posts anteriores, é impossível termos o dado de login de um usuário nulo, gerando o erro mostrado na Figura 2. Para contornarmos isso, que tal exibirmos o login apenas caso a coluna ‘user_id’ esteja preenchida? Substitua então a linha 4 pelo conteúdo a seguir:

<h3><%= post.created_at.to_s(:short) %> - <%= time_ago_in_words(post.created_at) %> - <%= post.user.login if post.user_id %></h3> 

Outra forma de evitar a inconsistência do campo user_id seria excluir todos os registros da tabela Posts. Isso poderia ser feito com o uso do método delete_all, conforme indicado na Nota DevMan 2.

Nota DevMan 2. delete_all

O Rails possui um método para excluir todos os registros de uma tabela: delete_all. Abra o console do Rails (./script/console) e experimente: Post.delete_all

A Figura 3 apresenta o blog com os nomes dos autores nos respectivos posts.

Figura 3. Integração do usuário terminada.

Categorias

Um recurso recorrente em blogs e sites de notícias é o agrupamento de posts por categoria. Desse modo, assuntos de um mesmo tema podem ser agrupados e assim, aprimoram a experiência do usuário na navegabilidade de um site. Vamos implementar agora esse recurso no nosso aplicativo, no qual o usuário, ao criar um post, definirá a qual categoria o post pertencerá. No console, crie o scaffold Category com o comando a seguir. A Nota DevMan 3 comenta sobre o script/generate:

./script/generate scaffold category name:string --skip-timestamps 

De acordo com o comando acima, a entidade Category possuirá apenas um campo: name. Observe que ao invocarmos a criação do scaffold, utilizamos a opção skip-timestamps. Essa opção faz com que não sejam criadas as colunas “created_at” e “updated_at”, que são criadas automaticamente quando geramos o scaffold. Após a execução do scaffold, conclua a implantação criando a tabela categories no banco. Para isso, digite o comando:

rake db:migrate

Nota DevMan 3. Mais sobre o script/generate

Conforme já vimos, o ./script/generate pode gerar models, controllers e scaffolds. Caso queira conferir uma breve documentação, digite no terminal: ./script/generate <recurso> para obter mais informações sobre o respectivo recurso. Por exemplo, ./script/generate scaffold traz uma documentação interessante sobre opções personalizadas do scaffold, incluindo o ‘--skip-timestamps’.

A URL http://localhost:3000/categories lista, edita e adiciona categorias. Assim como fizemos com Post, abra o controller de Category e adicione o filtro para que apenas usuários registrados possam ver esta página (Listagem 7).

Listagem 7. Arquivo app/controllers/categories_controller.rb


class CategoriesController < ApplicationController
  before_filter :require_user, :only => [:new, :edit, :create, :update] 

Além disso, remova o layout categories.html.erb (app/views/layouts/categories.html.erb). Uma vez que personalizamos o layout padrão (application.html.erb), o arquivo gerado pelo scaffold não é mais necessário.

Vamos agora fazer uma implementação semelhante ao que fizemos entre posts e usuários:

  1. criar uma coluna category_id na tabela posts;
  2. determinar os relacionamentos entre as duas entidades;
  3. criar o código responsável por categorizar um post;
  4. exibir a categoria do post.

Para criarmos a coluna category_id na tabela posts, crie a migration “add_category_id_column”. Assim como a migration “add_user_id_column”, essa migration cria um campo do tipo inteiro em nossa tabela. O conteúdo dessa migration está na Listagem 8.

Listagem 8. Migration add_category_id_column.rb


class AddCategoryIdColumn < ActiveRecord::Migration
  def self.up
    add_column :posts, :category_id, :integer
  end
 
  def self.down
    remove_column :posts, :category_id
  end
end

Após a execução da migration (com o nosso conhecido comando rake db:migrate), vamos determinar o relacionamento entre categorias e posts: um post possui uma categoria (belongs_to) e uma categoria possui muitos posts (has_many). A Listagem 9 apresenta como fazer isso.

Listagem 9. model Post.rb


class Post < ActiveRecord::Base
  has_many :comments

  belongs_to :user
  belongs_to :category

Criados os relacionamentos entre as entidades e o campo que atribui a categoria de um texto na tabela posts, é hora de interligarmos a atribuição da categoria em um post. Para isso vamos criar uma caixa de seleção na página de novo post que mostrará todas as categorias cadastradas. No HTML, criaríamos esse menu drop-down com a tag select, mas o Rails conta com o helper select, que permite a criação de menus drop-down de forma dinâmica. Abra a view que exibe o formulário de novo post e confira algumas mudanças que fizemos a partir do código inicial, gerado pelo scaffold (Listagem 10).

Listagem 10. view new.html.erb


<h1>New post</h1>

<%= error_messages_for(:post) %>
<% form_for(@post) do |f| %>
  <p>
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </p>

  <p>
 <%= f.label :category_id, "Categoria" %><br/>
 <%= f.select :category_id, Category.find(:all, :order => "name").collect {|p| [ p.name, p.id ] }, { :prompt => "Selecione", :include_blank => true } %>
  </p>

  <p>
    <%= f.label :body %><br />
    <%= f.text_area :body %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', posts_path %>

As novidades desse arquivo estão no trecho 10-13. Na linha 12 é criado um select que é povoado com todos os registros – ordenados por nome – da tabela categories. Esse select tem o valor inicial “Selecione” e o valor definido nessa caixa de seleção preencherá a coluna “category_id”.

A Figura 4 apresenta nossa nova tela de novo post incluindo a caixa de listagem com as categorias já cadastradas. Assim como fizemos para a exibição do nome do usuário, vamos agora exibir o nome da categoria no post. Abra a view index.html.erb da entidade Post e edite a linha 4 para:


  <h3><%= post.created_at.to_s(:short) %> - <%= time_ago_in_words(post.created_at) %> - <%= post.user.login if post.user_id %> - <%= post.category.name if post.category_id %></h3>

Figura 4. Página novo post.

O nome da categoria será exibido caso exista uma categoria definida, ao lado do autor do post. A Figura 5 mostra uma screenshot do blog atualizado. Na imagem, percebe-se que o post apresentado pertence à categoria Pessoal.

Figura 5. Post com categoria.

Outra pequena mudança necessária refere-se à alteração da view de edição de post. Agora que temos nosso sistema de categorias implementado, basta copiar o código da view new.html.erb para a view edit.html.erb, conforme exibido na Listagem 11.

Na próxima edição, usaremos partials para substituir essa redundância.

Listagem 11. view app/views/posts/edit.html.erb


<h1>Editing post</h1>
 
<% form_for(@post) do |f| %>
  <%= f.error_messages %>
 
  <p>
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </p>
 
  <p>
     <%= f.label :category_id, "Categoria" %><br/>
     <%= f.select :category_id, Category.find(:all, :order => "name").collect {|p| [ p.name, p.id ] }, { :prompt => "Selecione", :include_blank => true } %>
  </p>
 
  <p>
    <%= f.label :body %><br />
    <%= f.text_area :body %>
  </p>
  <p>
    <%= f.submit "Edit" %>
  </p>
<% end %>
 
<%= link_to 'Show', @post %> |
<%= link_to 'Back', posts_path %>

As únicas alterações necessárias referem-se ao título da página e ao rótulo do botão submit.

Aplicação de filtros

Agora que já temos categorias e usuários vinculados aos posts, que tal alterarmos nosso aplicativo para filtrar os posts de acordo com esses dados? Então, vamos alterar o controller, o model e a view da entidade Post, de modo que possa manipular os parâmetros que serão passados via URL. A primeira alteração refere-se ao controller, conforme indicado na Listagem 12.

Listagem 12. controller app/controllers/posts_controller.rb


 6   def index
 7     conditions = ["users.login = ?", params[:user]] if params[:user]
 8     conditions = ["categories.name = ?", params[:category]] if params[:category]
 9     conditions = ['body LIKE ?', params[:search]] if params[:search]
10     
11     @posts = Post.search(conditions, params[:page])
12
13     respond_to do |format|
14       format.html # index.html.erb
15       format.rss  # index.rss.builder
16       format.xml  { render :xml => @posts }
17     end
18   end

A busca de posts poderá ser feita com uma das três condições (linhas 7 a 9): usuário, categoria ou palavra filtrada no campo de busca. Com a alteração do controller, também precisamos modificar nosso model. Abra o model post.rb e substitua o método search, conforme o indicado na Listagem 13.

Listagem 13. model app/models/post.rb


14 def self.search(conditions, page)
15   paginate :all, :per_page => 10, :page => page, :conditions => conditions, :order => 'posts.created_at DESC', :include => [:user, :category]
16 end

Substituímos o método criado na edição anterior por apenas uma chamada ao método paginate. Observe que agora esse método recebe as condições enviadas pelo controller, ordena as publicações usando o campo created_at da tabela Posts e inclui em sua consulta as entidades User e Category.

Com as alterações acima, será possível filtrar os registros do banco passando os parâmetros user=NomedoUsuario ou category=Categoria na URL do navegador. Assim, a última alteração do artigo será transformar o nome da categoria e o nome do usuário em links com seus respectivos parâmetros. Desse modo, abra a view index.html.erb e altere a linha 4 para:


  <h3><%= post.created_at.to_s(:short) %> - <%= time_ago_in_words(post.created_at) %> - <%= link_to post.user.login, :controller => 'posts', :user => post.user.login if post.user_id %> - <%= link_to post.category.name, :controller => 'posts', :category => post.category.name if post.category_id %></h3>

A Figura 6 apresenta a versão final do blog desenvolvida neste artigo. Caso você clique em “pedro”, verá artigos escritos por esse autor e caso você clique em “Pessoal”, verá artigos dessa categoria.

Figura 6. Blog com links para o usuário e categoria.

Conclusão

Nessa edição vimos como é fácil e rápido interligar entidades no framework Ruby on Rails. Como o framework estipula convenções ao invés de configurações, os relacionamentos funcionam apenas com o uso de métodos como o has_many e belongs_to. Percebemos também como é fácil a criação de filtros em nossas buscas sem precisar digitar uma linha de SQL. A agilidade proporcionada pelo Rails confirma a escolha de tantos programadores ao procurar uma ferramenta de desenvolvimento ágil. Até a próxima edição.

Artigos relacionados