You are on page 1of 512

3

Ruby on Rails
Rails

Web

Michael Hartl

rev 1.0.2, 2015-01-14T20:00:00+08:00

................................................................................................................... 1
1.1.

1.2.

1.3.

1.4.

20

1.5.

29

1.6.

33

1.7.

33

.......................................................................................................................................... 35
2.1.

35

2.2.

38

2.3.

49

2.4.

58

2.5.

59

Git

............................................................................................................................... 63
3.1.

63

3.2.

65

3.3.

73

3.4.

78

3.5.

87

3.6.

88

3.7.

90

Rails

Ruby ........................................................................................................................... 97

4.1.

97

4.2.

100

4.3.

106

4.4. Ruby

115

4.5.

123

4.6.

123

........................................................................................................................................ 125
5.1.

125

5.2. Sass

140

5.3.

148

5.4.

154

5.5.

157

5.6.

158

........................................................................................................................................ 161
6.1.

161

6.2.

170

6.3.

183

6.4.

189

6.5.

190

............................................................................................................................................... 193
7.1.

193

7.2.

206

7.3.

213

7.4.

223

7.5.

231

7.6.

234

7.7.

235

.................................................................................................................................... 239
8.1.

239

8.2.

250

8.3.

263

8.4.

265

8.5.

288

8.6.

289

vi -

Asset Pipeline

.................................................................................................................. 293
9.1.

293

9.2.

303

9.3.

315

9.4.

327

9.5.

334

9.6.

336

10

.................................................................................................................... 339

10.1.

339

10.2.

364

10.3.

386

10.4.

388

10.5.

388

10.6.

391

11

.................................................................................................................................. 393

11.1.

393

11.2.

402

11.3.

412

11.4.

431

11.5.

442

11.6.

444

12
12.1.

...................................................................................................................................... 447

452

12.2.

461

12.3.

485

12.4.

493

12.5.

493

- vii

Ruby
Rails

Ruby

David Heinemeier Hansson


Rails
Ruby on Rails
Web
Ruby on Rails Tutorial
Rails

Web

Web

Ru-

by on Rails
Ruby on Rails Tutorial

Michael Hartl
Ruby on Rails Tutorial

Ruby is a delightful computer language explicitly designed to make programmers happy. This philosophy influenced
David Heinemeier Hansson to pick Ruby when implementing the Rails web framework. Ruby on Rails, as its often
called, makes building custom web applications faster and easier than ever before. In the past few years, the Ruby on
Rails Tutorial has become the leading introduction to web development with Rails.
In our interconnected world, computer programming and web application development are rapidly rising in importance, and I am excited to support Ruby on Rails in China. Although it is important to learn English, which is the international language of programming, its often helpful at first to learn in your native language. It is for this reason
that I am grateful to Andor Chen for producing the Chinese-language edition of the Ruby on Rails Tutorial book.
Ive never been to China, but I definitely plan to visit some day. I hope Ill have the chance to meet some of you when
I do!
Best wishes and good luck,
Michael Hartl
Author
The Ruby on Rails Tutorial

ix

CD Baby
PHP

Ruby on Rails

Google

Michael Hartl
Ruby on Rails Tutorial

Rails

Test-driven Development

Rails
Rails

TDD

Rails

TDD
Git Bitbucket Heroku

Derek Sivers (sivers.org)


CD Baby

1.

xi

Ruby on Rails
Prochazka
Debra Williams Cauley

RailsSpace

Aurelius

Aure
RailsSpace

Ruby
David Heinemeier Hansson Yehuda Katz Carl Lerche Jeremy Kemper Xavier Noria Ryan Bates Geoffrey Grosenbach Peter
Cooper Matt Aimonetti Mark Bates Gregg Pollack Wayne E. Seguin Amy Hoy Dave Chelimsky Pat
Maddox Tom Preston-Werner Chris Wanstrath Chad Fowler Josh Susser Obie Fernandez Ian McFarland
Steven Bristol Pratik Naik Sarah Mei Sarah Allen Wolfram Arnold Alex Chaffee Giles Bowkett Evan
Dorn Long Nguyen James Lindenbaum Adam Wiggins Tikhon Bernstam Ron Evans Wyatt Greene
Miles Forrest Pivotal Labs
Heroku
thoughtbot
GitHub

xiii

Michael Hartl
SoftCover
Rails

Ruby on Rails Web


RailsSpace

Rails

Ruby on
Ruby

Insoshi

2011
Y Combinator

Ruby Hero

Ruby
RSpec

Rails

Rails

Rails

xv

Ruby on Rails Tutorial: Learn Web Development with Rails (Third Edition)
Michael Hartl
Michael Hartl

MIT

Beerware

The MIT License


Copyright (c) 2014 Michael Hartl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

/*
* ---------------------------------------------------------------------------* "THE BEERWARE LICENSE" (Revision 43):
* Michael Hartl wrote this code. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return.
* ---------------------------------------------------------------------------*/

xvii

Ruby on Rails
Rails
Web
Ruby on Rails Web
Ruby Rails HTML CSS
Web
Rails
MVC

Web
Web
Web
Ru-

REST

by
1

Web
hello_app
ple_app

toy_app

1.3
3

sam-

12
Rails
Web

Web

1.1.1

Web

Web
Rails

1.1
Rails

1.2
Rails

MiniTest
RSpec Cucumber Capybara Factory Girl

Guard Spork
Rails

Web

Ruby on Rails
Rails

1.

RubyTest

1.2

hello_app

http://railstutorial-china.org

Git

1.4

1.5

Rails

1.2

toy_app

URI

URL

sample_app

Rails

integration test
Ruby
5

Test-driven Development
4

3
10

11

1.2

12

Rails

Rails

David Heinemeier Hansson


Rails
15

scaffold

TDD

15

generate scaffold

Rails

Rails
2

3
Rails

Web

1.1.
Ruby on Rails
Rails
Airbnb Basecamp

Rails
Web
Disney Github
Rails

Rails

2004
Rails
Hulu Kickstarter Shopify Twitter
Yellow Pages
Web
ENTP thoughtbot Pivotal Labs Hashrocket
HappyFunCorp

Rails
Rails
Ruby
Domain-specific Language
DSL
URL
Rails

HTML
Rails
Web

Web

Ruby

MIT

Rails
Web

Rails

Uniform Resource Identifier


URL

Web

REST
Rails

2. URI
Locator

2-

Web

URL

David

Uniform Resource

Heinemeier Hansson
Rails

Rails
Merb

Merb

API

Rails
gem

gem
IRC

Rails

1.1.1.

MiniTest

Unix

HTML

CSS
HTML

Rails
JavaScript

Ruby
SQL

Rails

Web

Rails

Ruby
Ruby

Chris Pine
Web

Learn to Program

Daniel Kehoe

Peter Cooper
Ruby
Ruby
Learn Ruby on Rails

Ruby

Ruby

Rails
Try Ruby

One Month Rails

Rails
Code School
Tealeaf Academy

Rails

Thinkful
Ryan Bates

RailsCasts

RailsApps
Rails

Rails
Rails

Rails

1.1.2.

Unix

$ echo "hello, world!"


hello, world!

3.

Rails

https://selfstore.io/products/13

1.1.

-3

1.2

Windows
Unix

1.2.1

Linux

Rails
Web

rails server

1.3.2
$ rails server

Unix
production.rb
config/environments/production.rb

IDE

/home/ubuntu/workspace/sample_app/
production.rb
/home/ubuntu/workspace/sample_app/config/environments/production.rb
config/environments/production.rb

shell

Ruby

Rails
RED

class User < ActiveRecord::Base


validates :name, presence: true
validates :email, presence: true
end

class User < ActiveRecord::Base


.
.
.
has_secure_password
end

4-

GREEN

1.2.
Rails

Ruby

Rails

Development Environment

Integrated

IDE
1.1
IDE

Rails

Web
Windows
4

InstallRails.com

1.2.1.
Rails
Cloud9
Rails

Cloud9
Ruby RubyGems
Git
IDE
Web
1.1
IDE

1.2.2

1.1

4.
5.

Windows

foo

Rails

Rails

IDE

InstallRails

Rails

def foo

1.2.

-5

1.

Cloud9

2.

Go to your Dashboard

3.

Create New Workspace

4.

rails-tutorial

5.

rails_tutorial
Rails

Private to the people I invite


Ruby on Rails
1.2

Create

6. Cloud9

Start editing

1.2

Cloud9

Ruby
Code Editor (Ace) Ace
Tabs

6-

1.3

Soft
Save

1.3

1.2.2.

Cloud9

Rails

gem

IDE
1.1

1.1

Rails
1.1

Rails

RubyGems

Rails

$ gem install rails -v 4.2.0


-v

Rails

1.3.

hello, world!

Rails
1.2.1

6. Cloud9

hello, world
1.3.4

rails new

Cloud9 IDE

1.5

Rails
workspace

Rails

1.3.

-7

1.2

1.2

cd

Unix

mkdir

1.3
1.2

workspace

Rails

$ cd
$ mkdir workspace
$ cd workspace/

#
#
#

workspace
workspace

1.3
Windows

Unix

Mac OS X

Unix
Unix

Linux

shell

Bash
mkdir
mv

cp

cd

Graphical User Interface

GUI
shell

Unix

1.1

Unix
Conquering the Command Line

Mark Bates

1.1

Unix

ls

$ ls -l

mkdir <dirname>

$ mkdir workspace

cd <dirname>

$ cd workspace/
$ cd ..
$cd ~

$ cd

$ cd ~/workspace/

IDE
Rails

8-

mv <source> <target>

$ mv README.rdoc README.md

cp <source> <target>

$ cp README.rdoc README.md

rm <file>

$ rm README.rdoc

rmdir <directory>

$ rmdir workspace/

rm -rf <directory>

$ rm -rf tmp/

cat <file>

$ cat ~/.ssh/id_rsa.pub

1.3
4.2.0

1.1

Rails

1.3
Rails

1.3

Could not find railties


1.1

Rails

rails new

$ cd ~/workspace
$ rails _4.2.0_ new hello_app
create
create README.rdoc
create Rakefile
create config.ru
create .gitignore
create Gemfile
create app
create app/assets/javascripts/application.js
create app/assets/stylesheets/application.css
create app/controllers/application_controller.rb
.
.
.
create test/test_helper.rb
create tmp/cache
create tmp/cache/assets
create vendor/assets/javascripts
create vendor/assets/javascripts/.keep
create vendor/assets/stylesheets
create vendor/assets/stylesheets/.keep
run bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Using rake 10.3.2
Using i18n 0.6.11
.
.
.
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
run bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted
rails new

1.3

bundle install

1.3.1
rails new

1.4

Rails

Rails
1.2

Pipeline

5.2.1
CSS

app/assets

Asset Pipeline

Asset

JavaScript

1.3.

-9

1.4
1.2

Rails

Rails
/

app/
app/assets

CSS

bin/
config/
db/
doc/
lib/
lib/assets
log/
public/
bin/rails
test/
tmp/

10 -

CSS

JavaScript

JavaScript

/
vendor/

gem

vendor/assets

CSS

JavaScript

README.rdoc
Rakefile

rake

Gemfile

gem

Gemfile.lock

gem

config.ru

Rack

.gitignore

Git

gem

1.3.1. Bundler
Rails

Bundler

rails new

gem
4

1.4

Bundler
Bundler
1.4
1.5
Ruby
Refresh File Tree
hello_app

gem

1.3

bundle install
Gemfile

Ruby
1.5

Gemfile

source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0.0.beta1'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.0.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby
# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more:
# https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

1.3.

- 11

# Use ActiveModel has_secure_password


# gem 'bcrypt', '~> 3.1.7'
# Use Unicorn as the app server
# gem 'unicorn'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
group :development, :test do
# Call 'debugger' anywhere in the code to stop execution and get a
# debugger console
gem 'byebug'
# Access an IRB console on exceptions page and /console in development
gem 'web-console', '~> 2.0.0.beta2'
# Spring speeds up development by keeping your application running in the
# background. Read more: https://github.com/rails/spring
gem 'spring'
end
#

gem
gem

gem

gem

Bundler

gem 'sqlite3'

gem

Rails

gem 'uglifier', '>= 1.3.0'


1.3.0
7.2
gem 'coffee-rails', '~> 4.0.0'

12 -

uglifier

Asset Pipeline

Bundler

Gemfile

1.5
4.0.0

4.1

coffee-rails

~> 4.0.0

>=

4.0.0
4.0

gem
Gemfile

4.0.1

4.1

gem

~>

1.4

Gemfile

1.5

sqlite3

7.1.1

Heroku

1.5
1.5

Gemfile

Ruby gem

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem
gem

'rails',
'sass-rails',
'uglifier',
'coffee-rails',
'jquery-rails',
'turbolinks',
'jbuilder',
'sdoc',

'4.2.0'
'5.0.0.beta1'
'2.5.3'
'4.1.0'
'4.0.0.beta2'
'2.3.0'
'2.2.3'
'0.4.0', group: :doc

group :development, :test do

1.3.

- 13

gem
gem
gem
gem
end

'sqlite3',
'byebug',
'web-console',
'spring',

'1.3.9'
'3.4.0'
'2.0.0.beta3'
'1.1.3'

Gemfile

1.5

bundle install

gem

$ cd hello_app/
$ bundle install
Fetching source index for https://rubygems.org/
.
.
.
bundle install

1.3.2. rails server


rails new

1.3

bundle install

1.3.1

Rails
rails server

1.6

Cloud9
1.7

IP
8

Rails
$IP

Cloud9
echo $IP

1.6

$PORT

IP

echo $PORT

Rails

$ cd ~/workspace/hello_app/
$ rails server
=> Booting WEBrick
=> Rails application starting on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server

1.7

IDE

Rails

$ cd ~/workspace/hello_app/
$ rails server -b $IP -p $PORT
=> Booting WEBrick
=> Rails application starting on http://0.0.0.0:8080
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
rails server

1.6

1.7

Ctrl-C

http://localhost:3000/

7.

3.1

8.

14 -

install
80

bundle

bundle install

IDE

Share

Application

1.8

1.9

1.6
About your applications environment
1.10

Rails

Rails

1.3.4

1.3.

- 15

1.7

1.8

16 -

Rails

1.9

rails server

Rails

1.10

1.3.

- 17

1.3.3.

Rails

1.11
1.4

app/
trollers

logic

Rails

business logic

models

MVC

views

con-

domain
Web

GUI

1.11
Rails

Rails

request

MVC
Web
view
model

Rails
HTML
Ruby

HTML

2.2.2
3.2

18 -

MVC
MVC
6.1

1.3.4

MVC
MVC

7.1.2

1.3.4. Hello, world!


MVC
2.2.2

hello, world!
Rails

hello, world!
1.9
ApplicationController

hello

ApplicationController

2
$ ls app/controllers/*_controller.rb
hello

render

1.8

hello, world!

Ruby

4
1.8
ApplicationController
app/controllers/application_controller.rb

hello

class ApplicationController < ActionController::Base


# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def hello
render text: "hello, world!"
end
end

Rails
1.11

Rails
1.11

2.2.2
URL

1.10

URL

http://www.example.com/

/
1.9
welcome

config/routes.rb

Rails

index
1.10

troller

hello

Rails

ApplicationCon-

1.1.2

1.9
config/routes.rb
Rails.application.routes.draw do
.
.
.
# You can have the root of your site routed with "root"
# root 'welcome#index'
.
.
.
end

1.3.

- 19

1.10
config/routes.rb
Rails.application.routes.draw do
.
.
.
# You can have the root of your site routed with "root"
root 'application#hello'
.
.
.
end

1.8

1.10

1.12

1.4.

Git
Rails

20 -

hello, world!

hello, world!

1.12

Rails

Git

Git

Linus Torvalds

Linux
Scott Chacon

Git
9

Pro Git

Git

Rails

1.4.3

Git

1.5

1.4.1.
1.2.1
Rails.com

IDE

Git
Git

IDE

Install-

Git
$
$
$
$

git
git
git
git

config
config
config
config

--global
--global
--global
--global

user.name "Your Name"


user.email your.email@example.com
push.default matching
alias.co checkout

Git
co

Git
co

checkout

checkout

git co

$ git init
Initialized empty Git repository in /home/ubuntu/workspace/hello_app/.git/
git add -A
$ git add -A
.gitignore
rails new

Rails

10

.gitignore
status

staging area

$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)

9.
10.

http://git-scm.com/book/zh

https://selfstore.io/products/46

3.7

1.4.

Git

- 21

new
new
new
new
new
.
.
.

file:
file:
file:
file:
file:

.gitignore
Gemfile
Gemfile.lock
README.rdoc
Rakefile

commit

Git

$ git commit -m "Initialize repository"


[master (root-commit) df0a62f] Initialize repository
.
.
.
-m

-m

Git

-m

Git

1.4.4

git push
log
$ git log
commit df0a62f3f091e53ffa799309b3e32c27b0b38eb4
Author: Michael Hartl <michael@michaelhartl.com>
Date:
Wed August 20 19:44:43 2014 +0000
Initialize repository
q

1.4.2.

Git

app/controllers/
$ ls app/controllers/
application_controller.rb concerns/
$ rm -rf app/controllers/
$ ls app/controllers/
ls: app/controllers/: No such file or directory

Unix

ls

$ git status
On branch master

22 -

app/controllers/

rm

-rf

Changed but not updated:


(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted:

app/controllers/application_controller.rb

no changes added to commit (use "git add" and/or "git commit -a")

checkout

-f

$ git checkout -f
$ git status
# On branch master
nothing to commit (working directory clean)
$ ls app/controllers/
application_controller.rb concerns/

1.4.3. Bitbucket
Git
Git
Git

Bitbucket
Bitbucket

GitHub

1.4 GitHub
Git

GitHub

Bitbucket
1.4

Bitbucket

Bitbucket

Bitbucket
GitHub
Bitbucket

GitHub
Web

Web

Bitbucket

GitHub
Github

Bitbucket

Bitbucket
1.

Bitbucket

2.

IDE

cat

1.11
1.11

bucket

Bit-

Git

- 23

1.4.

3.

Manage account

SSH keys SSH

1.13

1.13
1.11

SSH

cat

$ cat ~/.ssh/id_rsa.pub

Create
This is a private repository
Command line > I have an existing project
1.12
you sure you want to continue connecting (yes/no)?
1.12

1.14
Create repository
>

1.12
Are
yes

Bitbucket

$ git remote add origin git@bitbucket.org:<username>/hello_app.git


$ git push -u origin --all #

Git
-u

Bitbucket
git set upstream

$ git remote add origin git@bitbucket.org:mhartl/hello_app.git

Bitbucket

hello_app

1.15

24 -

<username>

1.14

Bitbucket

1.15

Bitbucket

1.4.

Git

- 25

1.4.4.
1.4.3

README.rdoc

Bitbucket
1.16
rdoc
Markdown

README

Bitbucket
README.rdoc

README.md

Git

1.16

11

Bitbucket

README

Git
master

checkout

topic branch

$ git checkout -b modify-README


Switched to a new branch 'modify-README'
$ git branch

11.

26 -

Git

Atlassian

SourceTree

-b

master
* modify-README
git branch

checkout -b modify-README

git

modify-README
co

1.4

git co -b modify-README
12

README

RDoc
.md

Markdown

Bitbucket

Git

Unix mv
$ git mv README.rdoc README.md
README.md

1.13
README

1.13

README.md

# Ruby on Rails Tutorial: "hello, world!"


This is the first application for the
[*Ruby on Rails Tutorial*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/).

$ git status
On branch modify-README
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed:

README.rdoc -> README.md

Changes not staged for commit:


(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified:

README.md
git add -A

1.4.1.2
git mv

12.

Pro Git

Git

git commit

-a

Git

1.4.

Git

- 27

$ git commit -a -m
2 files changed, 5
delete mode 100644
create mode 100644

"Improve the README file"


insertions(+), 243 deletions(-)
README.rdoc
README.md

-a

git add -A

Git
Git
Git
Shiny new commit styles

$ git checkout master


Switched to branch 'master'
$ git merge modify-README
Updating 34f06b7..2c92bef
Fast forward
README.rdoc
| 243 -------------------------------------------------README.md
|
5 +
2 files changed, 5 insertions(+), 243 deletions(-)
delete mode 100644 README.rdoc
create mode 100644 README.md
34f06b7

Git

Git

git branch -d

$ git branch -d modify-README


Deleted branch modify-README (was 2c92bef).

git branch -D
#
$
$
$
$
$
$

git checkout -b topic-branch


<really screw up the branch>
git add -A
git commit -a -m "Major screw up"
git checkout master
git branch -D topic-branch
-d

-D

README

1.4.3

28 -

Bitbucket
origin master

git push

$ git push

Bitbucket

Markdown

1.17

1.17

README

Markdown

1.5.
Rails
13

Rails

Rails

Phusion Passenger
Engine Yard
Rails Machine
Heroku
Heroku

13.
14.

Rails
Git

Apache

Nginx 14 Web
Engine Yard Cloud

Heroku

Rails

Web

Ninefold

Rails
1.4

Git
Heroku

1.5.4
Engine X

1.5.

- 29

1.5.1.

Heroku
PostgreSQL15

Heroku

pg

Rails

PostgreSQL

group :production do
gem 'pg',
'0.17.1'
gem 'rails_12factor', '0.0.2'
end
rails_12factor
Gemfile

Heroku

gem

1.14
1.14

gem

Gemfile

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem
gem

'rails',
'sass-rails',
'uglifier',
'coffee-rails',
'jquery-rails',
'turbolinks',
'jbuilder',
'sdoc',

group
gem
gem
gem
gem
end

'4.2.0'
'5.0.0.beta1'
'2.5.3'
'4.1.0'
'4.0.0.beta2'
'2.3.0'
'2.2.3'
'0.4.0', group: :doc

:development, :test do
'sqlite3',
'1.3.9'
'byebug',
'3.4.0'
'web-console', '2.0.0.beta3'
'spring',
'1.1.3'

group :production do
gem 'pg',
'0.17.1'
gem 'rails_12factor', '0.0.2'
end
bundle install

gem

pg

rails_12factor

$ bundle install --without production

1.14

gem
pg

gem

rails_12factor

Gemfile.lock

$ git commit -a -m "Update Gemfile.lock for Heroku"

15.

post-gres-cue-ell

Postgres

16.

SQLite
PostgreSQL

30 -

3.1

16

Heroku

Heroku

Heroku
$ heroku version

IDE

heroku

Heroku
Heroku Toolbelt
heroku

Heroku

SSH

$ heroku login
$ heroku keys:add
heroku create

Heroku

1.15
1.15

Heroku

$ heroku create
Creating damp-fortress-5769... done, stack is cedar
http://damp-fortress-5769.herokuapp.com/ | git@heroku.com:damp-fortress-5769.git
Git remote heroku added
heroku

1.5.2. Heroku
Git

Heroku

$ git push heroku master

7.5

1.5.3. Heroku
heroku create

1.15

IDE

heroku open

1.18

1.12

1.5.

- 31

1.18

Heroku

1.5.4. Heroku
Heroku
$ heroku rename rails-tutorial-hello

Heroku

hwpcbmze.herokuapp.com
seyjhflo.herokuapp.com
jhyicevg.herokuapp.com

Ruby

('a'..'z').to_a.shuffle[0..7].join
17

Heroku
Heroku

Heroku

Heroku

Heroku

17.

32 -

Heroku

Github Pages

http://railstutorial-china.org

1.6.

Rails
Facebook
Ruby on Rails
http://railstutorial-china.org/

Ruby on Rails

Rails

1.6.1.
Ruby on Rails

Ruby

Web
Rails

rails

Rails

rails new

rails server

hello, world!

Git
Bitbucket

Heroku

1.7.
1.

hello

1.8

hello, world!

Hola, mundo!
hello

2.
world!

18.

hola, mundo!

Rails
1.8
1.10

ASCII

18

1.19

goodbye
goodbye

goodbye,
1.20

invalid multibyte character

1.6.

- 33

1.19

1.20

34 -

hello

Hola, mundo!

goodbye, world!

Rails
Ruby on Rails

Web

1.2

URL
Rails

Rails

REST
users

microposts

Twitter
3
Rails

2.1.
rails new

1.3
$ cd ~/workspace
$ rails _4.2.0_ new toy_app
$ cd toy_app/
rails new

Rails

Could not find railties


1.1

Rails
1.2.1

IDE

Refresh File Tree


Gemfile

2.1

2.1

Gemfile

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem
gem

'rails',
'sass-rails',
'uglifier',
'coffee-rails',
'jquery-rails',
'turbolinks',
'jbuilder',
'sdoc',

group
gem
gem
gem
gem
end

'4.2.0'
'5.0.0.beta1'
'2.5.3'
'4.1.0'
'4.0.0.beta2'
'2.3.0'
'2.2.3'
'0.4.0', group: :doc

:development, :test do
'sqlite3',
'1.3.9'
'byebug',
'3.4.0'
'web-console', '2.0.0.beta3'
'spring',
'1.1.3'

35

group :production do
gem 'pg',
'0.17.1'
gem 'rails_12factor', '0.0.2'
end

2.1
1.5.1

1.14
--without production

gem

gem

$ bundle install --without production

Git
$ git init
$ git add -A
$ git commit -m "Initialize repository"

Bitbucket

Create

$ git remote add origin git@bitbucket.org:<username>/toy_app.git


$ git push -u origin --all #

2.1

36 -

Bitbucket

2.1

1.8

1.9

Heroku
$ git commit -am "Add hello"
$ heroku create
$ git push heroku master

1.5

7.5

Heroku

1.18
Web
2.1.1

data model

2.1.2

2.1.1.

id
email

integer

name

string

string

2.2

2.2
6.1.1

2.2

users

id

name

email

2.1.2.
id
2

content

text
user_id

2.3

2.3

1.
2.

Rails

string

Heroku

text

2.1.

- 37

user_id

2.3.3

11

2.2.
2.1.1
Users Resource
read

ate

scaffold
User

Web

update

rails generate

cre-

HTTP
delete

Rails

Rails

scaffold

$ rails generate scaffold User name:string email:string


invoke active_record
create
db/migrate/20140821011110_create_users.rb
create
app/models/user.rb
invoke
test_unit
create
test/models/user_test.rb
create
test/fixtures/users.yml
invoke resource_route
route
resources :users
invoke scaffold_controller
create
app/controllers/users_controller.rb
invoke
erb
create
app/views/users
create
app/views/users/index.html.erb
create
app/views/users/edit.html.erb
create
app/views/users/show.html.erb
create
app/views/users/new.html.erb
create
app/views/users/_form.html.erb
invoke
test_unit
create
test/controllers/users_controller_test.rb
invoke
helper
create
app/helpers/users_helper.rb
invoke
test_unit
create
test/helpers/users_helper_test.rb
invoke
jbuilder
create
app/views/users/index.json.jbuilder
create
app/views/users/show.json.jbuilder
invoke assets
invoke
coffee
create
app/assets/javascripts/users.js.coffee
invoke
scss
create
app/assets/stylesheets/users.css.scss
invoke scss
create
app/assets/stylesheets/scaffolds.css.scss

User

3.

38 -

Users

name:string
id

email:string

2.2

Rails

primary key

Rake

2.1

migrate

$ bundle exec rake db:migrate


== CreateUsers: migrating ==============================================
-- create_table(:users)
-> 0.0017s
== CreateUsers: migrated (0.0018s) =====================================

6.1.1
Gemfile

bundle exec

Rake

rake

IDE

bundle exec

bundle exec

Web
$ rails server -b $IP -p $PORT

1.7

`rails server`

1.3.2

IDE

IDE

2.1
Unix

Rake

make

$ ./configure && make && sudo make install

Unix
Rake

Linux
Ruby

Mac OS X

make
Ruby
Web

make

Rails

Rake

rake db:migrate

rake -T db
$ bundle exec rake -T db

Rake
$ bundle exec rake -T

2.2.1.
URL http://localhost:3000/

4. rails

Rails

1.9
/users

/users/

bundle exec

2.2.

- 39

new

2.1
URL

2.1

URL

URL
/users

index

/users/1

show

/users/new

new

/users/1/edit

edit

2.4

http://localhost:3000

ID

ID

2.4

/users
2.5

IDE

Create User
URL

40 -

2.6
/users/1

flash message
2.2
id 7.1

7.4.2

2.5

2.6

/users/new

/users/1

2.2.

- 41

2.7
2.8

Update User
6

9.1

2.7

/users/1/edit
2.9

2.10
JavaScript Rails

42 -

JavaScript

9.4

2.10

7.1

2.8

2.9

/users

2.2.

- 43

2.10

2.2.2. MVC
MVC

1.

1.3.3
MVC

2.11

/users

2. Rails

/users

UsersController

3. index

index

User.all

4.
5.
@users

6.
7.

Ruby

8.

HTML

HTML
5

HTML

5.

44 -

index

Web

Apache

Nginx

2.11

Rails

MVC
1

Rails

3.2

URL
URL

2.1

2.2

:users

Symbol

2.2
4.3.3

Rails

config/routes.rb
Rails.application.routes.draw do
resources :users
.
.
.
end

/users
1.10
2.2.

- 45

# root 'welcome#index'

root 'application#hello'
ApplicationController

hello

UsersController
ApplicationController

2.3

index

hello

UsersController

2.3
config/routes.rb

Rails.application.routes.draw do
resources :users
root 'users#index'
.
.
.
end
UsersController

2.2.1

class UsersController < ApplicationController

4.4
2.4
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def index
.
.
.
end
def show
.
.
.
end
def new
.
.
.
end
def edit
.
.
.

46 -

2.4
Ruby

2.3.4

end
def create
.
.
.
end
def update
.
.
.
end
def destroy
.
.
.
end
end
index
create

update

show

new

edit

2.2.1

destroy

2.2
2.2

REST

Representational State Transfer


/users/1

Rails
Roy Fielding

show

HTTP

HTTP

URL

GET

/users

index

GET

/users/1

show

GET

/users/new

new

POST

/users

create

GET

/users/1/edit

edit

PATCH

/users/1

update

ID

DELETE

/users/1

destroy

ID

2000

update

3.3

2.2

6.
chitectures

2.2

2.2

REST

HTTP

REST

Roy Thomas Fielding

ID

ID

Architectural Styles and the Design of Network-based Software Ar-

2.2.

- 47

2.2

REST

Ruby on Rails Web


REpresentational State Transfer
WWW Web
read

update

POST

GET

PATCH

Rails

REST
REST
REST
resource

Rails

DELETE

3.3

REST

create
CRUD

delete

3.2

HTTP

HTTP

REST


REST

12

index

2.5

index

2.5
app/controllers/users_controller.rb

class UsersController < ApplicationController


.
.
.
def index
@users = User.all
end
.
.
.
end
index

@users = User.all

4
2.6
Active Record

2.11
3
at-users
2.3.4

@users

5
4.4

Rails

User.all

2.6
app/models/user.rb
class User < ActiveRecord::Base
end
@users

2.7

@users

7. Rails

48 -

2.7
index.html.erb

instance variable

HTML

PUT

HTTP

PATCH

2.7
app/views/users/index.html.erb
<h1>Listing users</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th colspan="3"></th>
</tr>
</thead>
<% @users.each do |user| %>
<tr>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td><%= link_to 'Show', user %></td>
<td><%= link_to 'Edit', edit_user_path(user) %></td>
<td><%= link_to 'Destroy', user, method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
<br>
<%= link_to 'New User', new_user_path %>

HTML

2.2.3.
Rails

2.3.
2.2
Rails

REST

2.3.

- 49

2.3.1.
rails generate scaffold

2.3

$ rails generate scaffold Micropost content:text user_id:integer


invoke active_record
create
db/migrate/20140821012832_create_microposts.rb
create
app/models/micropost.rb
invoke
test_unit
create
test/models/micropost_test.rb
create
test/fixtures/microposts.yml
invoke resource_route
route
resources :microposts
invoke scaffold_controller
create
app/controllers/microposts_controller.rb
invoke
erb
create
app/views/microposts
create
app/views/microposts/index.html.erb
create
app/views/microposts/edit.html.erb
create
app/views/microposts/show.html.erb
create
app/views/microposts/new.html.erb
create
app/views/microposts/_form.html.erb
invoke
test_unit
create
test/controllers/microposts_controller_test.rb
invoke
helper
create
app/helpers/microposts_helper.rb
invoke
test_unit
create
test/helpers/microposts_helper_test.rb
invoke
jbuilder
create
app/views/microposts/index.json.jbuilder
create
app/views/microposts/show.json.jbuilder
invoke assets
invoke
coffee
create
app/assets/javascripts/microposts.js.coffee
invoke
scss
create
app/assets/stylesheets/microposts.css.scss
invoke scss
identical
app/assets/stylesheets/scaffolds.css.scss

Spring
2.2
$ bundle exec rake db:migrate
== CreateMicroposts: migrating ===============================================
-- create_table(:microposts)
-> 0.0023s
== CreateMicroposts: migrated (0.0026s) ======================================

generate Micropost

8.

50 -

2.2.1

Rails
2.8
2.3

MicropostsController

URL
2.8

resources :micropsts

Rails

config/routes.rb
Rails.application.routes.draw do
resources :microposts
resources :users
.
.
.
end

2.3

2.8

REST

HTTP

URL

GET

/microposts

index

GET

/microposts/1

show

GET

/microposts/new

new

POST

/microposts

create

GET

/microposts/1/edit

edit

PATCH

/microposts/1

update

ID

DELETE

/microposts/1

destroy

ID

MicropostsController

ID

ID

2.9

ostsController

UsersController

2.4

Microp-

REST

MicropostsController
app/controllers/microposts_controller.rb

2.9

class MicropostsController < ApplicationController


.
.
.
def index
.
.
.
end
def show
.

9.

2.8

Ruby

2.3.

- 51

.
.
end
def new
.
.
.
end
def edit
.
.
.
end
def create
.
.
.
end
def update
.
.
.
end
def destroy
.
.
.
end
end

/microposts/new

2.12
user_id

2.2.1

52 -

2.13

2.12

2.13

/microposts

2.3.

- 53

2.3.2.

Rails

tion

140
IDE
2.10

validaTwitter
2.10

app/models/micropost.rb

140

app/models/micropost.rb
class Micropost < ActiveRecord::Base
validates :content, length: { maximum: 140 }
end

6.2

140
2.14

Rails

7.3.3

2.14

2.3.3.
Rails

association
2.11

2.12
2.11
app/models/user.rb

54 -

class User < ActiveRecord::Base


has_many :microposts
end

2.12
app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :content, length: { maximum: 140 }
end
microposts

2.15

user_id

Rails

Active Record

2.15
11

12

Twitter
Rails

console
rails console

User.first

first_user

10

$ rails console
>> first_user = User.first
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2014-07-21 02:01:31", updated_at: "2014-07-21 02:01:31">
>> first_user.microposts
=> [#<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2014-07-21 02:37:37", updated_at: "2014-07-21 02:37:37">, #<Micropost id: 2,
content: "Second micropost", user_id: 1, created_at: "2014-07-21 02:38:54",
updated_at: "2014-07-21 02:38:54">]
>> micropost = first_user.microposts.first
# Micropost.first would also work.
=> #<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2014-07-21 02:37:37", updated_at: "2014-07-21 02:37:37">
>> micropost.user
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2014-07-21 02:01:31", updated_at: "2014-07-21 02:01:31">
>> exit
exit

Ctrl-D

first_user.microposts

ID

Ctrl-C

Ctrl-d

user_id

first_user

12

2.1.1 :001 >

10.
11.

Active Record
11

11

>>

Ruby

Ctrl-D

2.3.

- 55

2.3.4.
Rails

Object-oriented Programming

OOP

OOP
4.4

<

ActiveRecord::Base

2.13
Active Record

2.14

User

Micropost

2.16

ActiveRecord::Base

Ruby

2.13 User
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
end

2.14 Mcropost
app/models/micropost.rb
class Micropost < ActiveRecord::Base
.
.
.
end

2.16
2.15
croposts Controller
ActionController::Base

ApplicationController
ActionController::Base

2.17
UsersController
app/controllers/users_controller.rb

2.15

56 -

Rails

2.16
2.17
Action Pack

UsersController
ApplicationController

Mi-

class UsersController < ApplicationController


.
.
.
end

2.16 MicropostsController
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
.
.
.
end

2.17 ApplicationController
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
.
.
.
end

2.17

UsersController

MicropostsController

ActionController::Base

UsersController

HTTP

MicropostsController

HTML Rails

ApplicationController

8.4

ApplicationController

2.3.5.
Bitbucket
$ git status
$ git add -A

2.3.

- 57

$ git commit -m "Finish toy app"


$ git push

1.5

Heroku

$ git push heroku

2.1
ate

Heroku

git push heroku master

$ heroku run rake db:migrate

Heroku
PostgreSQL

2.18

2.18

2.4.
Rails

58 -

Rails

hreoku cre-

MVC

REST

Web

2.4.1.

Web

Rails

MVC

Rails

Web
REST

URL

Rails
Rails

Rails

Rails

2.5.
1.

2.18
2.19

2.

2.19

FILL_IN

name

email

2.20
2.18
app/models/micropost.rb

2.5.

- 59

class Micropost < ActiveRecord::Base


belongs_to :user
validates :content, length: { maximum: 140 },
presence: true
end

2.19
app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts
validates FILL_IN, presence: true
validates FILL_IN, presence: true
end

2.19

60 -

2.20

2.5.

- 61

Rails

HTML
Rails
automated testing

3.1.
2

sample_app

Rails

3.1

3.1
$ cd ~/workspace
$ rails _4.2.0_ new sample_app
$ cd sample_app/

2.1

IDE

Gemfile

2.1
1.5

test

2.1

gem

gem

gem

3.2
3.7

11.66

Gemfile

3.2

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem

'rails',
'sass-rails',
'uglifier',
'coffee-rails',
'jquery-rails',
'turbolinks',
'jbuilder',

1.

IDE

'4.2.0'
'5.0.0.beta1'
'2.5.3'
'4.1.0'
'4.0.0.beta2'
'2.3.0'
'2.2.3'

Goto Anything
Gemfile

workspace

Gemfile Gemfile.lock
rm -rf hello_app/ toy_app/
1.1

Bitbucket

63

gem 'sdoc',

'0.4.0', group: :doc

group
gem
gem
gem
gem
end

:development, :test do
'sqlite3',
'1.3.9'
'byebug',
'3.4.0'
'web-console', '2.0.0.beta3'
'spring',
'1.1.3'

group
gem
gem
gem
end

:test do
'minitest-reporters', '1.0.5'
'mini_backtrace',
'0.1.3'
'guard-minitest',
'2.3.1'

group :production do
gem 'pg',
'0.17.1'
gem 'rails_12factor', '0.0.2'
end
bundle install
production

Gemfile

--without

gem

gem

$ bundle install --without production

PostgreSQL

pg gem

SQLite Heroku
SQLite

PostgreSQL

Gemfile

Rails

bundle update

gem
gem

$ bundle update

Git
$ git init
$ git add -A
$ git commit -m "Initialize repository"
README

RDoc

Markdown

$ git mv README.rdoc README.md

3.3

2.

Bundler

3.
configure postgresql <your system>

64 -

bundle install
rails postgresql setup

IDE

PostgreSQL
<your system>

install
Ubuntu

README

3.3

# Ruby on Rails Tutorial: sample application


This is the sample application for the
[*Ruby on Rails Tutorial:
Learn Web Development with Rails*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/).

$ git commit -am "Improve the README"


git commit -a -m "Message"

1.4.4
-m

-a

git commit -am "Message"

Bitbucket
$ git remote add origin git@bitbucket.org:<username>/sample_app.git
$ git push -u origin --all #

1.8

Heroku
hello, world!

1.9

Heroku
$ git commit -am "Add hello"
$ heroku create
$ git push heroku master

1.5

7.5

Heroku

1.18

Heroku
$ heroku logs

Heroku

7.5

Unicorn

3.2.

Rails

HTML Rails
2

1.3
app/controllers

1.4.4

Git

Rails

MVC
C
REST
1.4

1.3.3

app/views

Git

$ git checkout master


$ git checkout -b static-pages

3.2.

- 65

master

static-pages

3.2.1.
generate

2
StaticPages

home

help

about

3.3

3.4

3.4
$ rails generate controller StaticPages home help
create app/controllers/static_pages_controller.rb
route get 'static_pages/help'
route get 'static_pages/home'
invoke erb
create
app/views/static_pages
create
app/views/static_pages/home.html.erb
create
app/views/static_pages/help.html.erb
invoke test_unit
create
test/controllers/static_pages_controller_test.rb
invoke helper
create
app/helpers/static_pages_helper.rb
invoke
test_unit
create
test/helpers/static_pages_helper_test.rb
invoke assets
invoke
coffee
create
app/assets/javascripts/static_pages.js.coffee
invoke
scss
create
app/assets/stylesheets/static_pages.css.scss
rails generate

rails g

Rails

3.1

Rails

3.1

Rails

$ rails server

$ rails s

$ rails console

$ rails c

$ rails generate

$ rails g

$ bundle install

$ bundle

$ rake test

$ rake

Git

66 -

generate

$
$
$
$

git
git
git
git

status
add -A
commit -m "Add a Static Pages controller"
push -u origin static-pages
static-pages

Bitbucket

$ git push

3.4
StaticPages

static_pages_controller.rb

$ rails generate controller static_pages ...


static_pages_controller.rb

Rails

Ruby
Ruby

4.4

underscore

3.1
Rails

3.1
Rails

Rails

Rails

3.4
2.2

rails generate

2.3

routes.rb

rails destroy

Rails

$ rails generate controller StaticPages home help


$ rails destroy controller StaticPages home help

6
$ rails generate model User name:string email:string

$ rails destroy model User

3.2.

- 67

$ bundle exec rake db:migrate

$ bundle exec rake db:rollback

$ bundle exec rake db:migrate VERSION=0

config/routes.rb

3.4
URL
Rails

home

3.5

config

help

home

config/routes.rb
Rails.application.routes.draw do
get 'static_pages/home'
get 'static_pages/help'
.

config

3.1

3.1

68 -

2.11

1.3.4

help

3.5

.
.
end

get 'static_pages/home'
home

/static_pages/home
GET HTTP

get

home

3.2

/static_pages/home

1.3.2
$ rails server -b $IP -p $PORT

/static_pages/home

GET

Hypertext Transfer Protocol


Rails

`rails server`

3.2

3.2

/static_pages/home

3.2 GET
HTTP

GET

Chrome Firefox
Apache

POST

PATCH

DELETE

Safari

Nginx
REST

Web
Rails
Web

3.2.

- 69

Rails

HTTP

REST

GET

HTTP
http://www.google.com

POST

POST

POST

Rails

POST

HTTP

POST
PATCH

GET

GET

http://www.wikipedia.org

DELETE

POST

Web

Rails

Rails

3.6
REST

2
REST

3.6

3.4

app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
end
def help
end
end
class

static_pages_controller.rb

troller

StaticPagesCon-

home
<

2.3.4

StaticPagesController

Rails

help

def

ApplicationController

4.4

def home
end
def help
end

Ruby
Ruby
/static_pages/home
MVC
V

ApplicationController
home

/static_pages/home

home

HTML
70 -

.erb

3.4
3.7

Rails

home

Rails
1.3.3

3.4
home.html.erb

StaticPagesController

Rails

.html

3.7

app/views/static_pages/home.html.erb
<h1>StaticPages#home</h1>
<p>Find me in app/views/static_pages/home.html.erb</p>
help

3.8
3.8

app/views/static_pages/help.html.erb
<h1>StaticPages#help</h1>
<p>Find me in app/views/static_pages/help.html.erb</p>
h1
p

3.2.2.
3.4

Rails
HTML

3.9

Rails

3.10
3.9

HTML

app/views/static_pages/home.html.erb
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>

3.10

HTML

app/views/static_pages/help.html.erb
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
book</a>.
</p>

3.3

3.4

3.2.

- 71

3.3

3.4

72 -

3.3.

Rails

Driven Development

TDD

TDD

TDD

Test-

TDD
3.3

3.3

1.

regression

2.
3.
TDD

HTML

4.

Rails

David Heinemeier Hansson

TDD is dead. Long live testing

3.3.

- 73

3.3.1.

3.3

Rails

Ruby
rails generate controller

Rails

3.4

$ ls test/controllers/
static_pages_controller_test.rb

3.11
3.11

GREEN

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
end
test "should get help" do
get :help
assert_response :success
end
end

3.4

GET

get

:success

3.2

HTTP

200 OK
test "should get home" do
get :home
assert_response :success
end
home

GET
rake

bundle exec

5. 2.2

bundle exec

74 -

IDE

2.1

3.12

GREEN

$ bundle exec rake test


2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

GREEN

3.7.1
1

MiniTest
Spring

Rails

Ruby

3.7.3

Guard

3.3.2.
3.3

TDD

3.3.3

3.4.3

3.11

3.13
3.13

RED

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
end
test "should get help" do
get :help
assert_response :success
end
test "should get about" do
get :about
assert_response :success
end
end

help

6.

home

about

rake test

3.7.1

3.3.

- 75

3.14

RED

$ bundle exec rake test


3 tests, 2 assertions, 0 failures, 1 errors, 0 skips

3.3.3.
RED

GREEN

3.15

RED

$ bundle exec rake test


ActionController::UrlGenerationError:
No route matches {:action=>"about", :controller=>"static_pages"}

3.5

3.16
about

3.16

RED

config/routes.rb
Rails.application.routes.draw do
get 'static_pages/home'
get 'static_pages/help'
get 'static_pages/about'
.
.
.
end

Rails

GET

/static_pages/about

about

3.17

RED

$ bundle exec rake test


AbstractController::ActionNotFound:
The action 'about' could not be found for StaticPagesController
about

3.6

3.18
about
app/controllers/static_pages_controller.rb

3.18

7.

76 -

RED

3.7.2

class StaticPagesController < ApplicationController


def home
end
def help
end
def about
end
end

$ bundle exec rake test


ActionView::MissingTemplate: Missing template static_pages/about

Rails

3.2.1

home

home.html.erb

app/views/static_pages
about.html.erb

Unix touch
$ touch app/views/static_pages/about.html.erb
touch

IDE

1.3.1

about.html.erb

3.19

3.19

GREEN

app/views/static_pages/about.html.erb
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
rake test

3.20

GREEN

$ bundle exec rake test


3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

3.5

3.3.

- 77

3.5

/static_pages/about

3.3.4.

3.4.3

3.4.

<title>

Search-Engine Optimization

SEO

<

ple App
rails new

78 -

> | Ruby on Rails Tutorial Sam-

3.2

$ mv app/views/layouts/application.html.erb layout_file

3.2
URL
/static_pages/home

"Ruby on Rails Tutorial Sample App"

"Home"

/static_pages/help

"Ruby on Rails Tutorial Sample App"

"Help"

/static_pages/about

"Ruby on Rails Tutorial Sample App"

"About"

3.4.1.
3.21
3.21

HTML

<!DOCTYPE html>
<html>
<head>
<title>Greeting</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>

HTML

document type declaration


HTML5

body

head

doctype
title

Greeting
HTML

Hello, world!

assert_select

3.2

assert_select

3.13
HTML

assert_select "title", "Home | Ruby on Rails Tutorial Sample App"


<title>

Home | Ruby on Rails Tutorial


3.22

Sample App
3.22

RED

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase

8. HTML
9. Rails

<!DOCTYPE html>

doctype
HTML

HTML5
MiniTest

3.4.

- 79

test "should get home" do


get :home
assert_response :success
assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
end
test "should get help" do
get :help
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
end
test "should get about" do
get :about
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App"
end
end

Ruby on Rails Tutorial Sample App

3.6

RED
3.23

RED

$ bundle exec rake test


3 tests, 6 assertions, 3 failures, 0 errors, 0 skips

3.4.2.
3.21
3.9

3.24
3.24

HTML

RED

app/views/static_pages/home.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Home | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>

3.6

80 -

10

HTML

3.6

3.25

HTML

3.25

3.26

RED

app/views/static_pages/help.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Help | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help
section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails
Tutorial</em> book</a>.
</p>
</body>
</html>

10.

Google Chrome

Safari

Chrome

3.4.

- 81

3.26

HTML

GREEN

app/views/static_pages/about.html.erb
<!DOCTYPE html>
<html>
<head>
<title>About | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>

GREEN
3.27

GREEN

$ bundle exec rake test


3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

3.4.3.

Ruby
Rails
Rails

HTML

Ruby on Rails Tutorial Sample App

HTML

Dont Repeat Yourself

Ruby Embedded Ruby

Rails
provide
Home
3.28

home.html.erb

3.28

Ruby

app/views/static_pages/home.html.erb

82 -

DRY

GREEN

DRY

<% provide(:title, "Home") %>


<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>

Ruby
.html.erb

ERb

HTML
11

ERb

<% provide(:title, 'Home') %>


<% %>

provide

Rails

<%= %>

"Home"

:title
13

yield

Ruby

12

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>


<% %>

Ruby

<%= %>

ERb

3.29

GREEN

GREEN

$ bundle exec rake test


3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

3.30

3.30
Ruby

3.31
GREEN

app/views/static_pages/help.html.erb
<% provide(:title, "Help") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>

11.

Haml

12.

Rails

13.

Ruby

content_for
Rails

provide

Asset Pipeline
Rails

3.4.

- 83

<body>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help
section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails
Tutorial</em> book</a>.
</p>
</body>
</html>

3.31

Ruby

GREEN

app/views/static_pages/about.html.erb
<% provide(:title, "About") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>

ERb
<% provide(:title, "The Title") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
Contents
</body>
</html>
title

Rails

84 -

body
application.html.erb

3.4

$ mv layout_file app/views/layouts/application.html.erb

Ruby
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

3.32
3.32

GREEN

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
<%= stylesheet_link_tag
'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

<%= yield %>

/static_pages/home

home.html.erb

<%=

HTML

yield %>

Rails
<%= stylesheet_link_tag ... %>
<%= javascript_include_tag "application", ... %>
<%= csrf_meta_tags %>

Rails

JavaScript

csrf_meta_tags

3.28

3.30

3.31
3.33

3.33

Asset Pipeline
5.2.1
Cross-Site Request Forgery

HTML

3.34

CSRF

HTML
3.35

GREEN

app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>

3.4.

- 85

sample application.
</p>

3.34

HTML

GREEN

app/views/static_pages/help.html.erb
<% provide(:title, "Help") %>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
book</a>.
</p>

3.35

HTML

GREEN

app/views/static_pages/about.html.erb
<% provide(:title, "About") %>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>

3.36

GREEN

$ bundle exec rake test


3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

3.4.4.
1.3.4
routes.rb

2.2.2

hello

3.1
get
root 'static_pages#home'

86 -

3.5

3.37

static_pages/home

static_pages#home

home

3.7

GET

/static_pages/home

3.37
config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get 'static_pages/help'
get 'static_pages/about'
end

3.7

3.5.

Rails

3.2
Git

$ git add -A
$ git commit -m "Finish static pages"

3.5.

- 87

1.4.4
$ git checkout master
$ git merge static-pages

1.4.3

Bitbuck-

et
$ git push

Heroku
$ bundle exec rake test
$ git push heroku

3.5.1.

Rails

gem

rails generate controller ControllerName <optional action names>

config/routes.rb

Rails

HTML

Ruby

ERb

Rails

3.6.

$ git checkout static-pages


$ git checkout -b static-pages-exercises

#
$
#
$
$
$
$

88 -

git commit -am "Eliminate repetition (solves exercise 3.1)"


git
git
git
git

add -A
commit -m "Add a Contact page (solves exercise 3.2)"
push -u origin static-pages-exercises
checkout master

1.

3.22
setup

Rails Tutorial Sample App


3.38
4.4.5
2.

Ruby on
3.38

2.2.2
4.2.2

14

Contact | Ruby on Rails Tutorial Sample App


3.39

3.39
3.38
3.38

3.13
/static_pages/contact

GREEN

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
def setup
@base_title = "Ruby on Rails Tutorial Sample App"
end
test "should get home" do
get :home
assert_response :success
assert_select "title", "Home | #{@base_title}"
end
test "should get help" do
get :help
assert_response :success
assert_select "title", "Help | #{@base_title}"
end
test "should get about" do
get :about
assert_response :success
assert_select "title", "About | #{@base_title}"
end
end

3.39

app/views/static_pages/contact.html.erb
<% provide(:title, "Contact") %>
<h1>Contact</h1>
<p>
Contact the Ruby on Rails Tutorial about the sample app at the
<a href="http://www.railstutorial.org/#contact">contact page</a>.
</p>

14.

5.3.1

3.6.

- 89

3.7.
3.7.1
3.7.2
3.7.3

$ git checkout master

3.7.1. MiniTest
Rails

3.40
3.2

minitest-reporters gem

3.40
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "minitest/reporters"
Minitest::Reporters.use!
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical
# order.
fixtures :all
# Add more helper methods to be used by all tests here...
end

IDE

3.8

3.8

rails new

15.
MiniTest

90 -

IDE

Ruby

4.2.2

15

3.7.2.

IDE
gem

Rails

3.2
rvm

IDE
er

mini_backtrace gem
Ruby Version Manag-

3.41
3.41

RVM

config/initializers/backtrace_silencers.rb
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't
# wish to see in your backtraces.
Rails.backtrace_cleaner.add_silencer { |line| line =~ /rvm/ }
# You can also remove all the silencers if you're trying to debug a problem
# that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

3.7.3.

Guard

rake test

Guard

stat-

Guard

ic_pages_controller_test.rb

Guard

home.html.erb

3.2

Guard
static_pages_controller_test.rb

guard gem

$ bundle exec guard init


Writing new Guardfile to /home/ubuntu/workspace/sample_app/Guardfile
00:51:32 - INFO - minitest guard added to Guardfile, feel free to edit it
Guardfile

Guard

3.42
3.42

Guardfile

# Defines the matching rules for Guard.


guard :minitest, spring: true, all_on_start: false do
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch('test/test_helper.rb') { 'test' }
watch('config/routes.rb')
{ integration_tests }
watch(%r{^app/models/(.*?)\.rb$}) do |matches|
"test/models/#{matches[1]}_test.rb"
end
watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|

3.7.

- 91

resource_tests(matches[1])
end
watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
["test/controllers/#{matches[1]}_controller_test.rb"] +
integration_tests(matches[1])
end
watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
integration_tests(matches[1])
end
watch('app/views/layouts/application.html.erb') do
'test/integration/site_layout_test.rb'
end
watch('app/helpers/sessions_helper.rb') do
integration_tests << 'test/helpers/sessions_helper_test.rb'
end
watch('app/controllers/sessions_controller.rb') do
['test/controllers/sessions_controller_test.rb',
'test/integration/users_login_test.rb']
end
watch('app/models/micropost.rb') do
['test/models/micropost_test.rb', 'test/models/user_test.rb']
end
watch(%r{app/views/users/*}) do
resource_tests('users') +
['test/integration/microposts_interface_test.rb']
end
end
# Returns the integration tests corresponding to the given resource.
def integration_tests(resource = :all)
if resource == :all
Dir["test/integration/*"]
else
Dir["test/integration/#{resource}_*.rb"]
end
end
# Returns the controller tests corresponding to the given resource.
def controller_test(resource)
"test/controllers/#{resource}_controller_test.rb"
end
# Returns all tests for the given resource.
def resource_tests(resource)
integration_tests(resource) << controller_test(resource)
end

guard :minitest, spring: true, all_on_start: false do

Guard

92 -

Rails

Spring

Guard

Spring
IDE

spring/

Git

.gitignore

Git

3.9
.gitignore

Show hidden files


.gitignore

3.11

3.10

3.43

3.9

3.7.

- 93

3.10

3.11

94 -

.gitignore

3.43
#
#
#
#
#
#

.gitignore

Spring

See https://help.github.com/articles/ignoring-files for more about ignoring


files.
If you find yourself ignoring temporary files generated by your text editor
or operating system, you probably want to add a global ignore instead:
git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.


/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
# Ignore Spring files.
/spring/*.pid

Spring

Spring
3.4

Spring

3.4
Unix
process

Linux

Unix

OS X

ps

aux

$ ps aux
|

Unix

ps

grep

$ ps aux | grep spring


ubuntu 12241 0.3 0.5 589960 178416 ? Ssl Sep20 1:46
spring app | sample_app | started 7 hours ago

ID
kill

pid

kill

pid

$ kill -9 12241
ps aux | grep server

Rails
spring

pid

spring

Spring

$ spring stop
pkill

spring

3.7.

- 95

$ pkill -9 -f spring
ps aux

kill -9 <pid>

pkill -9 -f <name>

Guard

1.3.2

Rails

$ bundle exec guard

3.42
guard>

Guard

96 -

Spring

Ctrl-D

Guard

3.42

Guard

3
Rails
Ruby

Rails
Rails

Rails

Ruby
Ruby
Ruby

Ruby

Ruby

4.1.
Ruby

Rails

Ruby
Rails
4.1

3.32

4.1
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
<%= stylesheet_link_tag
'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

<%= stylesheet_link_tag "application", media: "all",


"data-turbolinks-track" => true %>

Rails
application.css

stylesheet_link_tag

Rails API

Rails
Rails

Rails
helper

Ruby

4.1

<%= yield(:title) %> | Ruby on Rails Tutorial Sample App

97

provide
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>

provide

| Ruby on Rails Tutorial Sample App

full_title
full_title

Ruby on Rails Tutorial Sample App


4.2

full_title
app/helpers/application_helper.rb

4.2

module ApplicationHelper
#
def full_title(page_title = '')
base_title = "Ruby on Rails Tutorial Sample App"
if page_title.empty?
base_title
else
"#{page_title} | #{base_title}"
end
end
end

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

<title><%= full_title(yield(:title)) %></title>

4.3
4.3
full_title
app/views/layouts/application.html.erb

GREEN

1.

app/helper/static_pages_helper.rb
app/helper/application_helper.rb

98 -

Rails

Ruby

full_title

<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<%= stylesheet_link_tag
'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

Home
"Home"

4.4
@base_title

3.38
4.4

setup

RED

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
assert_select "title", "Ruby on Rails Tutorial Sample App"
end
test "should get help" do
get :help
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
end
test "should get about" do
get :about
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App"
end
end

4.5

RED

$ bundle exec rake test


3 tests, 6 assertions, 1 failures, 0 errors, 0 skips
provide

4.6

4.6

GREEN

app/views/static_pages/home.html.erb

4.1.

- 99

<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>

4.7

GREEN

$ bundle exec rake test


rake test

4.2

Rails

Ruby

4.2.
Ruby

Rails

Rails
Ruby

irb

Ruby
Rails
IDE

2.3.3
4.4.4

nano

irb

4.8

.irbrc
$ nano ~/.irbrc

4.8
4.8

irb
irb

~/.irbrc
IRB.conf[:PROMPT_MODE] = :SIMPLE
IRB.conf[:AUTO_INDENT_MODE] = false

$ rails console
Loading development environment
>>

Rails
7.1.1

Ctrl-C

2.

100 -

Ctrl-D

Ctrl-W

Rails

Ruby

Ctrl-E

nano

Ruby API

API

String

Ruby

4.2.1.
#

Ruby

Ruby

#
def full_title(page_title = '')
.
.
.
end

$ rails console
>> 17 + 42
#
=> 59

4.2.2.
Web

$ rails console
>> ""
#
=> ""
>> "foo"
#
=> "foo"
"

+
>> "foo" + "bar"
=> "foobar"
"foo"

"bar"

"foobar"

#{}

3.

foo bar

4.

Perl

PHP

foobar FUBAR

Jargon File

foo

"foo $bar"

4.2.

- 101

>>
=>
>>
=>

first_name = "Michael"
"Michael"
"#{first_name} Hartl"
"Michael Hartl"
"Michael"

>>
=>
>>
=>
>>
=>
>>
=>

#
#

first_name

first_name = "Michael"
"Michael"
last_name = "Hartl"
"Hartl"
first_name + " " + last_name
"Michael Hartl"
"#{first_name} #{last_name}"
"Michael Hartl"

"#{first_name} Hartl"

#
#

" "

puts

Ruby
>> puts "foo"
foo
=> nil

put ess

puts

puts "foo"

nil
nil

puts

>> print "foo"


foo=> nil
>> print "foo\n"
foo
=> nil

\n

puts

puts "foo"

Ruby

>>
=>
>>
=>

'foo'
"foo"
'foo' + 'bar'
"foobar"

Ruby
>> '#{foo} bar'
=> "\#{foo} bar"

102 -

Rails

Ruby

print

Ruby

\n
>> '\n'
=> "\\n"

Ruby

>> 'Newlines (\n) and tabs (\t) both use the backslash character \.'
=> "Newlines (\\n) and tabs (\\t) both use the backslash character \\."

Ruby

4.2.3.
nil

Ruby

4.4.2

length

>> "foobar".length
=> 6

>>
=>
>>
=>

length

true

Ruby

false

s = "foobar"
if s.empty?
"The string is empty"
else
"The string is nonempty"
end
"The string is nonempty"
elsif

5.

empty?

"foobar".empty?
false
"".empty?
true
empty?

>>
>>
>>
>>
>>
>>
=>

else + if

Ruby

4.2.

- 103

>>
>>
>>
>>
>>
>>
>>
=>

if s.nil?
"The variable is nil"
elsif s.empty?
"The string is empty"
elsif s.include?("foo")
"The string includes 'foo'"
end
"The string includes 'foo'"
&&

||

>> x = "foo"
=> "foo"
>> y = ""
=> ""
>> puts "Both strings are empty" if x.empty? && y.empty?
=> nil
>> puts "One of the strings is empty" if x.empty? || y.empty?
"One of the strings is empty"
=> nil
>> puts "x is not empty" if !x.empty?
"x is not empty"
=> nil
nil

Ruby

to_s

>> nil.to_s
=> ""

chain
>> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
>> nil.to_s.empty?
#
=> true
nil

>>
=>
>>
=>
>>
=>

empty?

nil.to_s

"foo".nil?
false
"".nil?
false
nil.nil?
true

puts "x is not empty" if !x.empty?


if

if

unless

104 -

Rails

Ruby

>> string = "foobar"


>> puts "The string '#{string}' is nonempty." unless string.empty?
The string 'foobar' is nonempty.
=> nil
nil
!!

false

Ruby

bang bang

>> !!nil
=> false

Ruby

>> !!0
=> true

4.2.4.
home

3.6

full_title

4.2

string_message
>> def string_message(str = '')
>>
if str.empty?
>>
"It's an empty string!"
>>
else
>>
"The string is nonempty."
>>
end
>> end
=> :string_message
>> puts string_message("foobar")
The string is nonempty.
>> puts string_message("")
It's an empty string!
>> puts string_message
It's an empty string!
def string_message(str =
'')

str

Ruby
str

Ruby

>> def string_message(str = '')


>>
return "It's an empty string!" if str.empty?
>>
return "The string is nonempty."
>> end
return
return

"The string is nonempty."

return

4.2.

- 105

str
the_function_argument
>> def string_message(the_function_argument = '')
>>
if the_function_argument.empty?
>>
"It's an empty string!"
>>
else
>>
"The string is nonempty."
>>
end
>> end
=> nil
>> puts string_message("")
It's an empty string!
>> puts string_message("foobar")
The string is nonempty.

4.2.5.
6

full_title

4.2

4.9

full_title
app/helpers/application_helper.rb

4.9

module ApplicationHelper
#
def full_title(page_title = '')
base_title = "Ruby on Rails Tutorial Sample App"
if page_title.empty?
base_title
else
"#{page_title} | #{base_title}"
end
end
end

#
#
#
#
#
#

module ApplicationHelper
include

Ruby
full_title

Rails

4.3.
Web

Rails
Ruby

6.

Rails

full_title

URL
Rails

Rails

106 -

Rails

Ruby

Obie Fernandez

The Rails 4 Way

Rails

4.3.1.
4.3.3
Rails

has_many

2.3.3

11.1.3
split

>> "foo bar


baz".split
=> ["foo", "bar", "baz"]

split

>> "fooxbarxbazx".split('x')
=> ["foo", "bar", "baz"]

Ruby

1
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

a = [42, 8, 17]
[42, 8, 17]
a[0]
42
a[1]
8
a[2]
17
a[-1]
17

# Ruby

Ruby

Ruby

>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

a
[42, 8, 17]
a.first
42
a.second
8
a.last
17
a.last == a[-1]
true

==

==
>>
=>
>>
=>

x = a.length
3
x == 3
true

second

7.
Rails

!=

Ruby

length

Ruby

Rails

Rails

Ruby

4.3.

- 107

>>
=>
>>
=>
>>
=>
>>
=>

x == 1
false
x != 1
true
x >= 1
true
x < 1
false

length
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

a
[42, 8, 17]
a.empty?
false
a.include?(42)
true
a.sort
[8, 17, 42]
a.reverse
[17, 8, 42]
a.shuffle
[17, 42, 8]
a
[42, 8, 17]
a

bang

>>
=>
>>
=>
>>
=>

a
[42, 8, 17]
a.sort!
[8, 17, 42]
a
[8, 17, 42]
push

>>
=>
>>
=>
>>
=>

<<

a.push(6)
#
6
[42, 8, 17, 6]
a << 7
#
7
[42, 8, 17, 6, 7]
a << "foo" << "bar"
#
[42, 8, 17, 6, 7, "foo", "bar"]

Ruby

split
>>
=>
>>
=>

108 -

join

a
[42, 8, 17, 7, "foo", "bar"]
a.join
#
"428177foobar"

Rails

Ruby

bang

>> a.join(', ')


=> "42, 8, 17, 7, foo, bar"

to_a

range

>> 0..9
=> 0..9
>> 0..9.to_a
#
to_a
9
NoMethodError: undefined method `to_a' for 9:Fixnum
>> (0..9).to_a
#
to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
0..9

>>
=>
>>
=>

a = %w[foo bar baz quux]


["foo", "bar", "baz", "quux"]
a[0..2]
["foo", "bar", "baz"]

# %w

-1

>>
=>
>>
=>
>>
=>

a = (0..9).to_a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
a[2..(a.length-1)]
[2, 3, 4, 5, 6, 7, 8, 9]
a[2..-1]
[2, 3, 4, 5, 6, 7, 8, 9]

#
#

-1

>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]

4.3.2.
block

Ruby

>> (1..5).each { |i| puts 2 * i }


2
4
6
8
10
=> 1..5
(1..5)

each

{ |i| puts 2 * i }

each

|i|
each

Ruby
i

each

4.3.

- 109

>> (1..5).each do |i|


?>
puts 2 * i
>> end
2
4
6
8
10
=> 1..5

do..end
>> (1..5).each do |number|
?>
puts 2 * number
>>
puts '-'
>> end
2
4
6
8
10
=> 1..5
number

map

>> 3.times { puts "Betelgeuse!" }


# 3.times
"Betelgeuse!"
"Betelgeuse!"
"Betelgeuse!"
=> 3
>> (1..5).map { |i| i**2 }
# **
=> [1, 4, 9, 16, 25]
>> %w[a b c]
#
=> ["a", "b", "c"]
>> %w[a b c].map { |char| char.upcase }
=> ["A", "B", "C"]
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
map
map

8.

110 -

closure

Rails

Ruby

%w

>>
=>
>>
=>

%w[A B C].map { |char| char.downcase }


["a", "b", "c"]
%w[A B C].map(&:downcase)
["a", "b", "c"]

4.3.3
Ruby

Rails

4.4
test "should get home" do
get :home
assert_response :success
assert_select "title", "Ruby on Rails Tutorial Sample App"
end
do

1.5.4

test

Ruby

('a'..'z').to_a.shuffle[0..7].join

>> ('a'..'z').to_a
#
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle
#
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7]
#
8
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join #
=> "mznpybuj"

4.3.3.
Hash
Perl

>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

user = {}
# {}
{}
user["first_name"] = "Michael"
#
"first_name"
"Michael"
user["last_name"] = "Hartl"
#
"last_name"
"Hartl"
user["first_name"]
#
"Michael"
user
#
{"last_name"=>"Hartl", "first_name"=>"Michael"}

"Michael"
"Hartl"

4.3.

- 111

{}
9

>> user = { "first_name" => "Michael", "last_name" => "Hartl" }


=> {"last_name"=>"Hartl", "first_name"=>"Michael"}

Ruby
Ruby

Rails

Symbol
:name

10

>> "name".split('')
=> ["n", "a", "m", "e"]
>> :name.split('')
NoMethodError: undefined method `split' for :name:Symbol
>> "foobar".reverse
=> "raboof"
>> :foobar.reverse
NoMethodError: undefined method `reverse' for :foobar:Symbol

Ruby

Rails

>> :foo-bar
NameError: undefined local variable or method `bar' for main:Object
>> :2foo
SyntaxError

user
>>
=>
>>
=>
>>
=>

user = { :name => "Michael Hartl", :email => "michael@example.com" }


{:name=>"Michael Hartl", :email=>"michael@example.com"}
user[:name]
#
:name
"Michael Hartl"
user[:password]
#
nil
nil

9.

Ruby 1.9

10.

112 -

Rails

Ruby

Ruby 1.9
>>
=>
>>
=>
>>
=>

h1 = { :name => "Michael Hartl", :email => "michael@example.com" }


{:name=>"Michael Hartl", :email=>"michael@example.com"}
h2 = { name: "Michael Hartl", email: "michael@example.com" }
{:name=>"Michael Hartl", :email=>"michael@example.com"}
h1 == h2
true

{ name: "Michael Hartl", email: "michael@example.com" }

JavaScript

Rails
:name

name:
"Michael Hartl" }

:name

name:

{ name: "Michael Hartl" }

{ :name
:name

4.10
4.10
>>
=>
>>
=>
>>
=>
>>
=>

params = {}
#
params parameters
{}
params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
{:name=>"Michael Hartl", :email=>"mhartl@example.com"}
params
{:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
params[:user][:email]
"mhartl@example.com"

Rails

each
:success

7.3
flash

:danger

>> flash = { success: "It worked!", danger: "It failed." }


=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?>
puts "Key #{key.inspect} has value #{value.inspect}"
>> end
Key :success has value "It worked!"
Key :danger has value "It failed."
each

each
each
inspect

>> puts (1..5).to_a


1
2
3
4

4.3.

- 113

5
>> puts (1..5).to_a.inspect
#
[1, 2, 3, 4, 5]
>> puts :name, :name.inspect
name
:name
>> puts "It worked!", "It worked!".inspect
It worked!
"It worked!"
inspect
>> p :name
:name

4.3.4.

'puts :name.inspect'

CSS
4.1

<%= stylesheet_link_tag 'application', media: 'all',


'data-turbolinks-track' => true %>

4.1

Rails

stylesheet_link_tag 'application', media: 'all',


'data-turbolinks-track' => true

Ruby

#
stylesheet_link_tag('application', media: 'all',
'data-turbolinks-track' => true)
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
media

#
stylesheet_link_tag 'application', { media: 'all',
'data-turbolinks-track' => true }
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
data-turbolinks-track
data-turbolinks-track: true

4.3.3

11.

114 -

Rails

Ruby

puts

nil

Katarzyna Siwek

11

'data-turbolinks-track' => true

Ruby
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
12

Ruby

80

13

stylesheet_link_tag 'application', media: 'all',


'data-turbolinks-track' => true
stylesheet_link_tag

Rails 4.0
%>

<%=

Turbolink

ERb

HTML

4.11

?body=1

CSS

Rails

CSS
4.11

CSS

HTML

<link data-turbolinks-track="true" href="/assets/application.css" media="all"


rel="stylesheet" />

http://localhost:3000/assets/application.css

CSS

4.4. Ruby
Ruby

Ruby

ming

Object-Oriented Program-

OOP

4.4.1.

>>
=>
>>
=>

s = "foobar"
"foobar"
s.class
String

class

\n

12.
13.
80
View > Wrap Column > 78

IDE
Sublime Text

View > Ruler > 78

1.5
TextMate
View > Ruler > 80

4.4. Ruby

- 115

named constructor

14

new
>>
=>
>>
=>
>>
=>

s = String.new("foobar")
"foobar"
s.class
String
s == "foobar"
true

>> a = Array.new([1, 3, 2])


=> [1, 3, 2]
Array.new

>>
=>
>>
=>
>>
=>
>>
=>

h = Hash.new
{}
h[:foo]
nil
h = Hash.new(0)
{}
h[:foo]
0

Hash.new

:foo

new

nil

new

class method
instance

length

instance method

4.4.2.
superclass
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

s = String.new("foobar")
"foobar"
s.class
#
s
String
s.class.superclass
#
String
Object
s.class.superclass.superclass # Ruby 1.9
BasicObject
s.class.superclass.superclass.superclass
nil
String

4.1
cObject

Ruby

Object

Object

Ruby

14.

116 -

BasicObject

Rails

Ruby

Ruby 1.9.3

BasicObject

Basi-

BasicObject

Ruby

4.1

Ruby

String

Word
palindrome?

true

>> class Word


>>
def palindrome?(string)
>>
string == string.reverse
>>
end
>> end
=> :palindrome?

>>
=>
>>
=>
>>
=>

w = Word.new
#<Word:0x22d0b20>
w.palindrome?("foobar")
false
w.palindrome?("level")
true

Word

Word

String

4.12
Word

4.12

Word

>> class Word < String


>>
#
>>
def palindrome?
>>
self == self.reverse
>>
end
>> end
=> nil

# Word
true

String

# self

4.4. Ruby

- 117

Word < String

>>
=>
>>
=>
>>
=>

Ruby

s = Word.new("level")
"level"
s.palindrome?
true
s.length
5

Word

palindrome?

3.2

Word

# Word

Word

"level"
palindrome?

# Word

String
>>
=>
>>
=>
>>
=>

s.class
Word
s.class.superclass
String
s.class.superclass.superclass
Object

4.2

4.2

4.12

Word

4.12
15

self

Word

Word

Ruby

self

self == self.reverse

self.

self == reverse

15.

118 -

Ruby

Rails

self

Ruby

RailsTips

Class and Instance Variables in Ruby

4.4.3.
palindrome?

String

palindrome?
>> "level".palindrome?
NoMethodError: undefined method `palindrome?' for "level":String

Ruby

>>
>>
>>
>>
>>
>>
=>
>>
=>

Ruby

class String
#
def palindrome?
self == self.reverse
end
end
nil
"deified".palindrome?
true

true

"deified"

Ruby

Rails

Web

blank

Rails

Rails

irb

Rails
>>
=>
>>
=>
>>
=>
>>
=>

blank?

Ruby

"".blank?
true
"
".empty?
false
"
".blank?
true
nil.blank?
true

empty
Rails

nil

Rails

nil

blank
blank?

String

Object

8.4

Ruby

4.4.4.
3.18
class StaticPagesController < ApplicationController
def home
end
def help
end

4.4. Ruby

- 119

def about
end
end
StaticPagesController
tionController

home

help

about

Applica-

Rails

Rails

16

>>
=>
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

controller = StaticPagesController.new
#<StaticPagesController:0x22855d0>
controller.class
StaticPagesController
controller.class.superclass
ApplicationController
controller.class.superclass.superclass
ActionController::Base
controller.class.superclass.superclass.superclass
ActionController::Metal
controller.class.superclass.superclass.superclass.superclass
AbstractController::Base
controller.class.superclass.superclass.superclass.superclass.superclass
Object

4.3

>> controller.home
=> nil
home

nil
home

StaticPagesController.new

Rails
Rails

Ruby

16.

120 -

Rails
Rails

Ruby

Rails
Ruby

Ruby

2005

Rails

Ruby

Ruby on Rails
Rails

4.3

4.4.5.
User

Ruby

example_user.rb

4.13

4.13

User

example_user.rb
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email

4.4. Ruby

- 121

"#{@name} <#{@email}>"
end
end

attr_accessor :name, :email

getter

Rails

attribute accessors
@name

setter

@email

2.2.2

3.6
Ruby

@
nil
initialize

User.new

Ruby

initialize

attributes
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
attributes
nil

4.3.3
nil

:name

attributes[:name]

attributes[:email]
formatted_email

@name

@email

def formatted_email
"#{@name} <#{@email}>"
end
@name

@email

formatted_email

require
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>
>>
=>

require './example_user'
#
example_user
true
example = User.new
#<User:0x224ceec @email=nil, @name=nil>
example.name
#
nil
attributes[:name]
nil
example.name = "Example User"
#
nil
"Example User"
example.email = "user@example.com"
#
nil
"user@example.com"
example.formatted_email
"Example User <user@example.com>"
.

Unix

attr_accessor
"Example User"

122 -

Rails

'./example_user'

Ruby

example.name = "Example User"


email

Ruby

formatted_email

nil

Ruby

@name

ini-

4.3.4
tialize
>>
=>
>>
=>

user = User.new(name: "Michael Hartl", email: "mhartl@example.com")


#<User:0x225167c @email="mhartl@example.com", @name="Michael Hartl">
user.formatted_email
"Michael Hartl <mhartl@example.com>"

mass assignment

Rails

4.5.
Ruby

5
example_user.rb

4.4.5
$ rm example_user.rb

Bitbucket
$
$
$
$
$

Heroku

git status
git commit -am "Add a full_title helper"
git push
bundle exec rake test
git push heroku

4.5.1.
Ruby

Ruby

Ruby

def

Ruby

class

Ruby
Ruby

Ruby

Ruby

deified

4.6.
1.
2.

split

4.14
4.15

shuffle

shuffle

join

String

4.5.

- 123

person1

3.
params
params[:child]

4.

person2

person3

params[:father]
person3

merge

Ruby API

4.14
def string_shuffle(s)
s.?('').?.?
end
string_shuffle("foobar")
"oobfra"

4.15
>>
>>
>>
>>
>>
>>
=>

124 -

String

class String
def shuffle
self.?('').?.?
end
end
"foobar".shuffle
"borafo"

Rails

Ruby

params[:mother]

params[:father][:first]

{ "a" => 100, "b" => 200 }.merge({ "b" => 300 })

>>
>>
>>
>>
=>

person1

:first

shuffle

:last
person2

Ruby

4.1
1

CSS

Sass 5.2

Pipeline
5.4

5.1

TDD

Rails

Asset

5.3.1
5.3.4

5.1.
Web
Twitter

Web
CSS
Bootstrap

Web

CSS

Web
mockup
3.2

Web

LOGO

5.1

5.7
Rails LOGO

1.
2.

Colm Tuite

Bootstrap
Mockingbird

125

5.1
Git
$ git checkout master
$ git checkout -b filling-in-layout

5.1.1.
application.html.erb

4.3

HTML
5.1

CSS
5.2

5.1
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

126 -

<%= csrf_meta_tags %>


<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
</script>
<![endif]-->
</head>
<body>
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", '#', id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
'#' %></li>
<li><%= link_to "Help",
'#' %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>
<div class="container">
<%= yield %>
</div>
</body>
</html>

3.4.1
html>

HTML5
JavaScript

Rails
IE

HTML5

<!DOCTYPE

HTML5 shim

<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
</script>
<![endif]-->

<!--[if lt IE 9]>

IE
Rails

if lt IE 9

[if lt IE 9]

IE
IE9

HTML5 shim
header

Firefox

Chrome

LOGO

Safari
div

<header class="navbar navbar-fixed-top navbar-inverse">


<div class="container">
<%= link_to "sample app", '#', id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
'#' %></li>
<li><%= link_to "Help",
'#' %></li>
<li><%= link_to "Log in", '#' %></li>

5.1.

- 127

</ul>
</nav>
</div>
</header>
header

header

navbar-fixed-top

CSS

navbar

navbar-inverse

<header class="navbar navbar-fixed-top navbar-inverse">

HTML

ID

CSS
ID
Bootstrap

5.1.2
header

5.1.2

ID
Bootstrap

div

<div class="container">
div

HTML
header

HTML5
CSS

div

nav

container

div

section
header

Bootstrap
div

ERb

<%= link_to "sample app", '#', id: "logo" %>


<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
'#' %></li>
<li><%= link_to "Help",
'#' %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
link_to

Rails

5.3.3
Web
CSS IDlogo

3.2.2

LOGO
Rails

div

HTML
ul

<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
'#' %></li>
<li><%= link_to "Help",
'#' %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>

128 -

Ruby

named route

Rails

3. CSS

link_to

li

nav

ul

right

Bootstrap

5.1.2

Bootstrap

Rails

nav

navbar-nav

pull-

CSS
4

ERb

<nav>
<ul class="nav navbar-nav pull-right">
<li><a href="#">Home</a></li>
<li><a href="#">Help</a></li>
<li><a href="#">Log in</a></li>
</ul>
</nav>

div
<div class="container">
<%= yield %>
</div>
container

Bootstrap

yield

3.4.3

5.1.3

home.html.erb

5.2

5.2

app/views/static_pages/home.html.erb
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails logo"),
'http://rubyonrails.org/' %>
link_to

<a href="#" class="btn btn-lg btn-primary">Sign up now!</a>


div

4.

CSS

jumbotron

btn

Bootstrap

HTML

btn-lg

btn-primary

3.4.1

5.1.

- 129

link_to

image_tag
alt

rails.png

Ruby on Rails

http://rubyonrails.org/images/
IDE Unix

app/assets/images/

rails.png

curl

$ curl -O http://rubyonrails.org/images/rails.png
$ mv rails.png app/assets/images/
image_tag

Rails

image_tag

Asset Pipeline

HTML

app/assets/images/

5.2

<img alt="Rails logo" src="/assets/rails-9308b8f92fea4c19a3a0d8385b494526.png" />


9308b8f92fea4c19a3a0d8385b494526

Rails
src

images

Rails

assets

JavaScript CSS

assets

app/assets/images
alt

5.2

5.

OS X

img

6.

130 -

Homebrew

CSS

brew install curl


<img></img>

<img />

curl

5.2
HTML

5.1.2. Bootstrap

CSS

CSS

HTML
Bootstrap

CSS

CSS
Bootstrap

Twitter

HTML5

Web
CSS

Bootstrap

bootstrap-sass

Bootstrap
Rails
LESS
bootstrap-sass
LESS

Bootstrap

gem

5.3

Rails Asset Pipeline


Bootstrap

Sass

Sass

5.3

bootstrap-sass

Gemfile

source 'https://rubygems.org'
gem 'rails',
gem 'bootstrap-sass',
.
.
.

'4.2.0'
'3.2.0.0'

bundle install

Bootstrap

$ bundle install
rails generate

CSS
CSS

CSS

$ touch app/assets/stylesheets/custom.css.scss
touch

3.3.3

app/assets/stylesheets/

Asset Pipeline

application.css

Asset Pipeline

custom.css.scss

.css

CSS

Sass

5.2.2

.scss

Sassy CSS

Sass

bootstrap-sass gem
@import

CSS
5.4

Bootstrap

Sprockets

5.4

Bootstrap

CSS

app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";

7.
8.

Asset Pipeline

LESS

less-rails-bootstrap gem
gem

5.1.

- 131

Bootstrap CSS

Web

5.3

Bootstrap CSS

CSS
5.4

5.5

5.5

CSS

app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";
/* universal */
html {
overflow-y: scroll;
}
body {
padding-top: 60px;
}
section {
overflow: auto;

132 -

5.5
CSS

/* ... */

rails server

Ctrl-C
LOGO

5.3

CSS

}
textarea {
resize: vertical;
}
.center {
text-align: center;
}
.center h1 {
margin-bottom: 10px;
}

5.4
5.5

CSS

CSS

ID

HTML

body {
padding-top: 60px;
}

60
Bootstrap 2.0

header
navbar-inverse

navbar-fixed-top

Bootstrap

CSS

5.1.

- 133

.center {
text-align: center;
}
.center

5.7

text-align: center;
#

.center
.center

ID
5.2

Bootstrap
5.6
5.5

5.6
app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";
.
.
.
/* typography */
h1, h2, h3, h4, h5, h6 {
line-height: 1;
}
h1 {
font-size: 3em;
letter-spacing: -2px;
margin-bottom: 30px;
text-align: center;
}
h2 {
font-size: 1.2em;
letter-spacing: -1px;
margin-bottom: 30px;
text-align: center;
font-weight: normal;
color: #777;
}
p {
font-size: 1.1em;
line-height: 1.7em;
}

134 -

div

5.5
sample app

LOGO

5.7
ID

5.7

CSS
LOGO

LOGO

app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";
.
.
.
/* header */
#logo {
float: left;
margin-right: 10px;
font-size: 1.7em;
color: #fff;
text-transform: uppercase;
letter-spacing: -1px;
padding-top: 9px;
font-weight: bold;
}
#logo:hover {

5.1.

- 135

color: #fff;
text-decoration: none;
}
color: #fff;

LOGO
#ffffff

CSS

HTML

3
#fff

3
white

HTML

#fff

16
#ffffff

5.7

5.6

5.6

LOGO

5.1.3.
5.1
IE
Rails
5.8
5.8

HTML shim
HTML

HTML shim

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

136 -

<%= csrf_meta_tags %>


<%= render 'layouts/shim' %>
</head>
<body>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
</div>
</body>
</html>

HTML shim

Rails

render

<%= render 'layouts/shim' %>


app/views/layouts/_shim.html.erb
9

<%= %>

Ruby

_shim.html.erb

HTML shim
5.9
5.9

HTML shim

app/views/layouts/_shim.html.erb
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
</script>
<![endif]-->
render

5.10

5.10
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", '#', id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
'#' %></li>
<li><%= link_to "Help",
'#' %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>

9.

Rails

shared
`layouts`

shared
shared

shared

5.1.

- 137

_footer.html.erb

10

5.11

5.11
app/views/layouts/_footer.html.erb
<footer class="footer">
<small>
The <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
</small>
<nav>
<ul>
<li><%= link_to "About",
'#' %></li>
<li><%= link_to "Contact", '#' %></li>
<li><a href="http://news.railstutorial.org/">News</a></li>
</ul>
</nav>
</footer>
link_to
header

footer

HTML5

HTML shim

5.12

5.12
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<%= stylesheet_link_tag "application", media: "all",
"data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
<%= render 'layouts/shim' %>
</head>
<body>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
<%= render 'layouts/footer' %>
</div>
</body>
</html>

5.13

10.

div

138 -

footer
footer

.footer

5.7

Bootstrap

5.7
5.13

CSS

app/assets/stylesheets/custom.css.scss
.
.
.
/* footer */
footer {
margin-top: 45px;
padding-top: 5px;
border-top: 1px solid #eaeaea;
color: #777;
}
footer a {
color: #555;
}
footer a:hover {
color: #222;
}
footer small {
float: left;
}

5.1.

- 139

footer ul {
float: right;
list-style: none;
}
footer ul li {
float: left;
margin-left: 15px;
}

5.2. Sass

Asset Pipeline

Rails

Asset Pipeline

CSS

CSS JavaScript
Asset Pipeline

Sass

5.2.1. Asset Pipeline


Asset Pipeline

Rails

Rails
11

public/

Rails 3.0
public/stylesheets
public/javascripts
public/images

http://example.com/stylesheets

Rails 3.0

Rails
app/assets
lib/assets
vendor/assets

$ ls app/assets/
images/ javascripts/ stylesheets/

5.1.2

custom.css.scss

custom.css.scss

app/assets/stylesheets

11.

140 -

Michael Erasmus
Asset Pipeline

The Rails 3 Asset Pipeline in (about) 5 Minutes

Rails

Rails
ets gem

Sprock-

CSS
JavaScript
5.14
5.14

app/assets/stylesheets/application.css
/*
* This is a manifest file that'll be compiled into application.css, which
* will include all the files listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets,
* vendor/assets/stylesheets, or vendor/assets/stylesheets of plugins, if any,
* can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear
* at the bottom of the compiled file so the styles you add here take
* precedence over styles defined in any styles defined in the other CSS/SCSS
* files in this directory. It is generally better to create a new file per
* style scope.
*
*= require_tree .
*= require_self
*/

CSS

Sprockets

/*
.
.
.
*= require_tree .
*= require_self
*/

*= require_tree .
app/assets/stylesheets

CSS

CSS

*= require_self
application.css

Rails
Asset Pipeline

CSS
Rails

5.2. Sass

Asset Pipeline - 141

Rails
Rails
.coffee

.erb

ERb

Script

3.4.3
JavaScript

ERb 5.2.2
RailsCast

Sass
.scss
Sass
CoffeeScript

CoffeeScript
Coffee-

foobar.js.coffee

CoffeeScript
foobar.js.erb.coffee

CoffeeScript

ERb

CoffeeScript

Asset Pipeline

CSS

JavaScript

Asset Pipeline
JavaScript

cation.css

appli-

CSS
JavaScript

application.js

5.2.2.
Sass

CSS

CSS

5.1.2

Sass

7.1.1
.scss

SCSS
12

CSS
SCSS

Rails

tom.css.scss

Asset Pipeline
Sass

CSS

SCSS

CSS
Bootstrap
.scss

Sass

5.5

.center

SCSS
cus-

.center h1

.center {
text-align: center;
}
.center h1 {

12. Sass

142 -

.sass

CSS

margin-bottom: 10px;
}

Sass
.center {
text-align: center;
h1 {
margin-bottom: 10px;
}
}
h1

.center

5.7
#logo {
float: left;
margin-right: 10px;
font-size: 1.7em;
color: #fff;
text-transform: uppercase;
letter-spacing: -1px;
padding-top: 9px;
font-weight: bold;
}
#logo:hover {
color: #fff;
text-decoration: none;
}

LOGO

ID #logo

hover
#logo

SCSS

&

#logo {
float: left;
margin-right: 10px;
font-size: 1.7em;
color: #fff;
text-transform: uppercase;
letter-spacing: -1px;
padding-top: 9px;
font-weight: bold;
&:hover {
color: #fff;
text-decoration: none;
}
}

SCSS

CSS

Sass

&:hover

#logo:hover

5.13

5.2. Sass

Asset Pipeline - 143

footer {
margin-top: 45px;
padding-top: 5px;
border-top: 1px solid #eaeaea;
color: #777;
a {
color: #555;
&:hover {
color: #222;
}
}
small {
float: left;
}
ul {
float: right;
list-style: none;
li {
float: left;
margin-left: 15px;
}
}
}

5.13

Sass
5.13

CSS

5.6

h2 {
.
.
.
color: #777;
}
.
.
.
footer {
.
.
.
color: #777;
}
#777
$light-gray: #777;

SCSS

144 -

$light-gray: #777;
.
.
.
h2 {
.
.
.
color: $light-gray;
}
.
.
.
footer {
.
.
.
color: $light-gray;
}
$light-gray

#999

Bootstrap
Less
Less

Bootstrap
Sass

Less
Sass

bootstrap-sass gem

Sass

Bootstrap

@gray-light: #777;
bootstrap-sass gem

SCSS

$gray-light

$light-gray
h2 {
.
.
.
color: $gray-light;
}
.
.
.
footer {
.
.
.
color: $gray-light;
}

Sass
Sass

5.15
white

Bootstrap Less

#fff

footer

5.15

SCSS

app/assets/stylesheets/custom.css.scss

5.2. Sass

Asset Pipeline - 145

@import "bootstrap-sprockets";
@import "bootstrap";
/* mixins, variables, etc. */
$gray-medium-light: #eaeaea;
/* universal */
html {
overflow-y: scroll;
}
body {
padding-top: 60px;
}
section {
overflow: auto;
}
textarea {
resize: vertical;
}
.center {
text-align: center;
h1 {
margin-bottom: 10px;
}
}
/* typography */
h1, h2, h3, h4, h5, h6 {
line-height: 1;
}
h1 {
font-size: 3em;
letter-spacing: -2px;
margin-bottom: 30px;
text-align: center;
}
h2 {
font-size: 1.2em;
letter-spacing: -1px;
margin-bottom: 30px;
text-align: center;
font-weight: normal;
color: $gray-light;

146 -

}
p {
font-size: 1.1em;
line-height: 1.7em;
}

/* header */
#logo {
float: left;
margin-right: 10px;
font-size: 1.7em;
color: white;
text-transform: uppercase;
letter-spacing: -1px;
padding-top: 9px;
font-weight: bold;
&:hover {
color: white;
text-decoration: none;
}
}
/* footer */
footer {
margin-top: 45px;
padding-top: 5px;
border-top: 1px solid $gray-medium-light;
color: $gray-light;
a {
color: $gray;
&:hover {
color: $gray-darker;
}
}
small {
float: left;
}
ul {
float: right;
list-style: none;
li {
float: left;
margin-left: 15px;
}
}
}

5.2. Sass

Asset Pipeline - 147

Sass
Sass

5.15

5.3.
#

<a href="/static_pages/about">About</a>

Rails

/about

/static_pages/about

Rails
<%= link_to "About", about_path %>
about_path
about_path

URL

URL
5.1

URL

3.4.4
8

5.1

URL
URL

root_path

/about

about_path

/help

help_path

/contact

contact_path

/signup

signup_path

/login

login_path

5.3.1.

3.22

5.16

5.16
@base_title

RED

test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
assert_select "title", "Ruby on Rails Tutorial Sample App"

148 -

5.16

end
test "should get help" do
get :help
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
end
test "should get about" do
get :about
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App"
end
test "should get contact" do
get :contact
assert_response :success
assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
end
end

5.16
5.17

RED

$ bundle exec rake test

3.3

5.19

contact

5.18

5.18

5.20

RED

config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get 'static_pages/help'
get 'static_pages/about'
get 'static_pages/contact'
end

5.19

RED

app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
.
.
.
def contact
end
end

5.20

GREEN

app/views/static_pages/contact.html.erb

5.3.

- 149

<% provide(:title, 'Contact') %>


<h1>Contact</h1>
<p>
Contact the Ruby on Rails Tutorial about the sample app at the
<a href="http://www.railstutorial.org/#contact">contact page</a>.
</p>

5.21

GREEN

$ bundle exec rake test

5.3.2. Rails
Rails
routes.rb

config/

URL

3.4.4
hello_app

1.10

root 'application#hello'
toy_app

2.3

root 'users#index'
sample_app

3.37

root 'static_pages#home'

root_path

URL
root_url

URL

root_path -> '/'


root_url -> 'http://www.example.com/'
_url

_path

HTTP

URL

5.18

get

get 'static_pages/help'

get 'help' => 'static_pages#help'

/help
help

GET

help

/static_pages/
help_path

/help
help_path -> '/help'
help_url -> 'http://www.example.com/help'

5.18

150 -

5.22

help_url

5.22
config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get 'help'
=> 'static_pages#help'
get 'about'
=> 'static_pages#about'
get 'contact' => 'static_pages#contact'
end

5.3.3.
link_to

5.22

<%= link_to "About", '#' %>

<%= link_to "About", about_path %>

_header.html.erb

LOGO

5.23

5.23
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home",
root_path %></li>
<li><%= link_to "Help",
help_path %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>

_footer.html.erb

5.24
5.24
app/views/layouts/_footer.html.erb
<footer class="footer">
<small>
The <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
by <a href="http://www.michaelhartl.com/">Michael Hartl</a>

5.3.

- 151

</small>
<nav>
<ul>
<li><%= link_to "About",
about_path %></li>
<li><%= link_to "Contact", contact_path %></li>
<li><a href="http://news.railstutorial.org/">News</a></li>
</ul>
</nav>
</footer>

3
5.8

5.8

/about

5.3.4.

site_layout
$ rails generate integration_test site_layout
invoke test_unit
create
test/integration/site_layout_test.rb

Rails

_test

HTML
152 -

/about

1.
2.
3.

Rails

assert_template

5.25
13

5.25

GREEN

test/integration/site_layout_test.rb
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
get root_path
assert_template 'static_pages/home'
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
end
end

5.25

assert_select

href

assert_select "a[href=?]", about_path


about_path

Rails

HTML

<a href="/about">...</a>

LOGO
assert_select "a[href=?]", root_path, count: 2

5.23
assert_select

5.2

assert_select

HTML
5.2

assert_select

HTML
assert_select "div"

<div>foobar</div>

13.
Ruby

MiniTest

5.3.

- 153

HTML
assert_select "div", "foobar"

<div>foobar</div>

assert_select "div.nav"

<div class="nav">foobar</div>

assert_select "div#profile"

<div id="profile">foobar</div>

assert_select "div[name=yo]"

<div name="yo">hey</div>

assert_select "a[href=?]", /, count: 1

<a href="/">foo</a>

assert_select "a[href=?]", /, text: "foo"

<a href="/">foo</a>

Rake
5.26

5.25

GREEN

$ bundle exec rake test:integration

5.27

GREEN

$ bundle exec rake test

5.4.

5.4.1.
3.2

generate

Rails
new

new

generate

5.28
5.28

new

$ rails generate controller Users new


create app/controllers/users_controller.rb
route get 'users/new'
invoke erb
create
app/views/users
create
app/views/users/new.html.erb
invoke test_unit
create
test/controllers/users_controller_test.rb
invoke helper
create
app/helpers/users_helper.rb
invoke
test_unit
create
test/helpers/users_helper_test.rb

154 -

REST

invoke
invoke
create
invoke
create

assets
coffee
app/assets/javascripts/users.js.coffee
scss
app/assets/stylesheets/users.css.scss
new

5.29

5.30
5.32

5.31

GREEN

$ bundle exec rake test


new

5.30
app/controllers/users_controller.rb

class UsersController < ApplicationController


def new
end
end

5.31
new
app/views/users/new.html.erb
<h1>Users#new</h1>
<p>Find me in app/views/users/new.html.erb</p>

5.32

GREEN

test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
test "should get new" do
get :new
assert_response :success
end
end

5.4.2.

URL

URL
/users/new
5.22

/signup

5.1

get '/signup'

5.33
5.33

config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get 'help'
=> 'static_pages#help'

5.4.

- 155

get 'about'
=> 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
end
get 'signup'
signup_path

5.34

5.34

app/views/static_pages/home.html.erb
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails logo"),
'http://rubyonrails.org/' %>

5.35

5.35

app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<p>This will be a signup page for new users.</p>

156 -

5.9

5.9

/signup

5.5.

Git
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Finish layout and routes"
git checkout master
git merge filling-in-layout

Bitbucket
$ git push

Heroku
$ git push heroku

5.10

5.5.

- 157

5.10

5.5.1.

HTML5

LOGO

Rails

CSS

ID

Bootstrap

Sass

Asset Pipeline

CSS

Rails

5.6.
3
1.

5.2.2

5.13

CSS

5.15

SS
2.

5.25

3.

5.36

get

full_title

5.37

158 -

SC-

Ruby on Rails Tutoial


full_title

5.38

FILL_IN

5.38

assert_equal

==

5.36
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
include ApplicationHelper
.
.
.
end

5.37
full_title
test/integration/site_layout_test.rb

GREEN

require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
get root_path
assert_template 'static_pages/home'
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
get signup_path
assert_select "title", full_title("Sign up")
end
end

5.38
full_title
test/helpers/application_helper_test.rb
require 'test_helper'
class ApplicationHelperTest < ActionView::TestCase
test "full title helper" do
assert_equal full_title,
FILL_IN
assert_equal full_title("Help"), FILL_IN
end
end

5.6.

- 159

5.4
7
8
6

9.2.1

10
Rails

10
Rails

6.1

6.1
Web
Rails

Web
Clearance Authlogic
OpenID
OAuth

Rails
Rails

Devise

CanCan

Rails

6.3

6.1.

6.1

161

6.1
Rails

model MVC

1.3.3

Rails
Active

Record

Active Record

Language

SQL

Rails

Ruby
DDL

SQL

Active Record
PostgreSQL
Heroku

SQLite
1.5

Rails

Git
$ git checkout master
$ git checkout -b modeling-users

1. Active Record
2. SQL

162 -

ess-cue-ell

Active Record

Structured Query

migration
Data Definition Language

sequel

Martin Fowler

6.1.1.
User

4.4.5

name

email

Rails

User

4.4.5

name
3

6.3

email

4.13

email

Ruby

at-

tr_accessor
class User
attr_accessor :name, :email
.
.
.
end

Rails

Rails

users

6.3

name

email

6.2

6.3

name

6.4

6.2

email

Active Record

users

6.3
new

5.28
$ rails generate controller Users new

3.

10

6.1.

- 163

generate model

name

email

6.1
6.1
$ rails generate model User name:string email:string
invoke active_record
create
db/migrate/20140724010738_create_users.rb
create
app/models/user.rb
invoke
test_unit
create
test/models/user_test.rb
create
test/fixtures/users.yml
Users
name:string

email:string

Rails
3.4

User

5.28

generate
generate
name

email

users

6.2

6.2.5
users

6.2
db/migrate/[timestamp]_create_users.rb

class CreateUsers < ActiveRecord::Migration


def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps null: false
end
end
end

change
t

create_table
create_table

table

string

change

6.2

create_table

Rails

users

name
User

email

Rails

t.timestamps null: false


created_at

updated_at

6.1.3

6.4

6.3

4.

164 -

6.4
rake

6.2

2.1

$ bundle exec rake db:migrate


db:migrate

2.2
SQLite 5

ment.sqlite3
velopment.sqlite3

6.5

velopment.sqlite3

id

db/developdb/de-

SQLite
Download
2.2

6.5

db/de-

IDE
6.4
Rails

users

SQLite

Rake

db:rollback
$ bundle exec rake db:rollback

5. SQLite

ess-cue-ell-ite

sequel-ite

6.1.

- 165

drop_table

3.1
change

create_table

users
drop_table

drop_table
up

change

down

Rails

$ bundle exec rake db:migrate

6.1.2.
6.1

6.2

db/development.sqlite3

6.5
updated_at

users

id

name

email

6.1
app/models/

user.rb

6.3
6.3
app/models/user.rb
class User < ActiveRecord::Base
end
class User < ActiveRecord::Base

4.4.2

User

ActiveRecord::Base

ActiveRecord::Base
ActiveRecord::Base

6.1.3.
4

Rails

$ rails console --sandbox


Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>

Any modifications you make will be rolled back on exit

4.4.5
4.4.4

User.new

4.13
Rails

Rails

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

User.new

nil

Active Record

166 -

User

4.4.5
Active Record

created_at

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")


=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil,
updated_at: nil>
name

email

Active Record

user

6.2
valid?

>> user.valid?
true
User.new

user.valid?
user

save

>> user.save
(0.2ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".
"email") = LOWER('mhartl@example.com') LIMIT 1
SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)
VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],
["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
"2014-09-11 14:32:14.199519"]]
(0.9ms) commit transaction
=> true
save

true

false

6.2

user.save

Rails

"users"

SQL

INSERT INTO

SQL

SQL

Active Record

SQL
id

created_at

updated_at

nil

>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
id

6.1.5
User

4.4.5
>>
=>
>>
=>

user.name
"Michael Hartl"
user.email
"mhartl@example.com"

6. 12.3.3
7.

"2014-07-24 00:57:46"
Coordinated Universal Time

UTC

Greenwich Mean Time

GMT
UTC

1970

ITU
ITU

CUT

ITU
CUT

TUC

UTC

6.1.

- 167

>> user.updated_at
=> Thu, 24 Jul 2014 00:57:46 UTC +00:00

Active Record

User.create
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2014-07-24 01:05:24", updated_at: "2014-07-24 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
User.create

true

foo

false

create

destroy

>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
destroy

create

destroy

>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">

Active Record

6.1.4.
Active Record
foo
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
User.find

ID
ID

Active Record

ID

>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3

6.1.3

Active Record
ID

Active Record

168 -

find

ActiveRecord::RecordNotFound

>> User.find_by(email: "mhartl@example.com")


=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
find
find_by

6.2.5

first
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
first

all

>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2014-07-24 00:57:46",
updated_at: "2014-07-24 00:57:46">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2014-07-24 01:05:24",
updated_at: "2014-07-24 01:05:24">]>
User.all

ActiveRecord::Relation

4.3.1

6.1.5.
4.4.5

>> user
#
user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
reload
reload
>>
=>
>>
=>
>>
=>

user.email
"mhartl@example.net"
user.email = "foo@bar.com"
"foo@bar.com"
user.reload.email
"mhartl@example.net"

6.1.3

8.
12.4.1

Ruby

Ruby

6.1.

- 169

>>
=>
>>
=>

user.created_at
"2014-07-24 00:57:46"
user.updated_at
"2014-07-24 01:37:32"
9

update_attributes
>>
=>
>>
=>
>>
=>

user.update_attributes(name: "The Dude", email: "dude@abides.org")


true
user.name
"The Dude"
user.email
"dude@abides.org"

update_attributes
true

6.3

update_attributes

>>
=>
>>
=>

update_attribute

user.update_attribute(:name, "The Dude")


true
user.name
"The Dude"

6.2.
name

6.1

email
name

name

email

email

Active Record
2.3.2

6.3.2

7.3

6.2.1.
3.3

TDD

TDD

6.1
6.4
6.4
test/models/user_test.rb

9. update_attributes

170 -

update

update_attribute

require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
#
assert true
# end
end
setup

@user

setup

@user
valid?

6.5

6.5

GREEN

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
end
assert

6.5

6.6

@user.valid?

true

false

GREEN

$ bundle exec rake test:models


rake test:models

5.3.4

rake test:integration

6.2.2.

name

email

7.3.3
name

6.5
@user

name

6.7
name
test/models/user_test.rb

6.7
assert_not

RED

require 'test_helper'
class UserTest < ActiveSupport::TestCase

6.2.

- 171

def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = "
"
assert_not @user.valid?
end
end

6.8

RED

$ bundle exec rake test:models


name

validates

presence: true

6.9

5.1.1
name

6.9

presence: true

4.3.4
Rails
GREEN

app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true
end
validates

6.9
class User < ActiveRecord::Base
validates(:name, presence: true)
end

10

$ rails console --sandbox


>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false
valid?

user

false

true

er-

rors
>> user.errors.full_messages
=> ["Name can't be blank"]

User.new

10.

172 -

Rails

balnk?

4.4.3

>> user.save
=> false

6.7
6.10

GREEN

$ bundle exec rake test:models


email

6.7

6.11

6.12
email
test/models/user_test.rb

6.11

RED

require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = ""
assert_not @user.valid?
end
test "email should be present" do
@user.email = "
"
assert_not @user.valid?
end
end
email

6.12

GREEN

app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true
end

6.13

GREEN

$ bundle exec rake test

6.2.

- 173

6.2.3.

50

51
255

6.14
6.14
name
test/models/user_test.rb

RED

require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "name should not be too long" do
@user.name = "a" * 51
assert_not @user.valid?
end
test "email should not be too long" do
@user.email = "a" * 256
assert_not @user.valid?
end
end

51
>>
=>
>>
=>

"a" * 51
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
("a" * 51).length
51

6.14
6.15

RED

$ bundle exec rake test


length

6.16
6.16
name
app/models/user.rb

174 -

GREEN

maximum

class User < ActiveRecord::Base


validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end

6.17

GREEN

$ bundle exec rake test

6.2.4.
name

email

51

user@example.com

%w[]

>> %w[foo bar baz]


=> ["foo", "bar", "baz"]
>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?>
puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp
each

4.3.2

user@example,com

addresses

user@example.com
6.18

6.18

GREEN

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.

6.2.

- 175

.
.
test "email validation should accept valid addresses" do
valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
first.last@foo.jp alice+bob@baz.cn]
valid_addresses.each do |valid_address|
@user.email = valid_address
assert @user.valid?, "#{valid_address.inspect} should be valid"
end
end
end
assert
assert @user.valid?, "#{valid_address.inspect} should be valid"
inspect

4.3.3

user_at_foo.org

6.19

each

6.18

user@example,com
6.19

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "email validation should reject invalid addresses" do
invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
foo@bar_baz.com foo@bar+baz.com]
invalid_addresses.each do |invalid_address|
@user.email = invalid_address
assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
end
end
end

6.20

RED

$ bundle exec rake test


format

176 -

validates :email, format: { with: /<regular expression>/ }

11

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
VALID_EMAIL_REGEX

6.1

6.1

/\A[\w+\-.]@[a-z\d\-.]\.[a-z]+\z/i
/
\A
[\w+\-.]+
@

[a-z\d\-.]+
\.
[a-z]+
\z
/
i

6.1
lar

11.
12.

6.6

Rubu12

Rubular
Rubular
Rubular

6.1
6.1

\A

Rubular
Rubular

\z

"Michael Hartl"@example.com
Rubular

Michael Lovitt

6.2.

- 177

6.6

Rubular

email

6.21
6.21

GREEN

app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
end
VALID_EMAIL_REGEX

Ruby

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
foo@bar..com

6.5

6.22

GREEN

$ bundle exec rake test:models

178 -

6.2.5.
validates

:unique

User.new
13

Ruby
6.23

6.23

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
end
@user.dup

@user

@user

@user

duplicate_user

email

uniqueness: true

6.24

6.23

6.24

GREEN

app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
foo@bar.com
14

FOO@BAR.COM

FoO@BAr.coM

6.25

db/test.sqlite3

13.

foo@bar.com

14.
about.com

Foo@bar.com

ISP
Riley Moses

6.2.

- 179

6.25

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "email addresses should be unique" do
duplicate_user = @user.dup
duplicate_user.email = @user.email.upcase
@user.save
assert_not duplicate_user.valid?
end
end
upcase

4.3.2

$ rails console --sandbox


>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true
duplicate_user.valid?
false

:uniqueness

true
:case_sensitive

6.26

6.26

GREEN

app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
true

180 -

case_sensitive: false

Rails

:uniqueness

true

6.27

GREEN

$ bundle exec rake test

Active Record
1. Alice

alice@wonderland.com

2. Alice
3.

2
4.
Rails
email

6.2

6.2
email

6.2
7

email

email
"foobar"
"foobar"

"foobar"

email

Rails

6.1.1
migration

6.2

$ rails generate migration add_index_to_users_email

6.28

15

6.28
db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end

15.

users

6.2

Rails

6.2.

- 181

add_index

Rails

users

email

unique: true

$ bundle exec rake db:migrate

6.1

6.29

fixture
6.29

RED

test/fixtures/users.yml
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString

6.30

6.30

GREEN

test/fixtures/users.yml
# empty

Foo@ExAMPle.CoM foo@example.com
foo@example.com

16

callback

Foo@ExAMPle.CoM
Active Record

before_save

6.31

8.4

6.31
email
app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },

16. Rails

182 -

Rails API

GREEN

format: { with: VALID_EMAIL_REGEX },


uniqueness: { case_sensitive: false }
end
before_save

6.31

downcase

6.31
self.email = self.email.downcase
self

self

reverse

palindrome

4.4.2

self.email = email.downcase
self
email = email.downcase

8.4
Alice

email

Rails
6.2

6.1.4

email

6.3.
name

email

4.3.3

Ruby
8

6.3.1.
Rails

has_secure_password

class User < ActiveRecord::Base


.
.
.
has_secure_password
end

6.3.

- 183

password_digest

17

password

password_confirmation

authenticate

false

has_secure_password

password_digest

gest

di-

18

6.7

password_digest

6.7

password_digest

6.7
to_users

users

Rails

add_password_digest_to_users
$ rails generate migration add_password_digest_to_users password_digest:string
password_digest:string
users
password_digest:string

6.1
name:string

email:string

Rails

6.32
users
password_digest
db/migrate/[timestamp]_add_password_digest_to_users.rb

6.32

class AddPasswordDigestToUsers < ActiveRecord::Migration


def change
add_column :users, :password_digest, :string
end
end
add_column

password_digest

users

$ bundle exec rake db:migrate

17.

18.

Rails

Andy Philips

184 -

has_secure_password

bcrypt

bcrypt
bcrypt

file

bcrypt gem

Gem-

6.3.

- 185

6.33
bcrypt gem

6.33

Gemfile

source 'https://rubygems.org'
gem 'rails',
gem 'bcrypt',
.
.
.

'4.2.0'
'3.1.7'

bundle install
$ bundle install

6.3.2.
password_digest
has_secure_password

bcrypt

6.34
has_secure_password

6.34

RED

app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
end

6.34
6.35

RED
RED

$ bundle exec rake test

6.3.1

has_secure_password

6.25

password

password_confirmation

@user

def setup
@user = User.new(name: "Example User", email: "user@example.com")
end

6.36
6.36

GREEN

test/models/user_test.rb

require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
end

6.37

GREEN

$ bundle exec rake test


has_secure_password

6.3.4

6.3.3.
Rails
6
6.38

6.38
RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?
end
end

@user.password = @user.password_confirmation = "a" * 5


password
name

186 -

maximum

password_confirmation

6.16

validates :password, length: { minimum: 6 }

6.39
6.39

GREEN

app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end

6.40

GREEN

$ bundle exec rake test:models

6.3.4.
7.1
has_secure_password

authenticate

7
create

6.1.3

rails console

$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?>
password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-09-11 14:26:42", updated_at: "2014-09-11 14:26:42",
password_digest: "$2a$10$sLcMI2f8VglgirzjSJOln.Fv9NdLMbqmR4rdTWIXY1G...">
db/development.sqlite3

SQLite
6.8

19

19.

6.7

1
7

users

2
$ rm -f development.sqlite3
$ bundle exec rake db:migrate

6.3.

- 187

6.8

SQLite

db/development.sqlite3

password_digest

6.39

has_secure_password

>> user = User.find_by(email: "mhartl@example.com")


>> user.password_digest
=> "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"
"foobar"

bcrypt

20

has_secure_password

6.3.1

authenticate
password_digest

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
user.authenticate

false

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-25 02:58:28", updated_at: "2014-07-25 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">

20. bcrypt

188 -

authenticate

authenticate

authenticate
nil

21

false

>> !!user.authenticate("foobar")
=> true

6.4.
name

email

password

6.3
Git
$ bundle exec rake test
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"

$ git checkout master


$ git merge modeling-users
$ git push

Heroku

heroku run

$ bundle exec rake test


$ git push heroku
$ heroku run rake db:migrate

$ heroku run console --sandbox


>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>
password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2014-08-29 03:27:50", updated_at: "2014-08-29 03:27:50",
password_digest: "$2a$10$IViF0Q5j3hsEVgHgrrKH3uDou86Ka2lEPz8zkwQopwj...">

6.4.1.

Active Record

Active Record

21. 4.2.3

!!

6.4.

- 189

has_secure_password

6.5.
3
1.

6.31

6.41

reload
before_save

6.41
before_save

2.
3.

email.downcase!

6.2.4
foo@bar..com
6.43
6.41

assert_equal

email

6.21
6.19

6.31

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "email addresses should be unique" do
duplicate_user = @user.dup
duplicate_user.email = @user.email.upcase
@user.save
assert_not duplicate_user.valid?
end
test "email addresses should be saved as lower-case" do
mixed_case_email = "Foo@ExAMPle.CoM"
@user.email = mixed_case_email
@user.save
assert_equal mixed_case_email.downcase, @user.reload.email
end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?

190 -

6.42

end
end

6.42 before_save
app/models/user.rb

GREEN

class User < ActiveRecord::Base


before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end

6.43

GREEN

app/models/user.rb
class User < ActiveRecord::Base
before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence:
true,
format:
{ with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end

6.5.

- 191

7.2
7.4

REST

2.2.2

5.3.4
6

10

7.1.

7.1

193

7.1
7.2

7.2

lorem ipsum
12

$ git checkout master


$ git checkout -b sign-up

7.2

7.1.1.

Rails

7.1

Rails

7.1.2

1. Mockingbird
2.

194 -

7.1

GIMP

http://www.flickr.com/photos/43803060@N00/24308857/

debug

params

7.1
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
</body>
</html>
if Rails.env.development?

Rails

7.1

true

Rails.env.development?

Ruby

<%= debug(params) if Rails.env.development? %>

7.1
Rails

Rails
Rails

$ rails console
Loading development environment
>> Rails.env
=> "development"
>> Rails.env.development?
=> true
>> Rails.env.test?
=> false
Rails

env

Rails.env.test?

true

false
console

$ rails console test


Loading test environment

3.

RailsCast

7.1.

- 195

>>
=>
>>
=>

Rails.env
"test"
Rails.env.test?
true

Rails
$ rails server --environment production
rake db:migrate

$ bundle exec rake db:migrate RAILS_ENV=production

heroku run console

Heroku

$ heroku run console


>> Rails.env
=> "production"
>> Rails.env.production?
=> true

Heroku

5
7.2
7.2
app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";
/* mixins, variables, etc. */
$gray-medium-light: #eaeaea;
@mixin box_sizing {
-moz-box-sizing:
border-box;
-webkit-box-sizing: border-box;
box-sizing:
border-box;
}
.
.
.
/* miscellaneous */

196 -

Sass

.debug_dump {
clear: both;
float: left;
width: 100%;
margin-top: 45px;
@include box_sizing;
}

Sass

box-sizing

mixin

.debug_dump {
.
.
.
@include box_sizing;
}

.debug_dump {
.
.
.
-moz-box-sizing:
border-box;
-webkit-box-sizing: border-box;
box-sizing:
border-box;
}

7.2.1

7.3

7.3
--controller: static_pages
action: home
params

4. Rails

YAML 4

YAML

7.1.2

YAML Aint Markup Language

7.1.

- 197

7.3

7.1.2.

6.3.4

Rails

$ rails console
>> User.count
=> 1
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-08-29 02:58:28", updated_at: "2014-08-29 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">

6.3.4
ID
Rails
HTTP

REST

2.2
POST

GET

PATCH

DELETE

3.2

REST

ID
/users/1

GET

2.2.1
7.4

198 -

Rails

REST

show

ID

GET

URL

/users/1

URL

7.4

/users/1

config/routes.rb

/users/1

resources :users

7.3
7.3
config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get 'help'
=> 'static_pages#help'
get 'about'
=> 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
resources :users
end
resources :users

URL

/users/1

REST
7.1

URL

5.3.3

2.2

7.1

REST
7.1

7.3

HTTP

URL

GET

/users

index

users_path

GET

/users/1

show

user_path(user)

GET

/users/new

new

new_user_path

POST

/users

create

users_path

GET

/users/1/edit

edit

edit_user_path(user)

PATCH

/users/1

update

user_path(user)

5.

REST

ID

edit

/users/1/edit

edit

7.1.

- 199

HTTP

URL

DELETE

/users/1

destroy

user_path(user)

7.3
7.1.4

7.5

7.5

/users/1
app/views/users/show.html.erb

new.html.erb

5.28

7.4

7.4
app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>
@user

show
find

ERb

@user

6.1.4

show
app/controllers/users_controller.rb

7.5

class UsersController < ApplicationController


def show

200 -

7.5

@user = User.find(params[:id])
end
def new
end
end

ID

1
"1"

params

ID

6.1.4

User.find(1)

params[:id]
params[:id]

find

/users/1

7.6

bcrypt

Rails

params[:id]
--action: show
controller: users
id: '1'

7.5

User.find(params[:id])

7.6

ID

show

7.1.3.
7.1.2
3.2

byebug gem

Rails 4.2
debugger

gem

7.6

7.1.

- 201

7.6
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
debugger
end
def new
end
end

/users/1

byebug

Rails

(byebug)

Rails
(byebug) @user.name
"Example User"
(byebug) @user.email
"example@railstutorial.org"
(byebug) params[:id]
"1"
byebug

Ctrl-D

show

debugger

7.7

7.7
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
end
end
debugger

Rails

byebug

7.1.4. Gravatar

6.

202 -

Avatar

Gravatar

Avatar

Gravatar
URL
11.4
gravatar_for

7.8

Gravatar

7.8

Gravatar

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
gravatar_for

Gravatar
MD5

Ruby

URL

Digest

MD5

hexdigest

>> email = "MHARTL@example.COM".


>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968"
downcase

MD5
hexdigest

6.31
gravatar_for

7.9
7.9
gravatar_for
app/helpers/users_helper.rb
module UsersHelper
#
Gravatar
def gravatar_for(user)
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
gravatar_for

img

7.7
example.com

Gravatar

Gravatar

img

gravatar

CSS

alt

user@example.com

7.1.

- 203

7.7

Gravatar

7.8

204 -

update_attributes

6.1.5

$ rails console
>> user = User.first
>> user.update_attributes(name: "Example User",
?>
email: "example@railstutorial.org",
?>
password: "foobar",
?>
password_confirmation: "foobar")
=> true
example@railstutorial.org

LOGO

7.8
aside

7.1

aside
aside

md-4

row col-

Bootstrap

7.10

7.10
show
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
</div>

HTML
CSS
Asset Pipeline

SCSS

7.11

Sass

7.9

7.11
app/assets/stylesheets/custom.css.scss
.
.
.
/* sidebar */
aside {
section.user_info {
margin-top: 20px;
}
section {
padding: 10px 0;

7.

.gravatar_edit

7.1.

- 205

margin-top: 20px;
&:first-child {
border: 0;
padding-top: 0;
}
span {
display: block;
margin-bottom: 3px;
line-height: 1;
}
h1 {
font-size: 1.4em;
text-align: left;
letter-spacing: -1px;
margin-bottom: 3px;
margin-top: 0px;
}
}
}
.gravatar {
float: left;
margin-right: 10px;
}
.gravatar_edit {
margin-top: 15px;
}

7.2.
5.9

206 -

7.11

7.10

7.9

CSS

7.10

7.2.

- 207

6.3.4
db:migrate:reset
$ bundle exec rake db:migrate:reset

Web

7.11

7.2.1.

form_for

Rails
form_for

Active Record
new

/signup
form_for

@user

7.12
new
@user
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])

208 -

5.33
7.12

end
def new
@user = User.new
end
end

7.13

7.2.2
7.2

SCSS
7.12

7.14

7.13
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>

7.14
app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
input, textarea, select, .uneditable-input {
border: 1px solid #bbb;
width: 100%;
margin-bottom: 15px;
@include box_sizing;
}
input {

7.2.

- 209

height: auto !important;


}

7.12

7.2.2.

HTML
7.13

form_for

end

<%= form_for(@user) do |f| %>


.
.
.
<% end %>
do

form_for

f
form_for

Rails

@user
<%= f.label :name %>
<%= f.text_field :name %>

HTML

210 -

label

name

ERb

7.15

7.15

7.12

<form accept-charset="UTF-8" action="/users" class="new_user"


id="new_user" method="post">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
<label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="text" />
<label for="user_password">Password</label>
<input id="user_password" name="user[password]"
type="password" />
<label for="user_password_confirmation">Confirmation</label>
<input id="user_password_confirmation"
name="user[password_confirmation]" type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Create my account" />
</form>

7.13

7.15

ERb

<%= f.label :name %>


<%= f.text_field :name %>

HTML
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

ERb
<%= f.label :password %>
<%= f.password_field :password %>

HTML
<label for="user_password">Password</label>
<input id="user_password" name="user[password]" type="password" />

7.13

type="text"

type="password"

7.2.

- 211

7.13
input

7.4

name

<input id="user_name" name="user[name]" - - - />


.
.
.
<input id="user_password" name="user[password]" - - - />

7.3

name

Rails

form

4.4.1

Rails

params

@user

Rails
@user

HTTP

form
User

@user

Ruby
Rails

post

3.2

<form action="/users" class="new_user" id="new_user" method="post">


class

/users

id

action="/users"

POST
form

<div style="display:none">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden"

212 -

method="post"

Rails

value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
</div>

Unicode
authenticity token

&#x2713;

Rails

Rails

Cross-Site Request Forgery

CSRF

7.3.
7.12

HTML

7.15

7.14

7.14

7.3.1.
7.1.2
7.1

8.

routes.rb

REST

resources :users

URL

/users

7.3
POST

Rails

create

Stack Overflow

7.3.

- 213

create

User.new

form

<form action="/users" class="new_user" id="new_user" method="post">

7.2.2

POST

/users

render

7.16
render

5.1.3
if-else

@user.save
true

6.1.3

false

create
app/controllers/users_controller.rb

7.16

class UsersController < ApplicationController


def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(params[:user])
if @user.save
#
else
render 'new'
end
end
end

7.3.2
7.16

7.15

7.16

7.15

Web

Rails
params

Web

214 -

7.15

7.16
user

7.16

Rails

"user" => { "name" => "Foo Bar",


"email" => "foo@invalid",

7.3.

- 215

"password" => "[FILTERED]",


"password_confirmation" => "[FILTERED]"
}
params
params[:id]

/users/1

params

7.1.2
ID

POST

params

params

4.3.3
user

input

Rails
name

7.13

<input id="user_email" name="user[email]" type="text" />


name

user[email]

user

email
params[:user]

User.new

User.new

4.4.5

7.16

@user = User.new(params[:user])

@user = User.new(name: "Foo Bar", email: "foo@invalid",


password: "foo", password_confirmation: "bar")

Rails
@user = User.new(params[:user])

Rails 4.0
7.15

7.16

7.3.2.
4.4.5

@user = User.new(params[:user])

Ruby
#
params

7.16
User.new

admin
true

9.4.1
params[:user]
params

admin='1'

curl

HTTP

User.new

admin='1'

attr_accessible

Rails
Rails 4.0

Rails
strong parameter

params

Rails
params

:user

name

email

password

word_confirmation
params.require(:user).permit(:name, :email, :password, :password_confirmation)

216 -

pass-

params

:user
user_params

params[:user]

@user = User.new(user_params)
user_params

Ruby

vate

7.17

pripri-

8.4

vate
create
app/controller/users_controller.rb

7.17

class UsersController < ApplicationController


.
.
.
def create
@user = User.new(user_params)
if @user.save
#
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
private

user_params

7.17
7.3.3

9.

private

7.4

The Ruby Programming Language

7.3.

- 217

7.17

7.3.3.
Rails

$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?>
password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
errors.full_message

6.2.2
@user

7.16
new
form-control

CSS

Bootstrap
11.3.2

7.18
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

218 -

7.18

<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
shared/error_messages
mkdir

1.1

Rails

shared/

9.1.1
app/views/shared

$ mkdir app/views/shared
_error_messages.html.erb

7.19
7.19
app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

Rails/Ruby

Rails

count
>> user.errors.count
=> 2

7.3.

- 219

any?
>>
=>
>>
=>

empty?

user.errors.empty?
false
user.errors.any?
true
empty?

empty?

4.2.3
true

true

false

any?

false

count

empty?
empty?

any?

pluralize

Ruby

11.2

Action10

View::Helpers::TextHelper
>>
>>
=>
>>
=>

Rails

include ActionView::Helpers::TextHelper
pluralize(1, "error")
"1 error"
pluralize(5, "error")
"5 errors"
pluralize
pluralize

>>
=>
>>
=>

inflector

pluralize(2, "woman")
"2 women"
pluralize(3, "erratum")
"3 errata"
pluralize

<%= pluralize(@user.errors.count, "error") %>


"0 errors"

"1 error"

"2 errors"

"1 errors"

7.19

CSS ID
ID

CSS

field_with_errors

CSS

error_explanation

div

7.20

5.1.2
Rails
ID

Sass

@extend

SCSS
Bootstrap

has-error

7.20
app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
.
.

ActionView::Helpers::TextHelper

10.

220 -

Rails API

pluralize

.
#error_explanation {
color: red;
ul {
color: red;
margin: 0 0 30px 0;
}
}
.field_with_errors {
@extend .has-error;
.form-control {
color: $state-danger-text;
}
}

7.18

7.19

7.20

SCSS

7.18

7.18

7.3.4.
Web

7.3.

- 221

Rails
7.4.4
users_signup

$ rails generate integration_test users_signup


invoke test_unit
create
test/integration/users_signup_test.rb

7.4.4

Active Record

User

count

$ rails console
>> User.count
=> 0
User.count

sert_select

7.2

as-

5.3.4

HTML
get

get signup_path
users_path

POST

post

7.1

assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password:
"foo",
password_confirmation: "bar" }
end
create

User.new

sert_no_difference

params[:user]

post

7.24

assert_no_difference

as-

'User.count'

User.count

post

before_count = User.count
post users_path, ...
after_count = User.count
assert_equal before_count, after_count
assert_no_difference

Ruby
assert_template

7.21
new

7.21

GREEN

test/integration/users_signup_test.rb

222 -

7.7

require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password:
"foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
end
end

7.22

GREEN

$ bundle exec rake test

7.4.

7.19

7.4.

- 223

7.19

7.4.1.
7.17
7.20

224 -

Rails

create

7.20

redirect_to

7.23

7.23 create
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end

7.4.

- 225

redirect_to @user

redirect_to user_url(@user)
redirect_to @user

Rails

user_url(@user)

7.4.2.
7.23
Web

Rails

:success

flash message

Rails

7.24
7.24

app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
flash

flash

4.3.3
flash
$ rails console
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", danger: "It failed."}
>> flash.each do |key, value|
?>
puts "#{key}"

226 -

?>
puts "#{value}"
>> end
success
It worked!
danger
It failed.

<% flash.each do |message_type, message| %>


<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

HTML

ERb

alert-<%= message_type %>


:success

CSS

alert-success

:success

"success"

ERb

CSS
alert-danger

8.1.4
11

flash[:danger]

Bootstrap

7.19
success

CSS

info

warning

danger

flash[:success] = "Welcome to the Sample App!"

HTML
<div class="alert alert-success">Welcome to the Sample App!</div>

ERb

7.25

7.25
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
<%= yield %>

11.

flash.now

7.4.

- 227

<%= render 'layouts/footer' %>


<%= debug(params) if Rails.env.development? %>
</div>
.
.
.
</body>
</html>

7.4.3.
Rails Tutorial
example@railstutorial.org
7.22
5.1.2

7.21
.success

Bootstrap

db:migrate:reset Rake

7.2

7.23

7.21

228 -

7.22

7.23

7.4.

- 229

$ rails console
>> User.find_by(email: "example@railstutorial.org")
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org",
created_at: "2014-08-29 19:53:17", updated_at: "2014-08-29 19:53:17",
password_digest: "$2a$10$zthScEx9x6EkuLa4NolGye6O0Zgrkp1B6LQ12pTHlNB...">

7.4.4.
7.3.4
7.21
assert_no_difference 'User.count' do
post signup_path, ...
end
assert_difference
assert_difference 'User.count', 1 do
post_via_redirect signup_path, ...
end
assert_no_difference

assert_difference

'User.count'

User.count
assert_difference

1
7.21

post_via_redirect

7.26

7.26
users/show

GREEN

test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
name
= "Example User"
email
= "user@example.com"
password = "password"
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: name,
email: email,
password:
password,
password_confirmation: password }
end
assert_template 'users/show'
end
end

230 -

show
show

7.3

show.html.erb

7.5

7.8

assert_template 'users/show'

7.5.
3
Web

master
$
$
$
$

git
git
git
git

add -A
commit -m "Finish user signup"
checkout master
merge sign-up

7.5.1.

SSL

SSL

Secure Sockets Layer

12

SSL
SSL

8.4

production.rb

SSL
config

7.27

SSL

7.27

SSL

config/environments/production.rb
Rails.application.configure do
.
.
.
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
.
.
.
end

12.

SSL

TLS

Transport Layer Security

SSL

7.5.

- 231

SSL

SSL
Heroku

Heroku

SSL

7.5.2
Heroku

www.example.com

SSL

7.5.2.

SSL
SSL

Unicorn

SSL
Ruby

Web

Heroku

WEBrick

WEBrick
Unicorn

Heroku

Gemfile

Unicorn
Unicorn
Gemfile

7.28

unicorn

unicorn gem

7.28

:production

Unicorn

source 'https://rubygems.org'
.
.
.
group :production do
gem 'pg',
'0.17.1'
gem 'rails_12factor', '0.0.2'
gem 'unicorn',
'4.8.3'
end

Bundler
gem

gem
Bundler

3.1

7.28

Gemfile.lock

$ bundle install
config/unicorn.rb

7.29

13

7.29

Unicorn

config/unicorn.rb
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout 15
preload_app true
before_fork do |server, worker|
Signal.trap 'TERM' do
puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
Process.kill 'QUIT', Process.pid
end
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
end

13.

232 -

80

7.29

Heroku

after_fork do |server, worker|


Signal.trap 'TERM' do
msg = 'Unicorn worker intercepting TERM and doing nothing. '
msg += 'Wait for master to send QUIT'
puts msg
end
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
Procfile

Heroku

7.30

Procfile

Gemfile

7.30

Unicorn

Procfile

Unicorn

./Procfile
web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
14

Unicorn
$
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Use SSL and Unicorn in production"
git push
git push heroku
heroku run rake db:migrate

7.24
https://

14.

SSL

Heroku

heroku run rake db:migrate

7.5.

- 233

7.24

7.5.3. Ruby
Heroku
###### WARNING:
You have not declared a Ruby version in your Gemfile.
To set your Ruby version add this line to your Gemfile:
ruby '2.1.5'

Ruby
Ruby
Heroku

15

Gemfile

7.6.

9
7.1

REST

15.
2.1.5

234 -

Ruby 2.1.4

Ruby 2.1.5

Ruby

7.6.1.
Rails

debug

Sass

CSS
development

Rails

test

production

REST URL

Gravatar
form_for

Active Record

Active Record

Rails

flash

SSL

Unicorn

7.7.
3
1.

7.31

gravatar_for

7.1.4

gravatar_for user, size: 50

2.

size

9.3.1

7.18
7.32

3.

7.4.2

7.33

FILL_IN
nil

4. 7.4.2

7.25

HTML

7.34

content_tag

7.31
gravatar_for
app/helpers/users_helper.rb
module UsersHelper
#
Gravatar
def gravatar_for(user, options = { size: 80 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end

7.32
test/integration/users_signup_test.rb

7.7.

- 235

require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password:
"foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
assert_select 'div#<CSS id for error explanation>'
assert_select 'div.<CSS class for field with error>'
end
.
.
.
end

7.33
test/integration/users_signup_test.rb
require 'test_helper'
.
.
.
test "valid signup information" do
get signup_path
name = "Example User"
email = "user@example.com"
password = "password"
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: name,
email: email,
password:
password,
password_confirmation: password }
end
assert_template 'users/show'
assert_not flash.FILL_IN
end
end
content_tag
app/views/layouts/application.html.erb

7.34

<!DOCTYPE html>
<html>
.
.
.
<% flash.each do |message_type, message| %>

236 -

<%= content_tag(:div, message, class: "alert alert-#{message_type}") %>


<% end %>
.
.
.
</html>

7.7.

- 237

8.1

8.2

8.4

8.4.5

11

12

8.1.
HTTP

HTTP

Web

Rails

Rails
cookie

cookie cookie
cookie
Rails
session

8.2
2

8.4

Rails

session

ID

cookies

REST
cookie
8.2

$ git checkout master


$ git checkout -b log-in-log-out

8.1.1.
new
create

REST

POST

destroy

8.2

DELETE

8.3

HTTP

7.1

1.
2.

Rails

239

new
$ rails generate controller Sessions new
new

create

delete

7.2

8.1

8.1
resources

/login

REST
GET

POST

rails generate controller

8.1
config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'
get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'

240 -

7.3
/logout

DELETE

8.1

resources :users
end

8.1

URL

7.1

8.1

8.1

HTTP

URL

GET

/login

login_path

new

POST

/login

login_path

create

DELETE

/logout

logout_path

destroy

8.1

rake routes

$ bundle exec rake routes


Prefix Verb
URI Pattern
Controller#Action
root GET
/
static_pages#home
help GET
/help(.:format)
static_pages#help
about GET
/about(.:format)
static_pages#about
contact GET
/contact(.:format)
static_pages#contact
signup GET
/signup(.:format)
users#new
login GET
/login(.:format)
sessions#new
POST
/login(.:format)
sessions#create
logout DELETE /logout(.:format)
sessions#destroy
users GET
/users(.:format)
users#index
POST
/users(.:format)
users#create
new_user GET
/users/new(.:format)
users#new
edit_user GET
/users/:id/edit(.:format) users#edit
user GET
/users/:id(.:format)
users#show
PATCH /users/:id(.:format)
users#update
PUT
/users/:id(.:format)
users#update
DELETE /users/:id(.:format)
users#destroy

8.1.2.
8.1

8.2

7.11

7.3.3
Active Record
Active Record

8.1.

- 241

8.2
form_for

7.13

@user

form_for
<%= form_for(@user) do |f| %>
.
.
.
<% end %>
@user
form_for
form_for(@user)

/users

POST

URL

form_for(:session, url: login_path)


form_for

7.13

8.1

8.2

form_for

3.

242 -

form_tag

Rails

form_tag

8.2
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>

8.2

Log in

/login

8.3

8.2.3

8.3

8.1.

- 243

HTML

8.3

8.3

8.2

HTML

<form accept-charset="UTF-8" action="/login" method="post">


<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="session_email">Email</label>
<input id="session_email" name="session[email]" type="text" />
<label for="session_password">Password</label>
<input id="session_password" name="session[password]"
type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Log in" />
</form>

8.3
params[:session][:email]

params

7.15
params[:session][:password]

8.1.3.

8.2
8.2
create
create

8.4

new

8.4
8.4
create
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
render 'new'
end
def destroy
end
end

244 -

new

destroy

/login

8.4

8.4

8.4

create

params

8.1.2
:session

Rails

--session:
email: 'user@example.com'
password: 'foobar'
commit: Log in
action: create
controller: sessions

7.15

4.10

params

{ session: { password: "foobar", email: "user@example.com" } }

params[:session]

{ password: "foobar", email: "user@example.com" }

params[:session][:email]

8.1.

- 245

params[:session][:password]

create
has_secure_password

params
User.find_by

Active Record
6.3.4

6.1.4

authenticate

false

authenticate

8.5

8.5
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
#
else
#
render 'new'
end
end
def destroy
end
end

8.5

6.2.5
downcase

Rails
user && user.authenticate(params[:session][:password])
&&

nil

false

true

8.2
true

8.2

user && user.authenticate()

a && b
(nil && [anything]) == false
(true && false) == false
(true && true) == true

246 -

8.1.4.
7.3.3

Active Record
Active Record
8.6
8.6

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
#
else
flash[:danger] = 'Invalid email/password combination' #
render 'new'
end
end
def destroy
end
end
flash[:danger]

7.25
Bootstrap

CSS

8.5

8.6
render

7.24
8.6

8.1.5

8.1.

- 247

8.5

8.6

248 -

8.1.5.
bug

3.3

$ rails generate integration_test users_login


invoke test_unit
create
test/integration/users_login_test.rb

8.5

8.6

1.
2.
params

3.

post

4.
5.
6.
8.7
8.7

RED

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_template 'sessions/new'
post login_path, session: { email: "", password: "" }
assert_template 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
end

8.8

RED

$ bundle exec rake test TEST=test/integration/users_login_test.rb


TEST
flash

8.7
flash

flash.now

flash.now

flash.now

8.7

8.9

8.1.

- 249

8.9

GREEN

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
#
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end

8.10

GREEN

$ bundle exec rake test TEST=test/integration/users_login_test.rb


$ bundle exec rake test

8.2.

8.4

4.2.5
8.1.1

Rails

ApplicationCon-

Rails
troller

8.11
ApplicationController
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end

250 -

Ruby

8.11

8.2.1. log_in
session

Rails

session

8.1.1

session
session[:user_id] = user.id

cookie
sion[:user_id]

ID

8.4

ID
cookie

cookies

sessession

log_in

8.12
8.12 log_in
app/helpers/sessions_helper.rb
module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
end
session

cookie

8.12
session

cookie

cookie
8.4

session hijacking

log_in

create

8.13

cookies

8.13
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end

4.

8.11

log_in

8.2.

- 251

end
def destroy
end
end

redirect_to user

7.4.1

Rails

user_path(user)
create

8.2
8.2.2

ID

8.2.3

8.2.2.
cur-

ID
rent_user

current_user

ID

<%= current_user.name %>

redirect_to current_user
find

7.5

User.find(session[:user_id])

6.1.4

find

ID

session[:user_id]
create

find_by

nil
id

User.find_by(id: session[:user_id])

ID

find_by

nil
current_user

def current_user
User.find_by(id: session[:user_id])
end
current_user
User.find_by

Ruby

5.

252 -

memoization

memorization

if @current_user.nil?
@current_user = User.find_by(id: session[:user_id])
else
@current_user
end

4.2.3

||

@current_user = @current_user || User.find_by(id: session[:user_id])


User

@current_user

find_by
@current_user

Ruby

@current_user ||= User.find_by(id: session[:user_id])


||=

8.1

8.1
||=

Ruby

||=
||=

Rails

x = x + 1

Ruby

C C++ Perl Python

Java

x += 1

$ rails console
>> x = 1
=> 1
>> x += 1
=> 2
>> x *= 3
=> 6
>> x -= 8
=> -2
>> x /= 2
=> -1
x = x O y

x O=y

O
nil

Ruby
4.2.3

||

>> @foo
=> nil

8.2.

- 253

>>
=>
>>
=>

@foo = @foo || "bar"


"bar"
@foo = @foo || "baz"
"bar"

nil

nil || "bar"

"bar" || "baz"

"bar"

"bar"
nil

false

||

short-circuit evaluation
@foo = @foo || "bar"

x
x
x
x
@foo

=
x
+
1
=
x
*
3
=
x
8
=
x
/
2
= @foo || "bar"

->
->
->
->
->

@foo = @foo || "bar"

x = x O y

||

x
+=
1
x
*=
3
x
-=
8
x
/=
2
@foo ||= "bar"

@foo ||= "bar"

@current_user ||= User.find_by(id: session[:user_id])


6

current_user

8.14

8.14
app/helpers/sessions_helper.rb
module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
#
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
current_user

6.
||=
Or Equals) Really Does

254 -

Peter Cooper

Ruby Inside

What Rubys ||= (Double Pipe /

8.2.3.

8.7
Account

Bootstrap

8.16

8.7
3.3
Rails
8.2.4
ERb

if-else

<% if logged_in? %>


#
<% else %>
#
<% end %>

7.

http://www.flickr.com/photos/hermanusbackpackers/3343254977/

8.2.

- 255

logged_in?
current_user

4.2.3

bang

nil

logged_in?

8.15

logged_in?
app/helpers/sessions_helper.rb

8.15

module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
#
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
#
def logged_in?
!current_user.nil?
end
end

true

false

logged_in?

9
<%= link_to "Users",
'#' %>
<%= link_to "Settings", '#' %>

8.1
<%= link_to "Log out", logout_path, method: "delete" %>

HTTP DELETE

<%= link_to "Profile", current_user %>

<%= link_to "Profile", user_path(current_user) %>

Rails

current_user

user_path(current_user)

8.1

<%= link_to "Log in", login_path %>

8.16

8. Web

256 -

DELETE

Rails

JavaScript

8.16
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>

8.16
dropdown

Bootstrap CSS
Pipeline

dropdown-menu

Bootstrap

8.17
application.js
app/assets/javascripts/application.js
//=
//=
//=
//=
//=

Bootstrap

application.js

JavaScript

Asset

8.17

Bootstrap JavaScript

require jquery
require jquery_ujs
require bootstrap
require turbolinks
require_tree .
10

8.16

8.17

8.8

9.
10.

Bootstrap
IDE

IDE

8.2.

- 257

8.8

8.2.4.

8.7
1.
post

2.
3.
4.
5.

Rails

6.2.5
6.30

create
password_digest

6.3.1

258 -

bcrypt

6.7
digest

has_secure_password

BCrypt::Password.create(string, cost: cost)


string

cost

di-

gest
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost

?-:

8.4.4
digest

user.rb

8.4.1
digest

User

4.4.1

8.18
8.18

digest

app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
#
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
digest

8.19

8.19
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>

Ruby
<%= User.digest('password') %>

8.2.

- 259

has_secure_password

password_digest

8.19

password

Rails

'password'

user = users(:michael)
users

users.yml

:michael

8.19
8.20

get

post

POST

GET

8.20

GREEN

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "login with valid information" do
get login_path
post login_path, session: { email: @user.email, password: 'password' }
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
end
end
assert_redirected_to @user

follow_redirect!

assert_select "a[href=?]", login_path, count: 0


count: 0

assert_select
count: 2

8.21

GREEN

$ bundle exec rake test TEST=test/integration/users_login_test.rb \


>
TESTOPTS="--name test_login_with_valid_information"

260 -

5.25


TESTOPTS="--name test_login_with_valid_information"

test

8.2.5.

create

log_in

8.22

11

8.22
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end

7.26
8.15

is_logged_in?

ID

true

rent_user
logged_in?

11.

false
current_user

logged_in?

8.23
session

12

is_logged_in?

8.11

cur-

8.15

log_in

8.2.

- 261

8.23
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
#
true
def is_logged_in?
!session[:user_id].nil?
end
end

8.24
8.24

GREEN

test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: "Example User",
email: "user@example.com",
password:
"password",
password_confirmation: "password" }
end
assert_template 'users/show'
assert is_logged_in?
end
end

8.25

GREEN

$ bundle exec rake test

log_in
is_logged_in?

12.

log_in_as

262 -

8.50

8.3.
8.1
8.16

new

REST

create

destroy

REST

8.13
destroy

8.22
8.4.6
log_in

8.12

ID

delete

session.delete(:user_id)
nil
13

log_in

log_out

8.26

8.26 log_out
app/helpers/sessions_helper.rb
module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
.
.
.
#
def log_out
session.delete(:user_id)
@current_user = nil
end
end
destroy

log_out

8.27

8.27
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end

destroy

13.

@current_user

@current_user

nil
nil

8.3.

- 263

def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end
delete

8.20
8.1

DELETE

8.28
8.28

GREEN

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
.
.
.
test "login with valid information followed by logout" do
get login_path
post login_path, session: { email: @user.email, password: 'password' }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path,
count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
is_logged_in?
is_logged_in?

264 -

assert

destroy

8.29

GREEN

$ bundle exec rake test

8.4.
8.2

Facebook

Bitbucket

8.4.5
GitHub
Twitter

8.4.1.
8.2

Rails

session

ID
cookies

cookie

session

8.2.1

cookies

cookie
cookie

1
3
7.5

Cross-Site Scripting
SSL

XSS

14

cookie
4

6.3

Rails

1.
2.

cookie

3.
4.
5.

cookie
cookie

ID
ID

ID

cookie

authenticate
has_secure_password

8.5
remember_digest

14.

Firesheep

8.9

WIFI

8.4.

- 265

remember_digest

8.9
8.9

$ rails generate migration add_remember_digest_to_users remember_digest:string


_to_users

6.3.1
users

Rails

Rails

8.30
8.30
db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :remember_digest, :string
end
end
remember_digest

$ bundle exec rake db:migrate

SecureRandom

Ruby

15

urlsafe_base64

A-Z a-z 0-9

- _

22

64

base64

base64
$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"
16
17

base64

22

15.

RailsCast

16.

17.

266 -

64

bcrypt
ID

cookie

1/6422 = 2-132 10-40

URL

base64

safe_base64

url-

10
digest
new_token

8.18

18

digest

8.31
8.31
app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
#
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def User.new_token
SecureRandom.urlsafe_base64
end
end
user.remember
remember_digest

8.30

user.remember_token
password

has_secure_password

4.4.5

remember_token

cookie

6.3

password_digest

password
remember_token

attr_accessor

class User < ActiveRecord::Base


attr_accessor :remember_token
.
.
.
def remember
self.remember_token = ...
update_attribute(:remember_digest, ...)

18.

10.1.2

8.4.

- 267

end
end
remember

self

Ruby
remember_token

self

remember_token

before_save
remember

6.31

self.email

email

update_attribute

6.1.5

User.new_token
User.digest

remember
remember

8.32

8.32

GREEN

app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
#
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def User.new_token
SecureRandom.urlsafe_base64
end
#
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end

8.4.2.
user.remember

ID
cookies

cookie
cookie

value

20

268 -

cookie

session
expires

cookies[:remember_token] = { value:
remember_token,
expires: 20.years.from_now.utc }

Rails
ie

8.2
permanent

Rails

Rails
cookie

20

cook-

cookies.permanent[:remember_token] = remember_token
20.years.from_now

Rails

8.2

cookie

4.4.2
String
Object

20.years.from_now

Ruby
palindrome?

blank?

20.years.from_now

"deified"
"".blank?

cookie

" ".blank?

Rails

nil.blank?

cookies.permanent

true
permanent

Rails

Fixnum
$ rails console
>> 1.year.from_now
=> Sun, 09 Aug 2015 16:48:17 UTC +00:00
>> 10.weeks.ago
=> Sat, 31 May 2014 16:48:45 UTC +00:00
Fixnum

Rails
>>
=>
>>
=>

1.kilobyte
1024
5.megabytes
5242880
5.megabytes

Ruby

Rails

Ruby

session

ID

cookie

cookies[:user_id] = user.id

cookie
cookie
cookies.signed[:user_id] = user.id

ID

signed

ID

permanent
cookies.permanent.signed[:user_id] = user.id

cookie
User.find_by(id: cookies.signed[:user_id])

8.4.

- 269

cookies.signed[:user_id]
ies[:remember_token]

cookie

ID

bcrypt

cook-

remember_digest

8.32

ID

ID
ID
bcrypt
19

BCrypt::Password.new(password_digest) == unencrypted_password

BCrypt::Password.new(remember_digest) == remember_token
==

bcrypt
bcrypt
bcrypt

gem

bcrypt
==

BCrypt::Password.new(remember_digest).is_password?(remember_token)
==

is_password?

authenticated?
has_secure_password

authenticate

8.13

authenticated?

8.33
authenticated?

8.33
app/models/user.rb

class User < ActiveRecord::Base


attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
#
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def User.new_token
SecureRandom.urlsafe_base64

19. 6.3.1

270 -

unencrypted password

end
#
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
#
true
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
end

8.33

authenticated?

10

log_in

remember

8.34

8.34
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end

8.34
remember
cookies

user.remember

cookie

ID

8.35

8.35
app/helpers/sessions_helper.rb

8.4.

- 271

module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
#
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
#
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
#
def logged_in?
!current_user.nil?
end

true

false

#
def log_out
session.delete(:user_id)
@current_user = nil
end
end

8.14

current_user

@current_user ||= User.find_by(id: session[:user_id])


session[:user_id]
ies[:user_id]
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
user = User.find_by(id: cookies.signed[:user_id])
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end

8.5
session

272 -

cookies

user && user.authenticated

cook-

if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end

if (user_id = session[:user_id])
==

ID

ID

ID

current_user

ID

user_id

20

8.36

8.36
current_user
app/helpers/sessions_helper.rb

RED

module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
#
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
#
cookie
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
#

true

false

20.

8.4.

- 273

def logged_in?
!current_user.nil?
end
#
def log_out
session.delete(:user_id)
@current_user = nil
end
end

cookie
21

8.10

cookie
cookie

8.37

20

RED

$ bundle exec rake test

8.4.3.
user.forget
nil

8.38

user.remember

8.38

forget

app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }

21.

274 -

cookie

<

> inspect cookies

8.10

validates :name, presence: true, length: { maximum: 50 }


VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
#
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def User.new_token
SecureRandom.urlsafe_base64
end
#
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
#
true
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
#
def forget
update_attribute(:remember_digest, nil)
end
end
forget

8.39

forget

log_out
user.forget

cookie

forget
user_id

remember_token

8.39
app/helpers/sessions_helper.rb
module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
.
.
.
#

8.4.

- 275

def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
#
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
end

8.4.4.

current_user

8.39

22

Chrome
23

nil

8.38
ID

log_out

8.39

user.forget

Firefox

Firefox

Firefox

current_user

user

nil

#
cookie
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end

user && user.authenticated?(cookies[:remember_token])


false

user
user

8.33

22.

Paulo Clio Jnior

23.

Niels de Ron

276 -

nil
nil

Chrome
authenticated?

ID

def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember_digest

nil

BCrypt::Password.new(remember_digest)

authenticated?

8.28

false

8.40
8.40

RED

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
.
.
.
test "login with valid information followed by logout" do
get login_path
post login_path, session: { email: @user.email, password: 'password' }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
#
delete logout_path
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path,
count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
delete logout_path

8.41

RED

$ bundle exec rake test


logged_in?

8.42

true

log_out

8.42

GREEN

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.

8.4.

- 277

.
.
def destroy
log_out if logged_in?
redirect_to root_url
end
end

setup

@user

authenticated?

8.43
authenticated?

8.43

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?('')
end
end
BCrypt::Password.new(nil)

8.44

RED

$ bundle exec rake test


nil

8.45

authenticated?

authenticated?

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
true
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
end

278 -

false

8.45

nil

return

if remember_digest.nil?
false
else
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

8.45
8.46

GREEN

$ bundle exec rake test

8.4.5.

8.11

8.11

8.4.

- 279

8.2
Rails

label

<%= f.label :remember_me, class: "checkbox inline" do %>


<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>

8.47
8.47

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>

8.47
on this computer

CSS

checkbox

CSS

app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
.
.
.

280 -

Bootstrap
CSS

8.12
8.48

inline

Remember me
8.48

.checkbox {
margin-top: -10px;
margin-bottom: 10px;
span {
margin-left: 20px;
font-weight: normal;
}
}
#session_remember_me {
width: auto;
margin-left: 0;
}

8.12

params
params[:session][:remember_me]

'1'

'0'

params
if params[:session][:remember_me] == '1'
remember(user)
else
forget(user)
end

8.4.

- 281

if-then

8.3

24

params[:session][:remember_me] == '1' ? remember(user) : forget(user)


create

8.49
cost

8.18
8.49

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end

8.3
10
2

remember user

24.

282 -

10
10

11

if boolean?
do_one_thing
else
do_something_else
end

Ruby

C/C++ Perl PHP

Java

boolean? ? do_one_thing : do_something_else

if boolean?
var = foo
else
var = bar
end

var = boolean? ? foo : bar

def foo
do_stuff
boolean? ? "bar" : "baz"
end
foo

Ruby
"bar"

boolean?

"baz"

8.4.6.

8.49

params[:session][:remember_me] ? remember(user) : forget(user)

params[:session][:remember_me] == '1' ? remember(user) : forget(user)


params[:session][:remember_me]

8.20

'0'

'1'

true

post

session
log_in_as

8.4.

- 283

8.20
session
log_in_as

Ruby
defined?

true

defined?

false

post_via_redirect
defined?(post_via_redirect) ...
true

false

integration_test?

if-else
if integration_test?
#
else
#
session
end
log_in_as

8.50

8.50

log_in_as

test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
#
true
def is_logged_in?
!session[:user_id].nil?
end
#
def log_in_as(user, options = {})
password
= options[:password]
|| 'password'
remember_me = options[:remember_me] || '1'
if integration_test?
post login_path, session: { email:
user.email,
password:
password,
remember_me: remember_me }
else
session[:user_id] = user.id
end
end
private
#
true
def integration_test?
defined?(post_via_redirect)

284 -

end
end
log_in_as

8.50

'passowrd'

options

'1'

nil

remember_me = options[:remember_me] || '1'

8.1

8.50
log_in_as(@user, remember_me: '1')

log_in_as(@user, remember_me: '0')


remember_me

'1'

cookies

remember_token
user

cookie
@user

remember_token
nil

cookie

cookies
cookies[:remember_token]
nil

cookies

cookies['remember_token']

8.51

8.20

users(:michael)

8.19
8.51

GREEN

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_not_nil cookies['remember_token']
end

8.4.

- 285

test "login without remembering" do


log_in_as(@user, remember_me: '0')
assert_nil cookies['remember_token']
end
end

8.52

GREEN

$ bundle exec rake test

current_user

8.4.2
8.53
8.53

GREEN

app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
#
cookie
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
raise
#
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end

8.54

GREEN

$ bundle exec rake test

8.53
current_user

286 -

10

8.50

rent_user

log_in_as

session[:user_id]

cur-

current_user
$ touch test/helpers/sessions_helper_test.rb

user

1.
2.

remember

3.

current_user
remember

session[:user_id]

8.55

8.55
test/helpers/sessions_helper_test.rb
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
@user = users(:michael)
remember(@user)
end
test "current_user returns right user when session is nil" do
assert_equal @user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
@user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
nil

if

authenticated?
if user && user.authenticated?(cookies[:remember_token])

8.55
8.56

RED

$ bundle exec rake test TEST=test/helpers/sessions_helper_test.rb


raise

8.57

current_user
authenticated?

8.57
8.55

8.4.

- 287

8.57

GREEN

app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
#
cookie
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end

8.58

GREEN

$ bundle exec rake test


current_user

8.5.

master
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Finish log in/log out"
git checkout master
git merge log-in-log-out

$
$
$
$

bundle exec rake test


git push
git push heroku
heroku run rake db:migrate

288 -

$
$
$
$

heroku maintenance:on
git push heroku
heroku run rake db:migrate
heroku maintenance:off

Heroku

8.5.1.
Rails

cookie

cookie

flash.now

session

ID

cookies

cookie

ID

ID

cookie

if-else

8.6.
3
1.

User

8.32
User.new_token

8.59
8.59

User.digest

8.60
self

8.60

2. 8.4.5

User

self

remember_token

8.51
assigns
assigns
@user

create

assigns(:user)

ate

creuser

cookies

8.61

8.62

FILL_IN

8.6.

- 289

8.59

self

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
def self.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def self.new_token
SecureRandom.urlsafe_base64
end
.
.
.
end

8.60

class << self

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
class << self
#
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#
def new_token
SecureRandom.urlsafe_base64
end
end
.
.
.
create
app/controllers/sessions_controller.rb

8.61

class SessionsController < ApplicationController


def new

290 -

end
def create
?user = User.find_by(email: params[:session][:email].downcase)
if ?user && ?user.authenticate(params[:session][:password])
log_in ?user
params[:session][:remember_me] == '1' ? remember(?user) : forget(?user)
redirect_to ?user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end

8.62

GREEN

test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_equal assigns(:user).FILL_IN, FILL_IN
end
test "login without remembering" do
log_in_as(@user, remember_me: '0')
assert_nil cookies['remember_token']
end
.
.
.
end

8.6.

- 291

REST

edit

7.1

update

index

destroy

9.1.
new

7
edit
PATCH

create

3.2
8

POST

update

before filter

updating-users
$ git checkout master
$ git checkout -b updating-users

9.1.1.
9.1

edit

edit

7.1
ID

params[:id]

/users/1/edit
9.1

edit

ID

9.1
edit
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)

1.

http://www.flickr.com/photos/sashawolff/4598355045/

293

if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end

9.1

294 -

9.2

7.13

9.6
9.2
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
error_messages

7.3.3

Gravatar

target="_blank"
@user

9.1
Rails

9.2

Name Email

@user

9.1.

- 295

9.2
HTML
9.3

9.3

9.2

HTML

<form accept-charset="UTF-8" action="/users/1" class="edit_user"


id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" />
.
.
.
</form>

<input name="_method" type="hidden" value="patch" />


PATCH
PATCH

7.1

9.2
Rails
Active Record

2.

296 -

Rails

POST

form_for(@user)

7.13

POST

PATCH

new_record?

Rails

REST

Rails

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
form_for(@user)

@user.new_record?

true

POST

PATCH

7.1
it_user_path

8.36

ed-

current_user

<%= link_to "Settings", edit_user_path(current_user) %>

9.4
9.4

Settings

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>

9.1.2.
update

7.3
params

update_attributes

6.1.5

9.5

9.1.

- 297

false

else

create

7.16

update
app/controllers/users_controller.rb

9.5

class UsersController < ApplicationController


def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
#
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
update_attributes

ter

user_params

7.3.2
9.2
9.3

298 -

strong parame-

9.3

9.1.3.
9.1.2

3.3

$ rails generate integration_test users_edit


invoke test_unit
create
test/integration/users_edit_test.rb

9.6
patch

PATCH

get

post

delete

9.6

GREEN

test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do

9.1.

- 299

get edit_user_path(@user)
patch user_path(@user), user: { name: '',
email: 'foo@invalid',
password:
'foo',
password_confirmation: 'bar' }
assert_template 'users/edit'
end
end

9.7

GREEN

$ bundle exec rake test

9.1.4.

TDD
Gravatar
9.2

9.4

300 -

change

Gravatar

acceptance test

9.4

9.6
9.8

9.8
@user.reload

6.1.5

TDD
9.8

RED

test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit" do
get edit_user_path(@user)
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), user: { name: name,
email: email,
password:
"",
password_confirmation: "" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal @user.name, name
assert_equal @user.email, email
end
end
create

9.8

8.22

update

9.9
update
app/controllers/users_controller.rb

9.9

RED

class UsersController < ApplicationController


.
.
.
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'

9.1.

- 301

end
end
.
.
.
end

9.9

6.39

9.8
allow_blank: true

9.10

validates

9.10

GREEN

app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 }
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }, allow_blank: true
.
.
.
end

6.3
has_secure_password

9.5

9.11

GREEN

$ bundle exec rake test

302 -

9.5

9.2.
Web

edit

9.1

update

9.2.1
9.6
9.2.2

9.2.

- 303

9.6

9.2.1.
9.6

before_action

logged_in_user

before_action :logged_in_user

logged_in_user
app/controllers/users_controller.rb

9.12

9.12

RED

class UsersController < ApplicationController


before_action :logged_in_user, only: [:edit, :update]
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,

before_filter

3.

304 -

Rails

:password_confirmation)
end
#
#
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
:only
edit

update

/users/1/edit

9.7

9.7
9.12
9.13

RED

$ bundle exec rake test


edit

update

9.2.

- 305

edit

update

8.4.6

8.50
9.14

9.14
GREEN

test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
end
setup

9.2.3
setup

9.15

GREEN

$ bundle exec rake test

9.16
9.16

9.16

GREEN

app/controllers/users_controller.rb
class UsersController < ApplicationController
# before_action :logged_in_user, only: [:edit, :update]
.
.
.
end

306 -

log_in_as

edit
GET

update

7.1

PATCH

get

9.17
edit
update
test/controllers/users_controller_test.rb

patch

9.17

RED

require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
end
test "should get new" do
get :new
assert_response :success
end
test "should redirect edit when not logged in" do
get :edit, id: @user
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_redirected_to login_url
end
end
get

patch

get :edit, id: @user

patch :update, id: @user, user: { name: @user.name, email: @user.email }

Rails

id: @user

user

Rails
2

@user.id

patch

9.18
9.18

GREEN

app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.

9.2.

- 307

.
end

9.19

GREEN

$ bundle exec rake test


edit

9.2.2.
9.2.1
9.17

9.20
9.20
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>

8.50

log_in_as

9.21

9.21

RED

test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user
= users(:michael)
@other_user = users(:archer)
end
test "should get new" do
get :new
assert_response :success
end
test "should redirect edit when not logged in" do

308 -

edit

update

get :edit, id: @user


assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_redirected_to login_url
end
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
get :edit, id: @user
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_redirected_to root_url
end
end
correct_user

9.22
update

correct_user

@user

edit

@user

9.22
edit
update
app/controllers/users_controller.rb

correct_user

GREEN

class UsersController < ApplicationController


before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user,
only: [:edit, :update]
.
.
.
def edit
end
def update
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,

9.2.

- 309

:password_confirmation)
end
#
#
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
#
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless @user == current_user
end
end

9.23

GREEN

$ bundle exec rake test


current_user?
rect_user

9.24

unless @user == current_user

unless current_user?(@user)

9.24 current_user?
app/helpers/sessions_helper.rb
module SessionsHelper
#
def log_in(user)
session[:user_id] = user.id
end
#
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
#
def current_user?(user)

310 -

cor-

true

user == current_user
end
.
.
.
end

9.25
9.25 correct_user
app/controllers/users_controller.rb

GREEN

class UsersController < ApplicationController


before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user,
only: [:edit, :update]
.
.
.
def edit
end
def update
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
#
#
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
#
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)

9.2.

- 311

end
end

9.2.3.

/users/1

/users/1/edit
9.14
9.26

9.26

RED

test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_path(@user)
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), user: { name: name,
email: email,
password:
"foobar",
password_confirmation: "foobar" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal @user.name, name
assert_equal @user.email, email
end
end
4

store_location
rect_back_or

4.

312 -

9.27

thoughtbot

Clearance gem

redi-

9.27
app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
#
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
#
def store_location
session[:forwarding_url] = request.url if request.get?
end
end
session

8.2.1

request

9.27

request.url
store_location

session[:forwarding_url]
POST

GET
store_location

9.28
store_location
app/controllers/users_controller.rb

if request.get?
logged_in_user

GET
PATCH

DELETE

9.28

logged_in_user

class UsersController < ApplicationController


before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user,
only: [:edit, :update]
.
.
.
def edit
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end

5.

Yoel Adler

9.2.

- 313

#
#
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
#
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
create

redirect_back_or

9.29

redirect_back_or

||
session[:forwarding_url] || default
session[:forwarding_url]

nil

9.27
return

9.29
create
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end

314 -

9.26

9.30

GREEN

$ bundle exec rake test

9.3.
index

index

Users

9.8

9.4

9.8

9.3.1.

6.

http://www.flickr.com/photos/glasgows/338937124/

9.3.

- 315

index

index

9.31
index
RED
test/controllers/users_controller_test.rb

9.31

require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user
= users(:michael)
@other_user = users(:archer)
end
test "should redirect index when not logged in" do
get :index
assert_redirected_to login_url
end
.
.
.
end
index

logged_in_user

index
app/controllers/users_controller.rb

9.32

9.32

GREEN

class UsersController < ApplicationController


before_action :logged_in_user, only: [:index, :edit, :update]
before_action :correct_user,
only: [:edit, :update]
def index
end
def show
@user = User.find(params[:id])
end
.
.
.
end
index

2.5
@users

9.33
9.3.3

7. Twitter

316 -

User.all

9.33
index
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all
end
.
.
.
end

li

each

Gravatar

ul

9.34

index
app/views/users/index.html.erb

9.34

<% provide(:title, 'All users') %>


<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>

9.34

7.7

7.31

Gravatar
7.31

CSS
9.35

SCSS

9.35

CSS

app/assets/stylesheets/custom.css.scss
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;

9.3.

- 317

padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
users_path

7.1

9.36
9.36
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav pull-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>

9.37

GREEN

$ bundle exec rake test

9.9

318 -

9.9

9.3.2.

Ruby
Gemfile

9.38

Gemfile

faker gem

Rake

9.38

gem

faker

source 'https://rubygems.org'
gem 'rails',
gem 'bcrypt',
gem 'faker',
.
.
.

'4.2.0'
'3.1.7'
'1.4.2'

$ bundle install

Rake
9.39

Rails

db/seeds.rb

9.3.

- 319

9.39

Rake

db/seeds.rb
User.create!(name: "Example User",
email: "example@railstutorial.org",
password:
"foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password:
password,
password_confirmation: password)
end

9.39
create!

99
create

false
db:seed

Rake

$ bundle exec rake db:migrate:reset


$ bundle exec rake db:seed

db:seed Rake

100

9.10
Gravatar

rake db:reset

8.

320 -

Rails

9.10

100

9.3.3.

100
30
Rails

will_paginate

will_paginate

will_paginate
9.40

bootstrap-will_paginate

Gemfile

gem
Gemfile

Bootstrap

bootstrap-will_paginate

9.40

will_paginate

source 'https://rubygems.org'
gem
gem
gem
gem
gem
.
.
.

'rails',
'bcrypt',
'faker',
'will_paginate',
'bootstrap-will_paginate',

'4.2.0'
'3.1.7'
'1.4.2'
'3.0.7'
'0.0.10'

$ bundle install
9.3.

- 321

Web

gem
index

index

Rails

User.all

will_paginate

9.41

9.41
index
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
will_paginate

@users
@users

9.41
will_paginate

9.33

User.all

paginate

$ rails console
>> User.paginate(page: 1)
User Load (1.5ms) SELECT "users".* FROM "users" LIMIT 30 OFFSET 0
(1.7ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
paginate

:page

User.paginate

30
:page
index
nate

:page

nil
all

1-30

paginate

params[:page]

pagi-

9.42
params

index
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.paginate(page: params[:page])
end
.
9

31-60

paginate

9.42

322 -

:page

will_pagenate

.
.
end

9.11

Rails

will_paginate

9.11
2

Next

9.12

9.3.

- 323

9.12

9.3.4.

30
9.20

30

password_digest

30

Ruby
9.43

9.43

30

test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane

324 -

9.43

email: hands@example.gov
password_digest: <%= User.digest('password') %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>

$ rails generate integration_test users_index


invoke test_unit
create
test/integration/users_index_test.rb
pagination

9.44

9.44

GREEN

test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end

9.45

GREEN

$ bundle exec rake test

9.3.

- 325

9.3.5.
Rails

9.41

li

render

9.46

9.46
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
render

User

user

_user.html.erb

Rails
9.47

9.47
app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
@users

9.48

render

9.48

GREEN

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>

user
User

9.

326 -

@users.each do |foobar|

render foobar

<%= will_paginate %>

Rails

@users

User

render

_user.html.erb

9.49

Rails
9.48

GREEN

$ bundle exec rake test

9.4.
REST
9.13

destroy
destroy

9.13

9.4.

- 327

9.4.1.
admin

admin

admin?

Active Record

admin

9.14

9.14

admin

admin

boolean

$ rails generate migration add_admin_to_users admin:boolean


users

admin

9.50

9.50

default: false
admin

nil

Rails
9.50
admin
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false
end
end

$ bundle exec rake db:migrate

Rails
$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

328 -

add_column
default: false

admin

admin?

toggle!

admin

false

true

9.51
9.51
db/seeds.rb
User.create!(name: "Example User",
email: "example@railstutorial.org",
password:
"foobar",
password_confirmation: "foobar",
admin: true)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password:
password,
password_confirmation: password)
end

$ bundle exec rake db:migrate:reset


$ bundle exec rake db:seed

admin: true

9.51
10

PATCH

patch /users/17?admin=1

17

7.3.2

params

require

permit

def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
admin
:admin

10. curl

PATCH

9.4.

- 329

9.4.2. destroy
destroy

9.52
9.52
app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</li>
method: delete

DELETE

if

9.15

9.15
DELETE
DELETE

330 -

Rails

JavaScript
JavaScript
JavaScript

JavaScript
POST
11

destroy

Active Record

7.1

destroy

destroy

9.53
logged_in_user

9.53

:destroy

9.53
destroy
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user,
only: [:edit, :update]
.
.
.
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
.
.
.
end
destroy

find

destroy

User.find(params[:id]).destroy

DELETE
destroy

9.2.1

9.2.2

destroy

admin_user

9.54

destroy

9.54
app/controllers/users_controller.rb

class UsersController < ApplicationController


before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user,
only: [:edit, :update]
before_action :admin_user,
only: :destroy
.
.
.
private
.
.
.
#
def admin_user
redirect_to(root_url) unless current_user.admin?

11.

RailsCasts

Destroy Without JavaScript

9.4.

- 331

end
end

9.4.3.

9.55
9.55
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>

9.2.1

8.28
delete

destroy

DELETE

9.56
9.56

GREEN

test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user
= users(:michael)
@other_user = users(:archer)

332 -

end
.
.
.
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as(@other_user)
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to root_url
end
end
assert_no_difference

9.56

7.21

9.56
9.44

assert_difference 'User.count', -1 do
delete user_path(@other_user)
end
assert_difference

7.26
DELETE

User.count

-1

9.57

9.57

GREEN

test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@admin
= users(:michael)
@non_admin = users(:archer)
end
test "index as admin including pagination and delete links" do
log_in_as(@admin)
get users_path

9.4.

- 333

assert_template 'users/index'
assert_select 'div.pagination'
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete',
method: :delete
end
end
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
end
test "index as non-admin" do
log_in_as(@non_admin)
get users_path
assert_select 'a', text: 'delete', count: 0
end
end

9.57
9.52

9.58

GREEN

$ bundle exec rake test

9.5.
5.4

10

master
$
$
$
$
$

git
git
git
git
git

add -A
commit -m "Finish user edit, update, index, and destroy actions"
checkout master
merge updating-users
push
pg:reset

$ bundle exec rake test


$ git push heroku
$ heroku pg:reset DATABASE

334 -

$ heroku run rake db:migrate


$ heroku run rake db:seed
$ heroku restart

9.16

9.11
11.1.4

9.16

9.5.1.
update

Web

PATCH

HTTP

Rails
render @users

db/seeds.rb

rake db:seed

_user.html.erb

9.5.

- 335

admin

user.admin?
destroy

Ruby

9.6.
3
1.
session[:forwarding_url]

9.26
2.
log_in_as

5.25
3.

update

9.59

PATCH

admin

4.

new.html.erb

9.60
f

admin
user_params

edit.html.erb

9.61

9.59
admin
test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user
= users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_redirected_to root_url
end
test "should not allow the admin attribute to be edited via the web" do
log_in_as(@other_user)
assert_not @other_user.admin?
patch :update, id: @other_user, user: { password:
FILL_IN,
password_confirmation: FILL_IN,
admin: FILL_IN }
assert_not @other_user.FILL_IN.admin?
end
.

336 -

DELETE

.
.
end

9.60 new
edit
app/views/users/_fields.html.erb
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>

9.61
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'fields', f: f %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>

9.6.

- 337

10

7.1

REST

Rails

10.1.
7

8.2
1.

8.4

2.
3.
2

4.
5.

User.digest

User.new_token

user.authenticated?

10.1

10.1.3

10.2
authenticated?

10.1

email

password

password_digest

authenticate(password)

id

remember_token

remember_digest

authenticated?(:remember, token)

email

activation_token

activation_digest

authenticated?(:activation, token)

1.
2.

9.1
ID

URL

ID

ID

339

email

reset_token

reset_digest

authenticated?(:reset, token)

10.3
master

$ git checkout master


$ git checkout -b account-activation-password-resets

10.1.1.
8.1

REST URL
3

edit

$ rails generate controller AccountActivations

URL
edit_account_activation_url(activation_token, ...)
edit

resources

10.1

10.1
config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'
get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
end

10.2
5

3.

update
update

5.

340 -

GET
PATCH
edit

4.

10

8.4

edit

user.activation_token

user.authenticated?(:activation, token)
authenticated?

8.33

activated

9.4.1
if user.activated? ...

10.1

10.1

$ rails generate migration add_activation_to_users \


> activation_digest:string activated:boolean activated_at:datetime
admin

9.50

activated

false

10.2

10.2
db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration
def change
add_column :users, :activation_digest, :string
add_column :users, :activated, :boolean, default: false
add_column :users, :activated_at, :datetime
end
end

$ bundle exec rake db:migrate

10.1.

- 341

be-

6.2.5
fore_save

downcase

before_save

6.31

before_create
before_create :create_activation_digest

create_activation_digest

Rails

before_save

6.31

cre-

ate_activation_digest

7.3.2

Ruby

private
private
def create_activation_digest
#
end
private

$ rails console
>> User.first.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User>
before_create
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)

8.32

remember
#
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
remember

update_attribute

before_create

User.new
activation_token

7.17
activation_digest

10.1

at-

10.3
tr_accessor

10.3

GREEN

app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token
before_save
:downcase_email

342 -

10

activation_digest

before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
.
.
.
private
#
def downcase_email
self.email = email.downcase
end
#
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end

10.4
Time.zone.now

10.5

Rails

10.4
db/seeds.rb
User.create!(name: "Example User",
email: "example@railstutorial.org",
password:
"foobar",
password_confirmation: "foobar",
admin:
true,
activated: true,
activated_at: Time.zone.now)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password:
password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now)
end

10.5
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true

10.1.

- 343

activated: true
activated_at: <%= Time.zone.now %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
<% end %>

10.4
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

10.1.2.
Action Mailer
create

rails generate
$ rails generate mailer UserMailer account_activation password_reset
account_activation

10.2

Rails

HTML
10.6

344 -

10

password_reset

10.7

10.6
app/views/user_mailer/account_activation.text.erb
UserMailer#account_activation
<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb

10.7

HTML

app/views/user_mailer/account_activation.html.erb
<h1>UserMailer#account_activation</h1>
<p>
<%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>

10.8
form
@greeting
UserMailer
app/mailers/user_mailer.rb

10.8

class UserMailer < ActionMailer::Base


default from: "from@example.com"
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
#
en.user_mailer.account_activation.subject
#
def account_activation
@greeting = "Hi"
mail to: "to@example.org"
end
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
#
en.user_mailer.password_reset.subject
#
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end

user.email

10.9

mail

subject

10.9
app/mailers/user_mailer.rb

10.1.

- 345

class UserMailer < ActionMailer::Base


default from: "noreply@example.com"
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end

Ruby

10.1
edit_account_activation_url(@user.activation_token, ...)
edit_user_url(user)
http://www.example.com/users/1/edit

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit
q5lt38hQDc_959PVoo6b7A

URL

new_token

/users/1/edit

8.31

base64

AccountActivationsController

ID

params[:id]

query parameter

URL

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
%40

URL

Rails

edit_account_activation_url(@user.activation_token, email: @user.email)

Rails
params[:email]
@user

10.11

6. URL

it?name=Foo%20Bar&email=foo%40example.com

346 -

10

edit

10.9
10.10

10.11

&

/ed-

Ruby
link_to

edit

10.10
app/views/user_mailer/account_activation.text.erb
Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>

10.11

HTML

app/views/user_mailer/account_activation.html.erb
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
email: @user.email) %>

Rails
10.12

URL

10.12
config/environments/development.rb
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'example.com'
config.action_mailer.default_url_options = { host: host }
.
.
.
end

10.12

'example.com'

IDE
host = 'rails-tutorial-c9-mhartl.c9.io'
host = 'localhost:3000'

#
#

IDE

10.12
10.13
10.13
test/mailers/previews/user_mailer_preview.rb

10.1.

- 347

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer


class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
UserMailer.account_activation
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end

10.9

account_activation

10.13
user

UserMailer.account_activation
user.activation_token

10.10

10.14
acti-

10.11

vation_token

10.14
test/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end

URL
calhost:3000

348 -

10

URL

HTML

IDE
10.2

10.3

lo-

10.2

HTML

10.3

10.1.

- 349

Rails
10.15
UserMailer
test/mailers/user_mailer_test.rb

10.15

Rails

require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
mail = UserMailer.account_activation
assert_equal "Account activation", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
end
test "password_reset" do
mail = UserMailer.password_reset
assert_equal "Password reset", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
end
end
assert_match

10.15
assert_match
assert_match
assert_match
assert_match

10.16

'foo',
'baz',
/\w+/,
/\w+/,

'foobar'
'foobar'
'foobar'
'$#!*+@'

#
#
#
#

true
false
true
false

assert_match
CGI::escape(user.email)

10.16

RED

test/mailers/user_mailer_test.rb
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
user = users(:michael)
user.activation_token = User.new_token
mail = UserMailer.account_activation(user)
assert_equal "Account activation", mail.subject
assert_equal [user.email], mail.to

7.

ruby rails escape url

CGI.escape(str)

350 -

10

URI::encode(str)

assert_equal
assert_match
assert_match
assert_match
end
end

["noreply@example.com"],
user.name,
user.activation_token,
CGI::escape(user.email),

mail.from
mail.body.encoded
mail.body.encoded
mail.body.encoded

10.16
10.17
10.17
config/environments/test.rb
Rails.application.configure do
.
.
.
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'example.com' }
.
.
.
end

10.18

GREEN

$ bundle exec rake test:mailers


create

10.19

10.19

10.19

7.4

RED

app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.

10.1.

- 351

.
end

10.20
10.1.4
10.20

GREEN

test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password:
"foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
end
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: "Example User",
email: "user@example.com",
password:
"password",
password_confirmation: "password" }
end
# assert_template 'users/show'
# assert is_logged_in?
end
end

10.4

10.21
10.3

10.21
Sent mail to michael@michaelhartl.com (931.6ms)
Date: Wed, 03 Sep 2014 19:47:18 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <540770474e16_61d3fd1914f4cd0300a0@mhartl-rails-tutorial-953753.mail>
Subject: Account activation
Mime-Version: 1.0

352 -

10

Content-Type: multipart/alternative;
boundary="--==_mimepart_5407704656b50_61d3fd1914f4cd02996a";
charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_5407704656b50_61d3fd1914f4cd02996a
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Hi Michael Hartl,
Welcome to the Sample App! Click on the link below to activate your account:
http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com
----==_mimepart_5407704656b50_61d3fd1914f4cd02996a
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Sample App</h1>
<p>Hi Michael Hartl,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<a href="http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com">Activate</a>
----==_mimepart_5407704656b50_61d3fd1914f4cd02996a--

10.1.

- 353

10.4

10.1.3.
AccountActivationsController

10.21
edit

10.1.2
8.5

params[:id]

8.36

user = User.find_by(email: params[:email])


if user && user.authenticated?(:activation, params[:id])

authenticated?
authenticated?

8.33

#
true
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember_digest
self.remember_digest

354 -

10

params[:email]

self.activation_token

authenticated?

Rails

metaprogramming

Ruby
send

Ruby
send
$ rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send('length')
=> 3
:length

'length'

send

length

activation_digest
>>
>>
=>
>>
=>
>>
=>
>>
>>
=>

user = User.first
user.activation_digest
"$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
user.send(:activation_digest)
"$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
user.send('activation_digest')
"$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
attribute = :activation
user.send("#{attribute}_digest")
"$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
attribute
send

attribute
"#{attribute}_digest"

send

:activation
'activation'

"activation_digest"

7.4.2

authenticated?

def authenticated?(remember_token)
digest = self.send('remember_digest')
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(remember_token)
end

def authenticated?(attribute, token)


digest = self.send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end

10.1.

- 355

token
self
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
authenticated?
user.authenticated?(:remember, remember_token)
authenticated?

10.22
authenticated?

10.22

RED

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
true
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
.
.
.
end

10.22
10.23

RED

$ bundle exec rake test


current_user

nil

8.36

8.43

authenticated?

au-

thenticated?

10.24

current_user
app/helpers/sessions_helper.rb

authenticated?

10.24

module SessionsHelper
.
.
.
#
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])

356 -

10

10.25
GREEN

user = User.find_by(id: user_id)


if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end

10.25
UserTest
test/models/user_test.rb

authenticated?

GREEN

require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?(:remember, '')
end
end

10.26

GREEN

$ bundle exec rake test

8.4.2

10.22

authenticated?

8.4.6

edit

params

if user && !user.activated? && user.authenticated?(:activation, params[:id])


!user.activated?

activated_at
user.update_attribute(:activated,
true)
user.update_attribute(:activated_at, Time.zone.now)
edit

10.27

10.27

10.1.

- 357

10.27
edit
app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.update_attribute(:activated,
true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end

10.21

http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com

10.5

358 -

10

10.5

user.activated?

10.6

true

10.28

10.28
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else

10.1.

- 359

flash.now[:danger] = 'Invalid email/password combination'


render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end

10.6

10.1.4

10.1.4.

7.4.4

7.26
10.29

10.29
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest

360 -

10

GREEN

def setup
ActionMailer::Base.deliveries.clear
end
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password:
"foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
end
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, user: { name: "Example User",
email: "user@example.com",
password:
"password",
password_confirmation: "password" }
end
assert_equal 1, ActionMailer::Base.deliveries.size
user = assigns(:user)
assert_not user.activated?
#
log_in_as(user)
assert_not is_logged_in?
#
get edit_account_activation_path("invalid token")
assert_not is_logged_in?
#
get edit_account_activation_path(user.activation_token, email: 'wrong')
assert_not is_logged_in?
#
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end

assert_equal 1, ActionMailer::Base.deliveries.size
deliveries

10.2.5

setup

10.29
10.1.

- 361

assigns

assigns

create

@user

10.29

10.30

assigns(:user)

10.20

GREEN

$ bundle exec rake test

10.29
activate
send_activation_email

10.32

10.31

10.33

10.31
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
def activate
update_attribute(:activated,
true)
update_attribute(:activated_at, Time.zone.now)
end
#
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
private
.
.
.
end

10.32
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
362 -

10

end
end
.
.
.
end

10.33
app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
user

10.31

-user.update_attribute(:activated,
true)
-user.update_attribute(:activated_at, Time.zone.now)
+update_attribute(:activated,
true)
+update_attribute(:activated_at, Time.zone.now)
user

self

6.2.5

self

UserMailer

@user

self
-UserMailer.account_activation(@user).deliver_now
+UserMailer.account_activation(self).deliver_now

10.34

GREEN

$ bundle exec rake test

$ git add -A
$ git commit -m "Add account activations"

10.1.

- 363

10.2.

10.1

Forgot Password

10.7

10.7

Forgot Password

Forgot Password
10.8

364 -

10

10.8

Forgot Password
10.9

10.2.

- 365

10.9

1.
2.
3.
4.
5.

10.2.1.
10.1.1
$ rails generate controller PasswordResets new edit --no-test-framework

10.1.4

366 -

10

new

create

edit

10.35

10.8

update

10.9
resources

10.35

config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'
get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets,
only: [:new, :create, :edit, :update]
end

10.2
10.2

REST

REST

HTTP

URL

GET

/password_resets/new

new

new_password_reset_path

POST

/password_resets

create

password_resets_path

GET

/password_resets/<token>/edit

edit

edit_password_reset_path(token)

PATCH

/password_resets/<token>

update

password_reset_path(token)

Forgot Password
new_password_reset_path

10.36

10.10

10.36
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>

10.2.

- 367

<%= link_to "(forgot password)", new_password_reset_path %>


<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>

10.10

Forgot Password
10.1

reset_digest

368 -

10

reset_sent_at

10.11

8.4

10.1

10.11

$ rails generate migration add_reset_to_users reset_digest:string \


> reset_sent_at:datetime

$ bundle exec rake db:migrate

10.2.2.
8.2
10.37
10.37
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>

10.2.

- 369

<%= f.check_box :remember_me %>


<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
form_for

10.37
10.38
10.38
app/views/password_resets/new.html.erb
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>

370 -

10

10.12

10.12

Forgot Password
reset_token

10.12
reset_sent_at

reset_digest

8.9
flash.now

create

10.39
PasswordResetsController
create
app/controllers/password_resets_controller.rb

10.39

class PasswordResetsController < ApplicationController


def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end

10.2.

- 371

def edit
end
end
create_reset_digest

10.40

10.40
app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token, :reset_token
before_save
:downcase_email
before_create :create_activation_digest
.
.
.
#
def activate
update_attribute(:activated,
true)
update_attribute(:activated_at, Time.zone.now)
end
#
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
#
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
#
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
#
def downcase_email
self.email = email.downcase
end
#
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end

372 -

10

10.13
10.2.3

10.13

Forgot Password

10.2.3.
10.40
UserMailer.password_reset(self).deliver_now
UserMailer

10.1.2
password_reset

10.41

10.42

HTML

10.43
10.41
app/mailers/user_mailer.rb
class UserMailer < ActionMailer::Base
default from: "noreply@example.com"
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset(user)
@user = user
10.2.

- 373

mail to: user.email, subject: "Password reset"


end
end

10.42
app/views/user_mailer/password_reset.text.erb
To reset your password click the link below:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.

10.43

HTML

app/views/user_mailer/password_reset.html.erb
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= link_to "Reset password",
edit_password_reset_url(@user.reset_token,
email: @user.email) %>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>

10.1.2
10.14

Rails
10.44

10.44
test/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset

374 -

10

user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
end

HTML

10.14

10.14

10.15

HTML

10.2.

- 375

10.15
10.16

10.45
before_create

10.3

Forgot Password
10.52

10.45

GREEN

test/mailers/user_mailer_test.rb
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
user = users(:michael)
user.activation_token = User.new_token
mail = UserMailer.account_activation(user)
assert_equal "Account activation", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.name,
mail.body.encoded
assert_match user.activation_token,
mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
test "password_reset" do

376 -

10

user = users(:michael)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token,
mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
end

10.46

GREEN

$ bundle exec rake test

10.41

10.42

10.43
10.47

10.16

10.16
10.47
Sent mail to michael@michaelhartl.com (66.8ms)
Date: Thu, 04 Sep 2014 01:04:59 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <5407babbee139_8722b257d04576a@mhartl-rails-tutorial-953753.mail>

10.2.

- 377

Subject: Password reset


Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5407babbe3505_8722b257d045617";
charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
http://rails-tutorial-c9-mhartl.c9.io/password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<a href="http://rails-tutorial-c9-mhartl.c9.io/
password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com">Reset password</a>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
----==_mimepart_5407babbe3505_8722b257d045617--

10.2.4.

http://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=foo%40bar.com

9.2
edit
edit

378 -

10

update


update

10.48

10.48
app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>

10.48
hidden_field_tag :email, @user.email

f.hidden_field :email, @user.email


params[:email]
params[:user][:email]
PasswordResetsController

edit

@user

params[:email]

10.27
10.22

authenticated?

params[:id]

edit

@user

update

10.49

10.49
edit
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user,
only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
.
.
.
def edit

10.2.

- 379

end
private
def get_user
@user = User.find_by(email: params[:email])
end
#
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
end

10.49

authenticated?(:reset, params[:id])

cookies[:remember_token])

10.27

10.24

authenticated?(:remember,

authenticated?(:activation, params[:id])

authenticated?

10.1

10.47

10.17

10.17
edit

update

10.50
if

380 -

Active Record
10

9.10

10.50
update
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user,
only: [:edit, :update]
before_action :valid_user,
only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if both_passwords_blank?
flash.now[:danger] = "Password/confirmation can't be blank"
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end

8.

10.2.

- 381

#
true
def both_passwords_blank?
params[:user][:password].blank? &&
params[:user][:password_confirmation].blank?
end
#
def get_user
@user = User.find_by(email: params[:email])
end
#
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
#
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end

@user.password_reset_expired?
password_reset_expired?

10.2.3
Ruby

reset_sent_at < 2.hours.ago


<
<

word_reset_expired?

10.51

10.51
password_reset_expired?

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
def password_reset_expired?
reset_sent_at < 2.hours.ago
end

382 -

10

pass-

true

10.6

private
.
.
.
end

10.50
10.19

update

10.18

10.5

10.18

10.2.

- 383

10.19

10.2.5.
10.50

$ rails generate integration_test password_resets


invoke test_unit
create
test/integration/password_resets_test.rb

10.29
word
10.52
10.52
test/integration/password_resets_test.rb
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end

384 -

10

Forgot Pass-

test "password resets" do


get new_password_reset_path
assert_template 'password_resets/new'
#
post password_resets_path, password_reset: { email: "" }
assert_not flash.empty?
assert_template 'password_resets/new'
#
post password_resets_path, password_reset: { email: @user.email }
assert_not_equal @user.reset_digest, @user.reload.reset_digest
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
#
user = assigns(:user)
#
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
#
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
#
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
#
get edit_password_reset_path(user.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
#
patch password_reset_path(user.reset_token),
email: user.email,
user: { password:
"foobaz",
password_confirmation: "barquux" }
assert_select 'div#error_explanation'
#
patch password_reset_path(user.reset_token),
email: user.email,
user: { password:
" ",
password_confirmation: " " }
assert_not flash.empty?
assert_template 'password_resets/edit'
#
patch password_reset_path(user.reset_token),
email: user.email,
user: { password:
"foobaz",
password_confirmation: "foobaz" }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user

10.2.

- 385

end
end
input

10.52

assert_select "input[name=email][type=hidden][value=?]", user.email


name

input

<input id="email" name="email" type="hidden" value="michael@example.com" />

10.53

GREEN

$ bundle exec rake test

10.3.

SendGrid

Heroku

Heroku
200
$ heroku addons:add sendgrid:starter

SendGrid

SMTP
10.54

10.54

SendGrid

config/environments/production.rb
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
host = '<your heroku app>.herokuapp.com'
config.action_mailer.default_url_options = { host: host }
ActionMailer::Base.smtp_settings = {
:address
=> 'smtp.sendgrid.net',
:port
=> '587',
:authentication => :plain,
:user_name
=> ENV['SENDGRID_USERNAME'],
:password
=> ENV['SENDGRID_PASSWORD'],
:domain
=> 'heroku.com',
:enable_starttls_auto => true
}
.
.

386 -

10

host

.
end

10.54

SendGrid

user_name

password

ENV

SendGrid

11.4.4

$ heroku config:get SENDGRID_USERNAME


$ heroku config:get SENDGRID_PASSWORD

$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Add password resets & email configuration"
git checkout master
git merge account-activation-password-reset

Heroku
$ bundle exec rake test
$ git push heroku master
$ heroku run rake db:migrate

SendGrid
10.20
10.21

10.2

10.20

10.3.

- 387

10.21

10.4.

Twitter
Rails

11

12
has_many

has_many :through

10.4.1.

Active Record

Rails

Action Mailer

Action Mailer

HTML

Active Record

URL

SendGrid

10.5.
3

388 -

10

1.

10.55

10.50
response.body

10.55
10.55

HTML
expired

2.

/users/:id
where

tive Record
3.

10.40

activate

10.56
11.3.3

/users

create_reset_digest

/users/:id
update_attribute

transaction
update_attribute

Ac-

10.57

update_columns

10.55

GREEN

test/integration/password_resets_test.rb
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
.
.
.
test "expired token" do
get new_password_reset_path
post password_resets_path, password_reset: { email: @user.email }
@user = assigns(:user)
@user.update_attribute(:reset_sent_at, 3.hours.ago)
patch password_reset_path(@user.reset_token),
email: @user.email,
user: { password:
"foobar",
password_confirmation: "foobar" }
assert_response :redirect
follow_redirect!
assert_match /FILL_IN/i, response.body
end
end

10.56
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.

9.

10.56
and
redirect_to root_url

&&

&&

root_url

and

10.5.

- 389

.
def index
@users = User.where(activated: FILL_IN).paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless FILL_IN
end
.
.
.
end

10.57

update_columns

app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token, :reset_token
before_save
:downcase_email
before_create :create_activation_digest
.
.
.
#
def activate
update_columns(activated: FILL_IN, activated_at: FILL_IN)
end
#
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
#
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: FILL_IN,
reset_sent_at: FILL_IN)
end
#
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
.
.
.
end

390 -

10

10.6.
10.2.4

tr
te

tr > te
tN

tr

te
tr = tN - tr
te = tN - te

tr > te
tN - tr > tN - te
- tr > - te
-1
tr < te
te = 2.hours.ago

10.51

password_reset_expired?

def password_reset_expired?
reset_sent_at < 2.hours.ago
end

10.2.4

<

10.6.

- 391

11

Active Record

2.3
has_many

belongs_to

11.4
Twitter

12

11.1.
2.3

Git
$ git checkout master
$ git checkout -b user-microposts

11.1.1.
content

user_id

11.1

11.1
content

140

text

string
string

11.1.2

text
text

255
11.3.2

text

6.1

generate model

393

$ rails generate model Micropost content:text user:references


microposts
users

11.1
references

6.2

references

user_id
t.timestamps

11.1.4

created_at

6.1.1

updated_at

created_at

11.2.1

11.1
db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
t.references :user, index: true
t.timestamps null: false
end
add_index :microposts, [:user_id, :created_at]
end
end
user_id

created_at

6.2
add_index :microposts, [:user_id, :created_at]
user_id

dex

created_at

Rails

multiple key in-

Active Record

$ bundle exec rake db:migrate

11.1.2.

ID
11.1.3

Record

Active

6.7

setup

user_id

11.2

11.2
RED

test/models/micropost_test.rb
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup

394 -

11

@user = users(:michael)
#
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
end
setup

11.1.3
ID

11.3

RED

$ bundle exec rake test:models

ID
11.1

11.1.3
user_id

11.4

belongs_to

11.4

GREEN

app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end

11.5

GREEN

$ bundle exec rake test


content

user_id

2.3.2
140

content

6.2

11.6
11.6

RED

test/models/micropost_test.rb
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")

11.1.

- 395

end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
test "content should be present" do
@micropost.content = " "
assert_not @micropost.valid?
end
test "content should be at most 140 characters" do
@micropost.content = "a" * 141
assert_not @micropost.valid?
end
end

6.2

11.6

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
name

11.7

6.16

11.7

GREEN

app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end

11.8

GREEN

$ bundle exec rake test

11.1.3.
Web
2.3.3
11.3

396 -

11

11.2

belongs_to

11.2

has_many

11.3
belongs_to/has_many

Rails

11.1

Micropost.create
Micropost.create!
Micropost.new

user.microposts.create
user.microposts.create!
user.microposts.build
user_id

11.2
@user = users(:michael)
#
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)

11.1.

- 397

@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
new

build

@micropost

user_id

ID

11.1

micropost.user
user.microposts
user.microposts.create(arg)

user

user.microposts.create!(arg)

user

user.microposts.build(arg)

user

user.microposts.find_by(id: 1)

user

ID

@user.microposts.build
belongs_to :user

11.1
has_many :microposts

11.9

11.10

belongs_to

GREEN

app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end

11.10

has_many

GREEN

app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts
.
.
.
end

11.2

setup

11.11
11.11

GREEN

test/models/micropost_test.rb
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup

398 -

11

11.9

@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
.
.
.
end

11.12

GREEN

$ bundle exec rake test

11.1.4.

user.microposts

Twitter
1

most_recent

11.13

default scope

11.13
RED

test/models/micropost_test.rb
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
.
.
.
test "order should be most recent first" do
assert_equal Micropost.first, microposts(:most_recent)
end
end

1. 9.5

11.1.

- 399

11.14
11.14
test/fixtures/microposts.yml
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>

Ruby

11.15

created_at

Rails

RED

$ bundle exec rake test TEST=test/models/micropost_test.rb \


>
TESTOPTS="--name test_order_should_be_most_recent_first"
default_scope

Rails

default_scope

order

ated_at
order(:created_at)

SQL
order('created_at DESC')
DESC

SQL

Rails 4.0

Rails
Ruby

order(created_at: :desc)

11.16
11.16
default_scope
app/models/micropost.rb

400 -

11

GREEN

SQL

cre-

class Micropost < ActiveRecord::Base


belongs_to :user
default_scope -> { order(created_at: :desc) }
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end

11.16

->

Proc procedure
Proc

4.3.2

lambda
Proc

call

Proc
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil

Proc

Ruby
11.16
11.17

GREEN

$ bundle exec rake test

destroy
9.4

has_many

11.18

11.18
app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
.
.
.
end
dependent: :destroy

11.18
ID
11.19

9.57

11.19
dependent: :destroy GREEN
test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase

11.1.

- 401

def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "associated microposts should be destroyed" do
@user.save
@user.microposts.create!(content: "Lorem ipsum")
assert_difference 'Micropost.count', -1 do
@user.destroy
end
end
end

11.18
11.20

GREEN

$ bundle exec rake test

11.2.
11.3.2
Twitter
11.4

show

9.3.2

402 -

11

index

ERb

11.4

11.2.1.
show.html.erb

9.3
11.3
$ rails generate controller Microposts

9.3.5
<ul class="users">
<%= render @users %>
</ul>
_user.html.erb

@users

_micro-

post.html.erb
<ol class="microposts">
<%= render @microposts %>
</ol>

11.2.

- 403

ol

ul

11.21
11.21
app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</li>
time_ago_in_words

11.21

11.2.2

CSS ID

<li id="micropost-<%= micropost.id %>">

JavaScript
9.3.3
will_paginate
<%= will_paginate @microposts %>

9.41
<%= will_paginate %>
will_paginate
AvtiveRecord::Relation

9.3.3

@microposts
show

@microposts

show
app/controllers/users_controller.rb

11.22

will_paginate

11.22
@microposts

class UsersController < ApplicationController


.
.
.
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page])
end
.
.
.
end
paginate

microposts
count

404 -

11

@users

user.microposts.count
paginate

count

count
length

count

user_id

11.23
@user.microposts.any?

if

7.19

11.23
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
<div class="col-md-8">
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>

11.5

11.2.

- 405

11.5

11.2.2.
11.2.1
9.3.2
take
User.order(:created_at).take(6)
order

50
Lorem.sentence

Faker

30
2

11.24

11.24
db/seeds.rb
.
.
.
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)

2. Faker::Lorem.sentence

406 -

11

lorem ipsum

lorem ipsum

users.each { |user| user.microposts.create!(content: content) }


end

$ bundle exec rake db:migrate:reset


$ bundle exec rake db:seed

Rails
11.2.1

11.6

11.6
11.6

11.25

11.25

CSS

app/assets/stylesheets/custom.css.scss
.
.
.
/* microposts */

3. Faker
4.

lorem ipsum
11.25

CSS

11.2.

- 407

.microposts {
list-style: none;
padding: 0;
li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
.user {
margin-top: 5em;
padding-top: 0;
}
.content {
display: block;
margin-left: 60px;
img {
display: block;
padding: 5px 0;
}
}
.timestamp {
color: $gray-light;
display: block;
margin-left: 60px;
}
.gravatar {
float: left;
margin-right: 10px;
margin-top: 5px;
}
}
aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}
span.picture {
margin-top: 10px;
input {
border: 0;
}
}

11.7

11.8

Posted 1 minute ago.

408 -

11

11.9
11.21

time_ago_in_words

11.7

11.8

/users/1

/users/5

11.2.

- 409

11.9

/users/1?page=2

11.2.3.
10.29

$ rails generate integration_test users_profile


invoke test_unit
create
test/integration/users_profile_test.rb

Rails

orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
user

michael

Rails

michael:
name: Michael Example
email: michael@example.com
.
.
.

410 -

11

9.43

Ruby

<% 30.times do |n| %>


micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>

11.26
11.26
test/fixtures/microposts.yml
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
user: michael
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
user: michael
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
user: michael
<% 30.times do |n| %>
micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>

Gravatar
11.27

4.2

11.27

full_title

ApplicationHelper

GREEN

test/integration/users_profile_test.rb
require 'test_helper'

5.

full_title

3.38

test_helper.rb

ApplicationHelper

11.2.

- 411

class UsersProfileTest < ActionDispatch::IntegrationTest


include ApplicationHelper
def setup
@user = users(:michael)
end
test "profile display" do
get user_path(@user)
assert_template 'users/show'
assert_select 'title', full_title(@user.name)
assert_select 'h1', hext: @user.name
assert_select 'h1>img.gravatar'
assert_match @user.microposts.count.to_s, response.body
assert_select 'div.pagination'
@user.microposts.paginate(page: 1).each do |micropost|
assert_match micropost.content, response.body
end
end
end
response.body

response.body

10

body

HTML

assert_match @user.microposts.count.to_s, response.body


assert_match

assert_select

HTML

assert_select

11.27

assert_select 'h1>img.gravatar'
h1

11.28

gravatar

img

GREEN

$ bundle exec rake test

11.3.

12

new

11.29
2.3
2

412 -

11

11.29

edit

create

REST

destroy

11.2

11.29
config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'
get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets,
only: [:new, :create, :edit, :update]
resources :microposts,
only: [:create, :destroy]
end

11.2

11.29

HTTP

URL

POST

/microposts

create

DELETE

/microposts/1

destroy

ID

11.3.1.
create

9.17

destroy

9.56
11.30

11.30

RED

test/controllers/microposts_controller_test.rb
require 'test_helper'
class MicropostsControllerTest < ActionController::TestCase
def setup
@micropost = microposts(:orange)
end
test "should redirect create when not logged in" do
assert_no_difference 'Micropost.count' do
post :create, micropost: { content: "Lorem ipsum" }
end
assert_redirected_to login_url
end
test "should redirect destroy when not logged in" do

11.3.

- 413

assert_no_difference 'Micropost.count' do
delete :destroy, id: @micropost
end
assert_redirected_to login_url
end
end

9.2.1
logged_in_user

9.12
ApplicationController

11.31
logged_in_user
ApplicationController
app/controllers/application_controller.rb

11.31

class ApplicationController < ActionController::Base


protect_from_forgery with: :exception
include SessionsHelper
private
#
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
logged_in_user
logged_in_user

11.32
11.32

GREEN

app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
def create
end
def destroy
end
end

11.33

GREEN

$ bundle exec rake test

414 -

11

create

destroy

11.3.2.
7

create

HTML

POST

/microposts/new
/

11.10

11.10
5.6

Sign up now!
11.35

create
cropost_params

create

Web

7.23
11.34

mi-

content

create
app/controllers/microposts_controller.rb

11.34

class MicropostsController < ApplicationController


before_action :logged_in_user, only: [:create, :destroy]
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save

11.3.

- 415

flash[:success] = "Micropost created!"


redirect_to root_url
else
render 'static_pages/home'
end
end
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end

11.35
HTML
11.35
app/views/static_pages/home.html.erb
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
</div>
<% else %>
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails logo"),
'http://rubyonrails.org/' %>
<% end %>
if-else

416 -

11

11.35

11.36

11.36
app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>

11.23

11.36
Microposts
1 microposts
1 micropost 2 microposts

Microposts (1)
pluralize

7.3.3

11.37

label

7.13

11.37
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
@micro-

11.37
post
@micropost = current_user.microposts.build

11.38
home
@micropost
app/controllers/static_pages_controller.rb

11.38

class StaticPagesController < ApplicationController


def home
@micropost = current_user.microposts.build if logged_in?
end
def help
end
def about
end
def contact

11.3.

- 417

end
end
current_user

@micropost

11.37
<%= render 'shared/error_messages', object: f.object %>
@user

7.18
@micropost

f.object

form_for(@user) do |f|

form_for(@micropost) do |f|

f.object

f.object

@user

@micropost

11.37
object: f.object

object

error_messages

11.39
11.39

RED

app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(object.errors.count, "error") %>.
</div>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

11.40

RED

$ bundle exec rake test

7.18
11.43

10.48
11.42

9.2

11.41
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>

418 -

11

11.41

<%= f.label :name %>


<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>

11.42
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails">change</a>
</div>
</div>
</div>

11.43
app/views/password_resets/edit.html.erb

11.3.

- 419

<% provide(:title, 'Reset password') %>


<h1>Password reset</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>

$ bundle exec rake test

HTML

11.11

11.11

420 -

11

11.12

11.12

11.3.3.

11.11
11.13

12

11.3.

- 421

11.13
feed
where

10.5

11.44
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
#
12
def feed
Micropost.where("user_id = ?", id)
end
private
.
.

6. where

422 -

11

Rails Guides

Active Record Query Interface

11.44

.
end
Micropost.where("user_id = ?", id)

SQL
SQL

id

SQL
id

SQL injection

11.44
def feed
microposts
end

11.44

12
home

11.45

@feed_items

11.47

11.46
home
app/controllers/static_pages_controller.rb

11.45

class StaticPagesController < ApplicationController


def home
if logged_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
end
def help
end
def about
end
def contact
end
end

11.46
app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
<ol class="microposts">
<%= render @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>

11.21
<%= render @feed_items %>

11.3.

- 423

micropost

Rails

@feed_items

Micropost

app/views/microposts/_micropost.html.erb

11.47
11.14
11.47
app/views/static_pages/home.html.erb
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
<% else %>
.
.
.
<% end %>

11.15
@feed_items
@feed_items

424 -

11

11.48

Rails

11.14

11.15

11.3.

- 425

11.48
create
@feed_items
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
@feed_items = []
render 'static_pages/home'
end
end
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end

11.3.4.
9.4.2
11.16
11.21

11.49

11.49
app/views/microposts/_micropost.html.erb
<li id="<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>

426 -

11

11.16
UsersController
UsersController

destroy

correct_user

MicropostsController

9.54

admin_user

destroy

@user

ID

11.50

MicropostsController
destroy
app/controllers/microposts_controller.rb

11.50

class MicropostsController < ApplicationController


before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user,
only: :destroy
.
.
.
def destroy
@micropost.destroy
flash[:success] = "Micropost deleted"
redirect_to request.referrer || root_url
end
private

11.3.

- 427

def micropost_params
params.require(:micropost).permit(:content)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
destroy
request.referrer || root_url
request.referrer 7

request.url

URL

request.referrer
request.referrer

nil

root_url

8.50

11.17

11.17

7.

HTTP
referrer

8.

428 -

HTTP_REFERER
Rails

11

REFERER
URL

Rails
rails request previous url

Stack Overflow

11.3.5.

11.51

11.51
test/fixtures/microposts.yml
.
.
.
ants:
content: "Oh, is that what you want? Because that's how you get ants!"
created_at: <%= 2.years.ago %>
user: archer
zone:
content: "Danger zone!"
created_at: <%= 3.days.ago %>
user: archer
tone:
content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
created_at: <%= 10.minutes.ago %>
user: lana
van:
content: "Dude, this van's, like, rolling probable cause."
created_at: <%= 4.hours.ago %>
user: lana

11.52
11.52

GREEN

test/controllers/microposts_controller_test.rb
require 'test_helper'
class MicropostsControllerTest < ActionController::TestCase
def setup
@micropost = microposts(:orange)
end
test "should redirect create when not logged in" do
assert_no_difference 'Micropost.count' do
post :create, micropost: { content: "Lorem ipsum" }
end
assert_redirected_to login_url

11.3.

- 429

end
test "should redirect destroy when not logged in" do
assert_no_difference 'Micropost.count' do
delete :destroy, id: @micropost
end
assert_redirected_to login_url
end
test "should redirect destroy for wrong micropost" do
log_in_as(users(:michael))
micropost = microposts(:ants)
assert_no_difference 'Micropost.count' do
delete :destroy, id: micropost
end
assert_redirected_to root_url
end
end

$ rails generate integration_test microposts_interface


invoke test_unit
create
test/integration/microposts_interface_test.rb

11.53
post

follow_redirect!

11.53

post_via_redirect

GREEN

test/integration/microposts_interface_test.rb
require 'test_helper'
class MicropostInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "micropost interface" do
log_in_as(@user)
get root_path
assert_select 'div.pagination'
#
assert_no_difference 'Micropost.count' do
post microposts_path, micropost: { content: "" }
end
assert_select 'div#error_explanation'
#
content = "This micropost really ties the room together"
assert_difference 'Micropost.count', 1 do

430 -

11

11.68

post microposts_path, micropost: { content: content }


end
assert_redirected_to root_url
follow_redirect!
assert_match content, response.body
#
assert_select 'a', text: 'delete'
first_micropost = @user.microposts.paginate(page: 1).first
assert_difference 'Micropost.count', -1 do
delete micropost_path(first_micropost)
end
#
get user_path(users(:archer))
assert_select 'a', text: 'delete', count: 0
end
end

11.54

GREEN

$ bundle exec rake test

11.4.

11.18

9.

https://www.flickr.com/photos/grungepunk/14026922186

11.4.

- 431

11.18

11.4.1.
CarrierWave
carrierwave gem
mini_magick

11.55

Gemfile

11.55
11.4.3

Gemfile

gem
fog

CarrierWave

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem
gem
.
.
.

432 -

'rails',
'bcrypt',
'faker',
'carrierwave',
'mini_magick',
'fog',
'will_paginate',
'bootstrap-will_paginate',

11

'4.2.0'
'3.1.7'
'1.4.2'
'0.10.0'
'3.8.0'
'1.23.0'
'3.0.7'
'0.0.10'

11.55
11.4.4

$ bundle install

CarrierWave

picture

Rails

$ rails generate uploader Picture

CarrierWave

Active Record
11.19

11.19

10

picture

picture
$ rails generate migration add_picture_to_microposts picture:string
$ bundle exec rake db:migrate
mount_uploader

CarrierWave

mount_uploader :picture, PictureUploader


PictureUploader

picture_uploader.rb

11.4.2
11.56

11.56
app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end

Rails

10.

image

11.4.

- 433

file_field

11.18
11.57
11.57
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture %>
</span>
<% end %>
form_for

html: { multipart: true }


picture

micropost_params

Web

11.58
11.58
picture
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user,
only: :destroy
.
.
.
private
def micropost_params
params.require(:micropost).permit(:content, :picture)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
image_tag
picture?

img

11.59
CarrierWave
11.20

11.6
11.59
app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>

434 -

11

<span class="content">
<%= micropost.content %>
<%= image_tag micropost.picture.url if micropost.picture? %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>

11.20

11.4.2.

CarrierWave
11.60

PNG

GIF

JPEG

11.60
app/uploaders/picture_uploader.rb

11.4.

- 435

class PictureUploader < CarrierWave::Uploader::Base


storage :file
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
#
def extension_white_list
%w(jpg jpeg gif png)
end
end

Rails
picture_size
validate

11.61

validates

11.61
app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
validate :picture_size
private
#
def picture_size
if picture.size > 5.megabytes
errors.add(:picture, "should be less than 5MB")
end
end
end
:picture_size
errors

8.2

picture_size

5MB

6.2.2
file_field

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>

MIME

11.60
JavaScript

436 -

11

jQuery

accept

$('#micropost_picture').bind('change', function() {
size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});

jQuery

CSS ID

ID
alert

micropost_picture

11.57
11

11.62
11.62

jQuery

app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
</span>
<% end %>
<script type="text/javascript">
$('#micropost_picture').bind('change', function() {
size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});
</script>

11.62
Web

Web

POST

JavaScript

curl

11.61

11.4.3.

11.21
12

11.
12.

javascript maximum file size

Stack Overflow

CSS

11.4.

- 437

11.21
ImageMagick

11.4.4

Heroku

13

IDE
$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing

CarrierWave

MiniMagick

ImageMagick
resize_to_limit: [400, 400]

MiniMagick
400

CarrierWave
11.63

11.22
11.63
app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process resize_to_limit: [400, 400]
storage :file
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:

13. Ubuntu
m>
OS X

438 -

11

IDE
Homebrew

Linux

brew install imagemagick

imagemagick <your platfor-

def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
#
def extension_white_list
%w(jpg jpeg gif png)
end
end

11.22

11.4.4.
11.63
14

storage :file
15

fog gem

11.64

11.64
app/uploaders/picture_uploader.rb

14.

Heroku

15.

11.4.

- 439

class PictureUploader < CarrierWave::Uploader::Base


include CarrierWave::MiniMagick
process resize_to_limit: [400, 400]
if Rails.env.production?
storage :fog
else
storage :file
end
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
#
def extension_white_list
%w(jpg jpeg gif png)
end
end

11.64

production?

7.1

if Rails.env.production?
storage :fog
else
storage :file
end

Amazon
vice

16

S3

1.

Amazon Web Services

2.

AWS Identity and Access Management

3.

AWS Console

IAM

S3 bucket
S3

S3
11.65

CarrierWave
CarrierWave

S3

config/initializers/carrier_wave.rb
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
# Amazon S3
:provider
=> 'AWS',
:aws_access_key_id
=> ENV['S3_ACCESS_KEY'],

16. S3

440 -

11

11.65

Simple Storage Ser-

:aws_secret_access_key => ENV['S3_SECRET_KEY']


}
config.fog_directory
= ENV['S3_BUCKET']
end
end

10.54
10.3

11.65
SendGrid

Heroku

ENV

heroku config:set
$ heroku config:set S3_ACCESS_KEY=<access key>
$ heroku config:set S3_SECRET_KEY=<secret key>
$ heroku config:set S3_BUCKET=<bucket name>
master
$
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Add user microposts"
git checkout master
git merge user-microposts
git push

$
$
$
$

git push heroku


heroku pg:reset DATABASE
heroku run rake db:migrate
heroku run rake db:seed

Heroku

ImageMagick

11.23

11.4.

- 441

11.23

11.5.

12
master

11.4.4
$
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Add user microposts"
git checkout master
git merge user-microposts
git push

$
$
$
$

git push heroku


heroku pg:reset DATABASE
heroku run rake db:migrate
heroku run rake db:seed

gem

442 -

11

Gemfile

11.66

Gemfile

11.66

source 'https://rubygems.org'
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem
gem

'rails',
'bcrypt',
'faker',
'carrierwave',
'mini_magick',
'fog',
'will_paginate',
'bootstrap-will_paginate',
'bootstrap-sass',
'sass-rails',
'uglifier',
'coffee-rails',
'jquery-rails',
'turbolinks',
'jbuilder',
'sdoc',

'4.2.0'
'3.1.7'
'1.4.2'
'0.10.0'
'3.8.0'
'1.23.0'
'3.0.7'
'0.0.10'
'3.2.0.0'
'5.0.0.beta1'
'2.5.3'
'4.1.0'
'4.0.0.beta2'
'2.3.0'
'2.2.3'
'0.4.0', group: :doc

group
gem
gem
gem
gem
end

:development, :test do
'sqlite3',
'1.3.9'
'byebug',
'3.4.0'
'web-console', '2.0.0.beta3'
'spring',
'1.1.3'

group
gem
gem
gem
end

:test do
'minitest-reporters', '1.0.5'
'mini_backtrace',
'0.1.3'
'guard-minitest',
'2.3.1'

group
gem
gem
gem
end

:production do
'pg',
'0.17.1'
'rails_12factor', '0.0.2'
'unicorn',
'4.8.3'

11.5.1.

Active Record

Rails
has_many

belongs_to

has_many/belongs_to
user.microposts.build()
Rails

default_scope

11.5.

- 443


dependent: :destroy

Rails

where

Active Record

CarrierWave

11.6.
3
if-else

1.
2.

11.67

3.

11.68

11.4
cp app/assets/images/rails.png test/fixtures/

.gitignore

Git
Car-

11.69

rierWave

11.70
11.68

picture

fixture_file_upload

17

picture

create

10.1.4

assigns

@micropost

11.67
test/integration/microposts_interface_test.rb
require 'test_helper'
class MicropostInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "micropost sidebar count" do
log_in_as(@user)
get root_path
assert_match "#{FILL_IN} microposts", response.body
#
other_user = users(:mallory)
log_in_as(other_user)

17.

444 -

Windows

11

:binary

fixture_file_upload(file, type, :binary)

get root_path
assert_match "0 microposts", response.body
other_user.microposts.create!(content: "A micropost")
get root_path
assert_match FILL_IN, response.body
end
end

11.68
test/integration/microposts_interface_test.rb
require 'test_helper'
class MicropostInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "micropost interface" do
log_in_as(@user)
get root_path
assert_select 'div.pagination'
assert_select 'input[type=FILL_IN]'
#
post microposts_path, micropost: { content: "" }
assert_select 'div#error_explanation'
#
content = "This micropost really ties the room together"
picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')
assert_difference 'Micropost.count', 1 do
post microposts_path, micropost: { content: content, picture: FILL_IN }
end
assert FILL_IN.picture?
follow_redirect!
assert_match content, response.body
#
assert_select 'a', 'delete'
first_micropost = @user.microposts.paginate(page: 1).first
assert_difference 'Micropost.count', -1 do
delete micropost_path(first_micropost)
end
#
get user_path(users(:archer))
assert_select 'a', { text: 'delete', count: 0 }
end
.
.
.
end

11.6.

- 445

11.69
#
#
#
#
#
#

.gitignore

See https://help.github.com/articles/ignoring-files for more about ignoring


files.
If you find yourself ignoring temporary files generated by your text editor
or operating system, you probably want to add a global ignore instead:
git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.


/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
# Ignore Spring files.
/spring/*.pid
# Ignore uploaded test images.
/public/uploads

11.70
config/initializers/skip_image_resizing.rb
if Rails.env.test?
CarrierWave.configure do |config|
config.enable_processing = false
end
end

446 -

11

12

12.1
Ajax

12.2

12.3
Ruby

SQL

Rails
12.4

12.1

12.2
Follow

12.3
Hobbes

Follow
Calvin

12.4
Hobbes

1.
9187111340

(John Calvin)
Thomas Hobbes
Unfollow

12.5

http://www.flickr.com/photos/john_lustig/2518452221/

https://www.flickr.com/photos/renemensen/

447

12.1

448 -

12

12.2

12

- 449

12.3

450 -

12

Follow

12.4

Unfollow

12

- 451

12.5

12.1.

has_many
has_many :through

Git
$ git checkout master
$ git checkout -b following-users

12.1.1.

Calvin
Hobbes

Hobbes
Hobbes
Calvin
followed
Rails
followers
hobbes.followers
Twitter

lowers

452 -

Calvin

12

calvin.following

Calvin

follower

Hobbes
followeds
following
50 following, 75 fol-

12.6

following

user.following

has_many

following

followed_id

follower_id

12.6
12.6

ID

users
followers
following

users
followers

7.1.2

REST

following

A
Calvin
Calvin

Facebook
Calvin

Hobbes

B
B
Hobbes
Hobbes
Calvin
Hobbes
Hobbes
Active Relationship
Hobbes
Calvin

Positive Relationship

followers

Twitter
Calvin

12.1.5

12.6

followed_id
active_relationships

following
followed_id

users

12.7

2.

12.6

3.

Paul Fioravanti

following

id

12.1.

- 453

12.7

Relationship

relationships
12.8

12.1.4

12.8

Relationship

$ rails generate model Relationship follower_id:integer followed_id:integer


follower_id

followed_id

12.1
relationships
db/migrate/[timestamp]_create_relationships.rb

12.1

class CreateRelationships < ActiveRecord::Migration


def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :followed_id
t.timestamps null: false
end

454 -

12

add_index :relationships, :follower_id


add_index :relationships, :followed_id
add_index :relationships, [:follower_id, :followed_id], unique: true
end
end

12.1

(follower_id, followed_id)

6.28

12.1.4
curl

relationships
$ bundle exec rake db:migrate

12.1.2.

has_many ,

11.1.3

belongs_to

user.active_relationships.build(followed_id: ...)

11.1.3

class User < ActiveRecord::Base


has_many :microposts
.
.
.
end

Rails

:microposts

Micropost

Relationship
has_many :active_relationships

Rails

class Micropost < ActiveRecord::Base


belongs_to :user
.
.
.
end

4.

Rails

classify

has_many

"foo_bars"

"FooBar"

12.1.

- 455

microposts

user_id
user_id

foreign key
<class>_id

Rails

<class>
follower_id

12.2

Rails
12.2

11.1.1
Rails

12.3

has_many

app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent:
:destroy
.
.
.
end

12.3

dependent: :destroy

belongs_to

app/models/relationship.rb
class Relationship < ActiveRecord::Base
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
followed

12.1.5

11.1
12.1

12.1

active_relationship.follower
active_relationship.followed
user.active_relationships.create(followed_id:
user.id)
user.active_relationships.create!(followed_id:
user.id)
user.active_relationships.build(followed_id:
user.id)

5.

Rails

"foo_bar"

456 -

12

underscore
FooBar

id

foo_bar_id

user

user

user

"FooBar".underscore

12.1.3.

6.29

12.1

12.4

6.30

12.5

12.6
12.4

test/models/relationship_test.rb
require 'test_helper'
class RelationshipTest < ActiveSupport::TestCase
def setup
@relationship = Relationship.new(follower_id: 1, followed_id: 2)
end
test "should be valid" do
assert @relationship.valid?
end
test "should require a follower_id" do
@relationship.follower_id = nil
assert_not @relationship.valid?
end
test "should require a followed_id" do
@relationship.followed_id = nil
assert_not @relationship.valid?
end
end

12.5

app/models/relationship.rb
class Relationship < ActiveRecord::Base
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end

12.6

test/fixtures/relationships.yml
# empty

12.7

GREEN

$ bundle exec rake test

12.1.

- 457

12.1.4.

following

has_many :through
has_many :through

followers

12.7

Rails

has_many :followeds, through: :active_relationships

Rails
lowed_id
user.following

followeds
12.1.1
Rails
12.8

relationships

followed

fol-

user.followeds
source

following

followed_id

following

12.8
app/models/user.rb

class User < ActiveRecord::Base


has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent:
:destroy
has_many :following, through: :active_relationships, source: :followed
.
.
.
end
include?

Active Record

4.3.1

user.following.include?(other_user)
user.following.find(other_user)
following

following

Rails

following.include?(other_user)
include?

Rails

user.microposts.count

11.2.1

follow
user.follow(other_user)

unfollow
6

following?

following?
follow

following?

12.9

6.

458 -

12

unfollow

12.9

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "should follow and unfollow a user" do
michael = users(:michael)
archer = users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
end

12.1

following

follow

unfollow

following?

12.10

self

12.10

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def feed
.
.
.
end
#
def follow(other_user)
active_relationships.create(followed_id: other_user.id)
end
#
def unfollow(other_user)
active_relationships.find_by(followed_id: other_user.id).destroy
end
#
def following?(other_user)
following.include?(other_user)
end

true

private
.

12.1.

- 459

.
.
end

12.11

GREEN

$ bundle exec rake test

12.1.5.

user.following

user.followers

12.7

relationships

12.2

active_relationships

follower_id
active_relationships

passive_relationships

12.9
12.8
12.12

12.12

12.9

12.9

user.followers

app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent:
:destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent:
:destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
.
.

460 -

12

followed_id

.
end
followers

source

has_many :followers, through: :passive_relationships

Rails

followers

source

follower

follower_id

12.12

has_many :following
followers.include?

following?

12.13

followed_by?

followers
test/models/user_test.rb

12.13

GREEN

require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "should follow and unfollow a user" do
michael = users(:michael)
archer
= users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
assert archer.followers.include?(michael)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
end

12.9
12.12

$ bundle exec rake test

12.2.
12.1

12.3

12.2.

- 461

12.2.1.
Rake

12.14

12.14

3-51

db/seeds.rb
# Users
User.create!(name: "Example User",
email: "example@railstutorial.org",
password:
"foobar",
password_confirmation: "foobar",
admin:
true,
activated: true,
activated_at: Time.zone.now)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password:
password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now)
end
# Microposts
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
# Following relationships
users = User.all
user = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

12.14
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

462 -

12

4-41

12.2.2.

12.1.1
following

Twitter

following

label
12.10

12.1

50

12.10
12.10
#

5
12.2.3

resources

12.15

:member
following

12.15

followers

config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'
get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users do
member do
get :following, :followers
end
end
resources :account_activations, only: [:edit]
resources :password_resets,
only: [:new, :create, :edit, :update]
resources :microposts,
only: [:create, :destroy]
end

URL

/users/1/following

/users/1/followers
get

12.15
member

GET
ID

collection

URL
URL

ID

resources :users do
collection do
get :tigers
end
end
12.2.

- 463

URL

/users/tigers

12.15

12.2

12.2

12.15

REST

HTTP

URL

GET

/users/1/following

following

following_user_path(1)

GET

/users/1/followers

followers

followers_user_path(1)
div

12.16

12.16
app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
<a href="<%= following_user_path(@user) %>">
<strong id="following" class="stat">
<%= @user.following.count %>
</strong>
following
</a>
<a href="<%= followers_user_path(@user) %>">
<strong id="followers" class="stat">
<%= @user.followers.count %>
</strong>
followers
</a>
</div>

12.16

<% @user ||= current_user %>


@user

8.1
nil

nil

@user
@user.following.count

@user.followers.count
@user.microposts.count

11.23
Rails
CSS ID
<strong id="following" class="stat">
...
</strong>

7.

464 -

Rails

12

Rails Routing from the Outside In

ID

12.2.5

Ajax

Ajax

ID
12.17

12.17
app/views/static_pages/home.html.erb
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="stats">
<%= render 'shared/stats' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
<% else %>
.
.
.
<% end %>

SCSS
12.11
12.18

12.18

SCSS

app/assets/stylesheets/custom.css.scss
.
.
.
/* sidebar */
.
.
.
.gravatar {
float: left;
margin-right: 10px;
}
.gravatar_edit {
margin-top: 15px;
}

12.2.

- 465

.stats {
overflow: auto;
margin-top: 0;
padding: 0;
a {
float: left;
padding: 0 10px;
border-left: 1px solid $gray-lighter;
color: gray;
&:first-child {
padding-left: 0;
border: 0;
}
&:hover {
text-decoration: none;
color: blue;
}
}
strong {
display: block;
}
}
.user_avatars {
overflow: auto;
margin-top: 10px;
.gravatar {
margin: 1px 1px;
}
a {
padding: 0;
}
}
.users.follow {
padding: 0;
}
/* forms */
.
.
.

466 -

12

12.11

12.19
12.19
app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
<div id="follow_form">
<% if current_user.following?(@user) %>
<%= render 'unfollow' %>
<% else %>
<%= render 'follow' %>
<% end %>
</div>
<% end %>
follow

12.20

12.20

unfollow

11.29

config/routes.rb
Rails.application.routes.draw do
root
'static_pages#home'
get
'help'
=> 'static_pages#help'
get
'about'
=> 'static_pages#about'

12.2.

- 467

get
'contact' => 'static_pages#contact'
get
'signup' => 'users#new'
get
'login'
=> 'sessions#new'
post
'login'
=> 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users do
member do
get :following, :followers
end
end
resources :account_activations, only: [:edit]
resources :password_resets,
only: [:new, :create, :edit, :update]
resources :microposts,
only: [:create, :destroy]
resources :relationships,
only: [:create, :destroy]
end
follow

unfollow

12.21

12.22

12.21
app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
<div><%= hidden_field_tag :followed_id, @user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

12.22
app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete }) do |f| %>
<%= f.submit "Unfollow", class: "btn" %>
<% end %>
form_for

12.22

POST

12.21

RelationshipsController

create

DELETE

destroy

12.2.4
followed_id
followed_id

hidden_field_tag

12.21
HTML

<input id="followed_id" name="followed_id" type="hidden" value="3" />


input

10.2.4

12.23
12.12
12.13
12.23
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">

468 -

12

<section>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
<section class="stats">
<%= render 'shared/stats' %>
</section>
</aside>
<div class="col-md-8">
<%= render 'follow_form' if logged_in? %>
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>

12.12

/users/2

12.2.

- 469

12.13

/users/5
12.2.4

Ajax

12.2.5

12.2.3.

12.14

470 -

12.15

12

12.14

12.2.

- 471

12.15
Twitter
12.24
12.24
test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect following when not logged in" do
get :following, id: @user
assert_redirected_to login_url
end

472 -

12

test "should redirect followers when not logged in" do


get :followers, id: @user
assert_redirected_to login_url
end
end

following

12.15
@user.followed_users

followers

@user.followers

12.25
12.25 following
followers
app/controllers/users_controller.rb

RED

class UsersController < ApplicationController


before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
:following, :followers]
.
.
.
def following
@title = "Following"
@user = User.find(params[:id])
@users = @user.following.paginate(page: params[:page])
render 'show_follow'
end
def followers
@title = "Followers"
@user = User.find(params[:id])
@users = @user.followers.paginate(page: params[:page])
render 'show_follow'
end
private
.
.
.
end

show.html.erb

Rails
12.25

show
render

show_follow

ERb

12.26
12.26

show_follow

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= gravatar_for @user %>

12.2.

- 473

<h1><%= @user.name %></h1>


<span><%= link_to "view my profile", @user %></span>
<span><b>Microposts:</b> <%= @user.microposts.count %></span>
</section>
<section class="stats">
<%= render 'shared/stats' %>
<% if @users.any? %>
<div class="user_avatars">
<% @users.each do |user| %>
<%= link_to gravatar_for(user, size: 30), user %>
<% end %>
</div>
<% end %>
</section>
</aside>
<div class="col-md-8">
<h3><%= @title %></h3>
<% if @users.any? %>
<ul class="users follow">
<%= render @users %>
</ul>
<%= will_paginate %>
<% end %>
</div>
</div>

12.25
12.16
12.18

474 -

12

12.26
12.17

12.16

12.17

12.2.

- 475

12.18

5.3.4

HTML
URL

$ rails generate integration_test following


invoke test_unit
create
test/integration/following_test.rb

11.2.3

orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
user_id: 1

12.27

test/fixtures/relationships.yml
one:
follower: michael

476 -

12

user: michael

12.27

followed: lana
two:
follower: michael
followed: mallory
three:
follower: lana
followed: michael
four:
follower: archer
followed: michael

Michael

Lana

Mallory

assert_match

Lana

Archer
11.27

Michael

12.28
12.28

GREEN

test/integration/following_test.rb
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
log_in_as(@user)
end
test "following page" do
get following_user_path(@user)
assert_not @user.following.empty?
assert_match @user.following.count.to_s, response.body
@user.following.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
test "followers page" do
get followers_user_path(@user)
assert_not @user.followers.empty?
assert_match @user.followers.count.to_s, response.body
@user.followers.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
end

assert_not @user.following.empty?

12.2.

- 477

@user.following.each do |user|
assert_select "a[href=?]", user_path(user)
end

12.29

GREEN

$ bundle exec rake test

12.2.4.

$ rails generate controller Relationships

12.31

12.30

RelationshipsController
test/controllers/relationships_controller_test.rb

12.30

RED

require 'test_helper'
class RelationshipsControllerTest < ActionController::TestCase
test "create should require logged-in user" do
assert_no_difference 'Relationship.count' do
post :create
end
assert_redirected_to login_url
end
test "destroy should require logged-in user" do
assert_no_difference 'Relationship.count' do
delete :destroy, id: relationships(:one)
end
assert_redirected_to login_url
end
end
RelationshipsController

logged_in_user

12.31 RelationshipsController
app/controllers/relationships_controller.rb

478 -

12

12.31

GREEN

class RelationshipsController < ApplicationController


before_action :logged_in_user
def create
end
def destroy
end
end
followed_id
follow

12.10

12.21

12.22

unfollow

12.32
12.32 RelationshipsController
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
before_action :logged_in_user
def create
user = User.find(params[:followed_id])
current_user.follow(user)
redirect_to user
end
def destroy
user = Relationship.find(params[:id]).followed
current_user.unfollow(user)
redirect_to user
end
end

curl

current_user

nil

12.2.6
12.19

12.20

12.2.

- 479

12.19

12.20

480 -

12

12.2.5.

Ajax

RelationshipsController

12.2.4

Ajax 8

create

destroy

Ajax
Ajax

Rails

form_for

form_for, remote: true

12.33
12.33

Rails

Ajax

12.34

Ajax

app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.
build(followed_id: @user.id),
remote: true) do |f| %>
<div><%= hidden_field_tag :followed_id, @user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

12.34

Ajax

app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete },
remote: true) do |f| %>
<%= f.submit "Unfollow", class: "btn" %>
<% end %>

ERb

HTML

<form action="/relationships/117" class="edit_relationship" data-remote="true"


id="edit_relationship_117" method="post">
.
.
.
</form>
form

Rails
Rails

data-remote="true"

JavaScript
JavaScript

Rails

JavaScript
JavaScript

unobtrusive JavaScript

RelationshipsController

HTML
respond_to

Ajax

respond_to do |format|
format.html { redirect_to user }

8.

Ajax

Asynchronous JavaScript and XML


Ajax

AJAX

Ajax

12.2.

- 481

format.js
end
respond_to
RelationshipsController
respond_to

12.32
@user

12.32

if-else
create

Ajax
12.35

user

Ajax

12.33

12.34
12.35
RelationshipsController
app/controllers/relationships_controller.rb

Ajax

class RelationshipsController < ApplicationController


before_action :logged_in_user
def create
@user = User.find(params[:followed_id])
current_user.follow(@user)
respond_to do |format|
format.html { redirect_to @user }
format.js
end
end
def destroy
@user = Relationship.find(params[:id]).followed
current_user.unfollow(@user)
respond_to do |format|
format.html { redirect_to @user }
format.js
end
end
end

12.35

12.36

JavaScript
12.36
config/application.rb
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
class Application < Rails::Application
.
.
.
#
Ajax
config.action_view.embed_authenticity_token_in_remote_forms = true
end
end

482 -

12

destroy

JavaScript

Ajax

.js.erb

Ruby

JavaScript

JS-ERb
Model

Rails

create.js.erb

JavaScript

destroy.js.erb

Ruby

Rails

jQuery
jQuery
DOM

DOM
ID

DOM

Document Object
follow_form

$("#follow_form")
div

12.19
CSS

ID

jQuery

CSS
.

CSS

html

CSS

HTML

"foobar"
$("#follow_form").html("foobar")

JavaScript

JS-ERb

escape_javascript

12.37

Ruby
JavaScript

create.js.erb

12.37
HTML

HTML

JS-ERb

app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')
destroy.js.erb

12.38

12.38

JS-ERb

app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

12.2.6.

POST
assert_difference '@user.following.count', 1 do
post relationships_path, followed_id: @other.id
end

Ajax

post

xhr :post

12.2.

- 483

assert_difference '@user.following.count', 1 do
xhr :post, relationships_path, followed_id: @other.id
end
xhr

XmlHttpRequest

Ajax

12.35
post

delete

ID

assert_difference '@user.following.count', -1 do
delete relationship_path(relationship),
relationship: relationship.id
end

Ajax
assert_difference '@user.following.count', -1 do
xhr :delete, relationship_path(relationship),
relationship: relationship.id
end

12.39
12.39

GREEN

test/integration/following_test.rb
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other = users(:archer)
log_in_as(@user)
end
.
.
.
test "should follow a user the standard way" do
assert_difference '@user.following.count', 1 do
post relationships_path, followed_id: @other.id
end
end
test "should follow a user with Ajax" do
assert_difference '@user.following.count', 1 do
xhr :post, relationships_path, followed_id: @other.id
end
end
test "should unfollow a user the standard way" do

484 -

12

respond_to

JavaScript

@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship)
end
end
test "should unfollow a user with Ajax" do
@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
xhr :delete, relationship_path(relationship)
end
end
end

12.40

GREEN

$ bundle exec rake test

12.3.

11.3.3
Rails

Ruby

SQL
12.5
12.21

12.3.1.
12.22

microposts
microposts

12.3.

- 485

12.21

12.22

9.43

486 -

12

11.51

ID

ID

Michael
12.41

10

Lana

Archer
11.44

feed

12.41

RED

test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "feed should have the right posts" do
michael = users(:michael)
archer = users(:archer)
lana
= users(:lana)
#
lana.microposts.each do |post_following|
assert michael.feed.include?(post_following)
end
#
michael.microposts.each do |post_self|
assert michael.feed.include?(post_self)
end
#
archer.microposts.each do |post_unfollowed|
assert_not michael.feed.include?(post_unfollowed)
end
end
end

12.42

RED

$ bundle exec rake test

12.3.2.
12.41
microposts

SELECT * FROM microposts


WHERE user_id IN (<list of ids>) OR user_id = <user id>
IN

SQL

11.3.3

Active Record
ID

SQL

where

11.44

Micropost.where("user_id = ?", id)

Micropost.where("user_id in (?) OR user_id = ?", following_ids, id)

12.3.

- 487

ID
Ruby

map

enumerable

4.3.2

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Ruby

4.3.2

&

>> [1, 2, 3, 4].map(&:to_s)


=> ["1", "2", "3", "4"]
join

4.3.1

>> [1, 2, 3, 4].map(&:to_s).join(', ')


=> "1, 2, 3, 4"
user.following

id

ID

>> User.first.following.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Active Record
>> User.first.following_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]
following_ids

Active Record
_ids

has_many :following
user.following

ID

>> User.first.following_ids.join(', ')


=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51"

SQL

?
following_ids

Micropost.where("user_id in (?) OR user_id = ?", following_ids, id)

each

9.

488 -

12

12.8
ID

feed

12.43
12.43

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
def password_reset_expired?
reset_sent_at < 2.hours.ago
end

true

#
def feed
Micropost.where("user_id in (?) OR user_id = ?", following_ids, id)
end
#
def follow(other_user)
active_relationships.create(followed_id: other_user.id)
end
.
.
.
end

12.44

GREEN

$ bundle exec rake test

5000

12.3.3.
12.3.2

5000

following_ids

12.3.2

ID
12.43
SQL

subselect

ID
12.45

12.45

where

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
12.3.

- 489

.
.
#
def feed
Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
following_ids: following_ids, user_id: user)
end
.
.
.
end

where("user_id IN (?) OR user_id = ?", following_ids, id)

where("user_id IN (:following_ids) OR user_id = :user_id",


following_ids: following_ids, user_id: id)

user_id

SQL

Ruby

following_ids

SQL
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"

SQL

ID

SELECT * FROM microposts


WHERE user_id IN (SELECT followed_id FROM relationships
WHERE follower_id = 1)
OR user_id = 1

12.46
following_ids

SQL
12.46

GREEN

app/models/user.rb
class User < ActiveRecord::Base
.
.
.
#
def feed
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"

490 -

12

Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)


end
.
.
.
end

Rails
12.47

Ruby

SQL

GREEN

$ bundle exec rake test

background job

11.3.3
11

12.48
11.14

12.46

12.23
12.48 home
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
if logged_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
end
.
.
.
end
master
$
$
$
$
$

bundle exec rake test


git add -A
git commit -m "Add user following"
git checkout master
git merge following-users

$
$
$
$
$

git push
git push heroku
heroku pg:reset DATABASE
heroku run rake db:migrate
heroku run rake db:seed

12.24

12.3.

- 491

12.23

12.24

492 -

12

12.4.
Rails
has_many/belongs_to

has_many :through

Rails

12.4.1.
Rails

RailsCasts

RailsCasts

Tealeaf Academy
Academy
Thinkful

Tealeaf
Tealeaf

Tealeaf

RailsApps

Rails

Code School
Ruby
Rails
Ruby
Peter Cooper
Beginning Ruby
David A. Black
The Well-Grounded Rubyist
Russ Olsen
Eloquent Ruby
Hal Fulton
The Ruby Way
Rails
Sam Ruby Dave Thomas David Heinemeier Hansson
Agile Web Development with Rails
Obie Fernandez
The Rails 4 Way
Ryan Bigg
Yehuda Katz
Rails 4 in Action

12.4.2.
has_many :through

has_many
has_many

has_many :through

Rails
where
Rails

SQL

12.5.
3

12.4.

- 493

1.

11.27

2.

12.49
CGI.escapeHTML

12.49

HTML

HTML GREEN

test/integration/following_test.rb
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
log_in_as(@user)
end
.
.
.
test "feed on Home page" do
get root_path
@user.feed.paginate(page: 1).each do |micropost|
assert_match CGI.escapeHTML(FILL_IN), FILL_IN
end
end
end

494 -

12