Blog do Urubatan
msgbartop
Desenvolvedor, Arquiteto, Palestrante, Coordenador do RSJUG, Patinador e Blogger
msgbarbottom

07 Jul 07 Quatro dias de Ruby On Rails - Terceiro Dia

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 :D )

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.



Modelo de dados simplificado
A tabela ItemsOs campos da tabela Items são os seguintes::

  • done - 1 quer dizer que a tarefa foi concluida
  • priority - 1 (Prioridade alta) a 5 (prioridade baixa)
  • description - Texto livre, descrevendo o que deve ser feito
  • due_date - Data em que o item ja precisa estar concluido
  • category_id - Link para a categoria que este item pertence (”id” na tabela categories)
  • note_id - um link para uma nota opcional, descrevendo este item (”id” na tabela notes)
  • private - 1 quer dizer que esta tarefa esta classificada como “Private”

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

  • o uso de belongs_to e validates_associated fazem o link entre a tabela Items usando o campo category_id com a tabela Category.

Documentação: ActiveRecord::Associations::ClassMethods

Validando a entrada de dados

  • validates_presence_of protege campos “NOT NULL” do caso de o usuário esquecer de digitar os dados
  • validates_format_of utiliza expressões regulares para validar o formato dos dados digitados
  • Quando um usuário digita dados em um campo numérico, o Rails vai sempre converter os dados para um numero ? se os dados não forem numéricos, sera convertido para “zero”. Se voê quiser verificar o que o usuário realmente digitou, você precisa fazer isto antes da conversão de dados usando _before_type_cast.
  • validates_inclusion_of verifica a entrada do usuário em uma lista de possíveis valores
  • validates_length_of valida o comprimento dos 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:

  • Clicando no “tick” do cabeçalho da coluna, vai remover todos os itens completados (aqueles marcados com um “tick”)
  • A listagem pode ser ordenada clicando nos caeçalhos “Pri”, “Description”, “Due Date”, e “Category”
  • os valores 0/1 para “Done” serão convertidos em um icone “tick”
  • Os itens que ja passaram do “Due Date” aparecem em vermelho e negrito
  • A existencia de uma nota associada é mostrada pelo icone de nota
  • os valores 0/1 para “Private” são convertidos em um pequeno cadeado
  • Cada item pode ser editado ou removido clicando nos icones a direita da tela
  • Novos itens podem ser adicionados clicando em “New To Do…” na parte de baixo da tela
  • Há um botão que mostra a tela de “Categories” criada no tutorial anterior

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', :onClick=>"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, :order_by => 'due_date,priority'
end
def list_by_priority
@item_pages, @items = paginate :item,:per_page => 10, :order_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', :onClick=>"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 :D.

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 :D
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.

Se você gostou deste post, lembre-se de assinar o RSS feed do blog, para ser notificado de novos posts!

Tags: , ,

Reader's Comments

  1. |

    [...] Tudo pronto? então siga para o Terceiro dia. [...]

    Reply to this comment
  2. |

    [...] Rails), eu traduzi a pouco tempo o ótimo tutorial Four Dais on Rails: Primeiro Dia, Segundo Dia, Terceiro Dia, Quarto e Último [...]

    Reply to this comment
  3. |

    [...] Rails), eu traduzi a pouco tempo o ótimo tutorial Four Dais on Rails: Primeiro Dia, Segundo Dia, Terceiro Dia, Quarto e Último Dia [...]

    Reply to this comment
  4. |

    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…

    Reply to this comment
  5. |

    Jeffeson, o has_many só é útil se for utilizado :D
    Se não for utilizado ele estaria só complicando a visa desnecessariamente :D

    Reply to this comment

Leave a Comment