Ruby on Rails at PROMPT ISEL '11

Preview:

DESCRIPTION

An introduction to Ruby on Rails. Presentation made at PROMP, a pos-graduation on ISEL university at Portugal.

Citation preview

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