Posts Tagged ‘Tutorial’

Tratando Múltiplos Models em Um Único Form

Tuesday, January 13th, 2009

O livro Advanced Rails Recipes é um livro muito interessante para quem quer aprender algumas dicas mais avançadas de Rails. Especialmente aquele tipo de dicas que você não encontra em tutoriais ou blogs por aí. Da descrição do site:

“Você irá aprender como os profissionais resolveram problemas complicados usando as técnicas mais atualizadas de Rails 2, para que você possa entregar a sua impressionante aplicação Web de forma mais fácil e mais rápida.”

Esta é uma tradução para o português, com autorização de Dave Thomas de Pragmatic Programers. Aproveite! :)

Tratando Múltiplos Models em Um Único Form

Por Ryan Bates (http://railscasts.com/). Ryan trabalha com desenvolvimento Web desde 1998. Começou a trabalhar profissionalmente com Ruby on Rails em 2005 e é mais conhecido pelo seu trabalho com os Railscasts, uma série gratuita de screencasts sobre Ruby on Rails.

Problema

A maioria do código Rails que você encontra geralmente trata um model de cada vez. Isso nem sempre é prático. Algumas vezes você precisa criar e/ou editar dois (ou mais) models em um único form, nas situações onde você tem uma associação one-to-many entre os models.

Solução

Digamos que nós precisamos salvar todas as tarefas pendentes para vários projetos. Quando nós criarmos ou atualizarmos um projeto, nós gostaríamos de adicionar, remover e atualizar suas tarefas, em um único form. O que estamos imaginando é:

Vamos começar criando um relacionamento has_many entre Project e Task. Para simplificar, vamos dar para cada model um atributo obrigatório chamado name.


# Arquivo: app/models/project.rb
class Project < ActiveRecord::Base
  has_many :tasks, :dependent => :destroy
  validates_presence_of :name
end

# Arquivo: app/models/task.rb
class Task < ActiveRecord::Base
  belongs_to :project
  validates_presence_of :name
end

Nós vamos usar a biblioteca de Javascript Prototype, então antes de qualquer coisa vamos garantir que ela está carregada em nosso arquivo de layout:


# Arquivo: app/views/layouts/application.html.erb
<%= javascript_include_tag :defaults %>

Vamos voltar a nossa atenção para o form, para criar um projeto com suas múltiplas tarefas, e associadas ao projeto. Quando precisamos tratar múltiplos models em um form, é muito útil eleger um model como sendo o foco primário ou principal, e a partir daí construir os models adicionais através da associação entre eles.

Neste caso, vamos fazer de Project o nosso model primário e construir suas tarefas através da associação has_many. Então para a action new de nosso ProjectsController, nós criamos um objeto Project da forma usual. No entanto, nós também inicializamos uma nova Task (em memória) que é associada com o Project de forma que nosso form tem alguma coisa para começar a trabalhar:


# Arquivo: app/controllers/projects_controller.rb
def new
  @project = Project.new
  @project.tasks.build
end

O template para o form precisa de algumas “manhas” já que nós precisamos tratar os campos para o model Project e também os campos de cada um dos models Task. Então, vamos quebrar o problema em partes menores e usar um partial para renderizar os campos da Task e adicionar um helper add_task_link para criar o link que adiciona uma nova tarefa:


# Arquivo: app/views/projects/_form.html.erb
<%= error_messages_for :project %>

<% form_for @project do |f| -%>
  <p>
  Name: <%= f.text_field :name %>
  </p>
  <div id="tasks">
    <%= render :partial => 'task', :collection => @project.tasks %>
  </div>
  <p>
    <%= add_task_link "Add a task" %>
  </p>
  <p>
    <%= f.submit "Submit" %>
  </p>
<% end -%>

Os templates new e edit simplesmente renderizam este partial de formulário, de forma que nós temos uma forma consistente para criar e atualizar um projeto. A partial do form vai e renderiza uma partial de tarefa para cada uma das tarefas do projeto. Antes de entrarmos no conteúdo da partial de tarefa, vamos dar uma olhada naquele helper add_task_link:


# Arquivo: app/helpers/projects_helper.rb
def add_task_link(name)
  link_to_function name do |page|
    page.insert_html :bottom, :tasks, :partial => 'task' , :object => Task.new
  end
end

Quando nós clicamos no link “Add a Task”, nós queremos um novo conjunto de campos de tarefas aparecendo abaixo dos campos de tarefa que já existem no formulário. Ao invés de ocupar o servidor com isto, nós podemos usar JavaScript para adicionar os campos dinamicamente. O método link_to_function aceita um bloco de código RJS. Geralmente nós associamos código RJS com chamadas assíncronas indo para o servidor. Mas neste caso o código RJS gera JavaScript que é executado no browser imediatamente quando o usuário clica no link. O resultado é que renderizar os campos para adicionar uma nova tarefa não requer uma requisição enviada ao servidor, o que leva a um tempo de resposta mais rápido no uso da aplicação.

Voltando ao partial do form, nós estamos usando form_for para dedicar o form para o model @project. Como então nós vamos adicionar campos para cada uma das tarefas do projeto? A resposta está dentro da partial de tarefa:


# Arquivo: app/views/projects/_task.html.erb
<div class="task">
<% new_or_existing = task.new_record? ? 'new' : 'existing' %>
<% prefix = "project[#{new_or_existing}_task_attributes][]" %>

<% fields_for prefix, task do |task_form| -%>
  <p>
    Task: <%= task_form.text_field :name %>
    <%= link_to_function "remove" , "$(this).up('.task').remove()" %>
  </p>
<% end -%>
</div>

O ingrediente chave aqui é o método fields_for. Ele se comporta de forma muito parecida com form_for mas ele não renderiza a tag HTML form. Isto nos permite mudar o contexto para um model diferente, no meio do form principal – como se nós estivéssemos embutindo um form dentro de outro.

O primeiro parâmetro para fields_for é muito importante. Esta string será usada como um prefixo para cada campo de tarefa. Como vamos usar esta partial também para renderizar tarefas existentes – e nós queremos mantê-las separadas quando o form é enviado – no prefixo nós incluímos uma indicação dizendo se a tarefa é nova ou existente. (O ideal seria criar esta string de prefixo em um helper, mas vamos simplificar um pouco as coisas aqui.)

O HTML gerado para uma nova tarefa se parece com isto:


<input name="project[new_task_attributes][][name]" size="30" type="text" />

Se esta fosse uma tarefa existente, o Rails iria colocar automaticamente o ID da tarefa entre as chaves, assim:


<input name="project[existing_task_attributes][7][name]" size="30" type="text" />

Agora, quando o form é enviado, o Rails irá decodificar o nome do campo de entrada, para forçar uma estrutura no hash params. As chaves ([]) que estão preenchidas se tornam keys em um hash aninhado. As chaves que estão vazias se tornam um array. Por exemplo, se nós enviarmos o form com duas novas tarefas, o hash params vai ficar assim:


"project" => {
  "name" => "Yard Work" ,
  "new_task_attributes" => [
    { "name" => "rake the leaves" },
    { "name" => "paint the fence" }
  ]
}

Note que os atributos para o projeto e cada tarefa estão aninhados dentro do hash project. Isto é conveniente porque significa que a action create em nosso controller pode simplesmente passar todos os atributos de projeto ao model Project sem se preocupar sobre o que está dentro do hash project:


# Arquivo: app/controllers/projects_controller.rb
def create
  @project = Project.new(params[:project])
  if @project.save
    flash[:notice] = "Successfully created project and tasks."
    redirect_to projects_path
  else
    render :action => 'new'
  end
end

Este código se parece com uma action create padrão, para um form de um único model. Mas existe algo sútil acontecendo aqui. Quando chamamos Project.new(params[:project]), o Active Record assume que nosso model Project tem um atributo correspondente chamado new_task_attributes porque ele procura uma key chamada new_task_attributes dentro do hash params[:project]. Isto é, o Active Record tentará fazer uma atribuição em massa (mass assign) de todos os dados contidos neste hash, para os atributos correspondentes no model Project. Mas nós não temos um atributo new_task_attributes em nosso model Project.

Um jeito conveniente de manter tudo isto transparente, da ponto de vista do controller, é usar um atributo virtual. Para fazer isso, nós simplesmente criamos um método setter em nosso model Project, chamado new_task_attributes=, que recebe um array e constrói a tarefa para cada elemento:


# Arquivo: app/models/project.rb
def new_task_attributes=(task_attributes)
  task_attributes.each do |attributes|
    tasks.build(attributes)
  end
end

Pode parecer que estas tarefas não estão sendo salvas em lugar nenhum. De fato, o Rails vai fazer isso automaticamente quando o projeto é salvo, porque ambos o projeto e suas tarefas associadas, são novos registros.

E isso é tudo que precisamos para criar um projeto. Vamos ver agora como atualizá-lo.

Assim como antes, nós precisamos de uma forma de adicionar e remover tarefas dinamicamente, mas desta vez se uma tarefa já existe, ela deve ser atualizada. As actions do controller só precisam se preocupar sobre o projeto, então elas são bem convencionais. Como antes, a atualização de tarefas será tratada no model Project:


# Arquivo: app/controllers/projects_controller.rb
def edit
  @project = Project.find(params[:id])
end

def update
  params[:project][:existing_task_attributes] ||= {}

  @project = Project.find(params[:id])
  if @project.update_attributes(params[:project])
    flash[:notice] = "Successfully updated project and tasks."
    redirect_to project_path(@project)
  else
    render :action => 'edit'
  end
end

Um detalhe importante: a primera linha da action update seta o parâmetro existing_task_attributes para um hash vazio, se ele já não está setado. Sem esta linha, não teria como deletar a última tarefa de um projeto. Se não existem campos de tarefa no form (porque nós removemos todos eles com JavaScript), então existing_task_attributes() não será setado pelo form, o que significa que o nosso método Project#existing_task_attributes= não será invocado. Setando um hash vazio aqui, se existing_task_attributes() está vazio, garante que o método Project#existing_task_attributes= é chamado ao deletar a última tarefa.

A partial de form não precisa de alterações. No entanto, quando nós submetemos o form com tarefas existentes, o hash params[:project] irá incluir uma key chamada existing_task_attributes. Isto é, quando nós atualizamos o projeto, os parâmetros do POST irão se parecer com isto:


"project" => {
  "name" => "Yard Work" ,
  "existing_task_attributes" => [
    {
      "1" => {"name" => "rake the leaves" },
      "2" => {"name" => "paint the fence" },
    }
  ]
  "new_task_attributes" => [
    { "name" => "clean the gutters" }
  ]
}

Para tratar isto, nós precisamos adicionar um método existing_task_attributes= ao nosso model Project, que irá receber cada tarefa existente e aí vai: ou atualizar a tarefa ou deletá-la, dependendo se os atributos são passados:


# Arquivo: app/models/project.rb
after_update :save_tasks

def existing_task_attributes=(task_attributes)
  tasks.reject(&:new_record?).each do |task|
    attributes = task_attributes[task.id.to_s]
    if attributes
      task.attributes = attributes
    else
      tasks.delete(task)
    end
  end
end

def save_tasks
  tasks.each do |task|
    task.save(false)
  end
end

Note que nós estamos salvando as tarefas em um callback chamado after_update. Isto é importante porque, diferente de antes, as tarefas existentes não serão automaticamente salvas quando o projeto for atualizado. E já que os callbacks são encapsulados em uma transação, se algum problema inesperado acontecer será feito um roll back.

Ao passar false para o método task.save, os dados são salvos sem passar pela validação. Ao invés disso, para garantir que todas as tarefas sejam validadas quando o projeto é validado, nós simplesmente adicionamos esta linha ao model Project:


validates_associated :tasks

Isto vai garantir que tudo é valido antes de salvar. E se a validação falha, então o uso de error_messages_for :project no template de formulário inclui os erros de validação para o projeto e qualquer uma de suas tarefas.

Então agora nós podemos criar e editar projetos e suas tarefas em uma tacada só. E ao usar atributos virtuais, nós mantemos o código do controller felizmente ignorante que nós estamos tratando vários models a partir de um único formulário.

Discussão

Uma vez que você começa a colocar mais de um model em um único formulário, você provavelmente vai querer criar um helper customizado para mensagens de erro, para fazer coisas como ignorar certos erros e detalhar outros. Veja a receita 17 “Customize as Mensagens de Erro”, na página 91 para saber como escrever um método error_messages_for customizado. (Nota do tradutor: capítulo disponível no livro Advanced Rails Recipes)

Campos de data causam alguns problemas porque, por algum motivo, o Rails remove as chaves [] do nome do campo. Isto pode ser arrumado especificando manualmente a opção :index e setá-lo para uma string vazia se a tarefa é nova:


<%= task_form.date_select :completed_at,
:index => (task.new_record? ? '' : nil) %>

Infelizmente, campos do tipo checkbox não vão funcionar com esta receita porque o valor destes campos não é passado pelo browser quando o checkbox é desmarcado. Portanto, você não tem como saber a qual tarefa um checkbox pertence quando estiver criando um novo projeto. Para resolver este problema, você pode usar um menu select para campos booleanos:


<%= task_form.select :completed, [['No' , false], ['Yes' , true]] %>

Copyright (c) 2008 The Pragmatic Programmers, LLC

Como adicionar suporte a feeds

Wednesday, November 5th, 2008

Imaginei que adicionar suporte a feeds em uma aplicação Rails fosse complicado. Mas uma vez “tomei na cabeça” vendo que o Rails realmente é produtivo de se trabalhar :)

1. Método de consulta que retorna todos os posts. Para criar uma URL de feed para o seu blog, antes de tudo você precisa criar um método que faça uma query que liste todos os posts do blog. Seguindo a filosofia “fat model / skinny controller” este método deve ficar dentro do próprio model “Post”. Vamos criar um método chamado “all_posts”.


# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :comments

  def self.all_posts
    find(:all, :order => "created_at DESC")
  end
end

2. Criando uma action para feeds. Agora vamos adicionar uma action chamada “feed” dentro do controller “posts’. Repare que aqui já estamos usando o método “all_posts” criado no passo anterior.


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def feed
    @posts = Post.all_posts

    respond_to do |format|
      format.atom
    end
  end
end

3. Adicionando uma rota. Do jeito que está, não existe como acessar a URL http//www.myblog.com.br/feed, porque ainda não temos uma rota para esta URL. Precisamos adicionar esta rota no arquivo config/routes.rb. Note a última linha do arquivo.


# config/routes.rb
  map.root :controller => 'posts'
  map.resources :posts, :has_many => :comments
  map.resources :sessions
  map.feed 'feed', :controller => 'posts', :action => 'feed'

4. Adicionando um template para o feed.. Ok, agora o Rails já sabe que existe uma URL /feed em nosso blog. Mas ele não sabe o que renderizar ali, na action “feed”. Ele só sabe que possui um objeto @posts disponível. Vamos dizer ao Rails como ele deve formatar esse retorno. O exemplo abaixo foi retirado da própria documentação do Rails, do Helper AtomFeedHelper.


# app/views/posts/feed.atom.builder
atom_feed(:language => "pt-BR") do |feed|
  feed.title(h "My blog")
  feed.updated((@posts.first.created_at))

  for post in @posts
    feed.entry(post) do |entry|
      entry.title(post.title)
      entry.content(post.body, :type => 'html')

      entry.author do |author|
        author.name(h "João da Esquina")
      end
    end
  end
end

Você pode testar essa URL em seu browser. Mas provavelmente é melhor testar com o comando curl e ver o resultado completo:


curl --get http://www.myblog.com.br/feed

5. Feed no <HEAD> do site. Pronto, já temos um feed funcionando. Agora precisamos colocar isto em nosso HTML, para dizer ao mundo que nosso blog já tem suporte a feeds :) A melhor forma é usar o helper auto_discovery_link_tag do Rails:


# app/views/layouts/application.html.erb
<head>
  <%= auto_discovery_link_tag(:atom, :controller => 'posts', :action => 'feed') %>
</head>

Agora nosso blog informa aos leitores de feeds, que nossa URL oficial é o /feed que criamos até agora. Mas se você como eu, prefere usar o FeedBurner para isso, ao invés disso faça assim:


# app/views/layouts/application.html.erb
<head>
 <%= auto_discovery_link_tag(:atom, 'http://feeds.feedburner.com/LevyOnRails', :title => "My blog's feed") %>
</head>

Ok. Agora se você informar a url de seu blog, por exemplo, http://www.myblog.com.br/ (sem o /feed) no Google Reader ou outro leitor de feeds experto, ele vai usar o feed do FeedBurner, no meu caso a URL http://feeds.feedburner.com/LevyOnRails. Perfeito!

Mas e se alguém usar a url http://www.myblog.com.br/feed, desconsiderando o que está na tag auto_discovery_link_tag acima? Para isso precisamos adicionar um redirect para o feed.

6. Redirect para o feed. Vamos criar um redirect, de forma que somente o FeedBurner consiga ler a nossa URL /feed. O restante do mundo será redirecionada para a URL do http://feeds.feedburner.com/LevyOnRails. Qual a vantagem disso? Você não terá metade de seus leitores usando o FeedBurner e outra metade usando o feed direto de teu site. Assim você consegue medir 100% a leitura/visitação de seu feed, usando as facilidades disponíveis no site do FeedBurner. Vamos precisar alterar o nosso controller:


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def feed
    redirect_to 'http://feeds.feedburner.com/LevyOnRails', :status=>307 and return unless request.env['HTTP_USER_AGENT'].match(/feedburner|feedvalidator/i)

    @posts = Post.all_posts

    respond_to do |format|
      format.atom
    end
  end
end

Com isso verificamos a variável do servidor HTTP_USER_AGENT, se esta contém a string “feedburner” ou “feedvalidator”. Se tiver, nós acreditamos que trata-se do FeedBurner, e somente este agent pode acessar nossa URL feed real.

E isso é tudo. Simples, não? :)

Processo de criação de um blog em Rails - Parte 1

Saturday, November 1st, 2008

Esse título poderia ser: “por que não dá para fazer um blog em 15 minutos”. Tudo bem, David Hanson consegue, afinal ele é o cara :)

Estou falando do famoso vídeo de uma criação de um blog em 15 minutos, feito pelo próprio criador do framework Ruby on Rails, David H. Hanson. Se você não viu ainda, veja agora! É interessante ver o quanto um vídeo pode fazer para propagar um produto ou idéia, e para empolgar as pessoas. Muitas pessoas que começam a mexer com Rails dizem “eu vi aquele vídeo de 15 minutos, e fiquei impressionado” (myself included!). Desde a criação deste vídeo em 2005, o Rails já evoluiu muito e hoje o próprio DHH diz que já é possível fazer a mesma coisa em 3 minutos. Para um vídeo mais atualizado veja o screencast do Akita, sobre criação de um blog em Rails 2.x (disponível em inglês e português).

Voltando ao assunto, criar um blog em 15 minutos é fácil. Agora para quem quer aprender Rails no processo de criação de um blog, a história é outra. Você precisa de um blog funcional com: autenticação de administrador, um layout mínimo, controle de versão, algum esquema de deployment automatizado, etc.

Pretendo descrever aqui não um processo de um blog, mas sim o processo que fiz para criar o meu blog. Vamos lá:

Layout: aqui é onde muitos programadores (principalmente eu!) empacam. Criar um layout do zero não é fácil, especialmente se você tem bom gosto e especialmente se já viu muitos sites bons por aí. O layout deste blog ficou bem diferente do que eu havia imaginado, mas até que quebra o galho. Levei 2 semanas para criar uma imagem no Photoshop deste layout. Muito tempo, né? Um designer faz isso em 1 hora, se já tiver uma boa direção sobre o que ele precisa criar. Outra coisa que consumiu um certo tempo foi procurar ícones na web. Você geralmente precisa de ícones para realçar o seu layout, e achar um icon set que te agrade, também pode demorar. No meio desse processo, encontrei um blog de um designer chamado Joel Watson que fala muito sobre o processo de criação de um layout de sites, e o que te faz ser um designer melhor. Está em inglês. Vale a pena ler.

Criar o CSS: essa parte é mais tranquila. Em 1 ou 2 horas você cria o CSS básico, e depois vai aperfeiçoando. Se você precisa aprender CSS nos padrões Web, alguns sites que recomendo são: o blog Tableless, o campus online de vídeos da Visie, o blog Pinceladas da Web e o Revolução Etc.

Hospedagem Rails: deployment de Rails não é simplesmente fazer upload dos arquivos e pronto, site funcionando (a menos que você use Mod_Rails e já tenha o Apache configurado). Com Rails você pode fazer o deployment de uma aplicação usando várias combinações de software: Apache + FastCGI, Mongrel, Mod_Rails (Phusion Passenger), Nginx, Thin, Lighttpd. A maioria dos provedores de hospedagem nem sabe como fazer funcionar um site em Rails. Alguns oferecem o Apache + FastCGI, que não funciona muito bem, e só aguenta um uso bem moderado da aplicação. Ao ver algumas opções no mercado, acabei escolhendo o Rails Playground (plano Developer, a US$ 5 / mês, com opções maiores de plano, para quando o site crescer), mas existem outros como Linode, SliceHost, e outros bem mais caros como Engine Yard, Joyent, e por aí vai. Na Rails Playground uso atualmente Apache + FastCGI (eu sei, totalmente básico sendo que temos Passenger, Mongrel, e outros), mas com a opção de mudar para Mongrel quando precisar. E é até interessante começar algo com a versão simples, e ir sentindo a necessidade de escalar a aplicação. E nesse processo todo, você aprende muito sobre deployment de Rails. Você primeiro escala o teu código, fazendo o melhor com o que você tem, e só aí parte pra mudanças no servidor. O investimento de ter uma conta de hospedagem em um provedor que entende de Rails vale muito a pena, altamente recomendado para quem quer aprender Rails.

Controle de versão (Git, é claro!): o Akita fala muito disso no Rails Brasil Podcast. E não é pra menos. É impossível você criar uma aplicação sem ter um controle de versão das alterações. E aqui entra o Git, um software opensource de controle de versão criado pelo Linus Torvalds, sim ele mesmo, o criador do Linux. Em 1 semana esse cara criou um controlador de versão, descontente com as opções existentes no momento (em 2006).

Repositório do código (GitHub, é claro!): depois de criar um código inicial da tua aplicação, ou pelo menos o arquivo README que seja, você precisa colocar isto em um repositório de onde você possa acessar remotamente e trabalhar no código sempre que quiser. Funciona assim: você cria uma conta no GitHub, e envia a tua aplicação para o site. Depois sempre que você quiser trabalhar em cima da aplicação, você “baixa” o código para tua máquina local, trabalha nas alterações, faz teus commits, e depois envia as atualizações para o repositório. Você pode usar o plano free, onde teus projetos são considerados open-source, e assim todos podem abrir e ver teu código. Ou você pode usar um plano pago, a partir de U$ 7/mês, e criar projetos privados, que só você pode ver.

Deployment da aplicação: Capistrano é uma solução em Ruby, que funciona na tua própria máquina. Funciona como um script que faz em sequência, todas as tarefas que você faria para colocar um site em produção:

  • entra por SSH no servidor de produção;
  • gera um tar.gz do conteúdo atual do site;
  • envia o conteúdo novo do site;
  • corrige permissões em arquivos;
  • roda Migrations, no servidor de banco de dados;
  • restarta o servidor Web;

É altamente recomendado, e depois de configurado você só precisa rodar “cap deploy” para subir uma nova versão para produção.

Em breve, teremos a próxima parte da série de posts sobre como foi criar este blog, usando Ruby on Rails. Stay tuned! :)