Web Development with
Ruby On Rails
Pedro Cunha
Ruby
Yukihiro "Matz" Matsumoto
Ruby is designed for programmer productivity and fun
Created February 1993
Ruby
Everything is an object
true.class # TrueClassnil.class # NilClass
Dynamic Typing
class Foo
 def initialize(x, y)
 @x = x @y = y endend
class Foo2end
Foo.new(2, Foo2.new)
Rubyclass Foo # Parenthesis can be omitted def method puts "Hello World" end # Default params def method2(x = 2) puts x end # Operator overload def +(x) endend
class Bar # Use ! if you change self def method! end # Use ? if you return a boolean def method? end
# Only conventionsend
Ruby
a = "Hello"b = "Hello"
a.equal? b # false
x = :helloy = :hello
x.equal? y # true
"hello".class # String:hello.class # Symbol
# Convention# Use string if you plan to compute text
# Use symbols if you want to define or/and set a behaviour which is not expected to change
Rubya = {}a[:first] = 2a[:things] = 3a[:foo] = "bar"
b = { :first => 2, :things => 3, :foo => "bar"}
b[:first] # 2
Ruby
x = [1,4,5,2,5,8,10]
x.sort # returns a copy of x sorted [1,2,4,5,5,8,10]x.sort! # modifies self
x.map{ |i| i + 4 } # [5,6,8,9,9,12,14]x.map! do |i| i + 4end # [5,6,8,9,9,12,14]
Ruby
class String def +() # override string default + operator endend
Monkey Patching
“With great power comes great responsability”Uncle Ben, Amazing Spiderman nº1
Ruby on Rails
Ruby on Rails
Created by David Heinemeir Hansson
• CEO at 37th Signals
• Personal Project, 2004
Present
• Rails 3.1
• Growing community
Ruby on Rails
Convention vs Configuration
MVC Architecture
REST routing
Convention vs Configuration
• Don’t Repeat Yourself
• Increased productivity through conventions. Ex.: following a pattern for foreign key columns.
• Take advantage of singular and plural word meanings
MVC
RoR
RESTRepresentational State Transfer
POST GET PUT DELETE
/posts/posts/1/posts/1/posts/1
CREATE READ UPDATE DELETE
CRUD REST ROUTES
# routes.rbBlog::Application.routes.draw do resources :postsend
Starting development
RoR
pcunha:prompt$ rails new Blog -d mysql
Blog /app /controllers /mailers /models /views /config database.yml /db /migrate Gemfile /public /javascripts /stylesheets
# config/database.ymldevelopment: adapter: sqlite3 database: db/development.sqlite3 test: adapter: sqlite3 database: db/test.sqlite3 production: adapter: sqlite3 database: db/production.sqlite3
development: adapter: mysql2 encoding: utf8 database: Blog_development username: root password:
test: adapter: mysql2 encoding: utf8 database: Blog_test username: root password:
production: adapter: mysql2 encoding: utf8 database: Blog_production username: root password: rails new with mysql option
pcunha:Blog$ rails server
=> Booting WEBrick=> Rails 3.0.7 application starting in development on http://0.0.0.0:3000=> Call with -d to detach=> Ctrl-C to shutdown server
localhost:3000
ModelDatabase Schema
pcunha:Blog$ rails generate scaffold Post title:string body:text
invoke active_record create db/migrate/20110715102126_create_posts.rb create app/models/post.rb invoke test_unit create test/unit/post_test.rb create test/fixtures/posts.yml invoke scaffold_controller create app/controllers/posts_controller.rb invoke erb create app/views/posts create app/views/posts/index.html.erb create app/views/posts/edit.html.erb create app/views/posts/show.html.erb create app/views/posts/new.html.erb create app/views/posts/_form.html.erb
# config/db/migrate/20110715102126_create_posts.rbclass CreatePosts < ActiveRecord::Migration def self.up create_table :posts do |t| t.string :title t.text :body
t.timestamps end end
def self.down drop_table :posts endend
pcunha:Blog$ rake db:createpcunha:Blog$ rake db:migrate== CreatePosts: migrating-- create_table(:posts) -> 0.0015s== CreatePosts: migrated (0.0018s)
pcunha:Blog$ rails generate model Comment body:text
invoke active_record create db/migrate/20110715103725_create_comments.rb create app/models/comment.rb invoke test_unit create test/unit/comment_test.rb create test/fixtures/comments.yml
pcunha:Blog$ rails generate migration AddPostIdToComments post_id:integer
invoke active_record create db/migrate/20110715103834_add_post_id_to_comments.rb
# 20110715103834_add_post_id_to_comments.rbclass AddPostIdToComments < ActiveRecord::Migration def self.up add_column :comments, :post_id, :integer end
def self.down remove_column :comments, :post_id endend
pcunha:Blog$ rake db:migrate== CreateComments: migrating-- create_table(:comments) -> 0.0011s== CreateComments: migrated (0.0012s)
== AddPostIdToComments: migrating-- add_column(:comments, :post_id, :integer) -> 0.0011s== AddPostIdToComments: migrated (0.0041s)
rake db:createrake db:migraterake db:migrate:redorake db:rollback
blog_db.schema_migrations - keeps the version number of all migrations already runned
Relations
Model
# app/models/post.rbclass Post < ActiveRecord::Base has_many :commentsend
Post.allPost.find(1).commentsComments.find(1).postPost.order(:created_at)Post.limit(5).offset(2)
# app/models/comment.rbclass Comment < ActiveRecord::Base belongs_to :postend
Validations
Model
# app/models/post.rbclass Post < ActiveRecord::Base has_many :comments validates_presence_of :title validates_format_of :title, :with => /\ASLB.*\z/ end
p = Post.newp.save # falsep.errors.full_messages # ["Title can't be blank", "Title is invalid"]
p.title = "SLB is the best"p.save # true
validates_presence_of :nif
validates_format_of :name
validates_acceptance_of :terms_and_conditions, :on => :create
validates_numericality_of :age, :greater_than_or_equal_to => 18
validates_uniqueness_of :model_fk_key, :scope => :model_fk_key2
validates_length_of :minimum => 5
ControllersManaging the CRUD
# app/controllers/posts_controller.rbclass PostsController < ApplicationController # GET /posts def index ...
# GET /posts/1 def show ...
# GET /posts/new def new ...
# GET /posts/1/edit def edit ...
# POST /posts def create ...
# PUT /posts/1 def update ...
# DELETE /posts/1 def destroy ...end
Generated with scaffold
# POST /posts # POST /posts.xml def create @post = Post.new(params[:post])
respond_to do |format| if @post.save format.html { redirect_to(@post, :notice => 'Post was successfully created.') } format.xml { render :xml => @post, :status => :created, :location => @post } else format.html { render :action => "new" } format.xml { render :xml => @post.errors, :status => :unprocessable_entity } end end end
def index @posts = Post.all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
pcunha:Blog$ curl http://localhost:3000/posts.xml<?xml version="1.0" encoding="UTF-8"?><posts type="array"> <post> <created-at type="datetime">2011-07-15T13:39:51Z</created-at> <body>This is the body of the first post</body> <title>The first very post of this blog</title> <updated-at type="datetime">2011-07-15T13:39:51Z</updated-at> <id type="integer">1</id> </post></posts>
Views
# app/views/posts/new.html.erb<h1>New post</h1>
<%= form_for(@post) do |f| %> <div class="field"> <%= f.label :title %><br /> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div><% end %>
<%= link_to 'Back', posts_path %>
# app/controllers/posts_controller.rb def new @post = Post.new respond_to do |format| format.html # new.html.erb} end end
# app/controllers/posts_controller.rb def edit @post = Post.find(params[:id]) respond_to do |format| format.html # edit.html.erb} end end
# app/views/posts/edit.html.erb<h1>Edit post</h1>
<%= form_for(@post) do |f| %> <div class="field"> <%= f.label :title %><br /> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div><% end %>
<%= link_to 'Show', @post %> |<%= link_to 'Back', posts_path %>
Rails builds the route for you
link_to 'Show', @post # GET posts/@post.id
form_for(@post)
if @post.new_record? POST /posts else PUT /posts/@post.id end
Partials
Views
# app/views/posts/edit.html.erb<h1>Edit post</h1>
<%= form_for(@post) do |f| %> <div class="field"> <%= f.label :title %><br /> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div><% end %>
<%= link_to 'Show', @post %> |<%= link_to 'Back', posts_path %>
# app/views/posts/new.html.erb<h1>New post</h1>
<%= form_for(@post) do |f| %> <div class="field"> <%= f.label :title %><br /> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div><% end %>
<%= link_to 'Back', posts_path %>
Bad pattern
# app/views/posts/new.html.erb<h1>New post</h1>
<%= render "form" %>%><%= link_to 'Back', posts_path %>
# app/views/posts/edit.html.erb<h1>Edit post</h1>
<%= render "form" %>%><%= link_to 'Show', @post %> |<%= link_to 'Back', posts_path %>
# app/views/posts/_form.html.erb<%= form_for(@post) do |f| %> <div class="field"> <%= f.label :title %><br /> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div><% end %>
The right way
AJAXImprove user experience
Improve user experience by not having the whole page reload when submitting a form or simple pagination link
Also save resources used (SQL queries, memory, more bandwidth usage,... etc)
Changing default forms to AJAX
AJAX
# config/routes.rbBlog::Application.routes.draw do resources :posts do resources :comments, :only => [:create] endend
POST /posts/:post_id/comments
Limiting actions is always the best practice
# app/controllers/comments_controller.rbclass CommentsController < ApplicationController
def create @post = Post.find(params[:post_id]) @comment = @post.comments.new(params[:comment]) respond_to do |format| if @comment.save format.html { redirect_to(@post } else format.html { render :template => "posts/show.html.erb" } end end endend
# app/views/posts/show.html.erb...<h1>Comments</h1><div id="comments"> <%= render :partial => "comments/comment", :collection => @post.commments %></div>
<%= render :partial => "comments/form", :locals => { :post => @post, :comment => @comment || Comment.new } %>
# app/views/comments/_form.html.erb<%= form_for [post,comment] do |f| %> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> <p><%= f.submit %></p> </div><% end %>
Our HTML formWhat needs to change?
# app/views/comments/_form.html.erb<%= form_for [post,comment], :remote => true do |f| %> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> <p><%= f.submit %></p> </div><% end %>
That’s it? Not yet!
# app/controllers/comments_controller.rbclass CommentsController < ApplicationController
def create @post = Post.find(params[:post_id]) @comment = @post.comments.new(params[:comment]) respond_to do |format| if @comment.save format.html { redirect_to(@post, :notice => 'Comment was successfully created.') } format.js else format.html { render :action => "new" } format.js end end endend
# app/views/comments/create.js.erb//Dump javascript here!document.getElementById...
Notice:- create.js.erb- writing native javascript is not optimal:
1. You will forget something about IE2. We are at 21st Century3. Lots of good frameworks
Rails 2.X and 3.0.X
- Prototype JS Framework as default
Rails 3.1 (released 2011)
- jQuery JS Framework as default
# app/views/comments/create.js.erb<% if @comment.new_record? %>
<% content = render(:partial => "comments/form", :locals => { :post => @post, :comment => @comment }) content = escape_javascript(content) %>
$('new_comment').replace("<%= content %>");
<% else %> <% comment_content = render(:partial => "comments/comment", :object => @comment) comment_content = escape_javascript(comment_content) %> $('comments').insert({ bottom : '<%= comment_content %>' }) $('new_comment').reset();<% end %>
Almost there... but
- Complex code- We can do better with Rails
RJSRuby (to) JavaScript Templates
# app/views/comments/create.js.rjsif @comment.new_record? page.replace :new_comment, :partial => "comments/form", :locals => { :post => @post, :comment => @comment }else page.insert_html :bottom, :comments, :partial => "comments/comment", :object => @comment page[:new_comment].resetend
Gems
GemsExtend Rails framework
Easy installation and usage
Increasing community
• Github
• Gemcutter
Bundler gem# Gemfilegem "rails", "2.3.10"gem "will_paginate"gem "authlogic"
gem "pg"gem "postgis_adapter", "0.7.8"gem "GeoRuby", "1.3.4"
# Sphinxgem "thinking-sphinx", "1.4.5"
group :development do gem "capistrano" gem "capistrano-ext" gem "ruby-debug" gem "wirble" gem "mongrel"end
Questions ?
References
http://rubyonrails.org/
http://railsapi.com/
http://railscasts.com/
http://railsforzombies.org/
References