kuma0319のブログ

webエンジニアになるためのアウトプットブログ

Ruby on Railsチュートリアル第7章

Railsチュートリアルの第7章を進めていきます。

第7章(ユーザー登録)

ユーザーの表示

ここはdebug機能の紹介や、gravatorで見た目を整える部分なので特にまとめず、演習のみして飛ばします。

ユーザー登録フォーム

form_withメソッド
  • HTMLフォーム要素(テキストフィールド、ラジオボタン、パスワードフィールドなど)に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返す。
  • <%= form_with(model: @user) do |f| %>の記述では、form_withのオプションとしてmodel: @userを渡してる。こうすることで、Userクラスの変数@user(新しいユーザー)なのでRailsがPOSTメソッドを実行すべきと判断出来る。
  • emailはtype="email"を使用した方が、一般的なtextフィールドよりも操作感がよく適している。
  • passwordはtype="password"とすることで、入力文字がドット表記になる。
  • formタグ内にはCross-Site Request Forgery(CSRF)を阻止するための真正性トークン(authenticity token)も生成される。

ERB形式で以下の記述の場合

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

以下のHTMLが返される。

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

ユーザー登録失敗

createアクション

resources :usersのルーティングで、/usersへのPOSTリクエストはcreateアクションに対応する。

コントローラにcreateアクションを追加。
status: :unprocessable_entityは後々Turboのレンダリングに必要。

[app/controllers/users_controller.rb]
  def create
    @user = User.new(params[:user])    # 実装は終わっていないことに注意!
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new', status: :unprocessable_entity
    end
  end
params[:user]について

paramsハッシュには各リクエストの情報が含まれる。更にparamsハッシュは入れ子のハッシュが含まれ、userハッシュに各属性に対応する値が保存されている。
createアクションの@user = User.new(params[:user])は、この入れ子のハッシュを展開すると下記のような形態となる。

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

このような複数情報を扱える機能をマスアサインメントと呼ぶ。但し、マスアサインメントにはセキュリティ上の注意が必要。

Strong parameters
  • マスアサインメントは複数の属性を更新できるが、その反面悪意ある情報まで更新される脆弱性がある。
  • これの対策がStrong Parametersで必須パラメータと許可済みパラメータを指定可能。
  • Strong Parametersは、通常補助メソッドの形で使用し、更に外部に公開する必要は無いため、private配下に記述する。
  def create
    @user = User.new(user_params)
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new', status: :unprocessable_entity
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end
エラーメッセージの表示
  • 現時点ではエラーが発生してもユーザー側へ表示されないため、そういう場合はエラーメッセージのパーシャルを作成し、それを該当ページでrenderする。
  • Railsの慣習として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」に配置する。
  • pluralizeヘルパーは、不規則系を含む種々の単語を複数形に変換可能。エラーメッセージでいうと、「1 error」、「5 errors」といった感じ。

ユーザー登録成功

redirect_to @userについて

createアクションの成功時の動作が無いため、対応する動作を作成する。通常はリダイレクトするのが普通。

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user #redirect_to user_url(@user)
    else
      render 'new', status: :unprocessable_entity
    end
  end

ここのredirect_to @userは明らかに分かりにくいのですが、以下のサイトが非常に丁寧にまとめられており、分かりやすかったです。

qiita.com

  1. Railsでは通常相対パスを指定するが、リダイレクトの場合は完全なURLを返す絶対パスでないといけない。
  2. そのためパスはredirect_to(user_url(@user.id))となる。
  3. 引数URLがモデルオブジェクトの場合は、Railsはidを自動で返してくれるため、.idは省略可能。
  4. この段階でredirect_to(user_url(@user))となっており、メソッドに渡す場合の()は省略可能なことは4章の通り。
  5. redirect_to user_url(@user)となり、redirect_to @userまで省略可能。
flashメッセージ

flash変数はハッシュのように取り扱うため、flash[:success] = "Welcome to the Sample App!"と記述すると、:successがkeyで、メッセージがvalueとなる。
flash変数に代入されたメッセージはリダイレクト直後に表示され、keyがあれば対応するメッセージを返す。

[app/views/layouts/application.html.erb]
<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

alert-<%= message_type %>の部分は、keyであるmessage_typeの種類によって適用させるCSSを変えている。

デプロイ

TLS設定
  • TLSはローカルのサーバーからネットワークにデータを送信する前に情報を暗号化する技術。
  • Webホスト側がこのように振る舞うことに依存しない方がよいため、ローカルで設定しておく。
  • production.rbファイルのコードの一部config.force_sslをtrueに変更するだけ。
  • 自分のドメインでこれを利用する場合、TLS/SSL証明書を購入して設定する必要があるが、Renderでは利用可能になっている。
  • カスタムドメインTLSをCloudflareで扱う場合は、Railsアプリ側でTLS/SSLを強制的に有効にする設定はしない
本番環境用のwebサーバー設定(Puma)

Pumaは、大量のリクエストを受信できる能力を備えたサーバー 1. config/puma.rbの内容を書き換える。 Getting Started with Ruby on Rails on Render | Render 2. RenderのStart Commandbundle exec puma -C config/puma.rbに置き換える。

本番環境用のデータベース(PostgreSQL)

config/database.ymlのproductionセクションをイジるだけ。

第7章ー演習ー

7.1.1

①ブラウザで/aboutにアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか?paramsの内容から確認してみましょう。
(解答) #<ActionController::Parameters {"controller"=>"static_pages", "action"=>"about"} permitted: false>

Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。
(解答) どちらもuserオブジェクトの情報がyaml形式で表示される。

7.1.2

①ERBを使って、マジックカラム(created_atとupdated_at)の値をshowページに表示してみましょう(リスト 7.4)。
(解答)ERBで追加したら、「Taro Yamada, michael@example.com, 2023-04-14 16:18:32 UTC, 2023-04-14 16:30:58 UTC」のようにタイムスタンプが表示された。

[app/views/users/show.html.erb]
<%= @user.name %>, <%= @user.email %>, <%= @user.created_at %>, <%= @user.updated_at %>

②ERBを使って、Time.nowの結果をshowページに表示してみましょう。ページを更新すると結果がどう変わるかも確認してみてください。
(解答) 現在時刻が表示される。「Taro Yamada, michael@example.com, 2023-04-14 16:18:32 UTC, 2023-04-14 16:30:58 UTC, 2023-04-15 03:49:51 +0000」

7.1.3

①showアクションの中にdebuggerを差し込み(リスト 7.6)、ブラウザから/users/1にアクセスしてみましょう。次にRailsコンソールを開き、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示してみましょう。(ヒント: 7.1.1.1の演習を参考にしてください。その演習ではdebugメソッドで表示したデバッグ情報を、どのようにしてYAML形式で表示していたでしょうか?)
(解答)

(ruby) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false
nil

②newアクションの中にdebuggerを差し込み、/users/newにアクセスしてみましょう。@userの内容はどのようになっているでしょうか? 確認してみてください。
(解答) nil

(ruby) @user
nil

7.1.4

①(任意)Gravatar上にアカウントを作成し、あなたのメールアドレスと適当な画像を紐付けてみてください。メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。
(解答) 飛ばします。

②7.1.4で定義したgravatar_forヘルパーをリスト 7.12のように変更して、sizeをオプション引数として受け取れるようにしてみましょう。正しく変更すると、gravatar_for user, size: 50のように呼び出せるようになります。重要: 改善したヘルパーはこの後の10.3.1で実際に使うので、忘れずに実装しておきましょう。
(解答)変更のみ

③オプション引数は今でもRubyコミュニティで一般的に使われていますが、Ruby 2.0から導入された新機能「キーワード引数(Keyword Arguments)」でも実現できます。先ほど変更したリスト 7.12を、リスト 7.13のように置き換えてもうまく動くことを確認してみましょう。この2つの実装方法はどこが違うのかを考えてみてください。
(解答) うまくいく。オプション引数の場合だと、引数を渡す順番によって意図しない結果を生み出す可能性がある。一方キーワード引数は、keyとvalueのハッシュ形式で表現し、値を取り出すときもハッシュで取り出すため順序に依存しない。

7.2.1

①試しに、リスト 7.15のform_withにあるブロック変数fをすべてfoobarに置き換えてみて、結果が変わらないことを確認してみてください。確かに結果は変わりませんが、変数名をfoobarとするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。
(解答) 結果は変わらない。変数fはformのfだと認識出来るが、foobarだと変数の意味が通じなくなるため。

7.2.2

①『HTML編』ではHTMLをすべて手動で書き起こしていますが、formタグは使っていません。その理由を考えてみてください。
(解答) 入力や送信のフォームを作成していなかった?

####7.3.2 ①/signup?admin=1にアクセスすると、paramsの中にadmin属性が含まれていることをデバッグ情報で確認してみましょう。
(解答) 含まれていることを確認。 #<ActionController::Parameters {"admin"=>"1", "controller"=>"users", "action"=>"new"} permitted: false>

####7.3.3 ①最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。
(解答)確認作業なので飛ばします。

②未送信のユーザー登録フォーム(図 7.14)のURLと、送信済みのユーザー登録フォーム(図 7.20)のURLを比べてみると、URLが違っています。その理由を考えてみてください。
(解答)未送信のユーザー登録フォームは、HTTPリクエストGETで/signupパスに対応するnewページを表示している。送信済みのユーザー登録フォームは、HTTPリクエストPOSTでcreateアクションに対応する。ここでは無効な情報でサインアップした場合newテンプレートがrenderされるが、createアクションは/usersのURLに対応するためURLが異なっている。

7.3.4

①リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どこまで細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。
(解答) エラーメッセージパーシャルのdivタグ内のid、classを指定。

[test/integration/users_signup_test.rb]
    assert_select 'div#error_explanation'
    assert_select 'div.alert'
    assert_select 'div.alert-danger'

7.4.1

①有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。
(解答) 作成出来た。

irb(main):001:0> User.second
  User Load (1.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]                                                      
=>                                                              
#<User:0x00007f864a41f500                                       
 id: 2,                                                         
 name: "test user",                                             
 email: "testesttest@test.com",                                 
 created_at: Sat, 15 Apr 2023 11:56:15.406868000 UTC +00:00,    
 updated_at: Sat, 15 Apr 2023 11:56:15.406868000 UTC +00:00,    
 password_digest: "[FILTERED]">        

②リスト 7.26のredirect_to @userをredirect_to user_url(@user)に書き換えても、同じ結果になることを確認してみましょう。
(解答) 飛ばします。

7.4.2

Railsコンソールを開いて、文字列内の式展開(4.2.1)でシンボルを呼び出してみましょう。例えば"#{:success}"といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。
(解答)

irb(main):002:0> "#{:success}"
=> "success"

②上の演習で試した結果を参考に、リスト 7.28のflashがどのような結果になるか考えてみてください。
(解答) 値のみが返ってくる。

7.4.3

飛ばします。

7.4.4

①7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.32に最小限のテンプレートを用意しておいたので、参考にしてください((コードを書き込む)の部分を適切なコードに置き換えると完成します)17 。
(解答) flashがemptyで無いことを確認。

[test/integration/users_signup_test.rb]
  #有効なサインアップ
  test "valid signup information" do
    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!                      #createアクションのredirect_to @userへ移動。
    assert_template 'users/show'          #リダイレクト先のusers/showページ。
    assert_not flash.empty?               #flashが表示されていることを確認。
  end

②本文中でも指摘したように、flash用のHTML(リスト 7.29)は読みにくくなってしまっています。これを、より読みやすくしたリスト 7.33のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。
(解答) 正しく動く。

③リスト 7.26のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。
(解答) リダイレクトされないエラー。

ERROR UsersSignupTest#test_valid_signup_information (1.55s)
Minitest::UnexpectedError:         RuntimeError: not a redirect! 204 No Content
            test/integration/users_signup_test.rb:29:in `block in <class:UsersSignupTest>'

④リスト 7.26で、@user.saveの部分をfalseに置き換えたとしましょう(バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。
(解答) assert_differenceの予期していた値と異なるといったエラーが発生。

 FAIL UsersSignupTest#test_valid_signup_information (1.54s)
        "User.count" didn't change by 1.
        Expected: 1
          Actual: 0
        test/integration/users_signup_test.rb:23:in `block in <class:UsersSignupTest>'

 FAIL UsersSignupTest#test_invalid_signup_information (1.58s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:16:in `block in <class:UsersSignupTest>'

7.5.4

①ブラウザから本番環境にアクセスし、TLSの鍵マークがかかっているか、URLがhttpsになっているかどうかを確認してみましょう。
(解答) 鍵マークが付いてTLS設定が有効化されている。

②本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?
(解答)飛ばします。

第7章ーまとめー

  • paramsハッシュはネストされたハッシュが複数あり、複数の属性を同時に更新できるマスアサインメント機能を有する。
  • マスアサインメントはセキュリティ上のリスクもあるため、Strong Parametersで特性の属性のみ更新可能とする。
  • form_withでActiveRecordに対応したフォームを生成可能。
  • Railsにおけるパスの指定は通常相対パスで示し、リダイレクトの際のみ絶対パスで示す。パスの引数がモデルオブジェクトの場合はidであることを明示的に示す必要は無く省略できる。
  • flash変数はリダイレクトの直後にkeyに対応するメッセージが表示される。
  • 本番環境ではセキュアな環境とするために、TLSを導入する。

第7章終わり🐻