Ruby on 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は明らかに分かりにくいのですが、以下のサイトが非常に丁寧にまとめられており、分かりやすかったです。
- Railsでは通常相対パスを指定するが、リダイレクトの場合は完全なURLを返す絶対パスでないといけない。
- そのためパスは
redirect_to(user_url(@user.id))
となる。 - 引数URLがモデルオブジェクトの場合は、Railsはidを自動で返してくれるため、
.id
は省略可能。 - この段階で
redirect_to(user_url(@user))
となっており、メソッドに渡す場合の()は省略可能なことは4章の通り。 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 Command
をbundle 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章終わり🐻