#目次
1.前回
2.概要
3.内容
4.用語のまとめ
5.感想
6.おわりに
#1. 前回
https://qiita.com/ak-matsu/items/f3ce9156005c89e91390
#2. 概要
ユーザー登録機能の実装。
HTMLフォームを作りウェブアプリケーションに登録情報を送信、
ユーザーを新規登録して情報をデータベースへ保存する。
#3. 内容
ユーザーを表示する
ユーザー名とプロフィール写真を表示するためのページ作成。
本実装に入る前にbrunch作成を忘れずに行うこと。
$ git checkout -b sign-up
完成予定のモック画像
この節で完成予定のユーザープロフィールのモック
デバッグとRails環境
動的ページの作成と各プロフィールページにデバック用の情報を表示させる。
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?
もしも開発環境であればデバック情報を表示させるという構文である。
@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 */
.debug_dump {
clear: both;
float: left;
width: 100%;
margin-top: 45px;
@include box_sizing;
}
Sassのミックスイン機能を使うとCSSグループをパッケージ化して複数の要素を省略することができる。
ここまで設定できたら、rails sにてローカルサーバを起動してテスト環境を立ち上げて
動作確認すると以下のデバック画面が表示されている。
演習
- ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか?paramsの内容から確認してみましょう
- Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。
実行結果はどちらとも変わらず。yメソッドを使ったほうが短縮できる。
>> user = User.first
(1.7ms) SELECT sqlite_version(*)
User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "yamada", email: "michael@example.com", created_at: "2021-09-13 02:04:26", updated_at: "2021-09-13 04:12:07", password_digest: [FILTERED]>
>> puts user.attributes.to_yaml
---
id: 1
name: yamada
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2021-09-13 02:04:26.706383000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2021-09-13 04:12:07.503505000 Z
zone: *2
time: *3
password_digest: "$2a$12$V4TEcxCwzcq6mRQZixos/e5bOrw5u1cmQpzDUC48UBzOMiSJKkq7G"
=> nil
>>
>>
>> y user.attributes
---
id: 1
name: yamada
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2021-09-13 02:04:26.706383000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2021-09-13 04:12:07.503505000 Z
zone: *2
time: *3
password_digest: "$2a$12$V4TEcxCwzcq6mRQZixos/e5bOrw5u1cmQpzDUC48UBzOMiSJKkq7G"
=> nil
>>
Usersリソース
ユーザープロフィールページを作成するには、その前に
データベースにユーザーが登録されている必要がある。
rials cでユーザー登録を行う。
>> User.count
(1.8ms) SELECT sqlite_version(*)
(0.8ms) SELECT COUNT(*) FROM "users"
=> 1
>> User.first
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "yamada", email: "michael@example.com", created_at: "2021-09-13 02:04:26", updated_at: "2021-09-13 04:12:07", password_digest: [FILTERED]>
>>
このコンソール上のユーザー情報をwebアプリケーション上にも反映させる。
RESTアーキテクチャの習慣を使う、データの作成、表示、更新、削除をリソースとして扱う。
HTTP標準には、これらに対応する4つの基本操作(POST、GET、PATCH、DELETE)が定義されている。
これらの基本操作を各アクションに割り当てていく。
RESTの原則に従う場合、リソースへの参照はリソース名とユニークなIDを使うのが普通です。ユーザーをリソースとみなす場合、id=1のユーザーを参照するということは、/users/1というURLに対してGETリクエストを発行するということ。
ここでshowというアクションの種類は、暗黙のリクエストになる。
RailsのREST機能が有効になっていると、GETリクエストは自動的にshowアクションとして扱われる。
/users/1のURLを有効にするため
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
resources :users
end
resource :usersというのは/ussers/1だけを表示するだけではなく、以下のアクションも作成される。
RESTfulなルート
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) | id=1のユーザーを編集するページ |
PATCH | /users/1 | update | user_path(user) | ユーザーを更新するアクション |
DELETE | /users/1 | destroy | user_path(user) | ユーザーを削除するアクション |
ルートを設定しても、実際の見るページ、ViewとUserコントローラー内に
showアクションが作成されていないので以下のエラーページが表示される。
View
app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>
Userコントローラー
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
end
end
ここまで設定すると、rails sを再起動してURLの最後に/users/1と入力すると
1に登録されたユーザー情報が表示される。
演習
- 埋め込みRubyを使って、マジックカラム(created_atとupdated_at)の値をshowページに表示させる。
- 埋め込みRubyを使って、Time.nowの結果をshowページへ表示させる。ページを更新すると、その結果はどう変わるか確認する。
更新する度に時間が過ぎている。
debuggerメソッド
アプリケーションの内容を理解するためdebugメソッドを使う。
もっと直接的に行う方法がbyebug gemによるdebuggerメソッドである。
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
debugger
end
def new
end
end
showアクションにdebuggerメソッドを差し込み後、rails sを行ったと
ブラウザへ/users/1へアクセスするとコンソール画面にbyebugが表示される。
Processing by UsersController#show as HTML
Parameters: {"id"=>"1"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/controllers/users_controller.rb:4:in `show'
Return value is: nil
[1, 10] in /home/ubuntu/environment/sample_app/app/controllers/users_controller.rb
1: class UsersController < ApplicationController
2:
3: def show
4: @user = User.find(params[:id])
5: debugger
=> 6: end
7:
8: def new
9: end
10: end
(byebug)
ユーザー情報を確認できる。
(byebug) @user.name
"yamada"
(byebug) @user.email
"michael@example.com"
(byebug) params[:id]
"1"
演習
- showアクションの中にdebuggerを差し込み(リスト 7.6)、
ブラウザから /users/1 にアクセスしてみる。
その後コンソールに移り、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示する。
debugメソッドで表示したデバッグ情報を、どのようにしてYAML形式で表示させるか。
(byebug) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
controller: users
action: show
id: '1'
permitted: false
nil
(byebug) puts params
{"controller"=>"users", "action"=>"show", "id"=>"1"}
nil
(byebug)
- newアクションの中にdebuggerを差し込み、/users/new にアクセスしてみる。
@userの内容はどのように表示されているか確認する。
2:
3: def show
4: @user = User.find(params[:id])
5: debugger
6: end
7:
8: def new
9: debugger
=> 10: end
11: end
(byebug) puts @user
nil
(byebug)
Gravatar画像とサイドバー
各ユーザーのプロフィール写真を肉付けさせるためのサイドバーを作る。
Gravatar を導入する。
gravatar_forヘルパーメソッドを使う。
<% provide(:title, @user.name) %>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
<%= は 計算されて表示されることを忘れない。
gravatar_forヘルパーを組み込んだ結果
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
rails sの後にプレビューを開くと以下の画面になる。
アプリケーションでGravatarを利用できるようにするために、
update_attributesを使いデータベース上のユーザー情報を更新する。
$ rails console
>> user = User.first
>> user.update(name: "Example User",
?> email: "example@railstutorial.org",
?> password: "foobar",
?> password_confirmation: "foobar")
=> true
更新すると画像が変わる。
ユーザーのサイドバーの最初のバージョンを作る。
<% 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クラスを配置したのでプロフィールページにSCSSでスタイルを与えることができる。
.
.
.
/* sidebar */
aside {
section.user_info {
margin-top: 20px;
}
section {
padding: 10px 0;
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;
}
演習
- Gravatar上にアカウントを作成し、自分のメールアドレスと適当な画像を紐付けてみる。
メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試す。
>> user = User.find(1)
>> user.update_attributes(name: "matsu",email: "matsu@test.com" , password: "foobar", password_confirmation: "foobar" )
- gravatar_forヘルパーを変更して、sizeをオプション引数として受け取れるようにしてみる。
gravatar_for user, size: 50といった呼び出し方ができるようになる。
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
- Ruby 2.0から導入された新機能「キーワード引数」を使ってオプション引数を実現する。
module UsersHelper
# 引数で与えられたユーザーのGravatar画像を返す
def gravatar_for(user, size: 80)
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
ユーザー登録フォーム
ユーザー登録フォームの作成
form_withを使用する
ユーザー登録ページで必要な入力フォームを作成するため、form_withヘルパーメソッドを使う。
ユーザー登録ページ/signupのルーティングはUsersコントローラーのnewアクションに紐付けられている。
form_withの引数で必要となるUserオブジェクトを作成すること、@user変数を定義する必要がある。
#newアクションに@user変数を追加する
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
end
フォームは下記の構文で示し、SCSSで見栄えを整える。
# 新規ユーザーのためのユーザー登録フォーム
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.email_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>
# ユーザー登録フォームのCSS
.
.
.
/* forms */
input, textarea, select, .uneditable-input {
border: 1px solid #bbb;
width: 100%;
margin-bottom: 15px;
@include box_sizing;
}
input {
height: auto !important;
}
完成図
演習
- 試しに、ブロックの変数fをすべてfoobarに置き換えて結果が変わらないことを確認する。
変数名をfoobarとするのは良くない、理由について考える。
結果は変わらなかったが、form用の変数を使っているのかわかりにくくなる。
フォームHTML
フォームの理解のためコードを小さく分けて考える。
埋め込みRubyにてform_withからendまでの外側構造を見る。
<%= form_with(model: @user, local: true) do |f| %>
.
.
.
<% end %>
doキーワードは、form_withが1つの変数を持つブロックを取り、変数fはformのfである。
ハッシュ引数local:trueが存在している。form_withはデフォルトで"remote"XHR requestを送信するが、
エラーメッセージが表示されるため、localフォームリクエストに送信する。
f変数はform入力の際に必要になるため@userの属性を設定するために設計されたHTMLを返す。
<%= f.label :name %>
<%= f.text_field :name %>
実際の入力画面にてブラウザのソース表示をさせると上記と同じ項目が出てくるはずである。
パスワードについてはSecureになっているため、文字が隠蔽される構造となる。
ユーザー作成で重要なものはinputごとに特殊なname属性であり、Railsはnameの値を使い
初期化したハッシュをparams変数経由で構成する。このハッシュは入力された値に基づいて
ユーザー作成するときに使われる。
演習
- 『HTML編』ではHTMLをすべて手動で書き起こしているが、なぜformタグを使わなかったのか?
コントローラーなどRailsを使ってなかったため。
ユーザー登録失敗
ユーザー登録の失敗をわざと行う。なぜエラーになるのかの動作テストを兼ねている。
この節では無効なデータ送信を受け付けるユーザー登録フォームを作成、
ユーザー登録フォームを更新してエラーの一覧を表示させる。
エラー画面
- Name can't be blank
- Email is invalid
- Password is too short
正しいフォーム
resources :users を routes.rb ファイルに追加すると自動的にRailsアプリケーションがRESTful URIに応答するようになったが、
/usersへのPOSTリクエストはcreateアクションに送られる。createアクションにてフォーム送信を受け取り、User.newを使って新しいユーザーオブジェクトを作成、保存をする。
if-else分岐構造を使い保存に成功したか否かを@user.saveの値がtrueまたfalseになるときに
それぞれ成功時の処理と失敗時の処理をわける。
#ユーザー登録の失敗に対応できるcreateアクション
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
コメント通り実装は終わってないため今の状態で、各フォームにテキスト情報を入力後
「create my accout」をクリックするとエラーが表示される。
エラー画面
デバック情報
デバッグ情報のうちパラメーターハッシュのuserの部分をみる。
"user" => { "name" => "Foo Bar",
"email" => "foo@invalid",
"password" => "[FILTERED]",
"password_confirmation" => "[FILTERED]"
}
このハッシュはUsersコントローラにparamsとして渡される。このparamsハッシュ(user)には各リクエストの情報が含まれる。
ユーザー登録情報の送信の場合、paramsには複数のハッシュに対するハッシュが入れ子として含まれる。
上記のデバック情報ではフォーム送信結果が、送信された値に対応する属性とともにuserハッシュに保存され、ハッシュキーがinputタグにあったname属性の値になる。
<input id="user_email" name="user[email]" type="email"/>
"user[email]"という値は、userハッシュの:emailキーの値と一致する。
ハッシュのキーはデバック情報では文字列となっているが、Railsは文字列ではなく、params[:user]のように
「シンボル」としてUsersコントローラに渡している点に注意する。
以下のコードはほぼ同じ内容である。
@user = User.new(params[:user])
@user = User.new(nama:"Foo Bar",email:"foo@invalid",
password:"foo",password_confirmation:"bar:)
これをマスアサインメントと呼ぶ。
Strong Parameters
値のハッシュを使ってRubyの変数を初期化する
@user=User.new(params[:user]) #実装は終わってないことに注意!
これは最終形ではない、paramsハッシュ全体を初期化するというのは危険であるため。
ユーザーが送信したデータをまるごとUser.newに渡していることになる。
危険を防ぐため、パラメータを使いやすくするためにuser_paramsという外部メソッドを使うのが慣習となっている。
このメソッド(user_params)は適切に初期化したハッシュを返し、params[:user]の代わりとして使われる。
@user=User.new(user_params)
このuser_paramsメソッドはUserコントローラの内部でのみ実行され、privateキーワードを使い
Web経由で外部ユーザーから使えないようにしている。
# createアクションでStrong Parametersを使う
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キーワード以降のコードはインデントを1段深くしている。
無効な情報をユーザー登録フォームで送信した結果
|
演習
- signup?admin=1 にアクセスし、paramsの中にadmin属性が含まれていることをデバッグ情報から確認する。
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
admin: '1'
controller: users
action: new
permitted: false
エラーメッセージ
登録失敗したときに画面に表示されるように実装する。
rails consoleだと以下の内容でエラーメッセージを表示させてくれる。
$ 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_messagesは実際にどのような理由でfalseになるのか表示させてくれる。
ブラウザでもこのメッセージを表示するため、newページでエラーメッセージのpartialを出力させる。
このとき、form-controlというCSSクラスも一緒に追加することで、
Bootstrapがうまく取り扱ってくれるようになる。
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_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_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'というパーシャルをrender(描画)
エラーを表示させるためのpartial
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 %>
cssでエラーメッセージの数を与えるスタイル
/* forms */
.
.
.
#error_explanation {
color: red;
ul {
color: red;
margin: 0 0 30px 0;
}
}
.field_with_errors {
@extend .has-error;
.form-control {
color: $state-danger-text;
}
}
演習
- 最小文字数を5に変更すると、エラーメッセージも自動的に更新される。
- 未送信のユーザー登録フォームのURLと、送信済みのユーザー登録フォームのURLを比べ、
なぜURLは違うのか答えよ。
ルーティングの設定により、未送信時のページはnew,送信済みのユーザー登録フォームはcreateと
それぞれアクションが違うため。デバックでも表示されている。
newアクション
createアクション
ユーザー登録成功
成功する画面を表示させる。成功すると自動的にデータベースに登録され、ブラウザ表示をリダイレクトして
登録されたユーザーのプロフィールが表示される。
成功の画像
登録フォームの完成
保存の成功をここで扱う とコメントアウトされた文に対して保存させるコードを記述する。
記述しない状態で保存しようとするとエラーが発生する。これはcrateアクションに対するViewがないことが原因。
サーバログからcreateテンプレートがないとメッセージが出てくる。
ユーザー登録成功時にはページに描画せず、別のページ(新しく作成されたユーザーのプロフィール)にリダイレクトする。
#保存とリダイレクトを行う、userのcreateアクション
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
redirect_to @user
と
redirect_to user_url(@user)
等価である。
演習
- 有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認する。
ubuntu:~/environment/sample_app (sign-up) $ rails c
Running via Spring preloader in process 4084
Loading development environment (Rails 6.0.3)
>> user = User.new(name:"tateyama", email: "tateyama@yahoo.co.jp", password:"nanashi",password_confirmation: "nanashi")
(1.6ms) SELECT sqlite_version(*)
=> #<User id: nil, name: "tateyama", email: "tateyama@yahoo.co.jp", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
>> user.save
(0.1ms) begin transaction
User Exists? (0.9ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "tateyama@yahoo.co.jp"], ["LIMIT", 1]]
User Create (2.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "tateyama"], ["email", "tateyama@yahoo.co.jp"], ["created_at", "2021-10-04 03:35:35.089281"], ["updated_at", "2021-10-04 03:35:35.089281"], ["password_digest", "$2a$12$MSS7eHa4W7saju8LuuxIJuNU8Wt05FVxr3seDeaVKopUf84VgP8qO"]]
(5.5ms) commit transaction
=> true
>>
>>
>> exit
ubuntu:~/environment/sample_app (sign-up) $ rails c
Running via Spring preloader in process 4986
Loading development environment (Rails 6.0.3)
>> user = User.new(name: "bakaya", email: "tommmm@yahoo.co.jp", password: "tamagoyaki", password_confirmation: "tamagoyaki")
(0.4ms) SELECT sqlite_version(*)
=> #<User id: nil, name: "bakaya", email: "tommmm@yahoo.co.jp", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
>> user.save
(0.1ms) begin transaction
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "tommmm@yahoo.co.jp"], ["LIMIT", 1]]
User Create (1.4ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "bakaya"], ["email", "tommmm@yahoo.co.jp"], ["created_at", "2021-10-04 03:38:10.174502"], ["updated_at", "2021-10-04 03:38:10.174502"], ["password_digest", "$2a$12$ZdYylyBIQCJcQJTV4mpLuu.GrFhaPdWg87AzjQlIkqGAnzizR1fYm"]]
(6.2ms) commit transaction
=> true
>>
>>
>> exit
flash
現在までユーザー登録フォームが動くところまでやり、ブラウザから正しいユーザー情報を登録できるようになった。
次に、初回登録時に「Welcomeページ」それ以降は通常のページを表示させる機能を追加する。
初回登録時にはflashという変数を使い:successというキーで代入する。
flash変数に代入したメッセージは、リダイレクト直後のページで表示できる。
flash内に存在するキーがあるかを調べ、初回であればそのメッセージを全て表示させる。
# ユーザー登録ページにフラッシュメッセージを追加する
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
rails cだと以下の方法となる
# コンソールでflashハッシュをイテレート(each do ... end)する
$ rails console
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", danger: "It failed."}
>> flash.each do |key, value|
?> puts "#{key}"
?> puts "#{value}"
>> end
success
It worked!
danger
It failed.
flash変数を使いWebサイト全体を表示させる
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
# flash変数の内容をWebサイトのレイアウトに追加する
<!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 %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
.
.
.
</body>
</html>
演習
- コンソールに移り、文字列内の式展開でシンボルを呼び出してみる。
"#{:success}"といったコードを実行すると、どんな値が返ってくるか。
>> puts "#{:success}"
success
=> nil
>>
- flashはどのような結果になるか
>> flash = {success: "It worked!", danger: "It failed."}
=> {:success=>"It worked!", :danger=>"It failed."}
>> "#{flash[:success]}"
=> "It worked!"
>> "#{flash[:danger]}"
=> "It failed."
実際のユーザー登録
今まで何度かフォームにユーザー登録をしているので一度リセットする。
rails db:migrate:reset
初回ログイン画面
初回以降、リロードすると以下の画面になる。
演習
- Railsコンソールを使って、新しいユーザーが本当に作成されたのかもう一度チェックする。
>> User.first
(1.4ms) SELECT sqlite_version(*)
User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-10-07 12:18:50", updated_at: "2021-10-07 12:18:50", password_digest: [FILTERED]>
- 自分のメールアドレスでユーザー登録を試す。
>> User.find_by(email: "example@railstutorial.org")
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "example@railstutorial.org"], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-10-07 12:18:50", updated_at: "2021-10-07 12:18:50", password_digest: [FILTERED]>
>>
成功時のテスト
有効な送信に対するテストを記述する。
今回の目的はデータベースの中身が正しいかを検出する。
assert_differenceというメソッドを使ってテストを書く。
assert_difference 'User.count', 1 do
post users_path, ...
end
このメソッドは第一引数に文字列('User.count')を取り、
assert_differenceブロック内の処理を実行する直前と、
実行した直後のUser.countの値を比較する。
第二引数はオプションですが、ここには比較した結果の差異(今回の場合は1)を渡す。
# 有効なユーザー登録に対するテスト
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
end
end
user_pathにPOSTリクエスト送信後、floow_redirect!を使っている。
これはPOSTリクエストを送信した結果、リダイレクト先に移動するメソッドである。
この行の次はusers/show
が表示されるはず。
演習
- 実装したflashに対するテストを書いてみる。
assert_not flash.blank?
# flashが空ならfalse,空じゃなければtrue
- applicationビューのflashメッセージをcontent_tagを使って見やすくする。
<%= content_tag(:div, message, class: "alert alert-#{message_type}" ) %>
- リダイレクトの行をコメントアウトすると、テストが失敗することを確認してみる。
ERROR["test_should_get_new", #<Minitest::Reporters::Suite:0x0000560973689c50 @name="UsersControllerTest">, 3.20091959399997]
test_should_get_new#UsersControllerTest (3.20s)
SyntaxError: SyntaxError: /home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:21: else without rescue is useless
else
^~~~
/home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
app/controllers/users_controller.rb:21: else without rescue is useless
app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
test/controllers/users_controller_test.rb:5:in `block in <class:UsersControllerTest>'
ERROR["test_layout_links", #<Minitest::Reporters::Suite:0x0000560973841bb0 @name="SiteLayoutTest">, 3.2765569769999274]
test_layout_links#SiteLayoutTest (3.28s)
SyntaxError: SyntaxError: /home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:21: else without rescue is useless
else
^~~~
/home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
app/controllers/users_controller.rb:21: else without rescue is useless
app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
test/integration/site_layout_test.rb:14:in `block in <class:SiteLayoutTest>'
ERROR["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x0000560973862978 @name="UsersSignupTest">, 3.2893298140002116]
test_invalid_signup_information#UsersSignupTest (3.29s)
SyntaxError: SyntaxError: /home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:21: else without rescue is useless
else
^~~~
/home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
app/controllers/users_controller.rb:21: else without rescue is useless
app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
test/integration/users_signup_test.rb:6:in `block in <class:UsersSignupTest>'
ERROR["test_valid_signup_information", #<Minitest::Reporters::Suite:0x000056097387e830 @name="UsersSignupTest">, 3.301933351000116]
test_valid_signup_information#UsersSignupTest (3.30s)
SyntaxError: SyntaxError: /home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:21: else without rescue is useless
else
^~~~
/home/ubuntu/environment/sample_app/app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
app/controllers/users_controller.rb:21: else without rescue is useless
app/controllers/users_controller.rb:33: syntax error, unexpected end, expecting end-of-input
test/integration/users_signup_test.rb:17:in `block in <class:UsersSignupTest>'
20/20: [=================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.30615s
20 tests, 36 assertions, 0 failures, 4 errors, 0 skips
- @user.saveをfalseに置き換えたとして、asset_differenceのテストでどのようにしてバグを検知するのか?
保存失敗だとすると、保存前(get)で読み込んだassert_differenceで、
User.countの値が変わらず(1→1)、postでcreateアクションに送るも、
paramsでuserハッシュの値を受け取れず失敗する。
成功時のテスト
assert_differenceというメソッドを使ってテストを書く。
assert_difference 'User.count', 1 do
post users_path, ...
end
assert_no_differenceと同様、第一引数に文字列('User.count')をとり、
assert_differenceブロック内の処理を実行する直前と、実行した直後のUser.countの値を比較する。
第2引数はオプションであるが、比較結果の際を渡す。
assert_differenceを使ったテストを追加すると、以下のようになる。
user_pathにPOSTリクエスト送信後、follow_redirect!というメソッドを使っている。
POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッドである。
この行の直後は'users/show'テンプレートが表示されている。
# 有効なユーザー登録に対するテスト
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
end
ユーザー登録に成功後、どのテンプレートが表示されているのか検証している。
このテストを成功させるためには、Userルーティング、Userのshowアクション、show.html.erbビューが
それぞれ正しく動作している必要がある。
assert_template 'users/show'
演習
- 実装したflashに対するテストを書く。最小限のテンプレートを用意しておいたので、参考にする
ちなみに、テキストに対するテストは壊れやすいです。
文量の少ないflashのキーであっても、それは同じです。筆者の場合、flashが空でないかをテストするだけの場合が多いです。
# flashをテストするためのテンプレート
require 'test_helper'
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
assert_not flash.empty?←テスト
end
end
- flash用のHTMLは読みにくい。より読みやすくしたリストのコードに変更してみる。
変更が終わったらテストスイートを実行し、正常に動作することを確認する。
なお、このコードでは、Railsのcontent_tagというヘルパーを使っている。
# content_tagを使ってレイアウトの中にflashを埋め込む
<!DOCTYPE html>
<html>
.
.
.
<% flash.each do |message_type, message| %>
<%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
.
.
.
</html>
- app/controllers/users_controller.rbの
リダイレクトの行をコメントアウトすると、テストが失敗することを確認してみる。
ubuntu:~/environment/sample_app (basic-login) $ rails t
Running via Spring preloader in process 3029
Started with run options --seed 31158
ERROR["test_valid_signup_information", #<Minitest::Reporters::Suite:0x000055cef78b2fb0 @name="UsersSignupTest">, 1.7564439129999982]
test_valid_signup_information#UsersSignupTest (1.76s)
RuntimeError: RuntimeError: not a redirect! 204 No Content
test/integration/users_signup_test.rb:25:in `block in <class:UsersSignupTest>'
21/21: [=========================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.85809s
21 tests, 42 assertions, 0 failures, 1 errors, 0 skips
- app/controllers/users_controller.rbで
@user.saveの部分をfalseに置き換えたとしましょう(バグを埋め込んでしまったと仮定してください)。
このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか?
テストコードを追って考えてみてください。
有効なユーザー情報を送ったときにはユーザーが一人増えるというテストに対して
createアクションではfalseに書き換えたため、ユーザーを作成する行が実行されないためテストでも
"User.count" didn't change by 1.→User.countが変わってないよと怒られる。
FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x0000559122566bd0 @name="UsersSignupTest">, 1.1665384099906078]
test_valid_signup_information#UsersSignupTest (1.17s)
"User.count" didn't change by 1.
Expected: 1
Actual: 0
test/integration/users_signup_test.rb:20:in `block in <class:UsersSignupTest>'
プロのデプロイ
ユーザー登録ページを動かすことができたので、このアプリケーションをデプロイして、本番でも動かせるようにする。
実際にデータを操作できるようにするデプロイは初である。
ユーザー登録をセキュアにするために本番用のアプリケーションに重要な機能を追加していく。
その後、デフォルトのWebサーバーを実際の世界で使われているWebサーバーに置き換えてから、本番データベースに設定を追加していく。
デプロイの下準備として、変更をmasterブランチにマージしておく。
$ git add -A
$ git commit -m "Finish user signup"
$ git checkout master
$ git merge sign-up
本番環境でのSSL
本性で開発したユーザー登録フォームで送信すると、
名前やメールアドレス、パスワードと言ったデータがネットワーク越しに流される。
しかしこれらのデータは途中で捕捉できるためサンプルアプリケーションの本質的なセキュリティ上の欠陥である。
これを修復するためにTransport Layer Security(TLS)を使う。
これはローカルサーバーからネットワークに流れる前に、大事な情報を暗号化する技術である。
今回はユーザー登録ページのためだけにSSLを導入しているが、これはWebサイト全体で適用できるため、次章で実装する
ログイン機構をセキュアにしたり、セッションハイジャック(Session Hijacking)の脆弱性に対しても多くの利点を生み出します。
SSLを有効化はproduction.rbの本番環境の設定ファイル1行を修正するだけ。
configに「本番環境ではSSLを使うようにする」と設定するだけ。
herokuはデフォルト設定でもSSLを使用できるが、SSLをブラウザに矯正するわけではない。
このため、httpsからhttpへアクセスするとWebサイトとユーザー間のやり取りが安全ではなくなる。
Railsではproduction.rbのコード1行を変えるだけでSSLを強制することができる。
# 本番環境ではSSLを使うように修正する
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
本番環境用のWebサーバー
SSL導入後、次は本番環境に適したWebサーバーを使う。herokuのデフォルトでは、
Rubyだけで実装されたWebrickというWebサーバを使っている。
しかし、Webrickは著しいトラフィックを扱うことには適していないため、pumaに置き換える。
下記内容にてpumaの設定を行う。
# 本番環境のWebサーバー設定ファイル
# Pumaの設定ファイル
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { ENV['RACK_ENV'] || "production" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
plugin :tmp_restart
最後に、Procfileと呼ばれる、Heroku上でPumaのプロセスを走らせる設定ファイルを作成する。
なお、このProcfileはルートディレクトリ(Gemfileと同じディレクトリ)に置いておく必要があるので、
ファイルを置くディレクトリには注意する。
# Pumaが使うようにProcfileで定義する
./Procfile
web: bundle exec puma -C config/puma.rb
本番データベースを設定する
本番データベースで使うのはPostgreSQL。
productionセクションを変更する。
# データベースを本番向けに設定する
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://railsguides.jp/configuring.html#データベース接続をプールする
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: sample_app_production
username: sample_app
password: <%= ENV['SAMPLE_APP_DATABASE_PASSWORD'] %>
本番環境へのデプロイ
$ rails test
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push && git push heroku
以下のようなアドレスバーに鍵アイコンが付いていたら成功。
本番環境(Web上)で実際にユーザー登録をしてみる
目(演習)(subsubsection)
節(section)
項(subsection)
目(演習)(subsubsection)
#4. 用語のまとめ
用語 | 意味 |
---|---|
テスト環境 | test |
開発環境 | development |
本番環境 | production |
Sassのミックスイン機能 | 複数の構文を省略することができる |
form_with | Active Recordのオブジェクトを取り込み、そのオブジェクトの属性を使いフォームを構築する。 |
renderメソッド | レスポンスの出力をしてくれるメソッド |
render | 描画 |
pluralize | 英語専用のテキストヘルパー |
assert_template | そのアクションで指定されたテンプレートが描写されているかをバリデーション |
Transport Layer Security(TLS) | ローカルのサーバーからネットワークに流れる前に、大事な情報を暗号化する技術 |
debugメソッド | 役立つデバック情報を表示できる。 |
Qitta(キータ) | エンジニアに関する知識を記録・共有するためのサービスです。 |
#5. 感想
- 節の感想
#6. おわりに
章を改めて振り返っての感想
次の目標