プログラミング年少組のど素人が、アウトプット用に素人目線の解説を書いていきます。
自分が自分に教えていることを想像しながら進めていきます。
下記と同じような境遇の方であれば参考になるかも
- まっさらなプログラミング初学者目線
- オール独学
- 勉強は得意じゃない
- 社会人のため、1日に勉強できる量は限られる。
電子書籍のwebテキスト(第6版)を購入して使用
Progateの「Web開発パス」完走済
ドットインストールのプレミアム会員卒業
Railsチュートリアル解説動画を見ながら学習中
#10章 ユーザーの更新・表示・削除#
ユーザーモデルのルーティングについては「routes.rb」で以下のように設定しました
resources :users
これだけでRailsで基本となる7つのアクションへのルーティングが定義されます。というのは何度か見てきました。
7つのアクション:index,show,new,cerate,edit,update,destroy
この章では未実装分の「index(閲覧)」「edit(編集)」「update(更新)」「destroy(削除)」を作成して、7つのアクションを完成させていきます。
これらはユーザー本人のみが操作可能であることが前提である機能ですので、まずは8章や9章でログイン機能で誰が操作しているかについて特定させていたワケです。
最後に絶対的な権限を持つ管理ユーザーを追加し、勝手にユーザーを削除する設定を加えます。
#10.1 ユーザーを更新する#
作成する手順はこれまでに作った「new」や「create」に似通っています
「edit」は「new」に、「update」は「create」に似ています。「destroy」は・・・特になし・・・
大きな違いは「ログイン中に!」がつくことくらいです
とりあえずはおなじみのトピックブランチ作成から始めましょう
$ git checkout -b updating-users
##10.1.1 編集フォーム##
まずは編集フォームを作り見た目を整えていくところから入っていきます
ルーティングは「resources :users」で終わってるのでアクションとビューを組み立てていきます
editアクションを設定
※editアクションはpatchリクエストを送るためのフォームを生成する
def edit
@user = User.find(params[:id])
end
edit.html.erbファイルでビューを作成(これは手動で作る必要あり)
<% provide(:title, "Edit user") %>
<h1>Update your profile</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 "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
とまあこれで2つ出来上がりです。
「error_messages」が入ってますので、エラー表示もバッチリです。
下の方には「gravatar」の変更までついています。クリックしたらgravatarのサイトに飛ばされるようになってます・・・
新規登録の「new」では「post」リクエストを使っていました。
編集では「patch」リクエストを使います。
ただし!「patch」リクエストって送信することができません。。
今のところブラウザ側が「GET」リクエストと「POST」リクエストのみにしか対応していないからです・・・
よって「post」リクエストに「patch」リクエストをくっつけて送信機能を補っています
ディベロッパーツールでコードをHTMLに変換して見てみるとこんな感じて書いてあります
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" /> # これをくっつけている
最初に「new」は「edit」が似通ってると言いましたが、ビューに関してはほぼほぼ同じです。
「form_with」の中身なんかまるっと同じです。。。
問題はRailsはどちらから送られてきた情報かをどこで区別しているのか・・・
「new_recored?」メソッドを使っているみたいです。。
こんなの書いた覚えはありませんが、「new_recored?」メソッドが「true」の時は「post」メソッド。「false」の時は「patch」メソッドが動くようになっているそうです。
それでもまだよくわかってません。
とりあえずmodel: @user
の部分に注目してください
newアクションの時は@user
は「空」のインスタンスでした。
それに対してeditアクションの時の@user
は「登録済みユーザー」のインスタンスです。
「new」アクション経由なら空のユーザーなので「post」。「edit」アクション経由なら既存のユーザーなので「patch」。
仕上げはヘッダーのリンクから「編集」ページに飛べるようにしたら完了です
:
:
<%= link_to "Settings", edit_user_path(current_user) %>
さっそくここのページを開いて、「Save changes」ボタンをクリックしてみると・・・
動くはずがありません。肝心の編集機能を動かす「update」アクションには何も手つかずだから・・・
##10.1.2 編集の失敗##
Usersコントローラーの「update」メソッドを編集して少し動きを付け加えます
1.該当のユーザーを取得する
2.if〜elseで成功と失敗を切り分けする
3.以上
def update
@user = User.find(params[:id])
if @user.update(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
##10.1.3 編集失敗時のテスト##
エラーを検知するための統合テストを書いていく
$ rails generate integration_test users_edit
中身がこれ
# テスト用のユーザー情報を決める
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
# editページにgetリクエストを送りますー
get edit_user_path(@user)
# editページが表示されるか確認しますー
assert_template 'users/edit'
# patchリクエストでテスト用のユーザー情報を失敗するやつに「更新」しますー
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
# 再度editページが表示されるか確認しますー
assert_template 'users/edit'
end
上から順に見ていきます
patch user_path(@user), params〜
patchリクエストを「user_path(@user)」
にparams以下の内容で送っています。
「user_path(@user)」
ってそもそもshowアクションの時に使ってた名前付きルートだったような。。。
で、名前付きルート一覧をもう一度見てみると(テキスト7.1.2)・・・「show」アクションも「patch」アクションも「user_path(user)」になっているじゃありませんか。ついでに「delete」アクションまでも・・・
パスは同じでも、リクエスト自体違うし送ってる内容も違います
##10.1.4 TDDで編集を成功させる##
今度は編集に成功するパターンのテストを見ていきましょう
この流れはテスト駆動開発です
失敗するテスト書いて→成功するようにコード書いて→テストが成功することを確認する。
という流れで行う作業のことです。
とりあえず出来上がりを見ながら内容を掴んでいきますか
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "Foo Bar"
email = "foo@bar.com"
# user_path(@user)にparams以下の内容でpatchリクエストを送ります
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
# flash表示されますよね?
assert_not flash.empty?
# ユーザープロフィールページに飛びますよね?
assert_redirected_to @user
# プロフィールページを再度読み込みします
@user.reload
# nameは@user.nameと同じ値になってますよね?
assert_equal name, @user.name
# emailは@user.nameと同じ値になってますよね?
assert_equal email, @user.email
end
まさに見たまんまです。特に言うことなし!です。
あえて言うならここで、引数に入れてる部分がこれまでになかったパターンでしょうか
name = "Foo Bar"
email = "foo@bar.com"
そしてここの部分がupdateアクションで未実装なのでテストで引っかかるでしょうね。
assert_not flash.empty?
assert_redirected_to @user
なので実装しましょう
def update
@user = User.find(params[:id])
if @user.update(user_params)
flash[:success] = "Profile updated" # フラッシュ表示させる機能追加
redirect_to @user # 成功後はプロフィールページに飛ばす機能追加
else
render 'edit'
end
end
ここでpatchリクエストで送っている情報をもう一度見てみると
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
「password」と「password_confirmation」に値が入ってありません
これは
validates :password, presence: true
にもれなく引っかかるやつ!
案の定テストは通らないのでここに**「allow_nil: true」(空白でもいいよ〜)**オプションを追加します
validates :password, presence: true validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
いやーでも、「presence: true」と「allow_nil: true」って相反する意味もあるし、成立するのか心配ですが、大丈夫!Railがいい感じに処理してくれます
具体的には、「has_secure_password」が、登録時においては「presence: true」を優先的に動かす!みたいなことをしてくれるので、登録時にパスワードが空白で通るようなことはありません。
加えて、これを入れることによって解決するものがもう一つあります
登録の際にpasswordを空白にした状態で登録しようとしたら、「Password can't be blank」のエラーメッセージが2つ出てきてしまってました。
これは「has_secure_password」がかけているバリデーションと「validates :password, presence: true」でかけているバリデーションが2つかかってしまっていたためということみたいです。
なので、その中の1つが「allow_nil: true」で置き換わるため、今後は「Password can't be blank」のメッセージは1つだけ出てくるように修正されます。
#10.2 認可#
実は現段階でセキュリティもへったくれもない状況になってます。
ページの見た目上ではログインしてないと進めないページなどをナビゲーションバーなどで切り分けしていますが、実はURLを直接入力することでログインしてないと見れないページに簡単にアクセスできるようになってます。
試しにログインしてない状態で、HOME画面上のURLのお尻に「users/1/edit」とか「users/2/edit」とか入れると個別のユーザーの編集ページに飛ぶことができます。
要はログインしてないユーザーが誰かがログインした保護されているはずのページにアクセスできるというとんでもない状況です。
この節ではそこんとこ解決していきます
##10.2.1 ユーザーにログインを要求する##
ログインしてないユーザーがURLから保護されたページに飛ぼうとしたらログインページに飛ばすようにします。
そのためにbeforeフィルターのbefore_actionを使います。
読んで字のごとく、「アクションする前に」ってことです。
「before_action :ログインしてなければログインページに飛ばす」
こうすることで、Userコントローラーで何らかのアクションを動かそうとする場合、アクションする前に「ログインしてなければログインページに飛ばす」という設定を入れることができます。
ただし、「new」や「create」アクションにもこの設定がかかってしまいます。今回は「edit」と「update」にその設定を入れたいので
「before_action :ログインしてなければログインページに飛ばす, only: [:edit, :update]」
とすれば、「edit」と「update」のみにこの設定が適用されるようになります。
「ログインしてなければログインページに飛ばす」についてはprivateの中でメソッドとして定義しています
def logged_in_user
# ログイン状態でない限りは
unless logged_in?
# 「ログインしてね」のフラッシュを表示するよ
flash[:danger] = "Please log in"
# ログインページに飛ばすよ
redirect_to login_url
end
end
「unless」なんて初めて出てきましたが「〜しない限り」なんて意味だったので、「unless logged_in?」で「ログイン状態ではない限りは」という解釈で問題ないでしょう
「before_action」なんて設定を入れたもんだから、今度はテストが通らなくなります。
まさに、あっちを立てればこっちが立たず状態です。
ですので、「edit」と「update」のテストを固有のログイン状態に入れてから、走らせるように変更しましょう。
テストの最初の手順に「log_in_as(@user)」
を加えるだけでOKです。
これでテストはOKになりましたが、今度は**「before_action」を消してみますと
それはそれでテストが通ってしまいます**。
これって気づかなかったらスルーしてしまわないかい??
これでは「before_action」がちゃんと機能しているか確認する術がありません。。
ですので、今度は「before_action」がちゃんと機能しているか確認するテストを書いていきます。
ちなみに「before_action」は「only: [:edit, :update]」とすることで、「edit」と「update」の2つのアクションで機能させるように設定しました。
ので!userのコントローラーテストでテストを書いていけばよさそうです
def setup
@user = users(:michael)
end
test "should redirect edit when not logged in" do
# 直接@userさんのURLでアクセスする
get edit_user_path(@user)
# そんなことしたらフラッシュ出てるよね
assert_not flash.empty?
# ログインページに飛ばしてるよね
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
# 直接@userさんの情報をpatchリクエストで更新させようとする
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
# そんなことしたらフラッシュ出てるよね
assert_not flash.empty?
# ログインページに飛ばしてるよね
assert_redirected_to login_url
end
まさにこれが、「before_action」が機能していないと通らないテストです。
これでやっとイタチごっこの終了です。お疲れ様でした。
##10.2.2 正しいユーザーを要求する##
ログインしていないと「edit」や「update」が動かせないようにできました
が、まだまだ足りません
なぜなら
ログインさえしていれば、さっきと同じように別の人のURLを直接入れて編集することが可能だからです。
ですので、今度は**「edit」や「update」がログインしている本人しか編集できないようにする**という機能を付け加えます。
さて、
先ほどと同様失敗するテストから書き始めるテスト駆動開発パターンで進めていきます
何はともあれ「before_action」なんかのセキュリティに関わるような機能の実装は必ずテストから書くようにしましょう!
機能の実装を先にしてテスト書くと、実装した機能が通るようなテストを書いてしまいがちです。これではテストする意味がありません。
特にセキュリティに関する見落としは、市場流出してしまうとシャレんなりません。
損害賠償になってしまう可能性もあり、そうなると何もかもオワタになります。
これから作成するテストでは別の人でログインしていたら・・・という条件を使用します
今まではテストで使っていたテスト用のユーザーは「まいける」さんただ一人でした(fixtureに入ってる)
ですので今回は「あーちゃー」さんを新たに加えます
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
二人になったんで「あーちゃー」さんがログインしてる時「まいける」さんの情報が扱えないという内容でテストを書いていきましょう
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect edit when logged in as wrong user" do
# あーちゃーさんでログイン
log_in_as(@other_user)
# まいけるさんの編集ページのURLを開こうとする
get edit_user_path(@user)
# フラッシュは出ないよね?
assert flash.empty?
# HOME画面に飛んでるよね?
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
assert flash.empty?
assert_redirected_to root_url
end
patchリクエストのやつはほぼ同じなので省略。めんどいから。
さあ失敗するテストができたので、成功させるためにusersコントローラーを編集していきましょう
別のユーザーにいたらんことしようとしたら、ノーメッセージでHOME画面に飛ばす設定が必要そうです
その機能を備えた**「correct_user」**を定義しますか。
※「current_user」とパッと見同じなので間違えないようにしないとですね
private
def correct_user
# @userが誰なのかを決める
@user = User.find(params[:id])
# インスタンスユーザーと今ログインしているユーザーが同じ「でない限り」ホーム画面に飛ばせ
redirect_to(root_url) unless @user == current_user
end
「unless」急に出だしたな・・・
そこのアクションを操作しようとしているヤツ = ログインしているヤツ
の図式が成り立たなかったら即退場!が出来上がりました
前回同様「before_action」で「only: [:edit, :update]」とし、「edit」と「update」の2つのアクションで機能させるように設定します
こんな感じで
before_action :correct_user, only: [:edit, :update]
これで機能の実装は完了したはずなので「rails test」すれば成功するはずです。
機能の実装は完了しましたが、ここでリファクタリングを行います
@user == current_user
このコードについて、現在ログインしているユーザーか?を確認するための「current_user?」メソッドを定義します
# 渡されたユーザーがカレントユーザーであればtrueを返す
def current_user?(user)
user && user == current_user
end
こうすることで若干省略できました。
むしろパッと見でわかりにくいので私は前者の方がいいですけどね。以上
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
##10.2.3 フレンドリーフォワーディング##
まったわけのわからない単語が登場しました
テキストの説明でよく分からなかったので整理して考えてみました
【現在の設定】
未ログインの状態で「まいける」さんの「編集ページ」のURL(users/:id/edit)を開く
↓
ログインページに飛ばされるので「まいける」さんとしてログインする
↓
プロフィールページが表示される
【変更後の設定】
未ログインの状態で「まいける」さんの編集ページのURL(users/:id/edit)を開く
↓
ログインページに飛ばされるので「まいける」さんとしてログインする
↓
「まいける」さんの「編集ページ」が表示される
もともと編集ページを開こうとしていたので、ログイン後は編集ページにそのまま飛ばすという流れです
ログインしていない状態で、「ブックマーク」や「お気に入り」なんかから「ログイン後に表示されるはずの」保護されたページに飛ぶケースなんかはよくあるはずです。
そんな時ってとりあえずログインページに飛ばされますよね。
ログインしたならば、大体のケースで最初見ようとしてたページが表示されるはずです。まさにこれです。
まずはテストから組み立てます
test "successful edit with friendly forwarding" do
# URLから直接まいけるさんの編集ページに入る
get edit_user_path(@user)
# まいけるさんとしてログインする
log_in_as(@user)
# 編集ページに飛ばされてますよね?
assert_redirected_to edit_user_url(@user)
# こんな感じで編集します
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
# フラッシュが表示されてますよね?
assert_not flash.empty?
# プロフィールページに飛ばされてますよね?
assert_redirected_to @user
# 再読み込みさせてみましょう
@user.reload
# 名前とemailが変更されてますよね?
assert_equal name, @user.name
assert_equal email, @user.email
end
まさにURLから編集ページに入って〜
の流れです。
このテストが通るように機能を搭載していきましょう
まずは2つの便利メソッドの定義からです。
リクエスト時点のページをどこかに保存する「store_location」メソッドと、その場所にリダイレクトさせる「redirect_back_or(default)」メソッドをSessionsヘルパーで定義します
# 記憶したURL(もしくはデフォルト値)にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
順番が逆のような気もしますがこんな感じで紹介されてます
見慣れないものが入ってますが、冷静に読み解いていけば何となくわかってきます
「store_location」メソッドで、アクセスしようとしていたURLを記憶するメソッドを定義しています
session[:forwarding_url]
sessionの[:forwarding_url]キーは初登場ですが、見た感じ「直前にアクセスしたURL」を保存するキー。ここに当初見ようとしてたページのURLを保存します
request.original_url if request.get?
「GETリクエストを要求していた場合に、元のURLを要求します」という感じでしょうか
「元のURL」が何かというと、**「ログインしない状態でアクセスしようとしてた保護されたページのURL」**となります。
それがsession[:forwarding_url]に入ります。
rederect_back_or(default)メソッドで、先ほどアクセスしようとしていたページか、それがなければデフォルトページにリダイレクトさせます(飛ばします)
redirect_to(session[:forwarding_url] || default)
そして、目的のページに飛ばしたあとはURLを消してます
「session.delete(:forwarding_url)」
消さないと毎回そこに飛んでしまいます
最後に、これらのメソッドを該当する箇所にはめていきます
store_locationメソッド(usersコントローラーのlogged_in_userメソッド内)
# ログイン済みユーザーかどうか確認
def logged_in_user
# ログイン状態じゃない限り
unless logged_in?
# 一応セッションにURLだけ保存しといてやる
store_location
# 先にログインしろよとメッセージを送る
flash[:danger] = "Please log in."
# ログインページに飛ばす
redirect_to login_url
end
end
redirect_back_or(default)メソッド(sessionsコントローラーのcreateアクション内)
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
# forwardint_urlがあればそちらに。なければプロフィールページに飛ぶ
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
当然ログイン時のcreateアクションで使われますが
「redirect_back_or user」が、定義したものと形が少し変わっているので補足しときましょう
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
こいつは、
該当するsession[:forwarding_url]があれば、そっちにリダイレクトする
なければ
defaultにリダイレクトする
ということを表しています。
ですので、defaultにユーザーのshowページを入れると
該当するsession[:forwarding_url]があれば、そっちにリダイレクトする
なければ
ユーザーのshowページにリダイレクトする
ということになります
今回はそれが「user」(ユーザーのプロフィールページ)になっているだけの話です
#10.3 すべてのユーザーを表示する#
ここからはユーザー一覧ページの搭載についてみていきます。
アクションでは「indexアクション」を使用します。
新しい機能としてページネーションが紹介されています。
##10.3.1 ユーザーの一覧ページ##
何はともあれ「index」アクションを整えていきますか
未ログインでユーザー一覧を見ようとしたら、「ログインページ」に飛ぶようになってるかのテストから作成する
test "should redirct index when not logged in" do
# 「show」アクションの「user_path」。似ているので注意
get users_path
# ログインページに飛んでますか
assert_redirected_to login_url
end
Usersコントローラーに「index」アクションを追加する
データベースから全てのユーザーを引っ張ってきて、@userに入れる
def index
@user = User.all
end
ユーザー一覧は「ログインしてるユーザー」しか見れないはずなので「before_action」に追加する
before_action :logged_in_user, only: [:index, :edit, :update]
アクションのお次はビューですね
「index.html.erb」ファイルを作成して、見た目を整えましょう
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
eachで一個ずつ取り出しては表示していく。といった感じですかね。
<%= gravatar_for user, size: 50 %>
「size: 50」で、サイズを指定しています
これを使うはusersヘルパーに定義した「gravatar_for」メソッドにデフォルト以外のサイズを指定する機能を付け加えてあげなくてはなりません。
# ここと
def gravatar_for(user, options = { size: 80 })
# ここと
size = options[:size]
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
ビューを整えたので今度はCSSです
とりあえず、テキストコピペしときます
最後に、ヘッダーで「'#'」となったままほったらかしになっていたユーザー一覧ページへのリンクを修正します
<li><%= link_to "Users", users_path %></li>
##10.3.2 サンプルのユーザー##
ここではダミーのユーザーを作っていきます。サイトが盛り上がってるように見せかける自作自演の「サクラ」みたいなもんです
しかも一個一個作るのではなくて、Rubyの機能を使って一気にダーっと作ります
まずはGemfileに「Faker gem」を追加します。。。
gem 'faker', '2.1.2'
Gemfileを追加したらこれですね
$ bundle install
と、ここまでは見慣れた感じですが、「データベース上にサンプルユーザーを生成する」という特殊なことをしますので、これまで習ったものからは進めません
Railsでは「db/seeds.rb」というファイルを使ってサンプルユーザー一覧を指定します。
ここにある 👉 dbの中
# メインのサンプルユーザーを1人作成する
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
# 追加のユーザーをまとめて生成する
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
サンプルユーザーを一人作成して、その後増殖する感じで組み立てています。今回は99人分timeで繰り返して作成しているみたいですね。
このコードは応用的で、理解できなくても問題ないみたいですので、ここでは眺めとくくらいにしときましょう
User.create!
「create」の後にある「 ! 」だけは少しみていきましょう。1人作って、あとはそれを99人分繰り返す作業をします。
仮に最初の一人目で失敗すると、「nil」になります。これを99回繰り返すと、「nil」を99回繰り返すことになります。それはよくありませんよね。
なので「 ! 」がついてます。これがつくことによって、失敗すると失敗として処理を止めるためその後99回繰り返したりしません。
無駄な処理を少しても省くための「 ! 」だったんですね〜
こちらを適用するに当たっては、いったん現在登録しているデータを削除しましょう
$ rails db:migrate:reset
で、seedsファイルに入力したものを適用します
$ rails db:seed
これで「ダミーユーザーが99人」ができました
##10.3.3 ページネーション##
100人のユーザーを一つのページに表示させると、ページがやったらなっがーーーーーくなります
ので、分けて表示するように設定します
これが「ページネーション」です
よく使われる機能ですので、gemが用意されています
gem 'will_paginate', '3.1.8'
gem 'bootstrap-will_paginate', '1.0.0'
gemを追加したら当然やることはこれ
$ bundle install
機能を入れたのでさっそくビュー取り込みます
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
入ったのはこれです<%= will_paginate %>
表示されるのはよく見かけるこいつです。
「前へ」「何ページ目か」「次へ」
何で2つあるんだーと思いましたが、1つでもいいけども2つあった方が便利だよねーてことで2つ入れてます。
ただ、これを入れるだけでは動きません。エラーです。
「index」アクションの中身を思い出すと「@users = User.all
」でデータベースから全てのユーザー引っ張ってくるよう設定していました。
が、<%= will_paginate %>はこれに反応しません。
ですので、今度は「index」アクションを修正しなくてはいけません。。。。
def index
@users = User.paginate(page: params[:page])
end
新手のメソッド**「paginate」**がでてきました・・・
これはgemを取り入れた時に同時に追加されているので、わざわざ定義する必要はありません。
機能としては、こいつは1つのページに表示する人数(デフォルトでは30人になっている・・・)をデータベースからボコっととってきます
とりあえず「id」順に、pageが「1」なら「idが1〜30」までの人をボコっと引っ張る。
pageが「2」なら「idが31〜60」までの人をボコっと引っ張る。
pageが「3」なら「idが61〜90」までの人をボコっと引っ張る。
といった具合です。
ただし、pageが「nil」だった場合は最初のページを表示させます。pageが「1」のときと同じページです。
とりあえずこれで「index」ビューが無事に表示されるようになります。
##10.3.4 ユーザー一覧のテスト##
ページネーションを追加したのでテストを書いていきます
統合テストの手順はこちら
1.「まいける」さんでログイン
2.ユーザー一覧ページにアクセス
3.ページネーションがあるか確認
4.ユーザーは表示されているか、リンクになっているかを確認
みたいな感じですかね。
先程本番環境・開発環境では「db:seed」で100人作りました。
テスト用に使う手順.4のユーザーはまた別です!
テスト用のユーザーは「fixture」に作ってましたね。今回も**「times」**を使って増殖します
# 1ページ30人なのでとりあえず30人
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
統合テストを作る
$ rails generate integration_test users_index
中身がこれ
def setup
@user = users(:michael)
end
test "index including pagination" do
# 「まいける」さんでログイン
log_in_as(@user)
# ユーザー一覧ページにGETリクエスト
get users_path
# ユーザー一覧ページが表示されますよね
assert_template 'users/index'
# ページネーションのリンクがありますよね
assert_select 'div.pagination'
# page1の30人を抽出
User.paginate(page: 1).each do |user|
# 名前のところがリンクになってるリンクがありますよね
assert_select 'a[href=?]',user_path(user), text: user.name
end
end
「div.pagination」
HTMLで見ると、paginationはdivタグのpaginationクラスとして入っている。
##10.3.5 パーシャルのリファクタリング##
ここで、コードを整理してスッキリさせるみたいです。別にしなくてもいいけど。
リファクタリングとは「動作を変えずにコードを整理すること」らしいです。
まずは
タグ<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
こいつをパーシャルに移動させます
呼び出すときは「render」を使います
「user」とするだけで呼び出せるようなパーシャルを作らないといけません
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
呼び出すときは
<%= render user %>
これでuserの中にあるパーシャルを引っ張ってくるようになるみたいっす
・・・逆にめんどくねーか
改良はまだまだ続きます
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
この「user」を「@users
」にします。@users
はそもそもpageの1ページをまるっと引っ張ってくるものですので、「<% @users.each do |user| %>」自体いらんくね?と、なります
<%= render @users %>
これだけになります。だいぶスッキリしましたね。
renderにインスタンス変数を引数にとっているのはパッと見で読み取れるか微妙ですけども。。
知っている人ぞ知ってい話ですよね。
@users
でパーシャルも引っ張ってきてるし、これは覚えるしかないみたいです。
#10.4 ユーザーを削除する#
ユーザーを削除するのは誰の仕事でしょうか
それは**「管理している人」**です
ユーザー誰もが他のユーザーを削除できるサイトなんて崩壊しています。
よって削除の機能を入れる前にその「管理者(adminユーザー)」を指定する項目をデータベースに加えます。
##10.4.1 管理ユーザー##
さっそくadminカラムをデータベースに加えましょう
$ rails generate migration add_admin_to_users admin:boolean
おなじみの 「add_カラム名_to_users 属性:データ型」です!
管理者かそうでないかが分かればいいので、データ型はYesかNoのbooleanです
とりあえずデフォルト値は「false」に設定します。
誰もが登録した瞬間adminになってたらおおごとです。とは言っても、登録時にadminをどうするか指定しなかったらnil(false)になるみたいなのですが、ちゃんと分かるようにすることがメンバーへの優しさです
add_colmn :users, :admin, :boolean, default: false
そしてこれを忘れずに
$ rails db:migrate
さっそく現在の環境で最初のユーザーをadminを指定してみましょう
# メインのサンプルユーザーを1人作成する
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
「seeds」を更新したのでさっき実行したマイグレーションからなんからやり直して、更新後のseedを取り込みましょう
$ rails db:migrate:reset
$ rails db:seed
さてさてさてさて
この「admin」についてですが、patchリクエストを直接送ることで変更可能だったらヤバいよね
という話を7章でやっていました。
その時に搭載したのが、Strong Parametersです
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
要求(require)に対する許可(permit)は「name,email,password,password_confirmation」のみで、もちろんadminはこの許可されたものに入っておりませんので直接patchリクエストを送ったところで変更は受け付けられません
##10.4.2 destroyアクション##
ユーザー「削除」が使える管理ユーザーを作成しましたので、destroyアクションを使えるようにしていきましょう
今回は珍しくビューから整えていきます
ユーザー一覧ページの各ユーザー名の隣に「delete」リンクを作成する
個別のユーザー表示については「パーシャル」で!
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</li>
これ
<% if current_user.admin? && !current_user?(user) %>
どういう意味でしょうか。全く理解できません。。
左は分かります**「現在ログインしているユーザーは管理者ユーザー?」ですよね。
問題は右側です
目的としては「管理者がうっかり管理者自身をdeleteしないようにする」ために書かれたコードです。
そのために、表示されているユーザー名が管理者自身の部分にはdeleteリンクを付けないようにしたいワケです
そしてcurrent_userの前の「 ! 」が混乱に拍車をかけてますね
「current_user?(user)」は「ログインしているのが本人?」
「 ! 」がつくことで、逆説になりますので
「!current_user?(user)」は「ログインしているのが本人以外?」**となりますよね
よってまとめますと
「現在ログインしているユーザーが管理者ユーザーであり、かつログインしている本人以外であった場合」「 | 」以降に「deleteリンク」を表示する・・・と言いたいのでしょう・・
試しに管理者ユーザーでログインし、「&& !current_user?(user)」を消してユーザー一覧画面を確認すると、確かに自分の名前のところに「delete」が表示されましたし、消さなかったら「delete」は表示されませんでした。
リンクの中身はこうなってます
<%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
解体してみていきますと
「"delete"」
→見た目上「delete」と表示される
「user」
→・・・え?なにこれ?
これは**「user_path(user)」を省略したもの**らしいです・・しかもこれ、デフォルトではGETリクエストを「users/:id」に送るということも指しています
「method: :delete」
→これを入れることで、「user_path(user)」がデフォルトではGETリクエストに送信するとなっているところをDELETEリクエストに変更します。よって「destroy」アクションが動きます。
「data: { confirm: "You sure?" }」
→一度やってみたら分かりますが、確認のダイアログみたいなのが表示されます。
さてさて、「ビュー」はこれくらいで完成ですので「destroy」アクションを追加しましょう
def destroy
# データベースから指定のidを消す
# DELETEアクションが/users/:idにきた時にdestroyアクションが動きます
User.find(params[:id]).destroy
# 「消したよー」のフラッシュを表示する
flash[:success] = "User deleted"
# ユーザー一覧に飛ばす
redirect_to users_url
end
destroyアクションにある**「destroy」メソッド**は文字通り破壊・・じゃなくて削除する機能を持ちます
もちろんdestroyアクションはログイン中にのみ開けるようにするべきですので「before_action :logged_in_user」に入れときましょう
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
これでDELETEは終わりではありません。。
編集の時と同様にDELETEリクエストを直接送られた場合の対処ができておりません。
こちらも「before_action」を使って「管理者ユーザー」のみが「destroy」アクションを使えるように設定しときましょう
メソッドの定義を先にやっといてbefore_actionを入れていきます
メソッドの定義
# 管理者かどうかを確認し、そうでなければHOME画面に飛ばす
def admin_user
redirect_to(root_url) unless current_user.admin?
end
機能の搭載
before_action :admin_user, only: :destroy
##10.4.3 ユーザー削除のテスト##
まずは「削除」の機能が管理者ユーザーではない場合では動かないことを確かめるために単体テスト(usersコントローラー)で見ていきます
まあ、beforeフィルターがちゃんと動作しているかの確認ですのでコントローラーテストでテストするのは当然っちゃあ当然です。
先程同様にテストで使うユーザーのデータはseedsではなくfixtureなので、今回は「まいける」さんをadbminユーザーにして進めていきます
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
準備は完了しましたので、テストを書いていきましょうか
まずは、管理者ユーザーを切り分け
def setup
# こいつが管理者ユーザー
@user = users(:michael)
@other_user = users(:archer)
end
テスト
# ログインしないでdeleteするパターン
# before_actionのlogged_in_userが動くかどうか確認するテスト
test "should redirect destroy when not logged in" do
# ブロックの中実行してもデータベースのユーザー数変わらないよね
assert_no_difference 'User.count' do
delete user_path(@user)
end
assert_redirected_to login_url
end
# before_actionのadmin_userが動くかどうか確認するテスト
test "should redirect destroy when logged in as a non-admin" do
# 「管理者ではない」ユーザーでログイン
log_in_as(@other_user)
# ブロックの中実行してもデータベースのユーザー数変わらないよね
assert_no_difference 'User.count' do
delete user_path(@user)
end
assert_redirected_to root_url
end
管理者ユーザーじゃなければdeleteが動作しないことを確認できたので
今度は削除が無事できていることを統合テストで確認します。
ページネーションで作ったテストを加工します
def setup
# まいけるさんを管理者ユーザー
@admin = users(:michael)
# あーちゃーさんは管理者ではないユーザー
@non_admin = users(:archer)
end
test "index as admin including pagination and delete links" do
# 「まいける」さんでログイン
log_in_as(@admin)
# ユーザー一覧ページにGETリクエスト
get users_path
# ユーザー一覧ページが表示されますよね
assert_template 'users/index'
# ページネーションのリンクがありますよね
assert_select 'div.pagination' # ここまでがページネーションと同じ
# page1の30人を抽出するのを変数「first_page_of_users」に入れる
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
# 管理者ではないユーザーに関しては・・・
unless user == @admin
# 「delete」リンクが表示されますよね?
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
end
# ブロック内を実行するとユーザー数が「-1」だけ変わりますよね?
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
end
test "index as non-admin" do
# 非管理者ユーザーでログインする
log_in_as(@non_admin)
# ユーザー一覧にGETリクエスト送る
get users_path
# 「delete」は「0」個表示されてますよね
assert_select 'a[href=?]', text: 'delete', count: 0
end
特につまづいたのがここ
# 管理者ではないユーザー部分に関しては・・
unless user == @admin
# 「delete」リンクが表示されますよね?
assert_select 'a[href=?]', user_path(user), text: 'delete'
これが表示上の話であることに行き着くのにだいぶ時間がかかりました。
管理者ユーザーでないユーザー部分については「delete」が表示されますよね?ということです
#10.5 最後に#
やっとこさこの章が完了です。
masterにmergeして終わらせましょう
$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updatin-users
$ git push
この章ではサンプルデータを大量に作成しましたのでそいつらもまとめてherokuに反映させましょう
$ git push heroku
$ heroku pg:reset
$ heroku run rails db:migrate
$ heroku run rails db:seed
実際の現場の運用ではdb:seedのダミーは反映しません。ここはあくまで、機能が正常に実装されたかどうかを確認するためにdb:seedを使ってます。
#10章の要約をようやく作り終えての感想#
ユーザーの編集、更新、削除とてんこ盛りの章でしたが、これまでの繰り返しで搭載できるものも多く楽できるかと思いきや、フレンドリーフォワーディングやページネーション、DELETEリクエスト内での動作については簡単に理解するのが難しい概念で、だいぶ泣かされました。