プログラミング備忘録

プログラミングの学習状況をメモしています

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 Recordnew_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ヘルパーメソッドを活用。

フォーム用のパーシャル作成

neweditform_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>

forest.watch.impress.co.jp

最近はブラウザ側が対応してくれるそうで、つけ忘れても大丈夫とあるが、念のために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で使用して、以下のテストを行った。

  1. 編集ページにアクセス
  2. editビューが描画されているかどうかassert_templateで確認
  3. patchメソッドを使い、無効な情報を送信してみる
  4. 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チュートリアルが提案している。

何らかの機能を実装する前のテストのことを受け入れテストと呼ぶらしい。

編集失敗時のテストを作ったときのように、今回は成功したときの流れを考える。

nameemailの変数を作成し、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)はそのユーザーが実行可能な操作を管理することらしい。

editupdateアクションが動作するようになったが、今のままではユーザー本人以外も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メソッドにより、editupdateの前に@user = User.find(params[:id])を行うようになったため、editupdateでの@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チュートリアルでは「フレンドリーフォワーディング」という単語が出てくる。

フォワーディングとは何かを転送することなので、おそらくリダイレクトのことを指しているのだろう。

フレンドリーという形容詞がついていて、かつその節で実装する内容が、「リダイレクト先をユーザーが開こうとしていたページにしてあげる方が親切だ」ということなので、おそらくユーザーフレンドリーなリダイレクト...的な意味だろうと思う。

今の状態だと、保護されたページにアクセスしようとすると自分のプロフィールページに移動させられる。

ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。

これはとてもよく分かる。実際にこういった設計になっていると、いつも「助かるなあ」と思う。フレンドリーフォワーディングとはそういう意味か。

ユーザーが元々希望していたページに転送するためには、以下のようにする必要がある。

  1. リクエスト時点のページをどこかに保存する

  2. 後ほどその場所にリダイレクト

この二つを、store_locationredirect_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というような誤字でこのエラーが起こっていた。

ファイル名を修正することで、エラーが起きなくなった。

同じエラー文に出くわしたときは、とりあえずファイル名がおかしくないかどうか確認するようにしたい。