0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

1周目 railsチュートリアル第7章

Last updated at Posted at 2021-09-21

#目次
1.前回
2.概要
3.内容
4.用語のまとめ
5.感想
6.おわりに

#1. 前回
https://qiita.com/ak-matsu/items/f3ce9156005c89e91390

#2. 概要
ユーザー登録機能の実装。
HTMLフォームを作りウェブアプリケーションに登録情報を送信、
ユーザーを新規登録して情報をデータベースへ保存する。

#3. 内容

ユーザーを表示する

ユーザー名とプロフィール写真を表示するためのページ作成。
本実装に入る前にbrunch作成を忘れずに行うこと。

$ git checkout -b sign-up

完成予定のモック画像

image.png

この節で完成予定のユーザープロフィールのモック

image.png

デバッグと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?
もしも開発環境であればデバック情報を表示させるという構文である。

app/assets/stylesheets/custom.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 */

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

Sassのミックスイン機能を使うとCSSグループをパッケージ化して複数の要素を省略することができる。

ここまで設定できたら、rails sにてローカルサーバを起動してテスト環境を立ち上げて
動作確認すると以下のデバック画面が表示されている。

image.png
演習
  • ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか?paramsの内容から確認してみましょう
演習1.jpg
  • 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アクションとして扱われる。

image.png

/users/1のURLを有効にするため

config/routes.rb

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アクションが作成されていないので以下のエラーページが表示される。

image.png

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に登録されたユーザー情報が表示される。

image.png
演習
  • 埋め込みRubyを使って、マジックカラム(created_atとupdated_at)の値をshowページに表示させる。
演習1.jpg
  • 埋め込みRubyを使って、Time.nowの結果をshowページへ表示させる。ページを更新すると、その結果はどう変わるか確認する。

更新する度に時間が過ぎている。

スクリーンショット 2021-09-21 20.45.15.jpg

debuggerメソッド

アプリケーションの内容を理解するためdebugメソッドを使う。
もっと直接的に行う方法がbyebug gemによるdebuggerメソッドである。

app/controllers/users_controller.rb

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ヘルパーメソッドを使う。

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

<%= は 計算されて表示されることを忘れない。

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

rails sの後にプレビューを開くと以下の画面になる。

image.png

アプリケーションでGravatarを利用できるようにするために、
update_attributesを使いデータベース上のユーザー情報を更新する。

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

更新すると画像が変わる。

image.png

ユーザーのサイドバーの最初のバージョンを作る。

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クラスを配置したのでプロフィールページにSCSSでスタイルを与えることができる。

app/assets/stylesheets/custom.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;
}

image.png
演習
  • 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といった呼び出し方ができるようになる。
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
  • Ruby 2.0から導入された新機能「キーワード引数」を使ってオプション引数を実現する。
app/helpers/users_helper.rb

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

ユーザー登録フォーム

ユーザー登録フォームの作成

image.png

form_withを使用する

ユーザー登録ページで必要な入力フォームを作成するため、form_withヘルパーメソッドを使う。
ユーザー登録ページ/signupのルーティングはUsersコントローラーのnewアクションに紐付けられている。
form_withの引数で必要となるUserオブジェクトを作成すること、@user変数を定義する必要がある。

app/controllers/users_controller.rb
#newアクションに@user変数を追加する

class UsersController < ApplicationController

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

  def new
    @user = User.new
  end
end

フォームは下記の構文で示し、SCSSで見栄えを整える。

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| %>
      <%= 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>
app/assets/stylesheets/custom.scss

# ユーザー登録フォームのCSS

.
.
.
/* forms */

input, textarea, select, .uneditable-input {
  border: 1px solid #bbb;
  width: 100%;
  margin-bottom: 15px;
  @include box_sizing;
}

input {
  height: auto !important;
}

完成図

image.png
演習
  • 試しに、ブロックの変数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になっているため、文字が隠蔽される構造となる。

image.png

ユーザー作成で重要なものはinputごとに特殊なname属性であり、Railsはnameの値を使い
初期化したハッシュをparams変数経由で構成する。このハッシュは入力された値に基づいて
ユーザー作成するときに使われる。

演習
  • 『HTML編』ではHTMLをすべて手動で書き起こしているが、なぜformタグを使わなかったのか?

コントローラーなどRailsを使ってなかったため。

ユーザー登録失敗

ユーザー登録の失敗をわざと行う。なぜエラーになるのかの動作テストを兼ねている。
この節では無効なデータ送信を受け付けるユーザー登録フォームを作成、
ユーザー登録フォームを更新してエラーの一覧を表示させる。

エラー画面

  • Name can't be blank
  • Email is invalid
  • Password is too short
image.png

正しいフォーム

resources :users を routes.rb ファイルに追加すると自動的にRailsアプリケーションがRESTful URIに応答するようになったが、
/usersへのPOSTリクエストはcreateアクションに送られる。createアクションにてフォーム送信を受け取り、User.newを使って新しいユーザーオブジェクトを作成、保存をする。

if-else分岐構造を使い保存に成功したか否かを@user.saveの値がtrueまたfalseになるときに
それぞれ成功時の処理と失敗時の処理をわける。

app/controllers/users_controller.rb
 #ユーザー登録の失敗に対応できる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」をクリックするとエラーが表示される。

エラー画面

スクリーンショット 2021-09-27 21.30.21.jpg

デバック情報

スクリーンショット 2021-09-27 21.38.54.jpg

デバッグ情報のうちパラメーターハッシュの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経由で外部ユーザーから使えないようにしている。

app/controllers/users_controller.rb
# 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段深くしている。

無効な情報をユーザー登録フォームで送信した結果

|

image.png
演習
  • signup?admin=1 にアクセスし、paramsの中にadmin属性が含まれていることをデバッグ情報から確認する。
スクリーンショット 2021-09-29 2.22.14.jpg
--- !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に変更すると、エラーメッセージも自動的に更新される。
スクリーンショット 2021-09-30 23.44.59.jpg
  • 未送信のユーザー登録フォームのURLと、送信済みのユーザー登録フォームのURLを比べ、
    なぜURLは違うのか答えよ。

ルーティングの設定により、未送信時のページはnew,送信済みのユーザー登録フォームはcreateと
それぞれアクションが違うため。デバックでも表示されている。

newアクション

スクリーンショット 2021-09-30 23.48.07.jpg

createアクション

スクリーンショット 2021-09-30 23.48.20.jpg

ユーザー登録成功

成功する画面を表示させる。成功すると自動的にデータベースに登録され、ブラウザ表示をリダイレクトして
登録されたユーザーのプロフィールが表示される。

成功の画像

image.png

登録フォームの完成

保存の成功をここで扱う とコメントアウトされた文に対して保存させるコードを記述する。

記述しない状態で保存しようとするとエラーが発生する。これはcrateアクションに対するViewがないことが原因。
サーバログからcreateテンプレートがないとメッセージが出てくる。

image.png

ユーザー登録成功時にはページに描画せず、別のページ(新しく作成されたユーザーのプロフィール)にリダイレクトする。

app/controllers/users_controller.rb

#保存とリダイレクトを行う、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

  • redirect_to user_url(@user)とredirect_to @userが同じ結果になることを確認。

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内に存在するキーがあるかを調べ、初回であればそのメッセージを全て表示させる。

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

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 %>
app/views/layouts/application.html.erb

# 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

image.png

初回ログイン画面

image.png

初回以降、リロードすると以下の画面になる。

image.png
演習
  • 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)を渡す。

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 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に対するテストを書いてみる。
users_signup_test.rb
assert_not   flash.blank?
# flashが空ならfalse,空じゃなければtrue
  • applicationビューのflashメッセージをcontent_tagを使って見やすくする。
application.html.erb
<%= 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'テンプレートが表示されている。

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 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が空でないかをテストするだけの場合が多いです。
test/integration/users_signup_test.rb

# 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というヘルパーを使っている。
app/views/layouts/application.html.erb

# 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サイトとユーザー間のやり取りが安全ではなくなる。

image.png

Railsではproduction.rbのコード1行を変えるだけでSSLを強制することができる。

config/environments/production.rb

# 本番環境では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の設定を行う。

config/puma.rb

# 本番環境の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と同じディレクトリ)に置いておく必要があるので、
ファイルを置くディレクトリには注意する。

config/puma.rb
# Pumaが使うようにProcfileで定義する
./Procfile
web: bundle exec puma -C config/puma.rb

本番データベースを設定する

本番データベースで使うのはPostgreSQL。
productionセクションを変更する。

config/database.yml

# データベースを本番向けに設定する

# 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

以下のようなアドレスバーに鍵アイコンが付いていたら成功。

image.png

本番環境(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. おわりに

章を改めて振り返っての感想
次の目標

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?