こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#10.5 RSpecでTutorialのテストを書き直す
次回:#12 ActionMailer, アクティベーション編
こんなことが分かる
- プロフィール編集画面の例
- Bootstrap4のCardの使用例
- FactoryBotで複数ユーザを生成する方法
- before_actionによる強制リダイレクトする方法
- 直前のリンクを記憶しリダイレクトする方法
一緒に勉強していきまっしょ
今回の流れ
- プロフィール編集のフォームをつくる
- プロフィール編集失敗/成功時を考える
- ユーザだけがプロフィール編集できるようにする
- ログイン後は編集ページにリダイレクトさせる
本ポートフォリオはTutorial10章のユーザ一覧機能、フォームのパーシャルは作成しません。
代わりに編集ページに記録した時間をリセットするボタンを付与します。
(ポートフォリオ#1を参照)
プロフィール編集のフォームをつくる
Tutorial 10.1.1 編集フォームを参考にフォーム画面をつくる。
Tutorialと異なるのは、記録時間をリセットするボタンを設けること。
それに伴ってセクションが2つ欲しい。
そこで新たなBootstrap4のコンポーネント、Cardを使用する。
<% provide(:title, 'プロフィール編集') %>
<div class="container edit-container">
<div class="edit-title">
<%= image_tag 'profile.png', class: 'edit-img' %>
<h1 class="form-title form-title-edit">プロフィール編集</h1>
</div>
<%= form_with(model: @user, url: user_path(@user), local: true) do |form| %>
<div class="card-deck">
<div class="col-md-6">
<section class="card">
<h2 class="card-header form-title">アカウント情報変更</h2>
<div class="card-body">
<%= render 'shared/error_messages' %>
<div class="form-group">
<%= form.text_field :name, class: 'form-control', placeholder: "名前" %>
</div>
<div class="form-group">
<%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
</div>
<div class="form-group">
<%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
</div>
<div class="form-group">
<%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード(再入力)" %>
</div>
<div class="form-group">
<%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
</div>
</div>
</section>
</div>
<div class="col-md-6">
<section class="card">
<h2 class="card-header form-title">記録時間リセット</h2>
<div class="card-body">
<div class="form-group form-group-reset_time">
<%= form.check_box :reset_time, class: 'form-check-input' %>
<%= form.label :reset_time, '記録時間をリセットする', class: 'form-check-label' %>
</div>
<div class="form-group">
<%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
</div>
</div>
</section>
</div>
</div>
<% end %>
</div>
ビューができたのでここへのリンク先も修正しよう。
(変更はヘッダーのほんの一部なので大幅省略)
<!-- 中略 -->
<% if logged_in? %>
<li class="nav-item nav-item-extend"><%= link_to "記録画面へ", user_path(current_user), class: "btn btn-info btn-md btn-extend" %></li>
<li class="nav-item nav-item-extend"><%= link_to "プロフィール", edit_user_path(current_user), class: "btn btn-secondary btn-md btn-extend" %></li>
<li class="nav-item nav-item-extend"><%= link_to "ログアウト", logout_path, method: :delete, class: "btn btn-danger btn-md btn-extend btn-logout-extend" %></li>
<!-- 中略 -->
プロフィール編集失敗時を考えよう
編集失敗したらエラーを出して同じ画面に戻るようにする。
じゃあまずはテスト、その後コントローラを整えよう。
失敗時のテストを書く
テストを行う前に準備を済ませる。
これは後述する、ログインユーザのみが編集できるテストの準備も加味している。
具体的にはログインしたユーザを用意する。
そこで少し戻るけど、Tutorial 9.3.1 [Remember me] ボックスをテストするを参考に、テスト用ログインメソッドlog_in_asを用意する。
module ApplicationHelpers
def is_logged_in?
!session[:user_id].nil?
end
def log_in_as(user, remember_me = 0)
get login_path
post login_path, params: {
session: {
email: user.email,
password: user.password,
remember_me: remember_me
}
}
follow_redirect!
end
end
おっけ。じゃあテストといこう。
$ rails g rspec:request users_edit
require 'rails_helper'
RSpec.describe "UsersEdits", type: :request do
let(:user) { create(:user) }
def patch_invalid_information
patch user_path(user), params: {
user: {
name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar"
}
}
end
describe "GET /users/:id/edit" do
context "invalid" do
it "is invalid edit informaiton" do
log_in_as(user)
expect(is_logged_in?).to be_truthy
get edit_user_path(user)
expect(request.fullpath).to eq '/users/1/edit'
patch_invalid_information
expect(flash[:danger]).to be_truthy
expect(request.fullpath).to eq '/users/1'
end
end
end
end
失敗時のコントローラを整える
テストが書けたらコントローラの振る舞いを書こう。
成功時の処理は後にする。
class UsersController < ApplicationController
# 中略
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# 成功した時の処理(後述)
else
flash.now[:danger] = 'プロフィールの編集に失敗しました'
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
これで失敗時の動作は終了。
備考:エラーを捕えるテストの課題
少々課題が残った。
本当はUserモデルが持つエラーを捕えるためにこういうテストを行いたいが、
require 'rails_helper'
RSpec.describe "UsersEdits", type: :request do
# 中略
describe "GET /users/:id/edit" do
context "invalid" do
it "is invalid edit informaiton" do
log_in_as(user)
expect(is_logged_in?).to be_truthy
get edit_user_path(user)
expect(request.fullpath).to eq '/users/1/edit'
patch_invalid_information
expect(user.errors.any?).to be_truthy
expect(request.fullpath).to eq '/users/1'
end
end
end
end
Failures:
1) UsersEdits GET /users/:id/edit invalid is invalid edit informaiton
Failure/Error: expect(user.errors.any?).to be_truthy
expected: truthy value
got: false
失敗する。
他にexpect(user.errors.full_messages)や評価前のuser.save!なども試したが、そもそも値が入っていなかった。
今回はuserコントローラにflash[:danger]の値を与えた場合のフラッシュを捕えるテストに成功したので、一応ここでのテストは締めくくる(要勉強)。
プロフィール編集失敗時を考えよう
さっきと同じようにテスト、コントローラの順で書く。
Tutorial 10.1.4 TDDで編集を成功させるとは異なり、リダイレクト先は再びプロフィール編集画面にする。
加えてパスワードを入力しないと変更不可にするので、バリデーションはいじらない。
ただこのままだと利用者が、
「新しいパスワード入力できるの?今までのパスワード入力したらいいの?」
となってしまうので、この問題は別の回で紹介する。
とりあえずテストとコントローラをつくる。
成功時のテストを書く
# 中略
context "valid" do
it "is valid edit information" do
log_in_as(user)
get edit_user_path(user)
patch_valid_information
expect(flash[:success]).to be_truthy
follow_redirect!
expect(request.fullpath).to eq '/users/1/edit'
end
end
# 中略
成功時のコントローラを整える
# 中略
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = 'プロフィールの更新に成功しました'
redirect_to @user
else
flash.now[:danger] = 'プロフィールの編集に失敗しました'
redirect_to edit_user_path(@user)
end
end
# 中略
ちなみにflashとflash.nowの使い分けはこう。
- リダイレクトあり → flash
- リダイレクトなし → flash.now
参考にさせていただきました↓
[WIP/初学者]flashとflash.nowの使い分け
ログインユーザだけがプロフィール編集できるようにする
非ログイン時にログインを促す
Tutorial 10.2 認可からプロフィール編集前にログインを要求することができる。
Tutorialでは/user/:idのページに誰でもアクセスできるが、ポートフォリオではここもログインを要求したい。
テスト→コントローラの順で(以下略
ログインを促すテスト
テストを追加、編集する。
$ rails g rspec:request users
require 'rails_helper'
RSpec.describe "Users", type: :request do
let(:user) { create(:user) }
describe "GET /users/:id" do
it "does not go to users/1 because of having not log in" do
get user_path(user)
follow_redirect!
expect(request.fullpath).to eq '/login'
end
end
end
# 中略
describe "GET /users/:id/edit" do
context "invalid" do
it "is invalid because of having not log in" do
get edit_user_path(user)
follow_redirect!
expect(request.fullpath).to eq '/login'
end
# 中略
end
end
ログインを促すコントローラ
あとはTutorialを参考につくればよい。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:show, :edit, :update]
# 中略
private
# 中略
def logged_in_user
unless logged_in?
flash[:warning] = 'ログインしてください'
redirect_to login_url
end
end
end
別ユーザでのログインでは編集できないようにする
ログインしているからといって別ユーザでは意味がない。
ホーム画面にリダイレクトすることにしよう。
別ユーザをホーム画面にリダイレクトするテスト
テスト環境には複数のユーザが必要だ。
なのでFactoryBotで新たなユーザを生成する。
FactoryBot.define do
factory :user do
name { "Michael Example" }
email { "michael@example.com" }
password { "password" }
password_confirmation { "password" }
end
factory :other_user, class: User do
name { "Sterling Archer" }
email { "duchess@example.gov" }
password { "foobar" }
password_confirmation { "foobar" }
end
end
これに注目。
factory :other_user, class: User do
本来FactoryBotのデータはクラスを明示的に示す必要がある。
だから複数のユーザを作成する際はクラス名を明記する。
参考にさせていただきました↓
FactoryBotでテストデータ作成する方法
それではテストを書く。
require 'rails_helper'
RSpec.describe "UsersEdits", type: :request do
let(:user) { create(:user) }
let(:other_user) { create(:other_user) }
# 中略
describe "GET /users/:id/edit" do
context "invalid" do
it "is invalid because of having not log in" do
# 中略
end
it "is invalid because of having log in as wrong user" do
log_in_as(other_user)
get edit_user_path(user)
follow_redirect!
expect(request.fullpath).to eq '/'
end
# 中略
it "is invalid edit informaiton" do
# 中略
end
it "does not redirect update because of having log in as wrong user" do
log_in_as(other_user)
get edit_user_path(user)
patch_valid_information
follow_redirect!
expect(request.fullpath).to eq '/'
end
# 中略
別ユーザをホーム画面にリダイレクトするコントローラ
書くことはさっきとほぼ一緒。
ところでこういう記述をしようと思うけれど、
@user == current_user
この箇所はリファクタリングしておく。
module SessionsHelper
# 中略
def current_user?(user)
user == current_user
end
よってこれを加味すると以下のコントローラが出来上がる。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:show, :edit, :update]
before_action :correct_user, only: [:show, :edit, :update]
# 中略
private
# 中略
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
ログイン後は編集ページにリダイレクトしてあげる
編集画面でログインを勧められたのに、ログイン後に別の画面だとユーザが悲しい。
というわけで元いた画面にリダイレクトしよう。
編集ページにリダイレクトするテスト
# 中略
context "valid" do
# 中略
it "goes to previous link because they had logged in as right user" do
get edit_user_path(user)
follow_redirect!
expect(request.fullpath).to eq '/login'
log_in_as(user)
expect(request.fullpath).to eq '/users/1/edit'
end
end
# 中略
編集ページにリダイレクトする動作
では実際の動作を記述する。
手順はこう。
- 直前のリンクを記憶する
- ログイン後、記憶したリンクがあるならそこにリダイレクトする
そのためにsessionsヘルパーに2つのメソッドを用意しよう。
module SessionsHelper
# 中略
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
end
- store_location → 直前のリンクを記憶する
- redirect_back_or → 記憶したリンクか任意のリンクにリダイレクトする
これで準備はできた。
あとはコントローラを編集しよう。
class UsersController < ApplicationController
# 中略
def logged_in_user
unless logged_in?
store_location
flash[:warning] = 'ログインしてください'
redirect_to login_url
end
end
class SessionsController < ApplicationController
# 中略
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)
redirect_back_or user
else
flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
render 'new'
end
end
# 中略
よおし。
繰り返しになるが、本ポートフォリオはユーザ一覧画面が存在しない。
よってTutorial 10.3 すべてのユーザーを表示する以後のTutorial10章は使用しない。
したがって今回はここまで。
前回:#10.5 RSpecでTutorialのテストを書き直す
次回:#12 ActionMailer, アクティベーション編