Quatro dias de Ruby On Rails – Primeiro dia, Quatro dias de Ruby On Rails – Segundo dia
Seguindo com a tradução do tutorial, vamos ao terceiro dia (desculpem pela demora, mas não andava com muita vontade de escrever
)
Agora é hora de começar o “coração” da nossa aplicação. A tabela Itens contem a lista de “Tarefas”. Cada item pertence a uma categoria das que criamos no Segundo Dia, cada Item pode opcionalmente possuir uma nota, criada em uma tabela separada, mas esta vamos deixar para o próximo dia de Rails. Cada tabela possui uma chave primária chamada “id” que é utilizada também para fazer o link entre as tabelas.

Items table
CREATE TABLE items ( id smallint(5) unsigned NOT NULL auto_increment, done tinyint(1) unsigned NOT NULL default '0', priority tinyint(1) unsigned NOT NULL default '3', description varchar(40) NOT NULL default '', due_date date default NULL, category_id smallint(5) unsigned NOT NULL default '0', note_id smallint(5) unsigned default NULL, private tinyint(3) unsigned NOT NULL default '0', created_on timestamp(14) NOT NULL, updated_on timestamp(14) NOT NULL, PRIMARY KEY (id) ) TYPE=MyISAM COMMENT='List of items to be done';
The Model
Como fizemos antes, vamos gerar um “model” em branco.
W:ToDo>ruby script/generate model item exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/item.rb create test/unit/item_test.rb create test/fixtures/items.yml W:ToDo>
E agora vamos a edição do model:
class Item < ActiveRecord::Base belongs_to :category validates_associated :category validates_format_of :done_before_type_cast, :with => /[01]/, :message=>"must be 0 or 1" validates_inclusion_of :priority, :in=>1..5, :message=>"must be between 1 (high) and 5 (low)" validates_presence_of :description validates_length_of :description, :maximum=>40 validates_format_of :private_before_type_cast, :with => /[01]/, :message=>"must be 0 or 1"end
Validando links entre tabelas
Documentação: ActiveRecord::Associations::ClassMethods
Validando a entrada de dados
A tabela Notes
Esta tabela contem um campo de texto apenas, para armazenar informações extras sobre uma tarefa especifica. Estes dados poderiam ter sido armazenados em um campo da tabela “Itens”; mas fazendo desta forma, vamos aprender muito mais sobre o Rails
CREATE TABLE notes ( id smallint(6) NOT NULL auto_increment, more_notes text NOT NULL, created_on timestamp(14) NOT NULL, updated_on timestamp(14) NOT NULL, PRIMARY KEY (id) ) TYPE=MyISAM COMMENT='Additional optional information for to-dos';
O Model
Vamos gerar mais um model em branco, e desta vez sem muitas novidades.
class Note < ActiveRecord::Base validates_presence_of :more_notes end
Apenas, precisamos lembrar de adicionar a referencia ao Model de Itens
class Item < ActiveRecord::Base belongs_to :note
Usando o Model para manter a integridade referencial
O código que vamos desenvolver, permite que o usuário adicione uma nota para qualquer tarefa. Mas o que acontece se o usuário deleta uma tarefa que estava associada a uma nota? Precisamos encontrar uma maneira de remover a nota também, caso contrário, vamos ter muitas notas orfans poluindo o banco de dados.
No maneira Model / View / Controller de fazer as coisas, o Model deve cuidar disto. Por que? bom, você vera depois que poderemos deletar tarefas, clicando em um icone na tela de tarefas, mas também podemos remove-las, clicando no link “Purge completed itens”. Colocando este código no Model, ele sera executado independente de por onde removemos as tarefas.
app\models\item.rb (excerpt)
def before_destroy unless note_id.nil? Note.find(note_id).destroy end end
Isto pode ser lido da seguinte forma: antes de remover um item da tabela Itens, procure um item na tabela Notes que tenha o “id” igual ao campo notes_id do item que esta para ser removido, e remova-o. A não ser que não haja nenhum ![]()
Igualmente, se um registro da tabela notes é removido, a referencia para este deve ser removida da tabela Itens:
app\models\note.rb (excerpt)
def before_destroy
Item.find_by_note_id(id).update_attribute('note_id',NIL)
end
end
Documentação: ActiveRecord::Callbacks
Mais Scaffolding
Vamos gerar mais um pouco de código com o scaffold. Vamos gerar código para as tabelas Itens e Notes. Nos ainda não estamos prontos para lidar com as Notas, mas tendo o código gerado, quer dizer que podemos referenciar as notas sem ter um monte de erros chatos no meio do caminho. Da mesma forma que na construção de uma casa ? o scaffolding permite que você construa uma parede por vez, sem que todo o resto desmorone a sua volta.
W:ToDo>ruby script/generate scaffold Item [snip] W:ToDo>ruby script/generate scaffold Note [snip] W:ToDo>
Nota: Como nos alteramos a folha de estilos no tutorial anterior, responda não quando o scaffold perguntar se você quer sobre escrevela.
Mais sobre as views
Criando um layout para a aplicação
Nesta altura ja esta ficando claro que todas as páginas da aplicação terão as mesmas “primeiras linhas” de código, então começa a fazer sentido mover o layout comum para um layout compartilhado por toda a aplicação. Remova todos os arquivos de app\views\layouts\*.rhtml, e crie um arquivo de nome application.rhtml com o seguinte conteúdo.
<html>
<head>
<title><%= @heading %></title>
<%= stylesheet_link_tag 'todo' %>
<script language="JavaScript">
<!-- Begin
function setFocus() {
if (document.forms.length > 0) {
var field = document.forms[0];
for (i = 0; i < field.length; i++) {
if ((field.elements[i].type == "text") || (field.elements[i].type == "textarea")
|| (field.elements[i].type.toString().charAt(0) == "s")) {
document.forms[0].elements[i].focus();
break;
}
}
}
}
// End -->
</script>
</head>
<body OnLoad="setFocus()">
<h1><%=@heading %></h1>
<% if @flash["notice"] %>
<span class="notice">
<%=h @flash["notice"] %>
</span>
<% end %>
<%= @content_for_layout %>
</body>
</html>
O @heading agora é utilizado para o <title> e também para o <h1>. Eu renomeei o arquivo public/stylesheets/scaffold.css para todo.css para melhor clareza do código, e também brinquei um pouquinho com as cores, e bordas da tabela, para criar um layot um pouquinho melhor. Também coloquei um pequeno javascript para posicionar o cursos no primeiro campo do tipo input para que o usuário possa sair digitando quando abrir qualquer tela.
A tela “To Do List”
O que estou tentando alcançar é um “look and feel” similar a um PalmPilot ou PDAde desktop. O produto final é mostrado na imagem a baixo.

Alguns pontos:
O código para atingir estas metas esta a baixo:
app\views\items\list.rhtml
<% @heading = "To Do List" %>
<%= start_form_tag :action => 'new' %>
<table>
<tr>
<th><%= link_to_image "done", {:action => "purge_completed"}, :confirm => "Are you sure you want to permanently delete all completed To Dos?" %></th>
<th><%= link_to_image "priority",{:action => "list_by_priority"}, "alt" => "Sort by Priority" %></th>
<th><%= link_to_image "description",{:action => "list_by_description"}, "alt" => "Sort by Description" %></th>
<th><%= link_to_image "due_date", {:action => "list"}, "alt" => "Sort by Due Date" %></th>
<th><%= link_to_image "category", {:action => "list_by_category"}, "alt" => "Sort by Category" %></th>
<th><%= show_image "note" %></th>
<th><%= show_image "private" %></th>
<th> </th>
<th> </th>
</tr>
<%= render_collection_of_partials "list_stripes", @items %>
</table>
<hr />
<%= submit_tag "New To Do..." %>
<%= submit_tag "Categories...", {:type => 'button',
nClick=>"parent.location='" + url_for( :controller => 'categories', :action => 'list' ) + "'" } %>
<%= end_form_tag %>
<%= "Page: " + pagination_links(@item_pages, :params => { :action => @params["action"] || "index" }) + "<hr />" if @item_pages.page_count>1 %>
Removendo as tarefas completadas clicando em um icone
Imagens clicáveis são criadas com o comando link_to_image, que por padrão procura imagens no diretório pub/images com um sufixo .png; Clicando na imagem o método especificado sera executado.
Adicionando o parametro :confirm gera um popup de confirmação com javascript como antes.
Documentação: ActionView::Helpers::UrlHelper
Clicando em “OK” vai chamar o método purge_completed. O novo método purge_completed precisa ser definido no controller:
app\controllers\items_controller.rb (excerpt)
def purge_completed Item.destroy_all "done = 1" redirect_to :action => 'list' end
Item.destroy_all remove todos os itens da tabela em que “done=1″ e retorna para a listagem.
Alterando a ordenação clicando nos cabeçalhos das colunas
Clicando na coluna “Pri” chama o método list_by_priority . Este método também precisa ser declarado no controller:
app\controllers\items_controller.rb (excerpt)
def list @item_pages, @items = paginate :item, :per_page => 10,rder_by => 'due_date,priority' end def list_by_priority @item_pages, @items = paginate :item,:per_page => 10,
rder_by => 'priority,due_date' render_action 'list' end
Nos especificamos uma ordenação para a consulta no método padrão “list”, e criamos um método list_by_priority novo. Perceba que precisamos especificar render_action ‘list’, pois por padrão o rails iria procurar um template de nome list_by_priority (que não existe
Adicionando um Helper
Os cabeçalhos para as colunas Note e Private são imagens, mas não são clicaveis. eu decidi criar um pequeno método show_image(name) para apenas mostrar a imagem:
app/helpers/application_helper.rb
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
def self.append_features(controller)
controller.ancestors.include?(ActionController::Base) ?
controller.add_template_helper(self) : super
end
def show_image(src)
img_options = { "src" => src.include?("/") ? src : "/images/#{src}" }
img_options["src"] = img_options["src"] + ".png" unless
img_options["src"].include?(".")
img_options["border"] = "0"
tag("img", img_options)
end
end
Ja que este helper é associado a todos os controllers.
Nota: list_by_description e list_by_category são bem parecidos com o metodo ja criado, mas se você tiver problemas com o list_by_category, no próximo tutorial eu mostro o código.
Documentação: ActionView::Helpers::UrlHelper
Usando Javascript para os botões de navegação
onClick é um método padrão para gerenciar clicks e navegação entre páginas. Entretanto, Rails faz um grande trabalho, para renderizar URLs amigáveis, então precisamos perguntar ao rails Rails qual a URL a ser utilizada.
Para um controller e uma action, url_for vai retornar a URL correta.
Documentação: ActionController::Base
Formatando uma tabela com “Partials”
Eu quiz criar um efeito “ajax” para a listagem de itens. Partials são a solução para implementar isto; eles podem ser chamados usando o método render_partial:
<% for item in @items %> <%= render_partial "list_stripes", item %> <% end %>
ou pelo método mais economico render_collection_of_partials:
render_collection_of_partials "list_stripes", @items
Documentação: ActionView::Partials
O Rails também passa um número sequential “list_stripes_counter” ara o Partial. este sequencial é a chave para implementar coloração alternada para a linhas da tabela. Uma maneira é simplesmente testar se o número é par ou impar: se for par, utilizar cinza claro; se impar, usar cinza escuro.
app\views\items\_list_stripes.rhtml
<tr class="<%= list_stripes_counter.modulo(2).nonzero? ? "dk_gray" : "lt_gray" %>">
<td style="text-align: center"><%= list_stripes["done"] == 1 ?show_image("done_ico.gif") : " " %></td>
<td style="text-align: center"><%= list_stripes["priority"] %></td>
<td><%=h list_stripes["description"] %></td>
<% if list_stripes["due_date"].nil? %>
<td> </td>
<% else %>
<%= list_stripes["due_date"] < Date.today ? '<td class="past_due" style="text-align: center">' : '<td style="text-align: center">' %>
<%= list_stripes["due_date"].strftime("%d/%m/%y") %></td>
<% end %>
<td><%=h list_stripes.category ? list_stripes.category["category"] : "Unfiled"%></td>
<td><%= list_stripes["note_id"].nil? ? " " : show_image("note_ico.gif")%></td>
<td><%= list_stripes["private"] == 1 ? show_image("private_ico.gif") : " "%></td>
<td><%= link_to_image("edit", { :controller => 'items', :action => "edit", :id =>list_stripes.id }) %></td>
<td><%= link_to_image("delete", { :controller => 'items', :action => "destroy",:id => list_stripes.id }, :confirm => "Are you sure you want to delete this item?")%></td>
</tr>
um pouco de Rubyé utilizado para testar se o contador é par ou impar e renderizar class=”dk_gray” ou
class=”lt_gray”: list_stripes_counter.modulo(2).nonzero? class=”dk_gray”?”dk_gray” :”lt_gray”
O código até a o simbolo ? pergunta: O restante da divisão por dois não é zero?
O rsto da linha é uma “complicação” de um if/then/else que sacrifica a legibilidade por menos código: se a expressão antes do ? for verdadeira retorna o valor antes do : caso contrario retorna o outro valor.
A mesma técnica é utilizada para outras formatações na tela.
A tela para “Nova Tarefa”
O template como podemos ver é bastante simples:
app/views/items/new.rhtml
<% @heading = "New To Do" %>
<%= error_messages_for 'item' %>
<%= start_form_tag :action => 'create' %>
<table>
<%= render_partial "form" %>
</table>
<hr />
<%= submit_tag "Save" %>
<%= submit_tag "Cancel", {:type => 'button',
nClick=>"parent.location='" + url_for(:action => 'list' ) + "'" } %>
<%= end_form_tag %>
O codigo real esta no partial:
app\views\items\_form.rhtml
<tr> <td><b>Description: </b></td> <td><%= text_field "item", "description", "size" => 40, "maxlength" => 40%></td> </tr> <tr> <td><b>Date due: </b></td> <td><%= date_select "item", "due_date", :use_month_numbers => true %></td> </tr> <tr> <td><b>Category: </b></td> <td><select id="item_category_id" name="item[category_id]"> <%= options_from_collection_for_select @categories, "id", "category",@item.category_id %> </select> </td> </tr> <tr> <td><b>Priority: </b></td> <% @item.priority = 3 %> <td><%= select "item","priority",[1,2,3,4,5] %></td> </tr> <tr> <td><b>Private? </b></td> <td><%= check_box "item","private" %></td> </tr> <tr> <td><b>Complete? </b></td> <td><%= check_box "item", "done" %></td> </tr>
Criando um Drop Down para um campo Date
date_select gera um dropdown para entrada de datas:
date_select “item”, “due_date”, :use_month_numbers => true
Documentação: ActionView::Helpers::DateHelper
Tratando exceções no Ruby
Infelizmente, date_select aceita coisas como 31 de fevereiro. O Rails então se perde ao tentar salvar esta “data” no banco de dados. Uma forma de contornar isto é capturar esta excessão usando rescue, uma forma de tratar excessões no Rails
def create begin @item = Item.new(@params[:item]) if @item.save flash['notice'] = 'Item was successfully created.' redirect_to :action => 'list_by_priority' else @categories = Category.find_all render_action 'new' end rescue flash['notice'] = 'Item could not be saved.' redirect_to :action => 'new' end end
Criando um Drop Down a partir de uma tabela secundária:
Este é um outro exemplo do rails resolvendo problemas do dia a dia de uma maneira bastante economica. neste exemplo:
options_from_collection_for_select @categories, “id”, “category”, @item.category_id
options_from_collection_for_select lê todos os registros da coleção @categories e renderiza para cada um: <option
value=”[value of id]“>[value of category]</option>. O valor que for igual a @item_category_idvai ser marcado como “selected”. Como se isto não fosse o suficiente, este código ja faz o “escape” de caracteres HTML para você.
Documentação: ActionView::Helpers::FormOptionsHelper
Lembre-se que estes dados devem vir de algum lugar – o que significa uma pequena adição de código ao controller:
app\controllers\items_controller.rb (excerpt)
def new @categories = Category.find_all @item = Item.new end def edit @categories = Category.find_all @item = Item.find(@params[:id]) end
Criando um Drop Down de uma lista de constantes
Esta é uma versão simplificada do cenário anterior. Deixar listas de valores hard-coded no HTML não é sempre uma boa idéia – é mais fácil alterar listas em tabelas do que valores no código. Entretanto, existem casos em que esta é uma solução válida, então com o Rails você faz o seguinte:
select “item”,”priority”,[1,2,3,4,5]
Criando uma Checkbox
Outro problema do dia a dia; outro helper do Rails:
check_box “item”,”private”
Toques finais
Retocando a folha de estilos
Neste ponto, a tela “To Do List” ja deve estar funcionando, e também o botão “New To Do”. para melhoras as telas até agora, eu também fiz as seguintes alterações na folha de estilos:
public\stylesheets\ToDo.css
body { background-color: #c6c3c6; color: #333; }
.notice {
color: red;
background-color: white;
}
h1 {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 14pt;
font-weight: bold;
}
table {
background-color:#e7e7e7;
border: outset 1px;
border-collapse: separate;
border-spacing: 1px;
}
td { border: inset 1px; }
.notice {
color: red;
background-color: white;
}
.lt_gray { background-color: #e7e7e7; }
.dk_gray { background-color: #d6d7d6; }
.hightlight_gray { background-color: #4a9284; }
.past_due { color: red }
A tela “Edit To Do”
O resto deste terceiro dia vai ser tomado pela criação da tela “Edit To Do”, que é muito parecida com a “New To Do”. Eu ficava bastante irritado com os livros da faculdade que diziam: deixo isto como um exercicio para o leitor, mas agora é uma ótima hora para fazer o mesmo com vocês
.
Se vocês quiserem uma cópia da folha de estilos e das imagens, eu as disponibilizei neste link.
E até a ultima parte do tutorial, que se tudo der certo, vai demorar muito menos do que esta para ser publicada
PS.: agradeço se os leitores que estão gostando do tutorial colocarem links em seus blogs para as 3 partes ja publicadas.
PS2.: precisei alterar os campos done e priority da tabela itens para int(5) para que tudo funcionasse, e executei rake db:schema:dump, e para que vocês ja comecem a se acostumar com os “migrations” do rails, o código gerado por ele segue a baixo:
# This file is autogenerated. Instead of editing this file, please use the # migrations feature of ActiveRecord to incrementally modify your database, and # then regenerate this schema definition. ActiveRecord::Schema.define() do create_table "categories", :force => true do |t| t.column "category", :string, :limit => 20, :default => "", :null => false t.column "created_on", :timestamp, :null => false t.column "updated_on", :timestamp, :null => false end add_index "categories", ["category"], :name => "category_key", :unique => true create_table "items", :force => true do |t| t.column "description", :string, :limit => 40, :default => "", :null => false t.column "due_date", :date t.column "category_id", :integer, :limit => 5, :default => 0, :null => false t.column "note_id", :integer, :limit => 5 t.column "private", :integer, :limit => 3, :default => 0, :null => false t.column "created_on", :timestamp, :null => false t.column "updated_on", :timestamp, :null => false t.column "done", :integer, :limit => 5, :default => 0, :null => false t.column "priority", :integer, :limit => 5, :default => 3, :null => false end create_table "notes", :force => true do |t| t.column "more_notes", :text, :default => "", :null => false t.column "created_on", :timestamp, :null => false t.column "updated_on", :timestamp, :null => false end end
Tudo pronto? então siga para o Quarto e último dia do nosso tutorial.
PS.: agradeço se os leitores que estão gostando do tutorial colocarem links em seus blogs para o tutorial, indicando para seus amigos.
Tags: artigo, produtividade, Ruby
[...] Tudo pronto? então siga para o Terceiro dia. [...]
[Translate]
[...] Rails), eu traduzi a pouco tempo o ótimo tutorial Four Dais on Rails: Primeiro Dia, Segundo Dia, Terceiro Dia, Quarto e Último [...]
[Translate]
[...] Rails), eu traduzi a pouco tempo o ótimo tutorial Four Dais on Rails: Primeiro Dia, Segundo Dia, Terceiro Dia, Quarto e Último Dia [...]
[Translate]
Urubatan… pq quando vc usa o belongs_to para fazer a ligação entre as tabelas vc não coloca o has_many do outro lado? não acha que ficaria mais didatico? claro que na real vc não esta usando mais pensando em implementações de pesquisas isso podia ser util não acha…
[Translate]
Jeffeson, o has_many só é útil se for utilizado
Se não for utilizado ele estaria só complicando a visa desnecessariamente
[Translate]
Como ler o nome de uma imagem no disco sem pegar todo o caminho fisico da imagem, apenas o nome, em ruby.
tentei com file_field mas não dá.
[Translate]