impacta - show day de rails
Post on 12-Sep-2014
5.140 views
DESCRIPTION
Apostilha do mini-curso ministrado na Impacta sobre Ruby e Rails.TRANSCRIPT
Show DayTest Drive Ruby on Rails
com Fabio Akita
AkitaOnRails.com
• Mais conhecido pelo blog AkitaOnRails.com e pelo Rails Brasil Podcast (podcast.rubyonrails.pro.br)
• Escritor do primeiro livro de Rails em português: “Repensando a Web com Rails”
• Revisor Técnico da recém-lançada tradução de “Desenvolvimento Web Ágil com Rails”
• Trabalhou um ano como Rails Practice Manager para a consultoria americana Surgeworks LLC
• Atualmente é Gerente de Produtos Rails na Locaweb
Introdução
Ruby
• Criado por Yukihiro Matsumoto (Matz)
• Desde 1993
• “Ruby” inspirado por “Perl”, Python, Smalltalk
• Livro “Programming Ruby” (PickAxe) por Dave Thomas, The Pragmatic Programmer
• “MRI” (Matz Ruby Interpretor)
Ruby
• Linguagem “Humana”
• Linguagem Dinâmica
• Princípio da Menor Surpresa
• Quase totalmente orientada a objetos
• Multi-paradigma (Funcional, Imperativa, Reflexiva, Objetos, etc)
• Interpretada (até 1.8)
Instalação
• Mac (Leopard) - pré-instalado
• Linux (Ubuntu) - apt-get
• Windows - One-Click Installer
Mac OS X Leopard• Atualizar para versões mais recentes:
• sudo gem update --system
• sudo gem update
• Instalar MacPorts (macports.org)
Ubuntu 8.04
• apt-get para instalar ruby
• baixar tarball rubygems
• gem install rails
Ubuntu 8.04
sudo apt-get install ruby irb ri rdoc ruby1.8-dev libzlib-ruby libyaml-ruby libreadline-ruby libncurses-ruby libcurses-ruby libruby libruby-extras libfcgi-ruby1.8 build-essential libopenssl-ruby libdbm-ruby libdbi-ruby libdbd-sqlite3-ruby sqlite3 libsqlite3-dev libsqlite3-ruby libxml-ruby libxml2-dev
wget http://rubyforge.org/frs/download.php/38646/rubygems-1.2.0.tgztar xvfz rubygems-1.2.0.tgzcd rubygems-1.2.0sudo ruby setup.rb
sudo ln -s /usr/bin/gem1.8 /usr/bin/gemsudo gem install rails sqlite3-ruby mongrel capistrano
Windows
• Baixar One-Click Installer
• gem install rails
Windows
• gem install RubyInline
• FreeImage
• freeimage.sourceforge.net/download.html
• copiar FreeImage.dll no c:\windows\system32
• mmediasys.com/ruby (image_science)
RubyGems
• gem update --system
• gem install rubygems-update
• update_rubygems
• gem install rails --version=2.0.2
• gem list
• gem uninstall rails --version=1.2.6
Ferramentas
• Subversion
• Ubuntu - apt-get install subversion
• Mac - port install subversion
• Windows - http://subversion.tigris.org/getting.html#windows
• Git
• Ubuntu - apt-get install git-core git-svn
• Mac - port install git-core +svn
• Windows - http://code.google.com/p/msysgit/
Ferramentas
• MySQL 5 (banco de dados relacional)
• Ubuntu - apt-get install mysql-server mysql-client libdbd-mysql-ruby libmysqlclient15-dev
• Mac - port install mysql5 +server
• Windows - http://dev.mysql.com/downloads/mysql/5.0.html
Ferramentas
• ImageMagick (processador de imagens)
• Ubuntu - apt-get install libmagick9-dev
• Mac - port install tiff -macosx imagemagick +q8 +gs +wmf
• Windows - (rmagick-win32) http://rubyforge.org/projects/rmagick/
Editores
• Windows - Scite, UltraEdit, Notepad++
• Ubuntu - gEdit, Emacs, Vi
• Mac - TextMate (macromates.com)
• Qualquer um server - Aptana, Netbeans
IRB
• Interpreted Ruby
• Shell interativo que executa qualquer comando Ruby
[20:42][~]$ irb
>> 1 + 2
=> 3
>> class Foo; end
=> nil
>> f = Foo.new
=> #<Foo:0x11127e4>
>>
Aprendendo Ruby
Adaptado de “10 Things Every Java Programmer Should Know”
por Jim Weinrich
“é fácil escrever Fortran em qualquer linguagem”
Jim Weinrich
“uma linguagem que não afeta seu jeito de
programar não vale a pena aprender”
Alan Perlis
Convenções
• NomesDeClasse
• nomes_de_metodos e nomes_de_variaveis
• metodos_fazendo_pergunta?
• metodos_perigosos!
• @variaveis_de_instancia
• $variaveis_globais
• ALGUMAS_CONSTANTES ou OutrasConstantes
$WORLD = "WORLD "
class ClassePai
HELLO = "Hello "
end
class MinhaClasse < ClassePai
def hello_world(nome)
return if nome.empty?
monta_frase(nome)
@frase.upcase!
puts @frase
end
def monta_frase(nome)
@frase = HELLO + $WORLD + nome
end
end
>> obj = MinhaClasse.new
=> #<MinhaClasse:0x10970e4>
>> obj.hello_world "Fabio"
HELLO WORLD FABIO
Convenções
Estruturasclass MinhaClasse < ClassePai
def self.metodo_de_classe
"nao é a mesma coisa que estático"
end
def metodo_de_instancia
if funciona?
while true
break if completo?
end
else
return "não funciona"
end
return unless completo?
@teste = "1" if funciona?
...
...
case @teste
when "1"
"primeira condicao"
when "2"
"segunda condicao"
else
"condicao padrao"
end
end
end
Arrays e Strings>> a = [1,2,3,4]
=> [1, 2, 3, 4]
>> b = ["um", "dois", "tres"]
=> ["um", "dois", "tres"]
>> c = %w(um dois tres)
=> ["um", "dois", "tres"]
>> hello = "Hello"
=> "Hello"
>> a = hello + ' Fulano'
=> "Hello Fulano"
>> b = "#{hello} Fulano"
=> "Hello Fulano"
>> c = <<-EOF
multiplas
linhas
EOF
=> " multiplas\n linhas\n"
>>
Arrays e Strings
>> lista_longa = [<<FOO, <<BAR, <<BLATZ]
teste1
teste2
FOO
foo1
foo2
BAR
alo1
alo2
BLATZ
=> ["teste1\nteste2\n", "foo1\nfoo2\n", "alo1\nalo2\n"]
Apenas curiosidade. Não se costuma fazer isso.
Carregar
• require
• carrega arquivos .rb relativos a onde se está
• carrega gems
• pode ser passado um caminho (path) absoluto
• carrega apenas uma vez (load carrega repetidas vezes)
• não há necessidade do nome da classe ser a mesma que o nome do arquivo
require 'activesupport'
@teste = 1.gigabyte / 1.megabyte
puts @teste.kilobyte
Rubismos
• Parênteses não obrigatórios
• Argumentos se comportam como Arrays
• não precisa de “return”
• Arrays podem ser representados de diversas formas
• Strings podem ser representados de diversas formas
“Splat”def foo(*argumentos)
arg1, *outros = argumentos
[arg1, outros]
end
>> foo(1, 2, 3, 4)
=> [1, [2, 3, 4]]
>> a,b,c = 1,2,3
=> [1, 2, 3]
>> *a = 1,2,3,4
=> [1, 2, 3, 4]
Joga uma lista de objetosem um Array
Strings e Symbols>> "teste".object_id
=> 9417020
>> "teste".object_id
=> 9413000
>> :teste.object_id
=> 306978
>> :teste.object_id
=> 306978
>> :teste.to_s.object_id
=> 9399150
>> :teste.to_s.object_id
=> 9393460
•String são mutáveis•Symbols são imutáveis
•Symbols são comumente usados como chaves
Hashes
>> html = { :bgcolor => "black",
:body => { :margin => 0, :width => 100 } }
>> html[:bgcolor]
=> "black"
>> html[:body][:margin]
=> 0
Tudo é Objeto
>> 1.class
=> Fixnum
>> "a".class
=> String
>> (1.2).class
=> Float
>> true.class
=> TrueClass
>> nil.class
=> NilClass
>> Array.class
=> Class
>> Class.class
=> Class
>> {}.class
=> Hash
>> [].class
=> Array
Tudo é Objeto
>> Array.class
=> Class
>> MeuArray = Array
=> Array
>> a = Array.new(2)
=> [nil, nil]
>> b = MeuArray.new(3)
=> [nil, nil, nil]
>> a.class
=> Array
>> b.class
=> Array
>> b.is_a? MeuArray
=> true
>> a.is_a? MeuArray
=> true
Toda Classe é um objeto, instância de Class
Tudo é Objeto
def fabrica(classe)
classe.new
end
>> fabrica(Array)
=> []
>> fabrica(String)
=> ""
>> 1 + 2
=> 3
>> 1.+(2)
=> 3
>> 3.-(1)
=> 2
>> 4.* 3
=> 12
>> 4 * 3
=> 12
Classe é um objeto
Operações aritméticas sãométodos de Fixnum
Não há “primitivas”
Não Objetos
• Nomes de variáveis não são objetos
• Variáveis não costumam ter referência a outros objetos
• Blocos (mais adiante)
>> foo = "teste"
=> "teste"
>> a = foo
=> "teste"
>> b = a
=> "teste"
>> foo.object_id
=> 8807330
>> a.object_id
=> 8807330
>> b.object_id
=> 8807330
Quase tudo são Mensagens
• Toda a computação de Ruby acontece através de:
• Ligação de nomes a objetos (a = b)
• Estruturas primitivas de controle (if/else,while) e operadores (+, -)
• Enviando mensagens a objetos
Mensagens
• obj.metodo()
• Java: “chamada” de um método
• obj.mensagem
• Ruby: envio de “mensagens”
• pseudo: obj.enviar(“mensagem”)
Mensagens
>> 1 + 2
=> 3
>> 1.+ 2
=> 3
>> "teste".size
=> 5
Envio da mensagem “+”ao objeto “1”
com parâmetro “2”
Envio da mensagem“size” ao objeto “teste”
Mensagens
class Pilha
attr_accessor :buffer
def initialize
@buffer = []
end
def method_missing(metodo, *args, &bloco)
@buffer << metodo.to_s
end
end
method_missing irá interceptar toda mensagem não definida como método
Mensagens
>> pilha = Pilha.new
=> #<Pilha:0x1028978 @buffer=[]>
>> pilha.blabla
=> ["blabla"]
>> pilha.alo
=> ["blabla", "alo"]
>> pilha.hello_world
=> ["blabla", "alo", "hello_world"]
Meta-programação
• Ruby: Herança Simples
• Módulos:
• permite “emular” herança múltipla sem efeitos colaterais
• organiza código em namespaces
• não pode ser instanciado
Classes Abertas
class Fixnum
def par?
(self % 2) == 0
end
end
>> p (1..10).select { |n| n.par? }
# => [2, 4, 6, 8, 10]
abrindo a classe padrão Fixnum e acrescentando um
novo método
Mixins
module Akita
module MeuInteiro
def par?
(self % 2) == 0
end
end
end
Fixnum.class_eval do
include(Akita::MeuInteiro)
end
# ou
class Fixnum
include Akita::MeuInteiro
end
# ou
Fixnum.send(:include,
Akita::MeuInteiro)
Mixins
class Pessoa
include Comparable
attr_accessor :nome, :idade
def initialize(nome, idade)
self.nome = nome
self.idade = idade
end
def <=>(outro)
self.idade <=> outro.idade
end
end
>> carlos = Pessoa.new("Carlos", 20)
=> #<Pessoa:0x103833c>
>> ricardo = Pessoa.new("Ricardo", 30)
=> #<Pessoa:0x1033abc>
>> carlos > ricardo
=> false
>> carlos == ricardo
=> false
>> carlos < ricardo
=> true
Métodos Singletonclass Cachorro
end
rover = Cachorro.new
fido = Cachorro.new
def rover.fale
puts "Rover Vermelho"
end
rover.instance_eval do
def fale
puts "Rover vermelho"
end
end>> rover.fale
Rover Vermelho
>> fido.fale
NoMethodError: undefined method `fale' for #<Cachorro:0x10179e8>
from (irb):90
Geração de Código
class Module
def trace_attr(sym)
self.module_eval %{
def #{sym}
printf "Acessando %s com valor %s\n",
"#{sym}", @#{sym}.inspect
end
}
end
end
class Cachorro trace_attr :nome def initialize(string) @nome = string end end
>> Cachorro.new("Fido").nome # => Acessando nome com valor"Fido"
Acessando nome com valor "Fido"
Geração de Códigoclass Person
def initialize(options = {})
@name = options[:name]
@address = options[:address]
@likes = options[:likes]
end
def name; @name; end
def name=(value); @name = value; end
def address; @address; end
def address=(value); @address = value; end
def likes; @likes; end
def likes=(value); @likes = value; end
end
Tarefa chata! (repetitiva)
Geração de Código
def MyStruct(*keys)
Class.new do
attr_accessor *keys
def initialize(hash)
hash.each do |key, value|
instance_variable_set("@#{key}", value)
end
end
end
end
Filosofia “Don’t Repeat Yourself”
Geração de Código
Person = MyStruct :name, :address, :likes
dave = Person.new(:name => "dave", :address => "TX",
:likes => "Stilton")
chad = Person.new(:name => "chad", :likes => "Jazz")
chad.address = "CO"
>> puts "O nome do Dave e #{dave.name}"
O nome do Dave e dave
=> nil
>> puts "Chad mora em #{chad.address}"
Chad mora em CO
Dynamic Typing
Static Dynamic
Weak Strong
Dynamic Typing
Static/Strong Java
Dynamic/Weak Javascript
Dynamic/”Strong” Ruby
“Strong” Typing
class Parent
def hello; puts "In parent"; end
end
class Child < Parent
def hello; puts "In child" ; end
end
>> c = Child.new
=> #<Child:0x1061fac>
>> c.hello
In child
“Strong Typing”class Child
# remove "hello" de Child, mas ainda chama do Parent
remove_method :hello
end
>> c.hello
In parent
class Child
undef_method :hello # evita chamar inclusive das classes-pai
end
>> c.hello
NoMethodError: undefined method `hello' for #<Child:0x1061fac>
from (irb):79
Duck Typing
• Se anda como um pato
• Se fala como um pato
• Então deve ser um pato
• Compilação com tipos estáticos NÃO garante “código sem erro”
• Cobertura de testes garante “código quase sem erro”. Compilação não exclui testes.
Duck Typingclass Gato
def fala; "miau"; end
end
class Cachorro
def fala; "au au"; end
end
for animal in [Gato.new, Cachorro.new]
puts animal.class.name + " fala " + animal.fala
end
# Gato fala miau
# Cachorro fala au au
Chicken Typingclass Gato; def fala; "miau"; end; endclass Cachorro; def fala; "au au"; end; endclass Passaro; def canta; "piu"; end; end
def canil(animal) return "Animal mudo" unless animal.respond_to?(:fala) return "Nao e cachorro" unless animal.is_a?(Cachorro) puts animal.falaend
>> canil(Gato.new)=> "Nao e cachorro">> canil(Passaro.new)=> "Animal mudo">> canil(Cachorro.new)=> au au
Evite coisas assim!
Blocos e Fechamentos
lista = [1, 2, 3, 4, 5]
for numero in lista
puts numero * 2
end
lista.each do |numero|
puts numero * 2
end
# mesma coisa:
lista.each { |numero| puts numero * 2 }
loop tradicional
loop com bloco
Blocos e Fechamentos# jeito "antigo"
f = nil
begin
f = File.open("teste.txt", "r")
texto = f.read
ensure
f.close
end
# com fechamentos
File.open("teste.txt", "r") do |f|
texto = f.read
end
Blocos e Fechamentos
# com yield
def foo
yield
end
# como objeto
def foo(&block)
block.call
end
>> foo { puts "alo" }
=> alo
# como seria em "runtime"
def foo
puts "alo"
end
Construções Funcionais
>> (1..10).class
=> Range
>> (1..10).map { |numero| numero * 2 }
=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
>> (1..10).inject(0) { |numero, total| total += numero }
=> 55
>> (1..10).select { |numero| numero % 2 == 0 }
=> [2, 4, 6, 8, 10]
Construções Funcionais
“Dada uma coleção de números de 1 a 50, qual a soma de todos os números pares,
cada qual multiplicado por 2?”
lista = []
numero = 1
while numero < 51
lista << numero
numero += 1
end
total = 0
for numero in lista
if numero % 2 == 0
total += (numero * 2)
end
end
=> 1300
Construções Funcionais
>> (1..50).select { |n| n % 2 == 0 }.map { |n| n * 2 }.inject(0) { |n, t| t += n }
=> 1300
(1..50).select do |n|
n % 2 == 0
end.map do |n|
n * 2
end.inject(0) do |n, t|
t += n
end
(1..50).select { |n|
n % 2 == 0 }.map { |n|
n * 2 }.inject(0) { |n, t|
t += n }
Tudo Junto
def tag(nome, options = {})
if options.empty?
puts "<#{nome}>"
else
attr = options.map { |k,v| "#{k}='#{v}' " }
puts "<#{nome} #{attr}>"
end
puts yield if block_given?
puts "</#{nome}>"
end
Tudo Junto>> tag :div
<div>
</div>
>> tag :img, :src => "logo.gif", :alt => "imagem"
<img alt='imagem' src='logo.gif' >
</img>
>> tag(:p, :style => "color: yellow") { "Hello World" }
<p style='color: yellow' >
Hello World
</p>
Ruby on Rails
“Domain Specific Language for the Web”
Ruby on Rails
• Criado em 2004 por David Heinemeir Hansson
• Extraído da aplicação Basecamp, da 37signals
• “Convention over Configuration”
• DRY: “Don’t Repeat Yourself ”
• YAGNI: “You Ain’t Gonna Need It”
• Metodologias Ágeis
Convention over Configuration
• Eliminar XMLs de configuração
• As pessoas gostam de escolhas mas não gostam necessariamente de escolher
• Escolhas padrão são “Limitações”
• “Limitações” nos tornam mais produtivos
• O computador tem que trabalhar por nós e não nós trabalharmos para o computador
DRY
• A resposta de “por que Ruby?”
• Meta-programação é a chave
• Novamente: fazer o computador trabalhar para nós
• DSL: “Domain Specific Languages”
• RoR é uma DSL para a Web
YAGNI
• Se tentar suportar 100% de tudo acaba-se tendo 0% de coisa alguma
• Pareto em Software: “80% do tempo é consumido resolvendo 20% dos problemas”
• RoR tenta resolver 80% dos problemas da forma correta
Metodologias Ágeis
• Martin Fowler: metodologias monumentais não resolvem o problema
• Metodologias Ágeis: pragmatismo e qualidade de software
• Integração Contínua, Cobertura de Testes, Automatização de tarefas, etc.
• TDD: “Test-Driven Development”
Tecnologias
• Plugins: extendem as capacidades do Rails
• Gems: bibliotecas Ruby, versionadas
• RubyForge
• Agile Web Development (plugins
• Github
Complementos
• BDD: Behaviour Driven Development (RSpec)
• Automatização: Rake, Capistrano, Vlad
• Application Servers: Mongrel, Thin, Ebb, Phusion Passenger
• Web Servers: Apache 2, NginX, Lightspeed
• Bancos de Dados: MySQL, PostgreSQL, SQLite3, Oracle, SQL Server, etc
Iniciando um projeto Ruby on Rails
Obs.: aprenda a apreciar a linha de comando!
[20:57][~/rails/sandbox/impacta]$ rails _2.1.0_ tarefas
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
create config/environments
create config/initializers
...
create doc/README_FOR_APP
create log/server.log
create log/production.log
create log/development.log
create log/test.log
opcional
Estrutura Padrão
• app - estrutura MVC
• config - configurações
• public - recursos estáticos (imagens, CSS, javascript, etc)
• script - ferramentas
• test - suíte test/unit
• vendor - plugins, gems, etc
Pacotes
Ruby
Mongrel
Aplicação Rails
ActiveResource
ActiveSupport
ActionPack
ActiveWS
ActionMailer
Rails
ActionController
ActiveRecord
ActionView
Algumas convenções
• Nomes no Plural
• Quando se fala de uma coleção de dados (ex. nome de uma tabela no banco)
• Nomes do Singular
• Quando se fala de uma única entidade (ex. uma linha no banco
• Rails usa Chaves Primárias Surrogadas (id inteiro)
• Foreign Key é o nome da tabela associada no singular com “_id” (ex. usuario_id)
Configurações• database.yml
• configuração de banco de dados
• environment.rb
• configurações globais
• environments
• configurações por ambiente
• initializers
• organização de configurações
• routes.rb
Ambientes Separados# SQLite version 3.x# gem install sqlite3-ruby (not necessary on OS X Leopard)
development: adapter: sqlite3
database: db/development.sqlite3 timeout: 5000
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".# Do not set this db to the same as development or production.
test: adapter: sqlite3
database: db/test.sqlite3 timeout: 5000
production:
adapter: sqlite3 database: db/production.sqlite3
timeout: 5000
Ambientes Separados• Development
• Tudo que é modificado precisa recarregar imediatamente, cache tem que ser desativado
• Permitido banco de dados com sujeira
• Test
• Todos os testes precisam ser repetitíveis em qualquer ambiente, por isso precisa de um banco de dados separado
• O banco precisa ser limpo a cada teste. Cada teste precisa ser isolado.
• Production
• Otimizado para performance, as classes só precisam carregar uma única vez
• Os sistemas de caching precisam ficar ligados
YAML
• “Yet Another Markup Language”
• “YAML Ain’t a Markup Language”
• Formato humanamente legível de serialização de estruturas de dados
• Muito mais leve e prático que XML
• JSON é quase um subset de YAML
• Identação é muito importante!
Plugins: aceleradores
>> ./script/plugin install git://github.com/technoweenie/restful-authentication.gitremoving: /Users/akitaonrails/rails/sandbox/impacta/tarefas/vendor/plugins/restful-
authentication/.gitInitialized empty Git repository in tarefas/vendor/plugins/restful-authentication/.git/
remote: Counting objects: 409, done.remote: Compressing objects: 100% (259/259), done.
remote: Total 409 (delta 147), reused 353 (delta 115)Receiving objects: 100% (409/409), 354.52 KiB | 124 KiB/s, done.
Resolving deltas: 100% (147/147), done....
>> ./script/plugin install git://github.com/tapajos/brazilian-rails.git
>> rake brazilianrails:inflector:portuguese:enable
>> ./script/plugin install git://github.com/lightningdb/activescaffold.git>> ./script/generate authenticated Usuario Sessao
Rake (Ruby Make)>> rake -T(in /Users/akitaonrails/rails/sandbox/impacta/tarefas)
/Users/akitaonrails/rails/sandbox/impacta/tarefas/vendor/plugins/brazilian-rails/tasksrake brazilianrails:inflector:portuguese:check # Checks if Brazilian Por...
rake brazilianrails:inflector:portuguese:disable # Disable Brazilian Portu...rake brazilianrails:inflector:portuguese:enable # Enable Brazilian Portug...
rake db:abort_if_pending_migrations # Raises an error if ther...rake db:charset # Retrieves the charset f...
rake db:collation # Retrieves the collation...rake db:create # Create the database def...
...rake tmp:pids:clear # Clears all files in tmp...
rake tmp:sessions:clear # Clears all files in tmp...rake tmp:sockets:clear # Clears all files in tmp...
Executa tarefas automatizadas, como limpeza de logs, gerenciamento do banco de dados,
execução dos testes, etc.
Rake (Ruby Make)>> rake db:create:alldb/development.sqlite3 already exists
db/production.sqlite3 already existsdb/test.sqlite3 already exists
>> rake db:migrate
== 20080629001252 CreateUsuarios: migrating ===================================-- create_table("usuarios", {:force=>true})
-> 0.0042s-- add_index(:usuarios, :login, {:unique=>true})
-> 0.0034s== 20080629001252 CreateUsuarios: migrated (0.0085s) ==========================
>> rake
Started.............
Finished in 0.340325 seconds.13 tests, 26 assertions, 0 failures, 0 errors
Started
..............Finished in 0.306186 seconds.
14 tests, 26 assertions, 0 failures, 0 errors
Migrations>> ./script/generate scaffold Tarefa usuario:references duracao:integer descricao:string data_inicio:datetime
exists app/models/ ...
create db/migrate/20080629003332_create_tarefas.rb
class CreateTarefas < ActiveRecord::Migration def self.up
create_table :tarefas do |t| t.references :usuario
t.integer :duracao t.string :descricao
t.datetime :data_inicio
t.timestamps end
end
def self.down drop_table :tarefas
endend
Migrations
>> rake db:migrate
== 20080629001252 CreateUsuarios: migrating ===================================-- create_table("usuarios", {:force=>true})
-> 0.0047s-- add_index(:usuarios, :login, {:unique=>true})
-> 0.0039s== 20080629001252 CreateUsuarios: migrated (0.0092s) ==========================
== 20080629003332 CreateTarefas: migrating ====================================
-- create_table(:tarefas) -> 0.0039s
== 20080629003332 CreateTarefas: migrated (0.0044s) ===========================
MigrationsCREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
Migrating to CreateUsuarios (20080629001252)
SELECT version FROM schema_migrations CREATE TABLE "usuarios" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "login"
varchar(40) DEFAULT NULL NULL, "name" varchar(100) DEFAULT '' NULL, "email" varchar(100) DEFAULT NULL NULL, "crypted_password" varchar(40) DEFAULT NULL NULL, "salt" varchar(40)
DEFAULT NULL NULL, "created_at" datetime DEFAULT NULL NULL, "updated_at" datetime DEFAULT NULL NULL, "remember_token" varchar(40) DEFAULT NULL NULL, "remember_token_expires_at"
datetime DEFAULT NULL NULL) CREATE UNIQUE INDEX "index_usuarios_on_login" ON "usuarios" ("login")
INSERT INTO schema_migrations (version) VALUES ('20080629001252')
Migrating to CreateTarefas (20080629003332) SELECT version FROM schema_migrations
CREATE TABLE "tarefas" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "usuario_id" integer DEFAULT NULL NULL, "duracao" integer DEFAULT NULL NULL, "descricao" varchar(255)
DEFAULT NULL NULL, "data_inicio" datetime DEFAULT NULL NULL, "created_at" datetime DEFAULT NULL NULL, "updated_at" datetime DEFAULT NULL NULL)
INSERT INTO schema_migrations (version) VALUES ('20080629003332') SELECT version FROM schema_migrations
Migrations
• Versionamento do Schema do Banco de Dados
• O Schema completo fica em db/schema.rb
• Possibilita pseudo-”rollback” e, efetivamente, mais controle entre versões
• Garante que os diferentes ambientes sempre estejam consistentes
• Evita conflitos em times com mais de 2 desenvolvedores
• Aumenta muito a produtividade
Servidor>> ./script/server
=> Booting Mongrel (use 'script/server webrick' to force WEBrick)=> Rails 2.1.0 application starting on http://0.0.0.0:3000
=> Call with -d to detach=> Ctrl-C to shutdown server
** Starting Mongrel listening at 0.0.0.0:3000** Starting Rails with development environment...
** Rails loaded.** Loading any Rails specific GemPlugins
** Signals ready. TERM => stop. USR2 => restart. INT => stop (no restart).** Rails signals registered. HUP => reload (without restart). It might not work well.
** Mongrel 1.1.5 available at 0.0.0.0:3000** Use CTRL-C to stop.
Se não houver Mongrel instalado, ele sobe Webrick(não recomendado)
Servidor
não esquecer de apagar public/index.html
M.V.C.
Model-View-Controller feito direito
requisição HTTP ! Mongrel
Mongrel ! routes.rb
routes.rb ! Controller
Controller ! Action
Action ! Model
Action ! View
Action ! resposta HTTP
Controllers# app/controllers/application.rbclass ApplicationController < ActionController::Base helper :all # include all helpers, all the time protect_from_forgeryend
# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController # GET /tarefas # GET /tarefas.xml def index @tarefas = Tarefa.find(:all)
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @tarefas } end endend
Views - Templates<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Tarefas: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %>
</head><body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>
Views - Templates• app/views/layouts
• application.html.erb
• controller_name.html.erb
• app/views/controller_name
• action_name.html.erb
• action_name.mime_type.engine
• mime-type: html, rss, atom, xml, pdf, etc
• engine: erb, builder, etc
Models
# app/models/usuario.rbrequire 'digest/sha1'
class Usuario < ActiveRecord::Base
include Authentication include Authentication::ByPassword
include Authentication::ByCookieToken
validates_presence_of :login validates_length_of :login, :within => 3..40
validates_uniqueness_of :login, :case_sensitive => false validates_format_of :login, :with => RE_LOGIN_OK, :message => MSG_LOGIN_BAD
...end
Rotas# config/routes.rbActionController::Routing::Routes.draw do |map|
map.resources :tarefas
map.logout '/logout', :controller => 'sessoes', :action => 'destroy' map.login '/login', :controller => 'sessoes', :action => 'new'
map.register '/register', :controller => 'usuarios', :action => 'create' map.signup '/signup', :controller => 'usuarios', :action => 'new'
map.resources :usuarios
map.resource :sessao
# Install the default routes as the lowest priority. map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'end
jeito “antigo” (1.2)
Rotas - Antigo
• http://www.dominio.com/tarefas/show/123
• map.connect ':controller/:action/:id'
• tarefas_controller.rb ! TarefasController
• def show ... end
• params[:id] ! 123
Active Record
• “Patterns of Enterprise Application Architecture”, Martin Fowler
• Uma classe “Model” mapeia para uma tabela
• Uma instância da classe “Model” mapeia para uma linha na tabela
• Toda lógica de negócio é implementada no “Model”
• Suporte para Herança Simples, Polimorfismo, Associações
# app/models/tarefa.rb
class Tarefa < ActiveRecord::Base
belongs_to :usuario # uma tarefa pertence a um usuario
end
# app/models/usuario.rb
class Usuario < ActiveRecord::Base
has_many :tarefas # um usuario tem varias tarefas
...
end
Associações Simples
usuarios
id: integer
tarefas
id: integer
usuario_id: integer
Interatividade - Console>> Usuario.count
=> 0
>> Tarefa.count
=> 0
>> admin = Usuario.create(:login => 'admin', :password =>
'admin', :password_confirmation => 'admin', :email =>
=> #<Usuario id: nil, login: "admin", name: "", email:
"[email protected]", crypted_password: nil, salt: nil, created_at:
nil, updated_at: nil, remember_token: nil,
remember_token_expires_at: nil>
>> Usuario.count
=> 0
>> admin.errors.full_messages
=> ["Password deve ter no minimo 6 caractere(s)"]
Interatividade - Console>> admin = Usuario.create(:login => 'admin', :password =>
'admin123', :password_confirmation => 'admin123', :email =>
=> #<Usuario id: 1, login: "admin", name: "", email:
"[email protected]", crypted_password: "e66...abd", salt: "731...b96",
created_at: "2008-06-29 20:21:10", updated_at: "2008-06-29 20:21:10",
remember_token: nil, remember_token_expires_at: nil>
>> admin.tarefas
=> []
>> admin.tarefas.create(:descricao => "Criando demo de
Rails", :duracao => 2, :data_inicio => Time.now)
=> #<Tarefa id: 1, usuario_id: 1, duracao: 2, descricao: "Criando
demo de Rails", data_inicio: "2008-06-29 20:21:40", created_at:
"2008-06-29 20:21:40", updated_at: "2008-06-29 20:21:40">
Interatividade - Console
>> Tarefa.count
=> 1
>> tarefa = Tarefa.first
=> #<Tarefa id: 1, usuario_id: 1, duracao: 2, descricao: "Criando
demo de Rails", data_inicio: "2008-06-29 20:21:40", created_at:
"2008-06-29 20:21:40", updated_at: "2008-06-29 20:21:40">
>> tarefa.usuario
=> #<Usuario id: 1, login: "admin", name: "", email:
"[email protected]", crypted_password: "e66...abd", salt: "731...b96",
created_at: "2008-06-29 20:21:10", updated_at: "2008-06-29
20:21:10", remember_token: nil, remember_token_expires_at: nil>
Validações# app/models/usuario.rbclass Usuario < ActiveRecord::Base
... validates_presence_of :login
validates_length_of :login, :within => 3..40 validates_uniqueness_of :login, :case_sensitive => false
validates_format_of :login, :with => RE_LOGIN_OK, :message => MSG_LOGIN_BAD
validates_format_of :name, :with => RE_NAME_OK, :message => MSG_NAME_BAD, :allow_nil => true
validates_length_of :name, :maximum => 100
validates_presence_of :email validates_length_of :email, :within => 6..100 #[email protected]
validates_uniqueness_of :email, :case_sensitive => false validates_format_of :email, :with => RE_EMAIL_OK, :message => MSG_EMAIL_BAD
...end
Validaçõesvalidates_presence_of :firstname, :lastname # obrigatório
validates_length_of :password, :minimum => 8 # mais de 8 caracteres
:maximum => 16 # menos que 16 caracteres :in => 8..16 # entre 8 e 16 caracteres
:too_short => 'muito curto' :too_long => 'muito longo'
validates_acceptance_of :eula # Precisa aceitar este checkbox
:accept => 'Y' # padrão: 1 (ideal para checkbox)
validates_confirmation_of :password # os campos password e password_confirmation precisam ser iguais
validates_uniqueness_of :user_name # user_name tem que ser único
:scope => 'account_id' # Condição: # account_id = user.account_id
Validações
validates_format_of :email # campo deve bater com a regex :with => /^(+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
validates_numericality_of :value # campos value é numérico
:only_integer => true :allow_nil => true
validates_inclusion_of :gender, # gender é m ou f (enumeração)
:in => %w( m, f )
validates_exclusion_of :age # campo age não pode estar :in => 13..19 # entre 13 e 19 anos
validates_associated :relation
# valida que o objeto ‘relation’ associado também é válido
FindersTarefa.find 42 # objeto com ID 42Tarefa.find [37, 42] # array com os objetos de ID 37 e 42
Tarefa.find :allTarefa.find :last
Tarefa.find :first, :conditions => [ "data_inicio < ?", Time.now ] # encontra o primeiro objeto que obedece à! condição
Tarefa.all # idêntico à! Tarefa.find(:all)
Tarefa.first # idêntico à! Tarefa.find(:first)Tarefa.last # idêntico à! Tarefa.find(:last)
:order => 'data_inicio DESC'# ordenação
:offset => 20 # começa a partir da linha 20:limit => 10 # retorna apenas 10 linhas
:group => 'name' # agrupamento:joins => 'LEFT JOIN ...' # espaço para joins, como LEFT JOIN (raro)
:include => [:account, :friends] # LEFT OUTER JOIN com esses models # dependendo das condições podem ser
# 2 queries:include => { :groups => { :members=> { :favorites } } }
:select => [:name, :adress] # em vez do padrão SELECT * FROM:readonly => true # objetos não podem ser modificados
Named Scopes
# app/models/tarefa.rb
class Tarefa < ActiveRecord::Base
...
named_scope :curtas, :conditions => ['duracao < ?', 2]
named_scope :medias, :conditions => ['duracao between ? and ?', 2, 6]
named_scope :longas, :conditions => ['duracao > ?', 6]
named_scope :hoje, lambda {
{ :conditions => ['data_inicio between ? and ?',
Time.now.beginning_of_day, Time.now.end_of_day ] }
}
end
Named Scope>> Tarefa.hoje
SELECT * FROM "tarefas" WHERE (data_inicio between
'2008-06-29 00:00:00' and '2008-06-29 23:59:59')
>> Tarefa.curtas
SELECT * FROM "tarefas" WHERE (duracao < 2)
>> Tarefa.medias.hoje
SELECT * FROM "tarefas" WHERE ((data_inicio between
'2008-06-29 00:00:00' and '2008-06-29 23:59:59') AND
(duracao between 2 and 6))
Condições de SQL geradas de maneira automática
Muito Mais
• Associações complexas (many-to-many, self-referencial, etc)
• Associações Polimórficas
• Colunas compostas (composed_of)
• extensões acts_as (acts_as_tree, acts_as_nested_set, etc)
• Callbacks (filtros)
• Transactions (não suporta two-phased commits), Lockings
• Single Table Inheritance (uma tabela para múltiplos models em herança)
• Finders dinâmicos, Named Scopes
RESTful Rails
Rotas Restful# config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :tarefas
end
# app/views/tarefas/index.html.erb
...
<td><%= link_to 'Edit', edit_tarefa_path(tarefa) %></td>
# script/console
>> app.edit_tarefa_path(123)
=> "/tarefas/123/edit"
CRUD - SQL
Create INSERT
Read SELECT
Update UPDATE
Destroy DELETE
CRUD - AntigoGET /tarefas index
GET /tarefas/new new
GET /tarefas/edit/1 edit
GET /tarefas/show/1 show
POST /tarefas/create create
POST /tarefas/update update
POST /tarefas/destroy destroy
Verbos HTTP
GET
POST
PUT
DELETE
CRUD - REST - DRYGET /tarefas index
POST /tarefas create
GET /tarefas/new new
GET /tarefas/1 show
PUT /tarefas/1 update
DELETE /tarefas/1 destroy
GET /tarefas/1/edit edit
REST - Named Routes/tarefas tarefas_path
/tarefas tarefas_path
/tarefas/new new_tarefa_path
/tarefas/1 tarefa_path(1)
/tarefas/1 tarefa_path(1)
/tarefas/1 tarefa_path(1)
/tarefas/1/edit edit_tarefa_path(1)
REST Controller# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController
# GET /tarefas def index
# GET /tarefas/1
def show
# GET /tarefas/new def new
# GET /tarefas/1/edit
def edit
# POST /tarefas def create
# PUT /tarefas/1
def update
# DELETE /tarefas/1 def destroy
end
REST Templates# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController
# GET /tarefas/new def new
@tarefa = Tarefa.new
respond_to do |format| format.html # new.html.erb
format.xml { render :xml => @tarefa } end
end ...
end
# app/views/tarefas/new.html.erb<% form_for(@tarefa) do |f| %>
... <p>
<%= f.submit "Create" %> </p>
<% end %>
<!-- /tarefas/1 --><form action="/tarefas/3" class="edit_tarefa"
id="edit_tarefa_3" method="post"> <input name="_method" type="hidden"
value="put" /> ...
<p> <input id="tarefa_submit" name="commit"
type="submit" value="Create" /> </p>
</form>
Nested Controllers>> ./script/generate scaffold Anotacao tarefa:references
anotacao:text
>> rake db:migrate
# app/models/tarefa.rb
class Tarefa < ActiveRecord::Base
belongs_to :usuario
has_many :anotacoes
end
# app/models/anotacao.rb
class Anotacao < ActiveRecord::Base
belongs_to :tarefa
end
Nested Controllers# app/controllers/anotacoes_controller.rbclass AnotacoesController < ApplicationController
before_filter :load_tarefa ...
private
def load_tarefa @tarefa = Tarefa.find(params[:tarefa_id])
endend
# config/routes.rbActionController::Routing::Routes.draw do |map|
map.resources :tarefas, :has_many => :anotacoes ...
end
# trocar X# por Y
Anotacao.find
@tarefa.anotacoes.find
redirect_to(@anotacao)
redirect_to([@tarefa, @anotacao])
:location => @anotacao:location => [@tarefa, @anotacao]
anotacoes_url
tarefa_anotacoes_url(@tarefa)
Nested Views# trocar X# por Y em todos os arquivos app/views/anotacoes/*.html.erb
form_for(@anotacao)
form_for([@tarefa, @anotacao])
link_to 'Show', @anotacaolink_to 'Show', [@tarefa, @anotacao]
anotacoes_path
tarefa_anotacoes_path(@tarefa)
edit_anotacao_path(@tarefa)edit_tarefa_anotacao_path(@tarefa, @anotacao)
anotacoes_path
tarefa_anotacoes_path(@tarefa)
new_anotacao_pathnew_tarefa_anotacao_path(@tarefa)
<!-- apagar de new.html.erb e edit.html.erb -->
<p> <%= f.label :tarefa %><br />
<%= f.text_field :tarefa %></p>
<!-- acrescentar ao final de index.html.erb -->
<%= link_to 'Back to Tarefa', tarefa_path(@tarefa) %>
<!-- acrescentar ao final de
tarefas/show.html.erb --><%= link_to 'Anotações',
tarefa_anotacoes_path(@tarefa) %>
Namespaced Routes>> mv app/helpers/usuarios_helper.rb app/helpers/usuarios_helper.rb.old
>> ./script/generate controller Admin::Usuarios create app/controllers/admin
create app/helpers/admin create app/views/admin/usuarios
create test/functional/admin create app/controllers/admin/usuarios_controller.rb
create test/functional/admin/usuarios_controller_test.rb create app/helpers/admin/usuarios_helper.rb
>> mv app/helpers/usuarios_helper.rb.old app/helpers/usuarios_helper.rb
# config/routes.rb
ActionController::Routing::Routes.draw do |map| map.namespace :admin do |admin|
admin.resources :usuarios, :active_scaffold => true end
end
Active Scaffold
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title><%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %>
<%= javascript_include_tag :defaults %> <%= active_scaffold_includes %>
</head><body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %></body>
</html>
apague tudo em app/views/layouts e crie apenas este application.html.erb
Active Scaffold# app/controllers/admin/usuarios_controller.rb
class Admin::UsuariosController < ApplicationController
active_scaffold :usuario do |config|
config.columns = [:login,
:email,
:password,
:password_confirmation ]
config.list.columns.exclude [
:password,
:password_confirmation ]
config.update.columns.exclude [
:login]
end
end
Active Scaffold
RESTful RailsParte 2: has many through
many-to-many# app/models/anotacao.rbclass Anotacao < ActiveRecord::Base
belongs_to :tarefaend
# app/models/tarefa.rb
class Tarefa < ActiveRecord::Base belongs_to :usuario
has_many :anotacoes ...
end
# app/models/usuario.rbclass Usuario < ActiveRecord::Base
... has_many :tarefas
has_many :anotacoes, :through => :tarefas ...
end
anotacoes
id: int
tarefa_id: int
tarefas
id: int
usuario_id: int
usuarios
id: int
many-to-many>> admin = Usuario.find_by_login('admin')=> #<Usuario id: 1, login: "admin", name: "", email: "[email protected]",
crypted_password: "e66e...abd", salt: "731f...b96", created_at: "2008-06-29 20:21:10", updated_at: "2008-06-29 20:21:10", remember_token: nil, remember_token_expires_at: nil>
SELECT * FROM "usuarios" WHERE ("usuarios"."login" = 'admin') LIMIT 1
>> admin.tarefas
=> [#<Tarefa id: 1, usuario_id: 1, duracao: 2, descricao: "Criando demo de Rails", data_inicio: "2008-06-29 20:21:40", created_at: "2008-06-29 20:21:40", updated_at:
"2008-06-29 20:21:40">]
SELECT * FROM "tarefas" WHERE ("tarefas".usuario_id = 1)
>> admin.anotacoes=> [#<Anotacao id: 1, tarefa_id: 1, anotacao: "teste", created_at: "2008-06-29
21:29:52", updated_at: "2008-06-29 21:29:52">]
SELECT "anotacoes".* FROM "anotacoes" INNER JOIN tarefas ON anotacoes.tarefa_id = tarefas.id WHERE (("tarefas".usuario_id = 1))
Cenário Antigo# tabela 'revistas'class Revista < ActiveRecord::Base
# tabela 'clientes_revistas' has_and_belongs_to_many :clientes
end
# tabela 'clientes'class Cliente < ActiveRecord::Base
# tabela 'clientes_revistas' has_and_belongs_to_many :revistas
end
class RevistasController < ApplicationController def add_cliente() end
def remove_cliente() endend
class ClientesController < ApplicationController
def add_revista() end def remove_revista() end
end
clientes_revistas
cliente_id: int
revista_id: int
clientes
id: int
revistas
id: int
Qual dos dois controllers está certo?
A revista controla o cliente ou o cliente controla a revista?
Cenário RESTclass Assinatura < ActiveRecord::Base belongs_to :revista
belongs_to :clienteend
class Revista < ActiveRecord::Base
has_many :assinaturas has_many :clientes, :through => :assinaturas
end
class Cliente < ActiveRecord::Base has_many :assinaturas
has_many :revistas, :through => :assinaturasend
class AssinaturasController < ApplicationController
def create() end def destroy() end
end
assinaturas
cliente_id: int
revista_id: int
clientes
id: int
revistas
id: int
Tabelas many-to-many, normalmente podem ser
um recurso próprio
RESTful RailsParte 3: ActiveResource
Active Resource
• Consumidor de recursos REST
• Todo scaffold Rails, por padrão, é RESTful
• Para o exemplo: mantenha script/server rodando
Active Resource>> require 'activesupport'=> true
>> require 'activeresource'=> []
class Tarefa < ActiveResource::Base
self.site = "http://localhost:3000"end
>> t = Tarefa.find :first
=> #<Tarefa:0x1842c94 @prefix_options={}, @attributes={"updated_at"=>Sun Jun 29 20:21:40 UTC 2008, "id"=>1, "usuario_id"=>1, "data_inicio"=>Sun Jun 29 20:21:40 UTC
2008, "descricao"=>"Criando demo de Rails", "duracao"=>2, "created_at"=>Sun Jun 29 20:21:40 UTC 2008}
>> t = Tarefa.new(:descricao => 'Testando REST', :duracao => 1, :data_inicio =>
Time.now)=> #<Tarefa:0x183484c @prefix_options={}, @attributes={"data_inicio"=>Sun Jun 29
19:00:57 -0300 2008, "descricao"=>"Testando REST", "duracao"=>1}
>> t.save=> true
Via console IRB
Múltiplas Respostas
http://localhost:3000/tarefas/1/anotacoes/1.xml
Múltiplas Respostas# app/controllers/anotacoes_controller.rb
class AnotacoesController < ApplicationController
...
# GET /anotacoes/1
# GET /anotacoes/1.xml
def show
@anotacao = @tarefa.anotacoes.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @anotacao }
end
end
...
end
Testes
Compilar != Testar
• Testes Unitários: Models
• Testes Funcionais: Controllers
• Testes Integrados: Cenários de Aceitação
Tipos de Teste
Fixtures
• Carga de dados específicos de testes!
• test/fixtures/nome_da_tabela.yml
• Não se preocupar com números de primary keys
• Associações podem se referenciar diretamente através do nome de cada entidade
• Dar nomes significativos a cada entidade de teste
restful-authenticationquentin:
login: quentin
email: [email protected]
salt: 356a192b7913b04c54574d18c28d46e6395428ab # SHA1('0')
crypted_password: c38f11c55af4680780bba9c9f7dd9fa18898b939 # 'monkey'
created_at: <%= 5.days.ago.to_s :db %>
remember_token_expires_at: <%= 1.days.from_now.to_s %>
remember_token: 77de68daecd823babbb58edb1c8e14d7106e83bb
aaron:
login: aaron
email: [email protected]
salt: da4b9237bacccdf19c0760cab7aec4a8359010b0 # SHA1('1')
crypted_password: 028670d59d8eff84294668802470c8c8034c51b5 # 'monkey'
created_at: <%= 1.days.ago.to_s :db %>
remember_token_expires_at:
remember_token:
old_password_holder:
login: old_password_holder
email: [email protected]
salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
created_at: <%= 1.days.ago.to_s :db %>
tarefas e anotações
# test/fixtures/tarefas.ymlaula:
usuario: quentin duracao: 1
descricao: Dando Aula data_inicio: 2008-06-28 21:33:32
academia:
usuario: aaron duracao: 1
descricao: Exercitando data_inicio: 2008-06-28 21:33:32
# test/fixtures/anotacoes.ymlone:
tarefa: aula anotacao: Aula de Rails
two:
tarefa: aula anotacao: Precisa corrigir prova
three:
tarefa: academia anotacao: Mudando rotina
Ajustando - Parte 1require File.dirname(__FILE__) + '/../test_helper'
class AnotacoesControllerTest < ActionController::TestCase
fixtures :tarefas, :usuarios, :anotacoes
def setup
@tarefa = tarefas(:aula)
end
def test_should_get_index
get :index, :tarefa_id => @tarefa.id
assert_response :success
assert_not_nil assigns(:anotacoes)
end
def test_should_get_new
get :new, :tarefa_id => @tarefa.id
assert_response :success
end
def test_should_create_anotacao
assert_difference('Anotacao.count') do
post :create, :tarefa_id => @tarefa.id, :anotacao => { :anotacao => "teste" }
end
assert_redirected_to tarefa_anotacao_path(@tarefa, assigns(:anotacao))
end
declarando quaisfixtures carregar
adicionandoa chave :tarefa_idao hash params
Ajustando - Parte 2 def test_should_show_anotacao
get :show, :tarefa_id => @tarefa.id, :id => anotacoes(:one).id
assert_response :success
end
def test_should_get_edit
get :edit, :tarefa_id => @tarefa.id, :id => anotacoes(:one).id
assert_response :success
end
def test_should_update_anotacao
put :update, :tarefa_id => @tarefa.id, :id => anotacoes(:one).id, :anotacao => { :anotacao => "teste"}
assert_redirected_to tarefa_anotacao_path(@tarefa, assigns(:anotacao))
end
def test_should_destroy_anotacao
assert_difference('Anotacao.count', -1) do
delete :destroy, :tarefa_id => @tarefa.id, :id => anotacoes(:one).id
end
assert_redirected_to tarefa_anotacoes_path(@tarefa)
end
end
hash params
ajustandorotas nomeadas
Ajustando - Parte 3
# test/functional/tarefas_controller.rb
require File.dirname(__FILE__) + '/../test_helper'
class TarefasControllerTest < ActionController::TestCase
fixtures :tarefas, :usuarios
...
get :show, :id => tarefas(:aula).id
...
end
mudar de “one” para “aula”conforme foi modificado em
tarefas.yml
Executando$ rake test
(in /Users/akitaonrails/rails/sandbox/impacta/tarefas)/Users/akitaonrails/rails/sandbox/impacta/tarefas/vendor/plugins/brazilian-rails/tasks/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/anotacao_test.rb" "test/unit/tarefa_test.rb" "test/unit/usuario_test.rb" Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loaderStarted...............Finished in 0.370074 seconds.
15 tests, 28 assertions, 0 failures, 0 errors/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/functional/admin/usuarios_controller_test.rb" "test/functional/anotacoes_controller_test.rb" "test/functional/sessoes_controller_test.rb" "test/functional/tarefas_controller_test.rb" "test/functional/usuarios_controller_test.rb" Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loaderStarted.............................Finished in 0.832019 seconds.
29 tests, 53 assertions, 0 failures, 0 errors/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb"
Estatísticas$ rake stats
(in /Users/akitaonrails/rails/sandbox/impacta/tarefas)
+----------------------+-------+-------+---------+---------+-----+-------+| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+| Controllers | 280 | 192 | 6 | 21 | 3 | 7 |
| Helpers | 104 | 44 | 0 | 4 | 0 | 9 || Models | 54 | 30 | 3 | 1 | 0 | 28 |
| Libraries | 198 | 96 | 0 | 21 | 0 | 2 || Integration tests | 0 | 0 | 0 | 0 | 0 | 0 |
| Functional tests | 260 | 204 | 7 | 37 | 5 | 3 || Unit tests | 119 | 98 | 3 | 16 | 5 | 4 |
+----------------------+-------+-------+---------+---------+-----+-------+| Total | 1015 | 664 | 19 | 100 | 5 | 4 |
+----------------------+-------+-------+---------+---------+-----+-------+ Code LOC: 362 Test LOC: 302 Code to Test Ratio: 1:0.8
Esta taxa é muito baixa. Busque 1:3.0 pelo menos!
Assertions
http://topfunky.com/clients/rails/ruby_and_rails_assertions.pdf
Assertions
Assertions
Views
Ajustando Layouts<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title>Admin <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %> <%= javascript_include_tag :defaults %> <%= active_scaffold_includes %></head><body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body></html>
Ajustando Tarefas<!-- index.html.erb -->...<td><%=h tarefa.usuario.login if tarefa.usuario %></td><td><%=h tarefa.duracao %></td><td><%=h tarefa.descricao %></td><td><%=h tarefa.data_inicio.to_s(:short) %></td>...
<!-- new.html.erb --><h1>New tarefa</h1><% form_for(@tarefa) do |f| %> <%= f.error_messages %> <%= render :partial => f, :locals => { :submit_text => 'Create' } %><% end %><%= link_to 'Back', tarefas_path %>
<!-- edit.html.erb --> <h1>Editing tarefa</h1><% form_for(@tarefa) do |f| %> <%= f.error_messages %> <%= render :partial => f, :locals => { :submit_text => 'Update' } %><% end %><%= link_to 'Show', @tarefa %> |<%= link_to 'Back', tarefas_path %>
Ajustando Tarefas<!-- _form.html.erb --> <p> <%= form.label :usuario %><br /> <%= form.collection_select :usuario_id, Usuario.all, 'id', 'login' %></p><p> <%= form.label :duracao %><br /> <%= form.text_field :duracao %></p><p> <%= form.label :descricao %><br /> <%= form.text_field :descricao %></p><p> <%= form.label :data_inicio %><br /> <%= form.datetime_select :data_inicio %></p><p> <%= form.submit submit_text %></p>
<!-- show.html.erb --><p>
<b>Usuario:</b> <%=h @tarefa.usuario.login %>
</p>...
calendar_helper
>> ./script/plugin install http://calendardateselect.googlecode.com/svn/tags/calendar_date_select
<!-- app/views/layouts/application.html.erb -->
<%= stylesheet_link_tag 'scaffold' %><%= stylesheet_link_tag 'calendar_date_select/default' %>
<%= javascript_include_tag :defaults %><%= javascript_include_tag 'calendar_date_select/calendar_date_select' %>
<!-- app/views/tarefas/_form.html.erb -->
...<p>
<%= form.label :data_inicio %><br /> <%= form.calendar_date_select :data_inicio %>
</p>
Sempre que instalar um plugin: reiniciar o servidor
calendar_helper
Ajax<!-- app/views/tarefas/index.html.erb --><h1>Listing tarefas</h1>
<table id='tarefas_table'>
<tr> <th>Usuario</th>
<th>Duracao</th> <th>Descricao</th>
<th>Data inicio</th> </tr>
<% for tarefa in @tarefas %>
<%= render :partial => 'tarefa_row', :locals => { :tarefa => tarefa } %><% end %>
</table>
<br />
<h1>Nova Tarefa</h1><% remote_form_for(Tarefa.new) do |f| %>
<%= render :partial => f, :locals => { :submit_text => 'Create' } %><% end %>
Ajax<!-- app/views/tarefas/_tarefa_row.html.erb --><tr id="<%= dom_id(tarefa) %>">
<td><%=h tarefa.usuario.login if tarefa.usuario %></td> <td><%=h tarefa.duracao %></td>
<td><%=h tarefa.descricao %></td> <td><%=h tarefa.data_inicio.to_s(:short) %></td>
<td><%= link_to 'Show', tarefa %></td> <td><%= link_to 'Edit', edit_tarefa_path(tarefa) %></td>
<td><%= link_to_remote 'Destroy', :url => tarefa_path(tarefa), :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
# app/views/tarefas/create.js.rjspage.insert_html :bottom, 'tarefas_table',
:partial => 'tarefa_row', :locals => { :tarefa => @tarefa }
page.visual_effect :highlight, dom_id(@tarefa)
# app/views/tarefas/destroy.js.rjspage.visual_effect :drop_out, dom_id(@tarefa)
# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController ... def create @tarefa = Tarefa.new(params[:tarefa])
respond_to do |format| if @tarefa.save flash[:notice] = 'Tarefa was successfully created.' format.html { redirect_to(@tarefa) } format.js # create.js.rjs format.xml { render :xml => @tarefa, :status => :created, :location => @tarefa } else format.html { render :action => "new" } format.xml { render :xml => @tarefa.errors, :status => :unprocessable_entity } end end end
def destroy @tarefa = Tarefa.find(params[:id]) @tarefa.destroy
respond_to do |format| format.html { redirect_to(tarefas_url) } format.js # destroy.js.rjs format.xml { head :ok } end endend
RJS - nos bastidores<%= link_to_remote 'Destroy', :url => tarefa_path(tarefa), :confirm => 'Are you sure?', :method => :delete %>
<a href="#" onclick="if (confirm('Are you sure?')) { new
Ajax.Request('/tarefas/10', {asynchronous:true, evalScripts:true, method:'delete', parameters:'authenticity_token=' +
encodeURIComponent('966...364')}); }; return false;">Destroy</a>
<% remote_form_for(Tarefa.new) do |f| %> ...
<% end %>
<form action="/tarefas" class="new_tarefa" id="new_tarefa" method="post" onsubmit="new Ajax.Request('/tarefas',
{asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
...</form>
FormHelperf.check_box :accepted, { :class => 'eula_check' }, "yes", "no"
f.file_field :file, :class => 'file_input'
f.hidden_field :token
f.label :title
f.password_field :pin, :size => 20
f.radio_button :category, 'rails'
f.text_area :obs, :cols => 20, :rows => 40
f.text_field :name, :size => 20, :class => 'code_input'
f.select :person_id, Person.all.map { |p| [ p.name, p.id ] },
{ :include_blank => true }
f.collection_select :author_id, Author.all, :id, :name,
{ :prompt => true }
f.country_select :country
f.time_zone_select :time_zone, TimeZone.us_zones,
:default => "Pacific Time (US & Canada)"
JavaScriptGeneratorpage[:element]page << "alert('teste')"
page.alert("teste")page.assign 'record_count', 33
page.call 'alert', 'My message!'page.delay(20) do
page.visual_effect :fade, 'notice'end
page.redirect_to(:controller => 'account', :action => 'signup')
page.show 'person_6', 'person_13', 'person_223'page.hide 'person_29', 'person_9', 'person_0'
page.toggle 'person_14', 'person_12', 'person_23'page.insert_html :after, 'list', '<li>Last item</li>'
page.remove 'person_23', 'person_9', 'person_2'page.replace 'person-45', :partial => 'person', :object => @person
page.replace_html 'person-45', :partial => 'person', :object => @personpage.select('p.welcome b').first.hide
page.visual_effect :highlight, 'person_12'
PrototypeHelper<%= link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete %><%= observe_field :suggest, :url => search_tarefas_path,
:frequency => 0.25, :update => :suggest, :with => 'q' %><%= periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
:update => { :success => "invoice", :failure => "error" } %>
<% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
...<% end %>
<%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
:update => { :success => "succeed", :failure => "fail" } %>
<%= draggable_element("my_image", :revert => true) %><%= drop_receiving_element("my_cart", :url => { :controller => "cart",
:action => "add" }) %><%= sortable_element("my_list", :url => { :action => "order" }) %>
Entendendo Forms
<% form_for(@tarefa) do |f| %> <%= f.error_messages %>
<p>
<%= form.label :usuario %><br /> <%= form.collection_select :usuario_id, Usuario.all, 'id', 'login' %>
</p> <p>
<%= form.label :duracao %><br /> <%= form.text_field :duracao %>
</p> <p>
<%= form.label :descricao %><br /> <%= form.text_field :descricao %>
</p> <p>
<%= form.label :data_inicio %><br /> <%= form.calendar_date_select :data_inicio %>
</p> <p>
<%= form.submit 'Create' %> </p>
<% end %>
Nome e prefixo do form
<form action="/tarefas" class="new_tarefa" id="new_tarefa" method="post">
<div style="margin:0;padding:0">
<input name="authenticity_token" type="hidden"
value="966974fa19db6efaf0b3f456c2823a7f46181364" />
</div>
<p>
<label for="tarefa_usuario">Usuario</label><br />
<select id="tarefa_usuario_id" name="tarefa[usuario_id]">
<option value="1">admin</option>
<option value="2">akita</option>
</select>
</p>
<p>
<label for="tarefa_duracao">Duracao</label><br />
<input id="tarefa_duracao" name="tarefa[duracao]" size="30" type="text" />
</p>
<p>
<label for="tarefa_descricao">Descricao</label><br />
<input id="tarefa_descricao" name="tarefa[descricao]" size="30" type="text" />
</p>
<p>
<label for="tarefa_data_inicio">Data inicio</label><br />
<input id="tarefa_data_inicio" name="tarefa[data_inicio]" size="30" type="text" />
<img alt="Calendar" onclick="new CalendarDateSelect( $(this).previous(),
{time:true, year_range:10} );" src="/images/calendar_date_select/calendar.gif?1214788838"
style="border:0px; cursor:pointer;" />
</p>
<p>
<input id="tarefa_submit" name="commit" type="submit" value="Create" />
</p>
</form>
HTTP PostProcessing TarefasController#create (for 127.0.0.1 at 2008-06-30 00:01:11) [POST] Session ID: BAh...c25
Parameters: {"commit"=>"Create",
"authenticity_token"=>"966974fa19db6efaf0b3f456c2823a7f46181364", "action"=>"create", "controller"=>"tarefas",
"tarefa"=>{"usuario_id"=>"1", "data_inicio"=>"June 28, 2008 12:00 AM", "descricao"=>"Teste de Criação", "duracao"=>"1"}}
Tarefa Create (0.000453) INSERT INTO "tarefas"
("updated_at", "usuario_id", "data_inicio", "descricao", "duracao", "created_at") VALUES('2008-06-30 03:01:11', 1, '2008-06-28 00:00:00', 'Teste de Criação', 1,
'2008-06-30 03:01:11')
Redirected to http://localhost:3000/tarefas/12Completed in 0.01710 (58 reqs/sec) | DB: 0.00045 (2%) | 302 Found [http://localhost/tarefas]
Valores do pacote HTTP serão desserializados no hash ‘params’Note também que :action e :controller foram extraídos de
POST /tarefas
# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController
def create # pegando o hash params, na chave :tarefa
@tarefa = Tarefa.new(params[:tarefa]) ...
endend
# equivalente a:
@tarefa = Tarefa.new { :duracao=>"1", :usuario_id=>"1", :data_inicio=>"June 28, 2008 12:00 AM", :descricao=>"Teste de Criação" }
# o hash params completo vem assim:
params = {:action=>"create", :authenticity_token=>"966...364", :controller=>"tarefas", :commit=>"Create",
:tarefa => {"usuario_id"=>"1", "duracao"=>"1", "data_inicio"=>"June 28, 2008 12:00 AM",
"descricao"=>"Teste de Criação" } }
Params
Action Mailer
Envio simples de e-mail
>> ./script/plugin install git://github.com/caritos/action_mailer_tls.git
>> ./script/generate mailer TarefaMailer importante
# config/environments/development.rb...
config.action_mailer.delivery_method = :smtpconfig.action_mailer.smtp_settings = {
:address => "smtp.gmail.com", :port => 587,
:authentication => :plain, :user_name => "fabioakita",
:password => "----------"}
config.action_mailer.perform_deliveries = true
# app/controllers/tarefas_controller.rbclass TarefasController < ApplicationController
... def create
@tarefa = Tarefa.new(params[:tarefa])
respond_to do |format| if @tarefa.save
TarefaMailer.deliver_importante(@tarefa) if @tarefa.descricao =~ /^\!/ ...
end
Para enviarvia Gmail
Ativa oenvio
# app/models/tarefa_mailer.rb
class TarefaMailer < ActionMailer::Base
def importante(tarefa, sent_at = Time.now)
recipients tarefa.usuario.email
subject 'Tarefa Importante'
from '[email protected]'
sent_on sent_at
body :tarefa => tarefa
end
# se tiver ActiveScaffold instalado
def self.uses_active_scaffold?
false
end
end
<!-- app/views/tarefa_mailer/importante.erb -->
Notificação de Tarefa Importante
Descrição: <%= @tarefa.descricao %>
Duração: <%= @tarefa.duracao %> hs
Início: <%= @tarefa.data_inicio.to_s(:short) %>
Template ERB
# test/unit/tarefa_mailer_test.rbrequire 'test_helper'
class TarefaMailerTest < ActionMailer::TestCase
tests TarefaMailer fixtures :usuarios, :tarefas
def test_importante @expected.subject = 'Tarefa Importante'
@expected.from = '[email protected]' @expected.to = tarefas(:aula).usuario.email
@expected.body = read_fixture('importante') @expected.date = Time.now
assert_equal @expected.encoded, TarefaMailer.create_importante(tarefas(:aula)).encoded
endend
# test/fixtures/tarefa_mailer/importante
Notificação de Tarefa Importante
Descrição: Dando AulaDuração: 1 hs
Início: 28 Jun 21:33
Observações
• Evitar enviar e-mails nas actions: é muito lento!
• Estratégia:
• Fazer a action gravar numa tabela que serve de “fila” com o status de “pendente”
• Ter um script externo que de tempos em tempos puxa os pendentes, envia os e-mails e remarca como ‘enviado’
• Exemplo: usar ./script/runner para isso aliado a um cronjob. O Runner roda um script Ruby dentro do ambiente Rails, com acesso a tudo.
Outras Dicas
Rotas
$ rm public/index.html
# config/routes.rb
# adicionar:
map.root :tarefas
# remover:
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
Redefine a raízda aplicação
Apps REST nãoprecisam disso
Time Zone
• No Rails 2.1, por padrão, todo horário é gravado em formato UTC
• Time.zone recebe um String, como definido em TimeZone.all
• ActiveRecord converte os time zones de forma transparente
Time Zone>> Time.now # horário atual, note zona GMT -03:00=> Mon Jun 30 13:04:09 -0300 2008
>> tarefa = Tarefa.first # pegando a primeira tarefa do banco=> #<Tarefa id: 1 ...>>> anotacao = tarefa.anotacoes.create(:anotacao => "Teste com hora") # criando anotacao=> #<Anotacao id: 4 ...>>> anotacao.reload # recarregando anotacao do banco, apenas para garantir=> #<Anotacao id: 4 ...>
>> anotacao.created_at # data gravada no banco=> Seg, 30 Jun 2008 16:04:31 UTC 00:00
# config/environment.rb
config.time_zone = 'UTC'
Time Zone
>> Time.now # horario atual, local em GMT -3=> Mon Jun 30 13:07:42 -0300 2008
>> tarefa = Tarefa.first # novamente pega uma tarefa=> #<Tarefa id: 1, ...>
>> anotacao = tarefa.anotacoes.create(:anotacao => "Outro teste com hora") # cria anotacao=> #<Anotacao id: 5, tarefa_id: 1, ...>
>> anotacao.created_at # horario local, automaticamente convertido de acordo com config.time_zone=> Seg, 30 Jun 2008 13:08:00 ART -03:00
>> anotacao.created_at_before_type_cast # horario em UTC, direto do banco de dados=> Mon Jun 30 16:08:00 UTC 2008
# config/environment.rb
config.time_zone = 'Brasilia'
Time Zone Rake Tasks$ rake -D time
rake time:zones:all
Displays names of all time zones recognized by the Rails TimeZone
class, grouped by offset. Results can be filtered with optional OFFSET
parameter, e.g., OFFSET=-6
rake time:zones:local
Displays names of time zones recognized by the Rails TimeZone
class with the same offset as the system local time
rake time:zones:us
Displays names of US time zones recognized by the Rails TimeZone
class, grouped by offset. Results can be filtered with optional OFFSET
parameter, e.g., OFFSET=-6
Time Zone Rake Tasks
$ rake time:zones:local
* UTC -03:00 *
Brasilia
Buenos Aires
Georgetown
Greenland
$ rake time:zones:us
* UTC -10:00 *Hawaii
* UTC -09:00 *
Alaska
* UTC -08:00 *Pacific Time (US & Canada)
* UTC -07:00 *
ArizonaMountain Time (US & Canada)
* UTC -06:00 *
Central Time (US & Canada)
* UTC -05:00 *Eastern Time (US & Canada)
Indiana (East)
Adicionando Time Zone$ ./script/generate migration AdicionaTimeZoneUsuario
# db/migrate/20080630182836_adiciona_time_zone_usuario.rbclass AdicionaTimeZoneUsuario < ActiveRecord::Migration def self.up add_column :usuarios, :time_zone, :string, :null => false, :default => 'Brasilia' end
def self.down remove_column :usuarios, :time_zone endend
$ rake db:migrate
<!-- app/views/usuarios/new.html.erb --><p><label for="time_zone">Time Zone</label><br/><%= f.time_zone_select :time_zone, TimeZone.all %></p>
Modificando ActiveScaffold# lib/active_scaffold_extentions.rb
module ActiveScaffold
module Helpers
# Helpers that assist with the rendering of a Form Column
module FormColumns
def active_scaffold_input_time_zone(column, options)
time_zone_select :record, column.name, TimeZone.all
end
end
end
end
# config/environment.rb
...
require 'active_scaffold_extentions'
Views
# app/controllers/admin/usuarios_controller.rbclass Admin::UsuariosController < ApplicationController
before_filter :login_required
active_scaffold :usuario do |config| config.columns = [:login,
:email, :time_zone,
:created_at, :password,
:password_confirmation ]
config.columns[:time_zone].form_ui = [:time_zone]
config.list.columns.exclude [ :password,
:password_confirmation ]
config.create.columns.exclude [ :created_at]
config.update.columns.exclude [
:login, :created_at]
end
end
Time Zone Views
Carregando Zonas# app/controllers/application.rbclass ApplicationController < ActionController::Base helper :all # include all helpers, all the time include AuthenticatedSystem
protect_from_forgery before_filter :load_time_zone private def load_time_zone Time.zone = current_usuario.time_zone if logged_in? endend
# app/controllers/admin/usuarios_controller.rbclass Admin::UsuariosController < ApplicationController before_filter :login_required ...end
Retirar include AuthenticatedSystem de
usuarios e sessoes
Carregando Zonas
Segurança
• No Rails 2, sessions são gravadas no cookie, mas não criptografadas.
• Best Practice: não grave nada importante na session
• Todo formulário HTML é protegido contra Cross Site Request Forgery (CSRF)
• Toda operação ActiveRecord é sanitizada para evitar SQL Injection
• Best Practice: não crie SQL manualmente concatenando strings originadas em formulários
Segurança# config/environment.rbconfig.action_controller.session = {
:session_key => '_tarefas_session', :secret =>
'aaa02677597285ff58fcdb2eafaf5a82a1c10572334acb5297ec94a0f6b1cf48fbcb8f546c00c08769a6f99695dd967a2d3fea33d6217548fcc4fd64e783caa6'
}
# app/controllers/application.rbclass ApplicationController < ActionController::Base
... protect_from_forgery
...end
<form action="/tarefas" class="new_tarefa" id="new_tarefa" method="post">
<div style="margin:0;padding:0"> <input name="authenticity_token" type="hidden"
value="5a2176fd77601a497a9d7ae8184d06b60df0ae28" /> </div>
...</form>
JRuby
O que é?
• Criado por Thomas Enebo e Charles Nutter
• Suportado pela Sun
• Compilador e Interpretador compatível com Ruby MRI 1.8.6
• Capaz de gerar bytecode Java a partir de código Ruby
• Roda sobre JDK 1.4 até 1.6
Vantagens
• Performance muitas vezes maior do que Ruby MRI atual
• Capacidade de tirar proveito do HotSpot para otimização em runtime
• Utilização de threads-nativas
• Suporte a Unicode compatível com Java
• Capaz de utilizar qualquer biblioteca Java
Desvantagens
• Tempo de inicialização um pouco mais lento
• Não é capaz de usar extensões feitas em C
• Não é compatível com todas as gems e plugins disponíveis
JRuby on Rails
• Warble: capaz de encapsular uma aplicação Rails em um arquivo WAR comum
• ActiveRecord-JDBC utiliza os drivers JDBC normais de Java
• jetty_rails: desenvolvimento ágil mesmo em ambiente Java
• Capaz de compartilhar objetos HTTPSession com aplicações Java no mesmo container
Instalação
• Instalar o JDK mais recente (de preferência 1.6)
• Baixar http://dist.codehaus.org/jruby/jruby-bin-1.1.2.zip
• Descompactar e colocar o diretório bin/ no seu PATH (depende do seu sistema operacional)
Instalação• jruby -v
• ruby 1.8.6 (2008-05-28 rev 6586) [x86_64-jruby1.1.2]
• jruby -S gem install rails
• jruby -S gem install jruby-openssl
• jruby -S gem install activerecord-jdbc-adapter
• jruby -S gem install activerecord-jdbcmysql-adapter
• jruby -S gem install activerecord-jdbcsqlite3-adapter
• jruby -S gem install jdbc-mysql
• jruby -S gem install jdbc-sqlite3
• jruby -S gem install jetty-rails
Configuração
• Alterar database.yml
• jruby -S jetty-rails
<% jdbc = defined?(JRUBY_VERSION) ? 'jdbc' : '' %>
development:
adapter: <%= jdbc %>mysql
database: tarefas
username: root
password: root
Configuração• jruby -S gem install warbler
• cd tarefas
• jruby -S warble config
# config/warble.rb
Warbler::Config.new do |config|
config.dirs = %w(app config lib log vendor tmp)
config.gems = ["activerecord-jdbc-adapter", "jruby-openssl",
"activerecord-jdbcmysql-adapter", "jdbc-mysql"]
config.gem_dependencies = true
config.webxml.rails.env = 'production'
end
Configuração# config/initializers/jruby.rbif defined?(JRUBY_VERSION) && defined?($servlet_context)
# Logger expects an object that responds to #write and #close device = Object.new
def device.write(message) $servlet_context.log(message)
end def device.close; end
# Make these accessible to wire in the log device class << RAILS_DEFAULT_LOGGER
public :instance_variable_get, :instance_variable_set end
old_device = RAILS_DEFAULT_LOGGER.instance_variable_get "@log" old_device.close rescue nil
RAILS_DEFAULT_LOGGER.instance_variable_set "@log", device end
# config/environment.rbconfig.action_controller.session_store = :java_servlet_store if defined?(JRUBY_VERSION)
# config/database.yml
production:
adapter: jdbcmysql
database: tarefas
username: root
password: root
•jruby -S warble config
InsoshiMichael Hartl
Download
Iniciando$ sudo gem install rails
$ sudo gem install ferret
$ sudo gem install sqlite3-ruby
$ sudo gem install mysql
$ sudo gem install image_science
$ rake install
$ rake db:test:prepare
$ rake spec
$ rake db:sample_data:reload
$ script/server
Admin:
email: [email protected]
password: admin
Testes$ rake spec(in /Users/akitaonrails/rails/sandbox/insoshi)...
Finished in 9.798022 seconds
342 examples, 0 failures
$ rake stats(in /Users/akitaonrails/rails/sandbox/insoshi)+----------------------+-------+-------+---------+---------+-----+-------+| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |+----------------------+-------+-------+---------+---------+-----+-------+| Controllers | 1436 | 1125 | 18 | 131 | 7 | 6 || Helpers | 455 | 355 | 0 | 36 | 0 | 7 || Models | 1307 | 766 | 18 | 103 | 5 | 5 || Libraries | 1351 | 772 | 9 | 119 | 13 | 4 || Model specs | 1470 | 1173 | 0 | 6 | 0 | 193 || View specs | 73 | 56 | 0 | 0 | 0 | 0 || Controller specs | 1199 | 1001 | 0 | 5 | 0 | 198 || Helper specs | 143 | 100 | 0 | 0 | 0 | 0 |+----------------------+-------+-------+---------+---------+-----+-------+| Total | 7434 | 5348 | 45 | 400 | 8 | 11 |+----------------------+-------+-------+---------+---------+-----+-------+ Code LOC: 3018 Test LOC: 2330 Code to Test Ratio: 1:0.8
Plugins
NÃO USE FERRET!Prefira Sphinx
Suporte a RSpec
Uploader
MelhorPaginador
Exemplo: Will Paginate
# app/controllers/forums_controller.rbdef show
@forum = Forum.find(params[:id]) @topics = @forum.topics.paginate(
:page => params[:page])end
<!-- app/views/forums/show.html.erb --><h2>Discussion topics</h2>
<ol class="list forum full">
<%= render :partial => @topics %></ol>
<%= will_paginate(@topics) %>
Analisando$./script/plugin install svn://rubyforge.org//var/svn/visualizemodels/visualize_models
$rake visualize_models(in /Users/akitaonrails/rails/sandbox/insoshi)
Looking at table: schema_infoLooking at table: people
Looking at table: photosLooking at table: communications
Looking at table: connectionsLooking at table: forums
Looking at table: topicsLooking at table: posts
Looking at table: blogsLooking at table: comments
Looking at table: activitiesLooking at table: feeds
Looking at table: preferencesLooking at table: email_verifications
Looking at table: page_viewsGenerated /Users/akitaonrails/rails/sandbox/insoshi/doc/model_overview_neato_hier.png
Generated /Users/akitaonrails/rails/sandbox/insoshi/doc/model_overview_neato_plain.png
Deploying$gem install passenger$passenger-install-apache2-module
# /etc/apache2/httpd.conf
...LoadModule passenger_module /opt/local/lib/ruby/gems/1.8/gems/passenger-2.0.1/ext/apache2/
mod_passenger.soPassengerRoot /opt/local/lib/ruby/gems/1.8/gems/passenger-2.0.1
PassengerRuby /opt/local/bin/ruby
<VirtualHost *:80> ServerName insoshi.local
DocumentRoot "/Users/akitaonrails/rails/sandbox/insoshi/public" RailsEnv development
RailsAllowModRewrite off <directory "/Users/akitaonrails/rails/sandbox/insoshi/public">
Order allow,deny Allow from all
</directory></VirtualHost>
Deploying
App
Rails
Ruby
App
Rails
Ruby
App
Rails
Ruby
Apache 2 + Passenger
MySQL
Phusion
Evoluindo
Websites
• akitaonrails.com
• nomedojogo.com
• rubyonda.com
• rubyonbr.org
• groups.google.com/group/rails-br
• rubyinside.com
• rubycorner.com
• rubylearning.com
• peepcode.com
• railscasts.com
• headius.blogspot.com
• nubyonrails.topfunky.com
• planetrubyonrails.com
• weblog.rubyonrails.org
Livros
• “Desenvolvendo a Web Ágil com Rails” - Artmed - Dave Thomas
• Beginning Ruby - Peter Cooper
• The Rails Way - Obie Fernandez
• The Ruby Way - Hal Fulton
• Advanced Rails Recipes - Mike Clark
• Deploying Rails Applications - Ezra
Obrigado!www.akitaonrails.com