Capítulo 10

Completando o Sistema

"O êxito parece doce a quem não o alcança"

10.1 - Um pouco mais sobre o Scaffold

Vimos que quando usamos o gerador scaffold o Rails cria os arquivos necessários em todas as camadas. Controller, Model, Views e até mesmo arquivos de testes e a Migration. Para concluir nossos projeto precisamos criar apenas Controller + Views pois tanto o modelo quanto as migrations já estão prontos. Para isso vamos usar o mesmo gerador scaffold mas vamos passar os parâmetros: --migration=false para ele ignorar a criação da migration e o parâmetro -s que é a abreviação para --skip que faz com que o rails "pule" os arquivos já existentes.

Para concluir os modelos que já começamos vamos executar o gerador da seguinte maneira: rails generate scaffold cliente nome:string idade:integer --migration=false -s

Outros parâmetros nos geradores

Para conhecer todas as opções de parâmetros que um gerador pode receber tente executá-lo passando o parâmetro -h Ex. rails generate scaffold -h

10.2 - Exercícios: Completando nosso domínio

Vamos gerar os outros controllers e views usando o scaffold:

  1. Primeiro vamos gerar o scaffold para cliente, no terminal execute:

    $ rails generate scaffold cliente nome idade:integer --migration=false -s
    
    scaffold-cliente.png
  2. Agora vamos gerar o scaffold de qualificacao, execute:

    $ rails generate scaffold qualificacao cliente_id:integer
        restaurante_id:integer nota:float valor_gasto:float
        --migration=false -s
    
    scaffold-qualificacao.png
    1. Olhe as views criadas (app/views/clientes e app/views/qualificacoes)views.png
    2. Olhe as rotas criadas (config/routes.rb)rotas.png
    3. Abra os arquivos app/views/clientes/index.html.erb e app/views/restaurantes/index.html.erb e apague as linhas que chamam a action destroy Lembre-se de que não queremos inconsistências na nossa tabela de qualificações
    4. Reinicie o servidor
    5. Teste: http://localhost:3000/clientes e http://localhost:3000/qualificacoes

    Note que precisamos utilizar a opção "--migration=false" no comando scaffold, além de informar manualmente os atributos utilizados em nossas migrations. Isso foi necessário, pois já tínhamos um migration pronto, e queríamos que o Rails gerasse os formulários das views para nós, e para isso ele precisaria conhecer os atributos que queríamos utilizar.

    css scaffold

    O comando scaffold, quando executado, gera um css mais bonito para nossa aplicação. Se quiser utilizá-lo, edite nosso layout (app/views/layouts/application.html.erb) e adicione a seguinte linha logo abaixo da tag <title>:

    <%= stylesheet_link_tag 'scaffold' %>

Saber inglês é muito importante em TI

O Galandra auxilia a prática de inglês através de flash cards e spaced repetition learning. Conheça e aproveite os preços especiais.

Pratique seu inglês no Galandra.

10.3 - Selecionando Clientes e Restaurante no form de Qualificações

Você já deve ter reparado que nossa view de adição e edição de qualificações está um tanto quanto estranha: precisamos digitar os IDs do cliente e do restaurante manualmente.

Para corrigir isso, podemos utilizar o FormHelper select, inserindo o seguinte código nas nossas views de adição e edição de qualificações:

<%= select('qualificacao', 'cliente_id',
                            Cliente.order(:nome)
                            {|p| [ p.nome, p.id]}) %>

em substituição ao:

<%= f.number_field :cliente_id %>

Mas existe um outro FormHelper mais elegante, que produz o mesmo efeito, o collection_select:

<%= collection_select(:qualificacao, :cliente_id,
    Cliente.order(:nome),
    :id, :nome, :prompt => true) %>

Como estamos dentro de um form_for, podemos usar do fato de que o formulário sabe qual o nosso ActiveRecord, e com isso fazer apenas:

<%= f.collection_select(:cliente_id,
    Cliente.order(:nome),
    :id, :nome, :prompt => true) %>

10.4 - Exercícios: Formulário com collection_select

  1. Vamos utilizar o FormHelper collection_select para exibirmos o nome dos clientes e restaurantes nas nossas views da qualificação:

    1. Abra o arquivo app/views/qualificacoes/_form.html.erb
    2. Troque a linha:
      <%= f.number_field :cliente_id %>
      
      por:
      <%= f.collection_select(:cliente_id, Cliente.order(:nome),
                                          :id, :nome, :prompt => true) %>
      
    3. Troque a linha:
      <%= f.number_field :restaurante_id %>
      
      por:
      <%= f.collection_select(:restaurante_id, Restaurante.order(:nome),
                                              :id, :nome, :prompt => true) %>
      
    4. Teste: http://localhost:3000/qualificacoes

10.5 - Exercícios Opcionais: Refatorando para respeitarmos o MVC

A forma como implementamos o seletor de restaurante e cliente não é a mais adequada pois ela fere o modelo arquitetural que o rails se baseia, o MVC. Isso ocorre, pois estamos invocando Cliente.order(:nome) dentro da view, ou seja, a view está se comunicando com o modelo.

  1. Vamos arrumar isso, criando as variáveis de instância @restaurantes e @clientes nas actions new e edit do QualificacoesController:

    1. Abra o QualificacoesController. (app/controllers/qualificacoes_controller.rb)
    2. Crie as variáveis de instância nas primeiras linhas da action new:
      def new
        @clientes = Cliente.order :nome
        @restaurantes = Restaurante.order :nome
      
        # resto do código
      end
      
    3. Crie as variáveis de instância nas primeiras linhas da action edit:
      def edit
        @clientes = Cliente.order :nome
        @restaurantes = Restaurante.order :nome
      
        # resto do código
      end
      
  2. Agora podemos usar as variáveis de instância no nosso partial form.

    1. Abra o partial form (app/views/qualificacoes/_form.html.erb)
    2. Substitua as antigas chamadas do select_collection por:
      f.collection_select(:restaurante_id, @restaurantes,
                                          :id, :nome, :prompt => true)
      
      e
      f.collection_select(:restaurante_id, @clientes,
                                          :id, :nome, :prompt => true)
      
  3. Observe que temos código duplicado no nosso QualificacoesController, vamos refatorar extraindo a criação das variáveis de instância para um método privado,

    1. Abra o controller de qualificacoes (app/controllers/qualificacoes_controller.rb).
    2. Ao final da classe, crie um método privado que cria as variáveis de instância:
      class QualificacoesController < ApplicationController
        # todas as actions
      
      private
        def preparar_form
          @clientes = Cliente.order :nome
          @restaurantes = Restaurante.order :nome
        end
      end
      
    3. Ainda no controller de qualificacoes, vamos usar o preparar_form na action new. Dentro da action new, substitua:
      @clientes = Cliente.order :nome
      @restaurantes = Restaurante.order :nome
      
      por:
      preparar_form
      
    4. Repita o exercício acima na action edit.
  4. As actions new e edit estão funcionando perfeitamente. Vamos analisar a action create. Observe que caso dê erro de validação, ela também irá renderizar a view new.html.erb. Logo, é necessário criarmos as variáveis de instância @restaurantes e @clientes também na action create.

    1. Abra o controller de qualificacoes (app/controllers/qualificacoes_controller.rb).
    2. Altere a action create para chamar o preparar_form:
      def create
        @qualificacao = Qualificacao.new(qualificacao_params)
      
        respond_to do |format|
          if @qualificacao.save
            format.html { redirect_to @qualificacao,
              notice: 'Qualificacao was successfully created.' }
            format.json { render json: @qualificacao,
              status: :created, location: @qualificacao }
          else
            preparar_form
            format.html { render action: "new" }
            format.json { render json: @qualificacao.errors, status: :unprocessable_entity }
          end
        end
      end
      
  5. O mesmo problema da action create ocorre na action update, portanto vamos alterá-la.

    1. Abra o controller de qualificacoes (app/controllers/qualificacoes_controller.rb).
    2. Altere a action update para chamar o preparar_form:
      def update
        @qualificacao = Qualificacao.find(params[:id])
      
        respond_to do |format|
          if @qualificacao.update_attributes(qualificacao_params)
            format.html { redirect_to @qualificacao,
              notice: 'Qualificacao was successfully updated.' }
            format.json { head :no_content }
          else
            preparar_form
            format.html { render action: "edit" }
            format.json { render json: @qualificacao.errors,
              status: :unprocessable_entity }
          end
        end
      end
      
  6. Inicie o servidor (rails server) e tente criar uma qualificação inválida para testar nossa correção.

Você não está nessa página a toa

Você chegou aqui porque a Caelum é referência nacional em cursos de Java, Ruby, Agile, Mobile, Web e .NET.
Faça curso com quem escreveu essa apostila.

Consulte as vantagens do curso Desenv. Ágil para Web com Ruby on Rails.

10.6 - Exercícios Opcionais: Exibindo nomes ao invés de números

  1. Agora vamos exibir o nome dos restaurantes e clientes nas views index e show de qualificações:

    1. Abra o arquivo app/views/qualificacoes/show.html.erb
    2. Troque a linha:
      <%= @qualificacao.cliente_id %>
      
      por:
      <%= @qualificacao.cliente.nome %>
      
    3. Troque a linha:
      <%= @qualificacao.restaurante_id %>
      
      por:
      <%= @qualificacao.restaurante.nome %>
      
    4. Abra o arquivo app/views/qualificacoes/index.html.erb
    5. Troque as linhas:
      <td><%= qualificacao.cliente_id %></td>
      <td><%= qualificacao.restaurante_id %></td>
      
      por:
      <td><%= qualificacao.cliente.nome %></td>
      <td><%= qualificacao.restaurante.nome %></td>
      
    6. Teste: http://localhost:3000/qualificacoes
  2. Por fim, vamos utilizar o FormHelper hidden_field para permitir a qualificação de um restaurante a partir da view show de um cliente ou de um restaurante. No entanto, ao fazer isso, queremos que não seja necessário a escolha de cliente ou restaurante. Para isso:

    1. Abra o arquivo app/views/qualificacoes/_form.html.erb
    2. Troque as linhas
      <p>
        <%= f.label :cliente_id %><br />
        <%= f.collection_select(:cliente_id,
            @clientes,
            :id, :nome, :prompt => true) %>
      </p>
      
      por:
      <% if @qualificacao.cliente %>
        <%= f.hidden_field 'cliente_id' %>
      <% else %>
        <p><%= f.label :cliente_id %><br />
      <%= f.collection_select(:cliente_id, @clientes,
                                          :id, :nome, :prompt => true) %></p>
      <% end %>
      
    3. Troque as linhas
      <p>
        <%= f.label :restaurante_id %><br />
        <%= f.collection_select(:restaurante_id, @restaurantes,
                                                :id, :nome, :prompt => true) %>
      </p>
      
      por:
      <% if @qualificacao.restaurante %>
        <%= f.hidden_field 'restaurante_id' %>
      <% else %>
        <p><%= f.label :restaurante_id %><br />
        <%= f.collection_select(:restaurante_id, @restaurantes,
                                                :id, :nome, :prompt => true) %></p>
      <% end %>
      
    4. Adicione a seguinte linha na view show do cliente (app/views/clientes/show.html.erb):
      <%= link_to "Nova qualificação", controller: "qualificacoes",
                                       action: "new",
                                       cliente: @cliente %>
      
    5. Adicione a seguinte linha na view show do restaurante (app/views/restaurantes/show.html.erb):
      <%= link_to "Qualificar este restaurante", controller: "qualificacoes",
                                                 action: "new",
                                                 restaurante: @restaurante %>
      
    6. Por fim, precisamos fazer com que o controlador da action new das qualificações receba os parâmetros para preenchimento automático. Abra o controller app/controllers/qualificacoes_controller.rb
    7. Adicione as seguintes linhas à nossa action new:
      if params[:cliente]
        @qualificacao.cliente = Cliente.find(params[:cliente])
      end
      if params[:restaurante]
        @qualificacao.restaurante = Restaurante.find(params[:restaurante])
      end
      
      controller-qualificacao-new-hidden.png
    8. Teste: http://localhost:3000/clientes, entre na página Show de um cliente e faça uma nova qualificação.

10.7 - Mais sobre os controllers

Podemos notar que nossas actions, por exemplo a index, fica muito parecida com a action index de outros controladores, mudando apenas o nome do modelo em questão.

#qualificacoes_controller
def index
  @qualificacoes = Qualificacao.all

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @qualificacoes }
  end
end

#clientes_controller
def index
  @clientes = Cliente.all

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @clientes }
  end
end

Normalmente precisamos fazer exatamente a mesma ação para formatos iguais e por isso acabamos repetindo o mesmo bloco de respond_to nas actions. Para solucionar esse problema, no rails 3 acrescentaram o método respond_to nos controllers e o método respond_with nas actions. Veja o exemplo:

class ClientesController < ApplicationController

  respond_to :html, :xml

  # GET /clientes
  # GET /clientes.xml
  def index
    @clientes = Cliente.all

    respond_with @clientes
  end

  # GET /clientes/1
  # GET /clientes/1.xml
  def show
    @cliente = Cliente.find(params[:id])

    respond_with @cliente
  end
  ...
end

Dessa forma estamos dizendo para o rails que esse controller irá responder para os formatos html e xml, dentro da action basta eu dizer qual objeto é pra ser usado. No caso da action index, se a requisição pedir o formato html, o rails simplesmente vai enviar a chamada para o arquivo views/clientes/index.html.erb e a variável @clientes estará disponível lá. Se a requisição pedir o formato xml, o rails fará exatamente o mesmo que estava no bloco de respond_to que o scaffold criou, vai renderizar a variável @clientes em xml. O bloco de código acima é equivalente ao gerado pelo scaffold:

class ClientesController < ApplicationController
  # GET /clientes
  # GET /clientes.xml
  def index
    @clientes = Cliente.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @clientes }
    end
  end

  # GET /clientes/1
  # GET /clientes/1.xml
  def show
    @cliente = Cliente.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @cliente }
    end
  end
   ...
end

Veja na imagem como ficaria o ClientesController usando essa outra maneira de configurar os controllers.

controller_respond_with.png