54日目
今日の学習
Ruby
paizaのCランク問題をやったときに、また入力受け取りの段階で手間取ってしまったのでメモ。
# 空白で区切られ、1行に並んでいる文字列の受け取り方 a, b = gets.split(" ") # 複数行の数値を配列として受け取る lines = readlines.map(&:to_i)
Ruby on Rails
Railsチュートリアル 第10章
前回の部分で、ログイン周りの機能の実装が終了した。
10章では、ユーザーの更新・表示・削除の部分を学習するようだ。
ユーザーが自分のプロフィールを更新できる様にするために、8章で実装した認証用のコードを用いるため、認可モデル(Authorization Model)
といったものの学習もする。
ユーザーの更新
edit
アクションは、new
アクションと同じようにして作成していくが、new
との違いはcreate
を経由するのではなく、update
アクションを経由すること。
また、ユーザー更新を行えるのは、ユーザー本人のみに限られている必要がある。認証機構を使うことで、beforeフィルター
というものを使いアクセス制御を行えるらしい。
app/controllers/users_controller.rb
def edit # params[:id]で、ユーザーのIDを取り出せる @user = User.find(params[:id])
edit
のビューファイルでは、@user
変数に属性情報が入っているため、編集時に値が自動的に入力されている状態となる。
edit.html.erb
ファイルを手動で作成する。このとき作成するビューの内容が、new.html.erb
と重なる点が非常に多いため、その部分をパーシャルにしていく。
form_with(@user)
のコードが完全に一致している。Railsは新規ユーザー用のPOST
リクエストと、ユーザー編集用のPATCH
リクエストを、Active Record
のnew_record?
論理値メソッドを使って区別している。
# すでに登録済みの場合は新しいユーザーではないのでfalse >> User.first.new_record? => false # これから登録するユーザーは新しいユーザーなのでtrue >> User.new.new_record? => true
form_with(@user)
を使っているときには、Rails側が勝手に@user.new_record?
の結果に従い、POST
にするかPATCH
にするかを判別してくれている。
編集用ビューが出来上がった後は、レイアウトに仮置きしておいたリンクの部分を正しいパスに変更する。
edit_user_path(current_user)
以前作成したcurrent_user
ヘルパーメソッドを活用。
フォーム用のパーシャル作成
new
とedit
のform_with(@user)
は内容がほぼ一緒だが、ボタンに書かれているテキストだけは異なっている。
ボタンのテキストを含めて、フォーム部分をパーシャルにするために、Railsチュートリアルではprovide
メソッドを利用している。
# パーシャル <%= f.submit yield(:button_text), class: "btn btn-primary" %>
ボタンのテキストを記述する箇所に対して、yield
を配置し、さらに:button_text
というわかりやすい名前をつけている。
# newファイル <% provide(:button_text, 'Create my account') %> # editファイル <% provide(:button_text, 'Save changes') %>
ページ上部でprovide
を利用し、:button_text
を指定してyield
に入れるためのテキストを記述する。
こうすることで、ボタンを含め全てのフォーム部分をパーシャルとして使用できる。
target="_blank"の問題点を解決
リンクを別のタブで開くことができるtarget="_blank"
。
(同じタブで開かせる場合はtarget="_self"
)
これにはセキュリティ上の問題があるようで、リンク先のページに問題のあるJavaScriptなどが含まれたりしていた場合は、リンク元のページを改竄されたりする可能性がある。
それを解決するには、rel="noopener"
をリンクに追加するだけでよいとのこと。
<a href="#" target="_blank rel="noopener">~~~</a>
最近はブラウザ側が対応してくれるそうで、つけ忘れても大丈夫とあるが、念のためにtarget="_blank"
を使う場合は忘れずにrel=noopener
を添えるようにしたい。
編集に失敗した場合の処理
無効な情報が送信された場合(パスワードが違う、変更内容が適切でない等)は、編集が失敗したとしてもう一度編集画面に移るようにする。
create
では、失敗するとif文
のelse
に分岐し、render 'new'
が行われるようにしていたが、update
でも同じく失敗した場合はrender 'edit'
が行われるようにしたい。
ひとまずupdate
アクションを定義する。
create
のときには、@user
変数に対してUser.new(user_params)
を代入した。
引数のuser_params
は、Strong Parametersとして用意したものだったが、これをupdate
アクションでも利用する。
def update @user = User.find(params[:id]) # 指定した値以外を受け取らないようにする if @user.update(user_params)
編集失敗時のテストも作成した。fixtureのユーザーデータをsetup
で使用して、以下のテストを行った。
- 編集ページにアクセス
- editビューが描画されているかどうか
assert_template
で確認 patch
メソッドを使い、無効な情報を送信してみる- 2と同じくeditビューが再度描画されているかを確認
また、assert_select
を使い、正しい数のエラーメッセージが出ているかどうかもテストした。
うまくテストできていれば、The form contains 4 errors
というエラーメッセージが出ているはず。
エラーメッセージはalert
クラスのdiv
タグなので、以下のようにして検出した。
assert_select "div.alert", "The form contains 4 errors"
(Railsチュートリアルでは、「The form contains 4 errors.」というテキストを精査してみましょう。
とあったが、実際にエラー画面を確認したところ.
はなかったのでコピペしていたら一生通らないところだった)
ユーザー編集機能
受け入れテスト(Acceptance Tests)
ユーザー編集機能を実装するにあたって、「実装前に」統合テストを書いてみましょうとRailsチュートリアルが提案している。
何らかの機能を実装する前のテストのことを受け入れテスト
と呼ぶらしい。
編集失敗時のテストを作ったときのように、今回は成功したときの流れを考える。
name
やemail
の変数を作成し、params
で渡すときはこの変数を使って情報を送信する。
パスワードを変更する必要がないときは、パスワードとパスワード確認の部分は空にしておくとよい。
ただし、空でテストをしようとすると、今のままではパスワードのバリデーションに引っかかってしまう。
そのため、パスワードが空のままでも更新できる様にする、allow_nil: true
というオプションを、app/models/user.rb
に追加する。
この変更で、空のパスワードが新規ユーザー登録時に有効になることはない。
テスト部分 # フラッシュがちゃんとあるかどうか assert_not flash.empty? assert_redirected_to @user # データベースから最新のユーザー情報を読み込み直す @user.reload # 変数で再代入した情報と合致しているかを確認 assert_equal name, @user.name assert_equal email, @user.email
updateアクション
最後にupdate
アクションを編集して(create
アクションとほぼ一緒)、テストが通ればOK。
def update @user = User.find(params[:id]) if @user.update(user_params) # 成功したことを知らせるフラッシュ flash[:success] = "Profile updated" redirect_to @user # 失敗した場合はまた入力画面へ戻る else render 'edit' end end
認可
ウェブアプリケーションの文脈上の認証(authentication)
とは、サイトのユーザーを識別することで、認可(authorization)
はそのユーザーが実行可能な操作を管理することらしい。
edit
、update
アクションが動作するようになったが、今のままではユーザー本人以外もURLにアクセスできて、勝手に更新などができてしまう状態になっている。
ユーザーに対してログインを要求して、かつ自分以外のユーザー情報を変更できないように制御する仕組みを実装する。
①ログインしていないユーザーが、ログインしていないと閲覧できないページにアクセスした際は、ログインページに転送し、「ログインしてください」などのメッセージを表示するようにする。
②ログイン中のユーザーが、許可されていないページ(例えば、自分以外のプロフィール編集画面)にアクセスしようとした場合は、ルートURLにリダイレクトさせる。
ユーザーにログインを要求
①を実現するために、before_action
メソッドを利用する。
これまでに何度も使ってきたので流石に覚えてきた。このメソッドで、何らかの処理が実行される前に、before_action
で指定した特定のメソッドを実行する。
devise
を利用してログイン機能を実装していると、before_action :authenticate_user!
を設定するだけでいいので簡単だった。
今回は、ログイン済みユーザーかどうかを確認するためのメソッドを設定するところから始める。
private def logged_in_user unless logged_in? flash[:denger] = "Please log in." redirect_to login_url end end
作成した後は、コントローラの直下にbefore_action
を設置する。デフォルトでは、beforeフィルターはコントローラ内の全てのアクションに適応されるため、:only
オプションを利用して適応したいアクションを絞る。
before_action :logged_in_user, only: [:edit, :update]
正しいユーザーの要求
次は②を実現して、ユーザーが自分の情報だけを編集できるようにする。
Railsチュートリアルでは、fixture
ファイルに二人目のユーザーを追加し、この二つのユーザー間で情報が編集できないようになっているかどうかのテストを行う。
# 二人目のユーザーを@other_userに格納 def setup @user = users(:hoge) @other_user = users(:fuga) end # editアクションのテスト内容 # 二人目のユーザーとしてログイン log_in_as(@other_user) # 一人目のユーザーの編集ページに移動 get edit_user_path # エラーを知らせるフラッシュが表示されているか確認 assert flash.empty? # ルートパスにリダイレクトされているか確認 assert_redirected_to root_url
先にテストを書いた後は、一番最後のルートパスにリダイレクトされる挙動を作るためにcorrect_user
というメソッドを作成してbeforeフィルターから呼び出すようにする。
before_action :correct_user, only: [:edit, :update] (略) # 正しいユーザーか確認する def correct_user @user = User.find(params[:id]) # 正しくなければリダイレクト redirect_to(root_url) unless @user == current_user end
correct_user
メソッドにより、edit
、update
の前に@user = User.find(params[:id])
を行うようになったため、edit
、update
での@user
への代入文は削除する。
先ほどのcurrect_user
のリファクタリングとして、一般的な慣習であるcurrent_user?
という論理値を返すメソッドを実装する。
app/helpers/sessions_helper.rb
def current_user?(user) user && user == current_user end
これにより、先ほどのメソッドの行を置き換えることができる。
unless @user == current_user
→ unless current_user?(@user)
フレンドリーフォワーディング
Railsチュートリアルでは「フレンドリーフォワーディング」という単語が出てくる。
フォワーディングとは何かを転送することなので、おそらくリダイレクトのことを指しているのだろう。
フレンドリーという形容詞がついていて、かつその節で実装する内容が、「リダイレクト先をユーザーが開こうとしていたページにしてあげる方が親切だ」ということなので、おそらくユーザーフレンドリーなリダイレクト...的な意味だろうと思う。
今の状態だと、保護されたページにアクセスしようとすると自分のプロフィールページに移動させられる。
ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。
これはとてもよく分かる。実際にこういった設計になっていると、いつも「助かるなあ」と思う。フレンドリーフォワーディングとはそういう意味か。
ユーザーが元々希望していたページに転送するためには、以下のようにする必要がある。
リクエスト時点のページをどこかに保存する
後ほどその場所にリダイレクト
この二つを、store_location
とredirect_back_or
というメソッドを作って実現する方法が掲載されている。
明日はここから再開したい。
今日のやらかし
html.erbファイル名のミス
ページの動作を確認しようと思い、リンクをクリックすると以下のようなエラーが出た。
No template for interactive request UsersController#edit is missing a template for request formats: text/html
No template for interactive requestの対処法 - Qiita
この方は意図的にhaml
という拡張子のファイルを作成されたようだが、自分の場合は単純にhtmll
というような誤字でこのエラーが起こっていた。
ファイル名を修正することで、エラーが起きなくなった。
同じエラー文に出くわしたときは、とりあえずファイル名がおかしくないかどうか確認するようにしたい。