Migrando o Brazilian Rails para Ruby 1.9
Relato passo a passo das alteraçoẽs para migrar o Brazilian Rails:
Adicionados comentários de encoding. commit commit
: (dois pontos) não são mais válidos como separador de instruções. Deve-se trocar por ; ou then, ambas funcionam no 1.8 e 1.9. commit
ParseDate foi removido, Date#parse faz grande parte do que fazia o ParseDate. commit
O 1.9 introduziu o método Numeric#real, e estava sobrescrevendo um alias. commit
Variável $KCODE não tem mais efeito no 1.9, será setada apenas se a versão for anterior a 1.9. commit
A parte mais chata foi relacionada ao parser datas. O Date#parse no 1.9 se comporta de maneira diferente do 1.8, foram introduzidos outros formatos para parseamento, e o parse tenta converter a data com os novos formatos. Exemplo do novo comportamento:
# ruby 1.8.7
Date.parse("13/12/200A")
ArgumentError: invalid date
from /usr/lib/ruby/1.8/date.rb:956:in `new_by_frags'
from /usr/lib/ruby/1.8/date.rb:1000:in `parse'
from (irb):7
from /usr/lib/ruby/1.8/date.rb:811
# ruby 1.9.1
Date.parse("13/12/200A")
=> #<date: 0200-12-13>
Outra mudança foi que o formato padrão do parse passou a ser dd/mm/yyyy em vez de mm/dd/yyyy:
# ruby 1.8.7
Date.parse("10/12/2009")
=> Seg, 12 Out 2009
# ruby 1.9.1
Date.parse("10/12/2009")
=> Qui, 10 Dez 2009
Levando em conta essas mudanças, alterei o formado de algumas datas nesse teste. Esse outro teste, não está sendo executado no 1.9, justamente pelas diferenças do parse.
Sim, sei que essa solução está longe da ideal, mas para manter o mesmo comportamento seria necessário sobrescrever o parser do 1.9, e não gosto dessa solução… Para quem usa apenas datas no formato brasileiro, não vejo problemas, o Date#valid? continua funcionando (afinal, o propósito da brdata é trabalhar com datas no formato brasileiro
). O problema é se forem usadas datas em outros formatos, pois aí quem se encarrega de fazer a conversão é o parser do Ruby. Então, se você escolheu usar uby 1.9, esteja ciente das mudanças na classe Date.
Talvez no futuro seja interessante repensar a forma como o método Date#valid? é implementado, mas por hora, creio que deixar a cargo do usuário é razoável, já que é dele a decisão e sobre qual versão do Ruby usar. O suporte da brdata no 1.9 ainda é experimental, então se algém tiver idéias melhores, se pronuncie =).
Finalizando, destaco o papel dos testes na migração. Por sorte o Brazilian Rails tem uma boa cobertura, o que facilitou bastante. A cada alteração feita eu rodava os testes no 1.9 e no 1.8 para ver se estava tudo certo
.
PS: para mudar a versão do Ruby eu uso o ruby_switcher. Também tem o rvm, mas acho o ruby_switcher mais simples.
Dica: adicionando comentários de codificação num projeto Rails
O Ruby 1.9 agora leva em consideração o encoding das strings nos arquivos, então caso algum arquivo ruby tenha algum caracter não ASCII (caracteres acentuados por exemplo), será exibido o erro:
invalid multibyte char (US-ASCII)
Para que o erro não ocorra, basta adicionar o comentário abaixo na primeira linha do arquivo *.rb:
# encoding: UTF-8
Ao migrar um projeto Rails para Ruby 1.9, esse erro acontecia em vários arquivos, então nada melhor que deixar o computador resolver o problema por você
. O comando abaixo vai colocar esse comentário em todos os controllers, helpers e models do seu projeto Rails:
cd seu_projeto
find app/{controllers,models,helpers} -type f -name \*.rb | xargs sed -i '1i # encoding: UTF-8'
Para quem usa windows, pode usar o seguinte script Ruby para fazer o mesmo:
# utf_comment.rb
Dir["app/{controllers,models,helpers}/**/*.rb"].each do |filename|
content = File.read(filename).sub(/^/, "# encoding: UTF-8\n")
File.open(filename, "r+") { |f| f.write(content) }
end
Execute com
cd seu_projeto ruby utf_comment.rb
Para mais informações sobre codificação de strings, aqui tem vários artigos abordando as mudanças. Recomendo a leitura!
Rails: Otimizando STI com default_scope
NOTA: se vc não faz idéia do que é STI, veja esse link.
Não dá pra negar que STI (Single Table Inheritance) é um recurso bacana do Rails. Claro que não é bom abusar, mas em alguns casos, ela se encaixa muito bem. No rails, quando queremos usar STI, basta ter uma coluna type do tipo string na tabela “mãe”, e nessa coluna o Rails se encarrega de gravar o nome da classe do registro, e a partir daí tudo funciona normalmente (para mais detalhes sobre como funciona STI no Rails, veja a api). Porém, apesar da implementação do Rails ser muito elegante e funcionar bem, ela tem alguns problemas:
- Comparações entre strings são lentas. Mesmo que haja um índice na coluna “type”, comparações entre strings ainda são bem mais lentas que comparações entre inteiros.
- Caso o model seja renomeado, os dados da coluna “type” terão que ser atualizados, ou ficaremos com dados incorretos na tabela. Para corrigir podemos criar uma migration, mas é mais uma coisa para se preocupar
Vamos tentar melhorar isso!
Criando um projetinho de exemplo (NOTA: estou usando Rails 2.3.3. Se usa uma versão mais antiga, veja algumas considerações mais abaixo):
rails sti cd sti script/generate model pessoa nome:string documento:string tipo:integer
Estamos criando nossa tabela base, e em vez de usar uma coluna type do tipo string, vamos usar uma coluna chamada tipo (o nome da coluna poderia ser qualquer um), do tipo inteiro.
Vamos editar nossa migração e adicionar um índice na migration para melhorar a performance nas buscas:
class CreatePessoas < ActiveRecord::Migration
def self.up
create_table :pessoas do |t|
t.string :nome
t.string :documento
t.integer :tipo
t.timestamps
end
#adicionando o indice
add_index :pessoas, [:id, :tipo]
end
def self.down
drop_table :pessoas
end
end
Criando o banco e nossa tabela de testes:
rake db:create rake db:migrate
Alterando o model Pessoa para adicionar algumas constantes com os tipos de pessoas:
#app/models/pessoa.rb class Pessoa < ActiveRecord::Base # tipos de pessoa JURIDICA = 1 FISICA = 2 end
Agora, vamos criar dois models que herdam de Pessoa, PessoaFisica e PessoaJuridica, e definir os seus default_scopes:
#app/models/pessoa_fisica.rb
class PessoaFisica < Pessoa
default_scope :conditions => {:tipo => FISICA}
end
#app/models/pessoa_juridica.rb
class PessoaJuridica < Pessoa
default_scope :conditions => {:tipo => JURIDICA}
end
Só isso? Só
. Aparentemente, o Rails está bem espertinho, e quando instânciamos um objeto ele já se encarrega de colocar o valor correto na coluna tipo, da mesma forma que STI faz:
Pessoa.new #<Pessoa id: nil, nome: nil, documento: nil, tipo: nil> PessoaJuridica.new #<PessoaJuridica id: nil, nome: nil, documento: nil, tipo: 1> PessoaFisica.new #<PessoaFisica id: nil, nome: nil, documento: nil, tipo: 2>
Criando algumas pessoas:
Pessoa.create(:nome => "Pessoa indefinida") PessoaFisica.create(:nome => "João da Silva", :documento => "123.456.789-10") PessoaFisica.create(:nome => "José Oliveira", :documento => "000.111.222-33") PessoaJuridica.create(:nome => "Padaria pão quentinho", :documento => "26.721.163/0001-52") PessoaJuridica.create(:nome => "Transportadora Leva e Traz", :documento => "70.562.197/0001-31")
Buscando:
>> Pessoa.all
# SELECT * FROM "pessoas"
=> [#<Pessoa id: 1, nome: "Pessoa indefinida", documento: nil, tipo: nil, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 2, nome: "João da Silva", documento: "123.456.789-10", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 3, nome: "José Oliveira", documento: "000.111.222-33", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 4, nome: "Padaria pão quentinho", documento: "26.721.163/0001-52", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 5, nome: "Transportadora Leva e Traz", documento: "70.562.197/0001-31", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]
>> PessoaJuridica.all
# SELECT * FROM "pessoas" WHERE ("pessoas"."tipo" = 1)
=> [#<PessoaJuridica id: 4, nome: "Padaria pão quentinho", documento: "26.721.163/0001-52", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<PessoaJuridica id: 5, nome: "Transportadora Leva e Traz", documento: "70.562.197/0001-31", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]
>> PessoaFisica.all
# SELECT * FROM "pessoas" WHERE ("pessoas"."tipo" = 2)
=> [#<PessoaFisica id: 2, nome: "João da Silva", documento: "123.456.789-10", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<PessoaFisica id: 3, nome: "José Oliveira", documento: "000.111.222-33", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]
Para quem ainda usa uma versão do Rails menor que 2.3 e maior que 2.0, uma solução é instalar o plugin com o backport do default_scope:
script/plugin install git://github.com/duncanbeevers/default_scope.git
Além disso, será necessário usar uma callback para setar a coluna tipo com o valor correto. Por exemplo:
class PessoaFisica < Pessoa
default_scope :conditions => {:tipo => FISICA}
before_save { |p| p.tipo = FISICA }
end
Para quem usa uma versão do mais antiga que a 2, já passou da hora de atualizar hem
.
Finalizando
Conseguimos o mesmo comportamento da STI, sem as desvantagens citadas anteriormente, usando apenas algumas linhas de código a mais (os default_scopes e as constantes que definimos, no total 4 linhas). Espero que essa dica seja útil, e lembre-se, aprecie STI com moderação
.
Deslocalizando datas e números usando o i18n do Rails
NOTA: Antes de começar, se você não sabe o que é ou não tem uma noção básica sobre o i18n do Rails, leia esse guia.
Fazer conversões de datas e números localizados sempre foi uma tarefa chata em Rails (e em outras linguagens/frameworks também). O usuário digita uma data no formato brasileiro (por exemplo, dd/mm/yyyy), e o Ruby não consegue converter a data corretamente. A maneira mais usada por aí é criar atributos virtuais (ou mesmo sobrecrever o setter padrão do atributo) no model para fazer a conversão, por exemplo:
class Model < ActiveRecord::Base
#recebe uma data no formato dd/mm/yyyy e converte para yyyy-mm-dd
def data_vencimento=value
self[:data_vencimento] = value.split("/").reverse.join("-")
end
end
Isso funciona, mas quando eu usava isso não ficava satisfeito, queria algo mais “mágico”
. Perguntei no fórum RubyOnBr se não teria um jeito de fazer a conversão automaticamente, usando o próprio i18n do Rails. Então meu camarada Rafael Rosa chegou a uma solução, usando o formato padrão de data configurado no aquivo de i18n para fazer o parsing de datas. Eu acrescentei suporte à números, o Rafael organizou tudo, escreveu testes, e surgiu o i18n_localize_core.
O plugin funciona bem, consegue converter datas e números usando as configurações do arquivo de internacionalização. O plugin resolvia bem para mim, inclusive tinha feito alguns hacks no plugin para fazer a conversão nos text_fields, mas não coloquei no meu fork porque estava muito feio
.
Só que a um tempo atrás, encontrei o plugin delocalize, escrito por Clemens Kofler, que faz basicamente a mesma coisa, mas melhor
. O i18n_localize_core é meio invasivo, sobrescreve os métodos Date#_parse, String#to_i e String#to_f. No delocalize, o Clemens sobrescreveu apenas onde era necessário, e ainda adicionou suporte à entrada de vários formatos de datas.
Usar o plugin é simples, como tudo na vida deve ser
. Tendo configurado seu arquivo de i18n e instalado o plugin, basta adicionar a chave :input no arquivo de i18n com os formatos de data que o usuário poderá digitar e sair pro abraço
. O plugin se encarrega de fazer o resto de forma transparente.
No README do plugin tem alguns exemplos de uso, dêem uma conferida no projeto:
Tradução: Delegação em Ruby
Hoje li um artigo interessante na primeira edição da Rails Magazine sobre delegação de métodos em Ruby, escrito por Khaled al Habache. Além da delegação, ele também aborda um tema antigo (e que gera muita discussão por aí) em se tratando de POO, Herança X Composição. Como não achei material sobre delegação em português, resolvi traduzir o artigo. Espero que gostem
.
Delegação em Ruby
“Separar partes mutáveis das partes que não mudam” e “composição é preferível a herança” são dois princípios de projeto comuns quando você inicia no mundo da Programação Orientada à Objetos. Entretanto, enquanto o primeiro parece lógico, alguém pode pensar porque é preferível usar composição em vez de herança, e é uma questão lógica, então vamos respondê-la com um exemplo.
Vamos supor que temos um robô que possui um sensor de temperatura, representado pelo seguinte diagrama UML:

Classe Robô
Esse projeto tem alguns inconvenientes:
1. Existe grande probabilidade de existir outros tipos de robôs que não possuem sensores de calor (quebra o primeiro princípio de projeto: separar código mutável do estático)
2. Sempre que eu desejar alterar qualquer coisa relacionada ao sensor de temperatura, terei que alterar a classe Robô (quebra o primeiro princípio de projeto).
3. Exposição dos métodos do sensor de temperatura na classe Robô.
Vamos melhorar essa classe um pouco:

Classe Robô Vulcão
Bem, agora esse é um projeto baseado em herança que resolve o primeiro problema, mas ainda é incapaz de resolver os outros dois problemas relacionados ao sensor de calor. Vamos melhorar um pouco mais:

Delegação
Esse é um típico modelo, baseado em composição em vez de herança. Com ele podemos resolver todos os 3 problemas, e ainda por cima ganhamos uma nova classe: agora podemos abstrair o SensorDeTemperatura para usos futuros.
O que é delegação?
Delegação é o processo de delegar funcionalidade às partes contidas.
Se você olhar cuidadosamente à figura anterior, você notará que o RoboVulcao ainda possui 3 métodos relacionados ao sensor; esses são métodos que apenas chamam os métodos correspondentes do sensor. Isso é exatamente o que delegação é, apenas repassar funcionalidade às partes contidas.
Delegação vem junto com composição para oferecer soluções flexíveis e elegantes como essa que vimos anteriormente, e também respeita o princípio “separar código mutável de código estático”, mas também cobra um preço por isso: a necessidade de métodos que “embrulhem” as chamadas impõem um tempo extra de processamento por causa das chamadas a esses métodos.
Ruby e delegação
Agora vamos ver um exemplo de código:
Temos um robô de propósito geral que possui um Braço e um SensorDeCalor. O robô é capaz de executar várias tarefas, como empacotar caixas, empilhá-las e medir a temperatura.
Usaremos composição e delegação assim:
class Robô
def initialize
@sensor_temperatura = SensorDeTemperatura.new
@braco = BracoDoRobo.new
end
def medir_temperatura(escala="c")
@sensor_temperatura.measure(escala)
end
def empilhar(quantidade_de_caixas=1)
@braco.empilhar(quantidade_de_caixas)
end
def empacotar
@braco.empacotar
end
end
class SensorDeTemperatura
# Escala Celsius ou Fahrenheit
def medir(escala="c")
t = rand(100)
t = escala=="c" ? t : t * (9/5)
puts "A temperatura é #{t}° #{escala.upcase}"
end
end
class BracoDoRobo
def empilhar(quantidade_de_caixas=1)
puts "Empilhando #{quantidade_de_caixas} caixa(s)"
end
def empacotar
puts "Empacotando"
end
end
robo = Robo.new #=>#<Robo:0xb75131e8 @arm=#<BracoDoRobo:0xb75131ac>, @sensor_temperatura=#<SensorDeTemperatura:0xb75131c0>>
robo.empilhar 2 #=>Empilhando 2 caixa(s)
robo.empacotar #=>Empacotando
robo.medir_temperatura #=> A temperatura é 59° C
Como pode ser visto, temos 3 métodos que “encapsulam” chamadas (empilhar, empacotar e medir_temperatura) na classe Robo que não fazem nada a não ser chamar o método correspondente dos componentes (BracoDoRobo e SensorDeTemperatura).
Isso é uma coisa péssima, especialmente quando existem vários objetos contidos no objeto principal.
Entretanto, em Ruby temos duas bibliotecas para nos salvar: Forwardable e Delegate, Vamos ver cada uma delas.
Biblioteca Forwardable
A biblioteca Forwardable possui dois módulos, Forwardable e SingleForwardable.
Módulo Forwardable
O módulo Forwardable oferece delegação dos métodos especificados para um objeto designado, usando os métodos def_delegator e def_delegators.
def_delegator(obj, method, alias = method): Define um método “method” que delega a chamada ao objeto “obj”. Se o alias for fornecido, será usado como o nome do método de delegação.
def_delegators(obj, *methods): Atalho para definir múltiplos métodos delegadores, mas sem permitir o uso de um nome diferente.
Vamos refatorar nosso exemplo de robô para utilizar o módulo Forwardable:
require 'forwardable'
class Robo
# Extend fornece métodos de classe
extend Forwardable
# Uso do def_delegators
def_delegators :@braco, :empacotar, :empilhar
# Uso do def_delegator
def_delegator :@sensor_temperatura, :measure, :medir_temperatura
def initialize
@sensor_temperatura = SensorDeTemperatura.new
@braco = BracoDoRobo.new
end
end
class SensorDeTemperatura
# Escala Celsius ou Fahrenheit
def medir(escala="c")
t = rand(100)
t = escala=="c" ? t : t * (9/5)
puts "A temperatura é #{t}° #{escala.upcase}"
end
end
class BracoDoRobo
def empilhar(quantidade_de_caixas=1)
puts "Empilhando #{quantidade_de_caixas} caixa(s)"
end
def empacotar
puts "Empacotando"
end
end
Como pode ser visto, é uma solução mais limpa e consisa.
Módulo SingleForwardable
O módulo SingleForwardable oferece delegação dos métodos especificados para um objeto designado, utilizando os métodos def_delegator e def_delegators. Esse módulo é similar ao Forwardable, mas trabalha com os próprios objetos, em vez de suas classes que os definem.
require "forwardable" require "date" date = Date.today #=> #<Date: 4909665/2,0,2299161> # Prepara o objeto para delegação date.extend SingleForwardable #=> #<Date: 4909665/2,0,2299161> # Adiciona delegação para Time.now date.def_delegator :Time, "now","with_time" puts date.with_time #=>Thu Jan 01 23:03:04 +0200 2009
Biblioteca Delegate
Delegate é outra biblioteca que oferece delegação, irei explicar duas formas de usá-la.
Método DelegateClass
Utiliza o método de nível superior DelegateClass, que permite inicializar a delegação por meio de herança. No exemplo seguinte, quero criar uma nova classe chamada CurrentDate, que armazene a data corrente e alguns métodos extras, ao mesmo em que delego a objetos data normais:
require "delegate"
require "date"
# Note a definição da classe
class CurrentDate < DelegateClass(Date)
def initialize
@date = Date.today
# Passa o objeto a ser delegado à superclasse
super(@date)
end
def to_s
@date.strftime "%Y/%m/%d"
end
def with_time
Time.now
end
end
cdate = CurrentDate.new
# Note como funciona a delegação
# Em vez de usar cdate.date.day e definir um
# attr_accessor para date, eu uso c.day
puts cdate.day #=>1
puts cdate.month #=>1
puts cdate.year #=>2009
# Testando métodos adicionados
# to_s
puts cdate #=> 2009/01/01
puts cdate.with_time #=> Thu Jan 01 23:22:20 +0200 2009
Classe SimpleDelegator
Use-a para delegar para um objeto que mode ser modificado:
require "delegate" require "date" today = Date.today #=> #<Date: 4909665/2,0,2299161> yesterday = today - 1 #=> #<Date: 4909663/2,0,2299161> date = SimpleDelegator.new(today) #=> #<Date: 4909665/2,0,2299161> puts date #=>2009-01-01 # Usa __setobj__ para trocar a delegação date.__setobj__(yesterday)#=> #<Date: 4909663/2,0,2299161> puts date #=>2008-12-31
Como pode ser visto, criamos dois objetos e delegamos a eles.
E o Rails?
Rails adiciona uma nova funcionalidade chamada “delegate”, que fornece um método de classe para expor facilmente os metodos dos objetos contidos como se fossem métodos próprios. Passe um ou mais métodos (especificados como símbolos ou strings) e como último parâmetro o nome do objeto alvo na opção :to (também como símbolo ou string). Pelo menos um método e a opção :to são exigidos.
Vá para o console, crie um novo projeto e inicie um console do rails:
$ rails dummy
$ cd dummy
$ruby script/console
Loading development environment (Rails 2.2.2)
>> Person = Struct.new(:name, :address)
=> Person
>> class Invoice < Struct.new(:client)
>> delegate :name, :address, :to => :client
>> end
=> [:name, :address]
>> john_doe = Person.new("John Doe", "Vimmersvej 13")
=> #<struct Person name="John Doe", address="Vimmersvej 13">
>> invoice = Invoice.new(john_doe)
=> #<struct Invoice client=#<struct Person name="John Doe", address="Vimmersvej 13">>
>> invoice.name
=> John Doe
>> invoice.address
=>Vimmersvej 13
Recomendo fortemente que você verifique todos os exemplos na documentação da API do Rails, para ver como usar esse recurso efetivamente com o ActiveRecord.
Antes de terminar esse artigo, quero compartilhar com você o código do método delegate do Rails. Adicionarei alguns comentários para explicar o que está acontecendo:
class Module
# método delegate
# Espera um array de argumentos contendo os métodos
# a serem delegados e um hash de opções
def delegate(*methods)
# Desempilha o hash de opções
options = methods.pop
# Verifica se o hash de opções foi passado, e se contém a opção :to
# Lança uma exceção se um dos dois não forem encontrados
unless options.is_a?(Hash) && to = options[:to]
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
end
# Certifica-se de que a opção :to segue algumas
# regras de sintaxe para nomes de métodos
if options[:prefix] == true && options[:to].to_s =~ /^[^a-z_]/
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
# Atribui o verdadeiro valor do prefixo
prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_"
# Aqui vem a mágica do Ruby
# Técnicas de reflexão são usadas aqui:
# module_eval é usado para adicionar novos métodos em tempo de execução, onde:
# expõe os métodos dos objetos contidos
methods.each do |method|
module_eval("def #{prefix}#{method}(*args, &block)\n#{to}.__send__(#{method.inspect}, *args, &block)\nend\n", "(__DELEGATION__)", 1)
end
end
end
Isso é tudo para esse artigo, foram cobertos 5 pontos:
1. Composição versus Herança.
2. O que é delegação, e porque ela é usada.
3. Biblioteca Forwardable.
4. Biblioteca Delegate.
5. Método delegate do Rails.
acts_as_active: mais um plugin no estilo acts_as_paranoid
Pra quem é do mundo Rails, o acts_as_paranoid já é velho conhecido, ele sobrescreve alguns métodos do ActiveRecord para marcar um registro como excluído em vez de efetivamente excluí-lo. Ele também adiciona um escopo para esconder os registros “exluídos” dos finders e outras operações comuns como count, sum, max, etc. Meu novo plugin, acts_as_active, faz basicamente a mesma coisa, porém utiliza um booleano em vez de um timestamp para indicar que o registro está ativo e utiliza um escopo padrão para mostrar apenas os ativos.
Instalando o acts_as_active
Pra instalar, o de sempre:
script/plugin install git://github.com/fernandoluizao/acts_as_active.git
IMPORTANTE: Meu plugin depende de um novo recurso adicionado no Rails 2.3 chamado default_scope
Para quem usa Rails 2.1 ou 2.2, instale o plugin default_scope.
script/plugin install git://github.com/duncanbeevers/default_scope.git
Uso
O plugin assume que vc tem um campo booleano com o nome “active”, com valor default TRUE. Exemplo de uso:
create_table :users do |t|
t.string :name
t.string :last_name
t.boolean :active, :null => false, :default => true
end
class User < ActiveRecord::Base
acts_as_active
end
User.first.destroy
UPDATE users SET active = 'f' WHERE id = 1
#chama o destroy original e acaba com nosso usuário
User.first.destroy!
DELETE FROM users WHERE id = 1
User.all
SELECT * FROM users WHERE active = 't'
User.all :conditions => {:name => "joe"}
SELECT * FROM users WHERE (name = 'joe') AND (active = 't')
Se quiser procurar incluindo os inativos:
User.find_with_inactive :conditions => {:name => "joe"}
SELECT * FROM users WHERE (name = 'joe')
O nome do campo booleano pode ser configurado da seguinte forma:
class User < ActiveRecord::Base acts_as_active :with => :nome_campo end
É isso, um plugin simples, mas que me poupa um bom trabalho. Espero que gostem
.
Dica: define_method com parâmetros opcionais
Quando estamos definindo um método com define_method, passamos os parâmetros nas variáveis do bloco, por exemplo:
class Teste
define_method "meu_metodo" do |foo, bar|
puts "foo: #{foo}, bar: #{bar}"
end
end
t = Teste.new
t.meu_metodo "param1", "param2"
#foo: param1, bar: param2
Porém, se tentarmos definir um parâmetro opcional (com valor default), da forma que estamos acostumados, temos um erro de sintaxe:
define_method "meu_metodo" do |foo, bar, options = {}|
#syntax error, unexpected tSTRING_BEG, expecting kDO or '{' or '('
Para contornar isso, podemos utilizar o splat (*), para colocar os parâmetros opcionais em um array:
class Teste
define_method "meu_metodo" do |foo, bar, *options|
#se o parametro opcional não for passado, inicializamos com um hash vazio
options = options.first || {}
puts "foo: #{foo}, bar: #{bar}"
puts options.inspect
end
end
t = Teste.new
t.meu_metodo "param1", "param2", :foo => "foo", :bar => "bar"
#foo: param1, bar: param2
#{:bar=>"bar", :foo=>"foo"}
t.meu_metodo "primeiro", "segundo"
#foo: primeiro, bar: segundo
#{}
Dessa forma podemos passar quantos parâmetros opcionais quisermos
.
Insensitivity: O plugin insensível
Consultas case insensitive e independentes de acentos que funcionem em qualquer SGBD são difíceis de conseguir, pois cada banco possui sua maneira específica de fazer isso. Se forem tomados alguns cuidados, é possível usar a sintaxe correta de acordo com o adaptador que estamos usando. No PostgreSQL por exemplo, para conseguir uma consulta case insensitive e independente de acentos, temos que fazer coisas assim ou assim.
O problema em usar conversões nas colunas é que, além delas não fazerem parte padrão SQL, perdemos os índices na coluna, o que vai fazer nossa consulta demorar um pouco mais (dependendo da quatidade de registros, usar ou não um índice pode fazer uma grande diferença). Para resolver esse problema, criei o plugin “Insensitivity“, que usa uma coluna extra para armazenar o valor sem acentos e em minúsculas. Assim, durante as consultas, o ActiveRecord irá utilizar essa coluna em vez da coluna com o valor real.
Vantagens dessa abordagem
Independente de banco
Se hover índices, serão usados
Desvantagens
Utiliza o dobro de espaço.
Instalando e usando o plugin
script/plugin install git://github.com/fernandoluizao/insensitivity.git
No seu model, você deve indicar quais campos usarão a pesquisa case insensitive:
class User < ActiveRecord:Base insensible :name, :email end
Depois, você DEVE adicionar à tabela, os campos com o mesmo nome dos que foram indicados e com o sufixo “_search”. Se desejar, também coloque índices para melhorar a performance. Por exemplo:
script/generate migration add_search_fields
def self.up
change_table :users do |t|
t.string :name_search
t.string :email_search
end
add_index :users, :name_search
add_index :users, :email_search
#Se sua tabela ja tiver dados, vc pode usar
#User.make_insensible!
#Isso vai inicializar os novos campos nos registros já existentes
end
def self.down
change_table :users do |t|
t.remove :name_search
t.remove :email_search
end
end
Para fazer as buscas, não muda nada. Veja as consultas geradas:
User.find_by_name('bla')
SELECT * FROM users WHERE name_search = 'bla'
User.find(:first, :conditions => ['name = ?', 'bla'],rder => 'name') SELECT * FROM users WHERE name_search = 'bla' ORDER BY name LIMIT 1
Tudo funciona de forma transparente, sem vc precisar se preocupar =). Sua única preocupação deve ser passar o valor nas conditions sem acentos e em minusculas, para isso pode usar o método “insensible”, adicionado à classe String:
User.find(:first, :conditions => ['name LIKE ?', "%#{params[:name].insensible}%"])
SELECT * FROM users WHERE name_search LIKE '%bla%'
Bom, é isso, pesquisa sem sentimentos
. Quem quiser colaborar:
http://github.com/fernandoluizao/insensitivity/
PS: Testei apenas com rails 2.2.2. Pode não funcionar com versões mais antigas (< 2.0)
Rails: Autocompletes à moda RESTful
Muita gente tem reclamado que o padrão RESTful do Rails é ruim, que limita, dificulta, etc. Eu não concordo, acho que limitações nos forçam a pensar em soluções mais simples. Realmente, manter-se RESTful não é tão simples quanto sair criando um monte de actions para qualquer coisa, principalmente nos casos de uso de ajax. Uma das reclamações que eu ouvi foi sobre dificuldade em usar autocompletes, por isso nesse artigo vamos ver como resolver esse “problema” e fazer um auto complete RESTful de forma simples e elegante.
Vamos lá, criando o projeto para teste e um resource Pessoa
rails restful_autocomplete cd restful_autocomplete script/generate scaffold pessoa nome:string rake db:migrate
Instalando o plugin auto_complete:
script/plugin install git://github.com/rails/auto_complete.git
É necessário ter o git instalado, caso vc não tenha, ou não esteja com vontade de instalar, baixe o plugin aqui e descompacte em vendor/plugins.
Esse plugin possui uma macro para gerar a action de resposta do auto complete, mas como estamos usando um controller RESTful, não vamos usá-lo dessa maneira. Em vez disso, vamos criar uma mini-API para lidar com o problema. A idéia central é que TODAS as requisições que necessitem recuperar uma coleção de pessoas usem como ÚNICO ponto de acesso a action index.
O plugin espera os registros formatados dentro de uma lista não ordenada, por exemplo:
<ul>
<li>Item 1</li>
<li>Item 2</li>
...
<li>Item N</li>
</ul>
Com isso em mente, vamos criar um novo mime-type para que nosso controller se encarregue de identificar a requisição do auto complete e devolver o formato correto. Adicione a seguinte linha ao arquivo config/initializers/mime_types.rb:
Mime::Type.register_alias "text/html", :autocomplete
Estamos registrando o tipo customizado “autocomplete” como HTML. Lembre-se de reiniciar o servidor depois de alterar o arquivo.
Agora criaremos um controller qualquer que fará a requisição pelo auto complete:
script/generate controller testes new
Na view testes/new.html.erb, coloque o seguinte:
<%=
text_field_with_auto_complete :bla, :ble, { :size => 25 }, {
:url => formatted_pessoas_path(:autocomplete),
:method => :get,
:param_name => 'nome'
}
%>
Passamos algumas opções ao helper text_field_with_auto_complete fornecido pelo plugin:
- Na linha 3 indicamos a url (ou melhor, o RECURSO) à qual a requisição será ser feita e o mime-type que desejamos (o “autocomplete” que nós criamos). A url gerada será /pessoas.autocomplete
- Na linha 4 estamos dizendo que queremos que a requisição seja feita pelo método GET (LEMBRE-SE: A action index só é acessivel por GET em um controller RESTful)
- Finalmente na linha 5 especificamos com qual nome o conteúdo do text_field será enviado. O valor do parâmetro depois poderá ser acessado com params[:nome].
A action index do controller pessoas (nossa “API”), responsável pelo trabalho sujo, ficará assim:
def index
conditions = ["nome LIKE ?", "%#{params[:nome]}%"] if params[:nome]
@pessoas = Pessoa.all :conditions => conditions
respond_to do |format|
format.html # index.html.erb
format.xml { render
ml => @pessoas }
format.autocomplete { render :inline => "<%= auto_complete_result @pessoas, :nome %>" }
end
end
A idéia básica de uma API, é que ela receba parâmetros, processe-os, e devolva um resultado. Nossa API é extremamente simples, se receber um parâmetro “nome”, faz a pesquisa pelos nomes que forem similares, senão retorna todos os registros. Podemos incrementar nossa API e adicionar várias outras coisas, como mais opções para pesquisa (pesquisa por idade, por CPF…), limite de registros a serem retornados, paginação, e o que mais for preciso. Essa é a maior vantagem do padrão RESTful, em vez de uma action, você ganha uma API
.
A linha 7 é que faz a mágica de responder ao auto complete. Ela devolve os resultados da pesquisa no formato que o auto complete espera, na forma de lista não ordenada, usando o helper auto_complete_result.
Inicie o servidor, e acesse a url http://localhost:3000/testes/new e teste o auto complete.
Pronto, temos um auto complete RESTful! =)
Lembre-se, o modelo RESTful veio para facilitar nossa vida, não para complicar. RESTful restringe visando simplificar o design de nossas aplicações e nos forçar a construir APIs que nós mesmos consumimos (e obviamente, também ser consumidas por terceiros). Limitações são impostas apenas por nós mesmos, por isso, pare de se limitar
.
NOTA 1: Não esqueça de incluir os javascripts necessários no seu layout, senão não vai funcionar:
<%= javascript_include_tag :defaults %>
NOTA 2: Os helpers de rotas formatados serão removidos nas próximas versões do Rails. Em vez de:
formatted_pessoas_path(:autocomplete)
Ficará:
pessoas_path(:format => :autocomplete)
Então atenção à versão do Rails que vc está usando. Mais detalhes aqui.
FormBuilders: Padronizando seus formulários no Rails
A um ano atrás, o Urubatan escreveu um artigo sobre como diminuir a repetição nas views e padronizar formulários. A abordagem dele foi escrever helpers para encapsular o formulário dentro de uma tabela (não vou entrar no mérito do uso de tabelas para formatação, não sou xiita
). Se você não leu o artigo dele, recomendo que leia. Vou revisitar o artigo dele e apresentar um recurso do Rails que muita gente desconhece (ou ignora): FormBuilders.
O que é um FormBuilder?
Como o nome sugere, um FormBuilder é uma classe para auxiliar a construção de formulários. É essa classe que nos oferece vários métodos que usamos diariamente em nossas views, como text_field, check_box, e vários outros. A maioria das pessoas usa esses métodos sem saber de onde eles vem. Para entender melhor a estrutura do form builder padrão do Rails, recomendo a leitura desse artigo.
Criando nosso FormBuilder
Para criarmos um builder personalizado, temos duas alternativas: implementar os métodos da classe ActionView::Helpers::FormBuilder OU estender essa classe. A segunda é obviamente mais fácil e rápida, e para atingirmos nosso objetivo vamos usá-la. Queremos que nosso FormBuilder nos permita trocar isso (retirado do exemplo do Urubatan):
<% form_for(@example) do |f| %>
<table>
<tr>
<td>Name</td>
<td><%= f.text_field :name %></td>
</tr>
<tr>
<td>Url</td>
<td><%= f.text_field :url %></td>
</tr>
<tr>
<td colspan="2"><%= f.submit "Update" %></td>
</tr>
</table>
<% end %>
Por isso:
<% form_for(@example) do |f| %> <%= f.text_field :name %> <%= f.text_field :url %> <%= f.submit "Update" %> <% end %>
Vamos ao trabalho!
class MyFormBuilder < ActionView::Helpers::FormBuilder
# vamos embulhar esses helpers em uma tr
%w[text_field password_field text_area].each do |method_name|
define_method(method_name) do |field_name, *args|
options = args.extract_options!
# cria o label dentro de uma td
label = @template.content_tag(:td, label(field_name, options[:label] || field_name.to_s.humanize))
# cria o elemento dentro de uma td
input = @template.content_tag(:td, super)
# cria uma tr com o label e o input criados
@template.content_tag(:tr, label + input)
end
end
def submit(value = "Save changes", options = {})
button = @template.content_tag(:td, super, :colspan => 2)
@template.content_tag(:tr, button)
end
end
Essa classe pode ser colocada em um arquivo dentro do diretório lib e requerida no environment. Implementei apenas alguns helpers apenas para demonstração, mas outros helpers (check_box, select, etc) também podem ser personalizados a gosto. Agora podemos testar a renderização do formulário usando nosso builder:
<% form_for(@example, :builder => MyFormBuilder) do |f| %> <%= f.text_field :name %> <%= f.text_field :url %> <%= f.submit "Update" %> <% end %>
Humm, nada de diferente… olhando o html gerado podemos ver que esta correto, os campos estão dentro de tr’s, porém não estão dentro de uma tabela. Vamos sobrescrever o helper form_for para conseguir isso:
module ActionView
module Helpers
module FormHelper
def form_for(record_or_name_or_array, *args, &proc)
raise ArgumentError, "Missing block" unless block_given?
options = args.extract_options!
case record_or_name_or_array
when String, Symbol
object_name = record_or_name_or_array
when Array
object = record_or_name_or_array.last
object_name = ActionController::RecordIdentifier.singular_class_name(object)
apply_form_for_options!(record_or_name_or_array, options)
args.unshift object
else
object = record_or_name_or_array
object_name = ActionController::RecordIdentifier.singular_class_name(object)
apply_form_for_options!([object], options)
args.unshift object
end
concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
concat('<table>')
fields_for(object_name, *(args << options), &proc)
concat('</table>')
concat('</form>')
end
end
end
end
Monkey patching não é a melhor coisa do mundo, mas nesse caso não vi outra solução melhor e achei válido usar. No artigo do Urubatan ele adicionou as tags “table” dentro do próprio formulário. Se tivesse que alterar a marcação no futuro, teria que ser alterado em vários formulários para tirar as tags table. O monkey patch introduz risco de quebrar o sistema em caso de mudanças no Rails, mas acho que vale mais a pena alterar apenas um arquivo do que um monte de formulários
.
Testando novamente… funcionou
. Muito bom, agora vamos criar um plugin para reusar esse form builder em outros projetos:
script/generate plugin my_builder
create vendor/plugins/my_builder/lib
create vendor/plugins/my_builder/tasks
create vendor/plugins/my_builder/test
create vendor/plugins/my_builder/README
create vendor/plugins/my_builder/MIT-LICENSE
create vendor/plugins/my_builder/Rakefile
create vendor/plugins/my_builder/init.rb
create vendor/plugins/my_builder/install.rb
create vendor/plugins/my_builder/uninstall.rb
create vendor/plugins/my_builder/lib/my_builder.rb
create vendor/plugins/my_builder/tasks/my_builder_tasks.rake
create vendor/plugins/my_builder/test/my_builder_test.rb
create vendor/plugins/my_builder/test/test_helper.rb
No arquivo vendor/plugins/my_builder/lib/my_builder.rb coloque o a nossa classe MyFormBuilder e também a reimplementação do form_for. No arquivo vendor/plugins/my_builder/init.rb, coloque o seguinte conteúdo:
require 'my_builder' # define como o FormBuilder padrao ActionView::Base.default_form_builder = MyFormBuilder
A última linha serve justamente para não termos que especificar o builder em cada form que fizermos, definindo nosso builder como o padrão.
Conclusões
FormBuilders são relativamente simples para personalizar e permitem remover muito código repetido de forma elegante, além de facilitar a manutenção. Pena que muita gente nem sabe que eles existem… espero que esse artigo tenha mostrado as possibilidades de uso dos builders e incentive o pessoal a usá-los mais. Quem quiser exemplos mais concretos, basta pesquisar no github, tem alguns plugins por lá
.
Bons estudos!
Me recomende