Criando seu primeiro projeto com Merb e DataMapper
Leia em 17 minutos
No começo do mês foi lançada a versão 1.0 do Merb. Esta versão trouxe a facilidade de instalação que faltava nas versões anteriores, permitindo configurar o ambiente de maneira bastante simples.
Merb é um framework web para Ruby totalmente agnóstico. Você escolher o seu ORM de preferência, seu framework de testes, bibliotecas Javascript. Totalmente modular, permite carregar somente o que será necessário para criar sua aplicação.
Eu já tinha tentado escrever sobre ele em ocasiões anteriores, sem sucesso. Agora, resolvi colocar a coisa para funcionar e, para minha surpresa, foi mais simples do que eu esperava, como você pode conferir.
Todo o código está no Github: http://github.com/fnando/merb-contacts-app; divirta-se!
Instalando o Merb
A maneira mais fácil de instalar o Merb é utilizando o pacote completo.
sudo gem install merb
Esta gem irá instalar muitas dependências e pode demorar um pouco!
Gerando a estrutura do aplicativo
Para criar um novo aplicativo, você utilizar o comando merb-gen
.
$ merb-gen app contacts
Generating with app generator:
[ADDED] tasks/merb.thor
[ADDED] .gitignore
[ADDED] public/.htaccess
[ADDED] tasks/doc.thor
[ADDED] public/javascripts/jquery.js
[ADDED] doc/rdoc/generators/merb_generator.rb
[ADDED] doc/rdoc/generators/template/merb/api_grease.js
[ADDED] doc/rdoc/generators/template/merb/index.html.erb
[ADDED] doc/rdoc/generators/template/merb/merb.css
[ADDED] doc/rdoc/generators/template/merb/merb.rb
[ADDED] doc/rdoc/generators/template/merb/merb_doc_styles.css
[ADDED] doc/rdoc/generators/template/merb/prototype.js
[ADDED] public/favicon.ico
[ADDED] public/images/merb.jpg
[ADDED] public/merb.fcgi
[ADDED] public/robots.txt
[ADDED] Rakefile
[ADDED] app/controllers/application.rb
[ADDED] app/controllers/exceptions.rb
[ADDED] app/helpers/global_helpers.rb
[ADDED] app/models/user.rb
[ADDED] app/views/exceptions/not_acceptable.html.erb
[ADDED] app/views/exceptions/not_found.html.erb
[ADDED] autotest/discover.rb
[ADDED] autotest/merb.rb
[ADDED] autotest/merb_rspec.rb
[ADDED] config/database.yml
[ADDED] config/dependencies.rb
[ADDED] config/environments/development.rb
[ADDED] config/environments/production.rb
[ADDED] config/environments/rake.rb
[ADDED] config/environments/staging.rb
[ADDED] config/environments/test.rb
[ADDED] config/init.rb
[ADDED] config/rack.rb
[ADDED] config/router.rb
[ADDED] public/javascripts/application.js
[ADDED] public/stylesheets/master.css
[ADDED] merb/merb-auth/setup.rb
[ADDED] merb/merb-auth/strategies.rb
[ADDED] merb/session/session.rb
[ADDED] spec
[ADDED] gems
[ADDED] app/views/layout/application.html.erb
Por padrão, o Merb vem configurado para utilizar o RSpec como framework de testes e o DataMapper como ORM. Ao contrário do Rails, você pode utilizar outros frameworks transparentemente.
Nota sobre o DataMapper: Se você armazena objetos no Memcache, o DataMapper pode não ser o ORM mais adequado. Acontece que seus objetos não podem passar pelo Marshal, devido à maneira como o DataMapper foi implementado. Para acompanhar os detalhes deste problema, veja este ticket no Lighthouse.
Para iniciar o servidor de desenvolvimento, basta digitar o comando merb
à partir da raíz de seu projeto. Ele responderá na porta 4000.
$ merb
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
~ Connecting to database...
~ Loaded slice 'MerbAuthSlicePassword' ...
~ Parent pid: 21182
~ Compiling routes...
~ Activating slice 'MerbAuthSlicePassword' ...
merb : worker (port 4000) ~ Starting Mongrel at port 4000
merb : worker (port 4000) ~ Successfully bound to port 4000
Ao acessar o endereço http://localhost:4000
, você verá uma página como esta:
Agora que já geramos a estrutura de nosso projeto, vamos fazer algo real!
Criando nosso aplicativo
Já deu para perceber que quase todos os artigos de frameworks web envolvem blogs e listas de tarefas? Para provar esta regra, seremos a exceção. Vamos criar um aplicativo onde os usuários podem se cadastrar e adicionar seus contatos. Será uma versão simplificada de uma agenda de contatos.
O primeiro passo é criar a área de registro e autenticação de usuários. O Merb possui um pacote chamado MerbAuth, um framework de autenticação simples, porém completo. Ele permite criar estratégias de autenticação, visto que cada aplicação pode possuir diferentes tipos de autententicação (HTTP, formulário, OpenID, Google Account, etc).
A classe User
é criada por padrão quando um aplicativo é gerado. Abra o arquivo "app/models/user.rb".
class User
include DataMapper::Resource
property :id, Serial
property :login, String
end
Nossa classe não possui nenhuma validação. Pelo menos é isso que aparenta o código acima. Na verdade, o MerbAuth já faz algumas validações como verificar a presença da senha e se a confirmação de senha está correta. Você provavelmente fará algumas validações adicionais para se certificar que o login é único e possui um tamanho mínimo.
class User
include DataMapper::Resource
property :id, Serial
property :login, String
validates_is_unique :login
validates_length :login, :within => 3..30
validates_length :password, :min => 4, :if => :password_required?
end
O MerbAuth também adiciona o método password_required?
, que permite verificar se a senha é necessária ou não.
Vamos fazer alguns testes no console. Para abrir o console, execute o comando abaixo:
merb -i
Será preciso instalar o Webrat, que pode ser feito com o comando sudo gem install webrat
.
Assim que você estiver no console, execute o comando DataMapper.auto_migrate!
para executar a migração de seu banco de dados.
>> DataMapper.auto_migrate!
~ DROP TABLE IF EXISTS "sessions"
~ DROP TABLE IF EXISTS "users"
~ PRAGMA table_info('users')
~ SELECT sqlite_version(*)
~ CREATE TABLE "users" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "login" VARCHAR(50), "crypted_password" VARCHAR(50), "salt" VARCHAR(50))
~ PRAGMA table_info('sessions')
~ CREATE TABLE "sessions" ("session_id" VARCHAR(32) NOT NULL, "data" TEXT DEFAULT 'BAh7AA== ', "created_at" DATETIME, PRIMARY KEY("session_id"))
=> [User, Merb::DataMapperSessionStore]
Você também pode executar as migrações rodado a tarefa rake db:automigrate
à partir da linha de comando.
Para criar um usuário, você pode chamar o método create
, semelhante ao ActiveRecord.
>> User.create :login => 'fnando', :password => 'test', :password_confirmation => 'test'
~ SELECT "id" FROM "users" WHERE ("login" = 'fnando') ORDER BY "id" LIMIT 1
~ INSERT INTO "users" ("crypted_password", "login", "salt") VALUES ('769bd718b654e5da69a79d1dc34a7ab5f8dea58b', 'fnando', '050f1d293dc86e105ed1e0cdada6b5802e71dc85')
=> #<User id=1 login="fnando" crypted_password="769bd718b654e5da69a79d1dc34a7ab5f8dea58b" salt="050f1d293dc86e105ed1e0cdada6b5802e71dc85">
Ótimo! Já conseguimos criar um usuário através do console. Agora, podemos implementar uma interface para fazermos isso de maneira mais agradável. Vamos criar nosso controller users
, que será responsável por esta tarefa. Execute o comando merb-gen controller users
.
$ merb-gen controller users
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
Generating with controller generator:
Loading init file from /Users/fnando/Sites/contacts/config/init.rb
Loading /Users/fnando/Sites/contacts/config/environments/development.rb
[ADDED] app/controllers/users.rb
[ADDED] app/views/users/index.html.erb
[ADDED] spec/requests/users_spec.rb
[ADDED] app/helpers/users_helper.rb
Don't forget to add request/controller tests first.
Vamos alterar nossas rotas, adicionando o recurso (resource) de usuários. Abra o arquivo "config/router.rb" e adicione a linha resources :users
.
Merb.logger.info("Compiling routes...")
Merb::Router.prepare do
resources :users
slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")
default_routes
end
Ao contrário do Rails, o Merb não nomeia os controllers como "_controller.rb". Abra o arquivo "app/controllers/users.rb". Ele é parecido com isto:
class Users < Application
# ...and remember, everything returned from an action
# goes to the client...
def index
render
end
end
Como o comentário diz, tudo o que for retornado de uma ação será renderizado ao cliente. Sendo assim, se tivermos uma ação como o exemplo abaixo, a string "Yay! I'm alive!" será exibida. Particularmente, prefiro a maneira como isso é feito no Rails, já que o conteúdo que será renderizado fica explícito (a precedência é do render, depois é a view, etc).
class Users < Application
def index
"Yay! I'm alive!"
end
end
Acesse o endereço "http://localhost:4000/users" e você verá uma página muito simples sendo renderizada.
Vamos melhorar as coisas. Primeiro, substituia o código do arquivo "app/views/layouts/application.html.erb" pelo abaixo.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Fresh Merb App</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="/stylesheets/master.css" type="text/css" media="screen" />
</head>
<body>
<div id="page">
<div id="header">
<h1>Contacts</h1>
</div>
<div id="content">
<div id="main">
<%= all_messages %>
<%= catch_content :for_layout %>
</div>
</div>
</div>
</body>
</html>
Iremos precisar do helper all_messages
, que exibirá todas as mensagens do tipo flash messages. Para adicioná-lo, abra o arquivo "app/helpers/global_helpers.rb".
module Merb
module GlobalHelpers
def all_messages
message.collect {|type, text| tag(:p, text, :class => "message #{type}") }.join
end
end
end
Agora, para estilizar a página, substitua o conteúdo do arquivo "public/stylesheets/master.css" por este:
* {
margin: 0;
padding: 0;
}
body {
background: #727272 url(../images/bg.png) repeat-x;
font-family: "Lucida Grande", Arial;
font-size: 13px;
padding: 20px;
}
#page {
background: #fff;
border: 1px solid #666;
margin: 0 auto;
padding: 20px;
width: 410px;
-webkit-box-shadow: 2px 2px #444;
}
h1, h2, h3 {
font-family: "Arial Rounded MT Bold", Arial;
}
h1 {
color: #03c;
}
h2 {
color: #777;
font-size: 20px;
}
p, ul {
padding: 10px 0;
}
ul {
padding-left: 30px;
}
label {
display: block;
font-size: 14px;
padding-bottom: 3px;
}
label span {
color: #999;
}
input[type=text],
input[type=password] {
font-size: 18px;
padding: 5px;
width: 390px;
}
a {
color: #03c;
}
a:hover {
color: #900;
}
p.submit a {
color: #f00;
padding: 5px;
}
p.submit a:hover {
background: #f00;
color: #fff;
}
/* error message */
div.error {
background: #fcc url(../images/error.png) no-repeat 15px 15px;
margin: 15px 0 25px;
min-height: 50px;
padding: 15px 15px 15px 70px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
}
div.error h2 {
color: #900;
font-size: 14px;
padding-bottom: 5px;
}
div.error li {
font-size: 12px;
padding-bottom: 5px;
}
div.error ul {
padding-left: 15px;
}
p.message {
background-repeat: no-repeat;
background-position: 20px center;
font-size: 15px;
margin: 20px 0;
padding: 15px 15px 15px 60px;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
}
p.notice {
background-color: #E2F9E3;
background-image: url(../images/ok.png);
color: #0E6F0E;
}
p.error {
background-color: #fcc;
background-image: url(../images/error.png);
color: #c00;
}
p.warning {
background-color: #ffc;
background-image: url(../images/warning.png);
color: #333;
}
/* highlight */
p.highlight {
background: #FFE640;
margin-top: 15px;
text-align: center;
}
p.highlight a {
color: #900;
font-weight: bold;
font-size: 18px;
}
/* header */
#header {
position: relative;
}
p.login-info {
background: #ddd;
font-size: 10px;
padding: 5px;
position: absolute;
right: 0;
top: 0;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
}
/* vcard */
#contacts {
padding-top: 10px;
}
.vcard {
background: #e9e9e9;
border: 1px solid #ddd;
font-size: 11px;
margin-bottom: 15px;
padding: 10px 10px 20px 60px;
position: relative;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
}
img.photo {
left: 10px;
position: absolute;
}
.vcard span {
font-weight: bold;
}
.vcard span.email,
.vcard span.street-address,
.vcard span.organization-name {
font-weight: normal;
}
.vcard .type:after,
.vcard .label:after {
content: ":";
}
.vcard .email-group .label {
content: url(../images/mail.gif);
}
.vcard .adr .type {
content: url(../images/home.gif);
}
.vcard .mobile .type {
content: url(../images/mobile.gif);
}
.vcard .phone .type {
content: url(../images/phone.gif);
}
.vcard .org .label {
content: url(../images/globe.gif);
}
.vcard h3 {
font-size: 16px;
}
.vcard div {
padding-bottom: 3px;
}
.vcard .actions {
display: none;
padding: 0;
}
.vcard:hover .actions {
display: block;
}
.vcard .actions form {
bottom: 5px;
cursor: pointer;
display: inline;
position: absolute;
right: 5px;
}
.vcard .actions input {
background: none;
border: none;
color: #333;
cursor: pointer;
}
.vcard .actions a {
bottom: 5px;
color: #333;
position: absolute;
right: 50px;
text-decoration: none;
}
.vcard .actions input,
.vcard .actions a {
font-size: 11px;
}
.vcard .actions form:before {
content: " | ";
}
Voltando ao nosso aplicativo… Nós não iremos listar os usuários que se cadastraram em nosso serviço. Renomeie esta ação para new
. Nós precisamos instanciar um objeto User
. Só que em vez de usar o método render
utilizamos um outro, o método display
.
class Users < Application
def new
@user = User.new
display @user
end
end
O Merb tenta seguir o protocolo HTTP até onde pode. Assim, toda URL representa um recurso e deve ser exibido de acordo com a requisição. O método display
irá detectar o tipo de resposta esperado (XML, Javascript, JSON, etc.) e automaticamente irá chamar o método correspondente (to_json
, to_xml
, etc), caso ele tenha sido configurado para responder diferentes tipos de requisição.
Agora é a hora de criar nossa view. Crie o arquivo "app/views/users/new.html.erb" e adicione o conteúdo abaixo. Ele é bastante semelhante a um formulário do Rails.
<h2>Create your account</h2>
<p>
Signup for a account! It's free. And it's quick.
</p>
<%= error_messages_for @user %>
<%= form_for @user, :action => url(:users) do %>
<p>
<%= text_field :login, :label => "Login: " %>
</p>
<p>
<%= password_field :password, :label => "Password: " %>
</p>
<p>
<%= password_field :password_confirmation, :label => "Password Confirmation: " %>
</p>
<p class="submit">
<%= submit "Sign up" %>
</p>
<% end =%>
Você precisa ficar atento a duas coisas: você deve utilizar abrir o bloco com <%= %>
e fechá-lo com <% =%>
. Se você não fizer isso, saberá, já que uma exceção referente a sintaxe inválida será lançada.
Temos que criar a ação que receberá os dados enviados pelo usuário. Ela irá se chamar create
e também é bem simples e muito parecida com o que fazemos no Rails.
def create(user)
@user = User.new(user)
if @user.save
message[:notice] = "Welcome to Contacts! Please login to access your account."
redirect url(:session, :new), :message => message
else
render :new
end
end
Uma coisa que você pode estar estranhando é o argumento passado ao método create
. A gem merb-action-args é responsável por esta funcionalidade, que retorna para o método os parâmetros enviados na requisição e que estão disponíveis no método params
. A única obrigatoriedade é que você precisa utilizar o mesmo nome do parâmetro como nome da variável.
Outra diferença em relação ao Rails, é a forma como uma flash message é definida no Merb; você especifica mensagem no próprio redirecionamento. Note que ao usar o MerbAuth, uma string horrível é enviada na hora do redirecionamento. Será algo como
http://localhost:4000/session/new?_message=BAh7BjoLbm90aWNlIj5XZWxjb21lIHRvI[...]
Ao enviar um formulário com erros, uma tela como esta será exibida:
Agora, os usuários cadastrados precisam acessar nosso aplicativo. O MerbAuth já possui uma tela de login implementada. Você pode se autenticar no endereço "http://localhost:4000/login". Como você dificilmente vai utilizar a tela padrão, vamos criar nossa própria tela. Para isso, crie um novo arquivo em "app/views/exceptions/unauthorized.html.erb". Coloque o seguinte conteúdo:
<h2>Access your account</h2>
<p>
If you don't have an account yet, <%= link_to "signup for free!", url(:new_user) %>
</p>
<%= error_messages_for session.authentication %>
<%= form :action => url(:login), :method => :put do %>
<p>
<%= text_field :login, :label => "Login: ", :id => 'login' %>
</p>
<p>
<%= password_field :password, :label => "Password: ", :id => 'password' %>
</p>
<p class="submit">
<%= submit "Log me in" %>
</p>
<% end =%>
Perceba que o método error_messages_for
recebe o objeto session.authentication
. Ele possui diversos métodos que serão usados ao longo deste artigo.
Quando um usuário não consegue se autenticar, uma mensagem é exibida.
Se você conseguir se autenticar, verá uma mensagem de erro, já que a página que você foi redirecionado ainda não foi criada. Vamos criar o controller home
com o comando merb-gen controller home
. Altere a view "app/views/home/index.html.erb", adicionando o conteúdo abaixo.
<h2>Welcome to Contacts!</h2>
<p>
Contacts provides a flexible and convenient way to store contact
information for family, friends, and colleagues online.
</p>
<ul>
<li>Keep contact info centralized, sharable, and safe online.</li>
<li>It's free. And it's simple.</li>
</ul>
<p class="highlight">
<%= link_to 'Signup for free', url(:new_user) %> or
<%= link_to 'Access your account', url(:login) %>
<br/>
Signup and be using Contacts in less than 30 seconds!
</p>
Precisamos diferenciar usuários logados de não-logados. Vamos exibir uma mensagem com um link para fazer o logout. Altere o layout "app/views/layouts/application.html.erb", adicionando o código abaixo.
<div id="header">
<h1>Contacts</h1>
<% if session.authenticated? %>
<p class="login-info">
Hi, <%= current_user.login %> |
<%= link_to 'Logout', url(:logout) %>
</p>
<% end %>
</div>
O helper current_user
deve ser adicionado ao arquivo "app/controllers/application.rb", tornando o método disponível tanto para o controller quanto para as views. Ao contrário do Rails, você não precisa definir um helper com o método helper_method
.
class Application < Merb::Controller
private
def current_user
session.authentication.user
end
end
A página inicial pode ser modificada para exibir uma mensagem diferente para usuários logados. Altere o arquivo "app/views/home/index.html.erb", adicionando uma condição que verifica a autenticação.
<h2>Welcome to Contacts!</h2>
<p>
Contacts provides a flexible and convenient way to store contact
information for family, friends, and colleagues online.
</p>
<ul>
<li>Keep contact info centralized, sharable, and safe online.</li>
<li>It's free. And it's simple.</li>
</ul>
<p class="highlight">
<% if session.authenticated? %>
<%= link_to 'View your contacts', url(:contacts) %> or
<%= link_to 'Create one', url(:new_contact) %>
<br />
It's so simple to manage your contacts. Check it out!
<% else %>
<%= link_to 'Signup for free', url(:new_user) %> or
<%= link_to 'Access your account', url(:login) %>
<br/>
Signup and be using Contacts in less than 30 seconds!
<% end %>
</p>
Se você tentar acessar a página inicial, verá que ela deixou de funcionar. O erro foi gerado porque não temos um recurso contacts
criado e a rota para ele ainda não existe. Abra o arquivo "config/router.rb" e adicione-o.
Merb.logger.info("Compiling routes...")
Merb::Router.prepare do
resources :users
resources :session
resources :contacts
slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")
match('/signup').to(:controller => 'users', :action =>'new')
match('/').to(:controller => 'home', :action =>'index')
default_routes
end
Agora, já podemos criar o modelo Contact
; execute o comando merb-gen model Contact
. Abra o arquivo "app/models/contact.rb" e adicione a estrutura abaixo.
class Contact
include DataMapper::Resource
property :id, Serial
property :name, String
property :company, String
property :email, String
property :phone, String
property :mobile, String
property :address, String
end
À partir do terminal, execute o comando rake db:autoupgrade
para criar a tabela.
ATENÇÃO: Se você executar o comando rake db:automigrate
, as tabelas serão recriadas e os dados apagados! Utilize SEMPRE o comando rake db:autoupgrade
quando não quiser recriar suas tabelas. Com certeza rake db:reset
seria um nome muito melhor.
Agora, iremos aplicar algumas validações ao nosso modelo.
- O nome é um campo obrigatório.
- O e-mail, quando informado, deve ser válido.
Em vez de usarmos os métodos validates_*
, vamos usar um recurso muito interessante do DataMapper chamado validação implícita. Basta adicionar alguns parâmetros ao método property
que a validação será feita automaticamente.
class Contact
include DataMapper::Resource
property :id, Serial
property :name, String, :nullable => false
property :company, String
property :email, String, :format => :email_address
property :phone, String
property :mobile, String
property :address, String
end
A opção :format
aceita um único formato :email_address
; segundo a documentação, mais formatos serão implementados.
Agora, precisamos adicionar os relacionamentos. Um usuário possui muitos contatos, um contato pertence a um usuário. No modelo User
devemos adicionar o método has n
:
class User
include DataMapper::Resource
property :id, Serial
property :login, String
validates_is_unique :login
validates_length :login, :within => 3..30
validates_length :password, :min => 4, :if => :password_required?
has n, :contacts
end
No modelo Contact
devemos adicionar o método belongs_to
:
class Contact
include DataMapper::Resource
property :id, Serial
property :name, String, :nullable => false
property :company, String
property :email, String, :format => :email_address
property :phone, String
property :mobile, String
property :address, String
belongs_to :user
end
Novamente, execute o comando rake db:autoupgrade
.
Agora que nosso modelo está pronto, podemos partir para o controller. Crie-o com o comando merb-gen controller contacts
.
Nossa primeira ação será new
, que irá exibir o formulário de contatos.
class Contacts < Application
def new
@contact = Contact.new
display @contact
end
end
Vamos criar a view que permitirá adicionar novos contatos. Crie o arquivo "app/views/contacts/new.html.erb" e adicione o conteúdo abaixo.
<h2>Create a new contact</h2>
<%= error_messages_for @contact %>
<%= form_for @contact, :action => url(:contacts) do %>
<%= partial :form %>
<p class="submit">
<%= submit "Add contact" %> or
<%= link_to "Cancel", url(:contacts) %>
</p>
<% end =%>
No Merb, partials podem ser usadas através do método partial
. Ele pode receber um símbolo ou uma string. Se você fornecer um símbolo, o arquivo será procurado no diretório da view corrente. Se você fornecer uma string como "shared/sidebar"
, pode utilizar outros diretórios. No nosso caso, temos que criar um arquivo em "app/views/contacts/_form.html.erb".
<p>
<%= text_field :name, :label => "Name: " %>
</p>
<p>
<%= text_field :email, :label => "E-mail: <span>(optional)</span>" %>
</p>
<p>
<%= text_field :company, :label => "Company: <span>(optional)</span>" %>
</p>
<p>
<%= text_field :mobile, :label => "Mobile: <span>(optional)</span>" %>
</p>
<p>
<%= text_field :phone, :label => "Phone: <span>(optional)</span>" %>
</p>
<p>
<%= text_field :address, :label => "Address: <span>(optional)</span>" %>
</p>
Nosso formulário será submetido para a ação create
.
def create(contact)
@contact = current_user.contacts.build(contact)
if @contact.save
message[:notice] = "A new contact has been added!"
redirect url(:contacts), :message => message
else
render :new
end
end
Ao acessar o endereço "http://localhost:4000/contacts/new", você verá uma tela como esta:
Quando você criar um contato, será redirecionado para a listagem de contatos. Como ela ainda não foi criada, você verá a página de erro. Adicione a ação index
ao controller.
def index
@contacts = current_user.contacts.sorted
display @contacts
end
Na ação index
estamos listando todos os contatos do usuário logado, ordenados alfabeticamente. O método sorted
é um named scope e deve ser adicionado ao seu modelo Contact
; basta adicionar um método de classe com as suas definições.
class Contact
include DataMapper::Resource
property :id, Serial
property :name, String, :nullable => false
property :company, String
property :email, String, :format => :email_address
property :phone, String
property :mobile, String
property :address, String
belongs_to :user
def self.sorted
all :order => [:name.asc]
end
end
O DataMapper adiciona diversos métodos à classe Symbol
, permitindo coisas como :name.asc
, :title.like
, dentre outros.
Agora precisamos criar a view index
. Crie o arquivo "app/views/contacts/index.html.erb".
<h2>Your contacts</h2>
<p>
<%= link_to "Add contact", url(:new_contact) %>
</p>
<div id="contacts">
<% unless @contacts.empty? %>
<%= partial :contact, :with => @contacts %>
<% else %>
<p>You have no contacts yet!</p>
<% end %>
</div>
A linha que renderiza a coleção @contacts
na partial contact
é semelhante a render :partial => 'contact', :collection => @contacts
, presente no Rails. Adicione o conteúdo abaixo ao um novo arquivo em "app/views/contacts/_contact.html.erb":
<div class="vcard">
<%= photo_tag contact.email %>
<h3 class="n">
<%= contact.name %>
</h3>
<div class="email-group">
<span class="label">Email</span>
<span class="email"><%= link_to h(contact.email), "mailto:#{h contact.email}" %></span>
</div>
<% unless contact.company.blank? %>
<div class="org">
<span class="label">Company</span>
<span class="organization-name"><%= h contact.company %></span>
</div>
<% end %>
<% unless contact.address.blank? %>
<div class="adr">
<span class="type">Home</span>
<span class="street-address"><%= h contact.address %></span>
</div>
<% end %>
<% unless contact.mobile.blank? %>
<div class="tel mobile">
<span class="type">Mobile</span>
<%= h contact.mobile %>
</div>
<% end %>
<% unless contact.phone.blank? %>
<div class="tel phone">
<span class="type">Phone</span>
<%= h contact.phone %>
</div>
<% end %>
<div class="actions">
<%= delete_button contact %>
<%= link_to "Edit", url(:edit_contact, contact) %>
</div>
</div>
Vamos analisar dois pontos importantes desta partial individualmente. Primeiro, temos um helper photo_tag
, que irá montar a URL para o Gravatar à partir do e-mail do contato. Adicione este método ao arquivo "app/helpers/global_helpers.rb".
def photo_tag(email)
info = [
Digest::MD5.hexdigest(email), # => hash
36, # => size
'http%3A%2F%2Ff.nandovieira.com.br%2Fgravatar.gif' # => default gravatar
]
src = "http://www.gravatar.com/avatar/%s?s=%s&r=g&d=%s" % info
'<img src="%s" alt="" class="photo" />' % src
end
O outro ponto é o helper delete_button
, que irá criar um formulário com método DESTROY
para o recurso contact
. Ele é semelhante ao button_to
do Rails (button_to 'Delete', :method => :destroy
).
Pronto! A listagem de contatos já deve estar funcionando. Ela se parece com isto:
Agora, temos que adicionar a funcionalidade de editar um contato. Crie a ação edit
, que irá retornar o objeto para o formulário.
def edit(id)
@contact = current_user.contacts.get(id)
raise NotFound unless @contact
display @contact
end
Se o contato não for encontrado, a exceção NotFound
será lançada e a página de 404 será exibida. O DataMapper não possui o método find
; em vez dele, você usar o método get
.
Nossa view não será muito diferente da que usamos para criar um novo contato. A única diferença importante é que o formulário será enviado através do método PUT
.
<h2>Edit contact</h2>
<%= error_messages_for @contact %>
<%= form_for @contact, :action => url(:contact, @contact), :method => :put do %>
<%= partial :form %>
<p class="submit">
<%= submit "Edit contact" %> or
<%= link_to "Cancel", url(:contacts) %>
</p>
<% end =%>
A ação responsável por salvar as alterações no contato é update
.
def update(id, contact)
@contact = current_user.contacts.get(id)
raise NotFound unless @contact
if @contact.update_attributes(contact)
message[:notice] = "The contact has been updated!"
redirect url(:contacts), :message => message
else
render :edit
end
end
Novamente, buscamos o contato e exibimos a página de 404 se ele não for encontrado. Depois, atualizamos o contato com os dados enviados pelo formulário. Se o formulário não tiver nenhum erro é redirecionado para a listagem; caso contrário, o formulário é novamente renderizado.
Para finalizar, precisamos permitir que contatos sejam removidos. A ação que irá fazer este trabalho é destroy
, que como você pode notar, também é simples e muito parecida com as ações edit
e update
.
def destroy(id)
@contact = current_user.contacts.get(id)
raise NotFound unless @contact
if @contact.destroy
message[:notice] = "The contact has been removed"
redirect url(:contacts), :message => message
else
raise InternalServerError
end
end
Não sei se você percebeu, mas ainda não falamos que o recurso contacts
só pode ser acessado por usuários autenticados. Se você não estiver logado e tentar listar os contatos, verá uma página de erro.
O MerbAuth permite que você defina áreas restritas de duas maneiras diferentes. Você pode usar o método authenticate
diretamente no arquivo "router.rb":
authenticate do
resources :contacts
end
Ou pode utilizar o método ensure_authenticated
no controller:
class Contacts < Application
before :ensure_authenticated
# ...
end
A vantagem do primeiro método é que você para o processamento bem antes da execução do filtro ensure_authenticated
do controller. Porém, para utilizá-lo, você precisa proteger o recurso inteiro, não podendo especificar ações que são públicas. Sempre que puder utilize o método authenticate
.
E é isso! O Contacts está quase pronto. Muitas outras coisas poderiam ser feitas. Você pode, como exercício, adicionar os testes (talvez eu escreva sobre isso em um outro artigo) e criar a página de atualização de senha do usuário logado.
Para finalizar…
O Ruby on Rails é excelente, mas você não deve se prender a um único framework. Conheça o Merb. Experimente o Sinatra, um framework muito rápido se você pretende disponibilizar APIs. Veja outras opções para não ficar limitado. E lembre-se: conhecimento nunca é demais!