Entendendo os contadores no Ruby on Rails
02/12/08
Aparentemente, os counters são as coisas que mais geram confusão para quem está começando no Ruby on Rails. Afinal, existem diversas maneiras de se obter o total de itens de uma coleção. Imagine que você possua dois modelos:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
Vamos popular o banco de dados com alguns registros. À partir do console, execute os comandos abaixo.
user = User.create
100.times { user.items.create }
A primeira maneira e que poderá causar problemas é carregando todos os itens da coleção em memória,
para depois usar o método size. Ela é ruim se você possuir uma base de dados com muitos registros.
Pensando bem, ela é ruim sempre!
user.things.all.size
# => SELECT * FROM "things" WHERE ("things".user_id = 1)
Você também pode utilizar o método length. Ele funciona como o método acima, só que de maneira mais automática.
user.things.length
# => SELECT * FROM "things" WHERE ("things".user_id = 1)
Outra maneira é utilizar o método size diretamente na associação.
Ela irá gerar uma consulta SQL para fazer a contagem.
user.things.size
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
Assim como o método size, o método count também irá gerar uma consulta
SQL para fazer a consulta. A diferença é que você pode enviar condições, já que o método size
não permite que você faça isso.
user.things.count
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
user.things.count(:conditions => ["created_at > ?", 2.days.ago])
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1 AND (created_at > '2008-11-30 10:26:05'))
Estamos no caminho. A saída, pelos exemplos acima, é utilizar sempre o método count.
Uma grande desvantagem, no entanto, é que isso trará problemas de performance quando você possuir uma
base de milhões de registros fazendo counts para todo lado. Para nossa sorte, o Rails possui
um recurso chamado counter cache.
O counter cache nada mais é que um campo na tabela que irá refletir o total de itens de um relacionamento, evitando uma consulta SQL para isso. Toda vez que um registro é criado, este campo é incrementado; quando um registro é destruído, esse campo é decrementado. A única desvatangem do counter cache é que ele só pode ser utilizado em relacionamentos.
Para adicionar um counter cache, basta adicionar um campo que segue o padrão [associação]_count.
Lembre-se de atualizar a contagem após adicionar o campo.
class AddThingsCountToUser < ActiveRecord::Migration
def self.up
add_column :users, :things_count, :integer, :default => 0, :null => false
User.all.each do |user|
User.update_counters user.id, :things_count => user.things.count
end
end
def self.down
remove_column :users, :things_count
end
end
Para que o Rails saiba que este campo existe, você deve adicionar a opção :counter_cache => true.
class Thing < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
Agora, toda vez que um registro for criado ou destruído, o Rails irá incrementar/decrementar este campo automaticamente.
E toda vez que você utilizar o método count, ele irá utilizar o valor armazenado neste campo. Vamos criar um
novo registro para ver a consulta gerada:
user.things.create
# => INSERT INTO "things" ("updated_at", "user_id", "created_at") VALUES('2008-12-02 10:59:25', 1, '2008-12-02 10:59:25')
# => UPDATE "users" SET "things_count" = COALESCE("things_count", 0) + 1 WHERE ("id" = 1)
Para saber quantas coisas um usuário possui, você deve utilizar o método size.
Se você utilizar o método count, a consulta será feita diretamente no banco.
Na dúvida, utilize o método things_count.
user.things.size
# => 101
user.things.count
# => 101
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
user.things_count
# => 101
Se precisa trabalhar com contadores personalizados (que dependem de condições para
serem incrementados ou não), pode utilizar callbacks no seu modelo juntamente
com os métodos increment_counter e decrement_counter.
Imagine que o modelo User possua um contador chamado cool_things_count
que refletirá o total de registros com o atributo kind com o valor cool.
class Thing < ActiveRecord::Base
belongs_to :user, :counter_cache => true
after_save :increment_cool_things_count
after_destroy :decrement_cool_things_count
private
def increment_cool_things_count
User.increment_counter(:cool_things_count, user_id) if kind == 'cool'
end
def decrement_cool_things_count
User.decrement_counter(:cool_things_count, user_id) if kind == 'cool'
end
end
É importante notar que você não pode usar nenhum relacionamento quando estiver tratando o
callback after_destroy, já que o objeto não existe mais.
- Permalink
- Trackback
- Comentários (8)
- Ao som de: The Killers – Mr. Brightside
Criando seu primeiro projeto com Merb e DataMapper
21/11/08
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; diverta-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.simplesideias.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!
- Permalink
- Trackback
- Comentários (11)
- Ao som de: Further Seems Forever – Say It Ain't So
Compactando Javascript e CSS antes de fazer um commit no Git
19/11/08
O Git tem alguns hooks que permitem fazer coisas bem interessantes. Eu, malandro que sou, escrevi um para compactar arquivos Javascript e CSS utilizando YUI Compressor, desenvolvido pelo Yahoo!.
Em seu repositório Git, crie o arquivo ".git/hooks/pre-commit" com o conteúdo abaixo:
#!/bin/bash
cd "$0/../../.."
rake git:precommit
Execute o comando chmod a+x .git/hooks/pre-commit. Se você não fizer isso,
o Git não irá executar este hook. Ele irá executar a tarefa Rake git:precommit.
Acesse a Yahoo! Developer Network e baixe o YUI Compressor. É um arquivo "jar", o que significa que você precisa ter Java 1.4 ou superior instalado. Alternativamente, você pode executar os comandos abaixo à partir da raíz de seu projeto Ruby on Rails.
wget http://www.julienlecomte.net/yuicompressor/yuicompressor-2.4.2.zip -O yuicompressor.zip
unzip yuicompressor.zip
mkdir tools
cp yuicompressor-2.4.2/build/yuicompressor-2.4.2.jar tools/yuicompressor.jar
rm -rf yuicompressor-2.4.2/
Agora, podemos fazer todo o trabalho sujo utilizando código Ruby.
Crie o arquivo lib/tasks/dev.rake e adicione o código abaixo:
require "config/environment"
def run_compressor(type, files, except=[])
output_refs = {:css => "stylesheets", :js => "javascripts"}
output_dir = "public/#{output_refs[type]}"
files.each do |input|
output_name = File.basename(input)
output_name.gsub!(/\.(js|css)$/, "-min#{File.extname(output_name)}")
output = "#{output_dir}/#{output_name}"
system "java -jar tools/yuicompressor.jar --type=#{type} #{input} > #{output}"
original = file_size(input)
compressed = file_size(output)
puts " - #{File.basename(input)} [#{original}] => #{output_name} [#{compressed}]"
end
end
def file_size(file)
ActionController::Base.helpers.number_to_human_size(File.size(file))
end
namespace :git do
desc "Run before a Git commit"
task :precommit do
Rake::Task['compress:css'].invoke
Rake::Task['compress:javascript'].invoke
end
end
namespace :compress do
desc "Compress all javascript files using YUI Compressor"
task :javascript do
puts "\nCompressing javascript files"
files = Dir['public/javascripts/*.js'].reject do |f|
f =~ /-min\.js$/ || f =~ /\/jquery\.js/
end
run_compressor(:js, files)
end
desc "Compress all stylesheet files using YUI Compressor"
task :css do
puts "\nCompressing stylesheet files"
files = Dir['public/stylesheets/*.css'].reject do |f|
f =~ /-min\.css$/
end
run_compressor(:css, files)
end
end
É um código bastante simples. Ele simplesmente pega todos os arquivos Javascript e CSS que não possuem "-min" no nome e aplica a compactação. O arquivo "jquery.js" não é compactado pois sempre utilizado a versão reduzida.
Como saber se está funcionando? Faça um commit! Se os arquivos compactados foram gerados, tudo saiu como esperado. Você receberá uma saída como esta:
Compressing stylesheet files
- application.css [15.1 KB] => application-min.css [12.3 KB]
Compressing javascript files
- application.js [4.6 KB] => application-min.js [3.1 KB]
- facebox.js [9.2 KB] => facebox-min.js [4.9 KB]
- jquery.form.js [22.3 KB] => jquery.form-min.js [8.2 KB]
- rails.js [1 KB] => rails-min.js [466 Bytes]
Eu estou utilizando dois helpers que exibem a versão original dos arquivos caso eu esteja no ambiente de desenvolvimento, tornando a tarefa de depurar erros de Javascript e CSS mais simples.
module ApplicationHelper
def compressed_stylesheets(*files)
files.collect! do |f|
f.gsub!(/\.css$/, '')
"#{f}-min.css"
end unless Rails.env == "development"
stylesheet_link_tag *files
end
def compressed_javascripts(*files)
files.collect! do |f|
f.gsub!(/\.js$/, '')
"#{f}-min.js"
end unless Rails.env == "development"
javascript_include_tag *files
end
end
Para utilizá-los, basta passar o nome dos arquivos compactados.
<%= compressed_stylesheets 'application' %>
<%= compressed_javascripts 'rails', 'facebox', 'jquery.form.js', 'application' %>
Importante!
Tenha sempre em mente que se seu Javascript for ruim — por ruim quero dizer nas coxas, mal-feito, gambiarra, tosco, nojento, um código que nem seu pior inimigo deveria ter acesso — a compactação irá, provavelmente, gerar erros de Javascript (sintaxe inválida).
Para garantir nada irá quebrar, siga os passos explicados na página do JSLint; eu já sigo há um bom tempo e nunca tive nenhum problema em versões compactadas de meus códigos! Veja os erros mais comuns:
- não finalizar linhas de código com ponto-e-vírgula
-
não utilize
eval; as funçõessetIntervalesetTimeouttambém devem ser evitadas se o argumento for uma string. Utilize algo comosetInterval(function(){ /* do something */ }, 1000). -
evite operadores de incremento (++) e decremento (--);
prefira algo como
i += 1ei -= 1
Para ver muitas outras dicas, leia a documentação do JSLint.
Infelizmente, não é possível garantir que bibliotecas de terceiros irão funcionar em 100% dos casos. Leia o código antes de aplicar a compactação, já que você pode estar lidando com algo bastante ruim.
- Permalink
- Trackback
- Comentários (5)
- Ao som de: Ryan Bates – Episode 132: Helpers Outside Views
