Edited at

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #11 プロフィール編集編


こんな人におすすめ


  • プログラミング初心者でポートフォリオの作り方が分からない

  • Rails Tutorialをやってみたが理解することが難しい

前回:#10.5 RSpecでTutorialのテストを書き直す

次回:#12 ActionMailer, アクティベーション編


こんなことが分かる


  • プロフィール編集画面の例

  • Bootstrap4のCardの使用例

  • FactoryBotで複数ユーザを生成する方法

  • before_actionによる強制リダイレクトする方法

  • 直前のリンクを記憶しリダイレクトする方法

一緒に勉強していきまっしょ:bow:


今回の流れ


  1. プロフィール編集のフォームをつくる

  2. プロフィール編集失敗/成功時を考える

  3. ユーザだけがプロフィール編集できるようにする

  4. ログイン後は編集ページにリダイレクトさせる

本ポートフォリオはTutorial10章のユーザ一覧機能、フォームのパーシャルは作成しません。

代わりに編集ページに記録した時間をリセットするボタンを付与します。

ポートフォリオ#1を参照)


プロフィール編集のフォームをつくる

Tutorial 10.1.1 編集フォームを参考にフォーム画面をつくる。

Tutorialと異なるのは、記録時間をリセットするボタンを設けること。

それに伴ってセクションが2つ欲しい。

そこで新たなBootstrap4のコンポーネント、Cardを使用する。


app/views/users/edit.html.erb

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


ビューができたのでここへのリンク先も修正しよう。

(変更はヘッダーのほんの一部なので大幅省略)


app/views/layouts/_header.html.erb

<!-- 中略 -->

<% 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>
<!-- 中略 -->

(scssは省略)

lantern_lantern_profile.png


プロフィール編集失敗時を考えよう

編集失敗したらエラーを出して同じ画面に戻るようにする。

じゃあまずはテスト、その後コントローラを整えよう。


失敗時のテストを書く

テストを行う前に準備を済ませる。

これは後述する、ログインユーザのみが編集できるテストの準備も加味している。

具体的にはログインしたユーザを用意する。

そこで少し戻るけど、Tutorial 9.3.1 [Remember me] ボックスをテストするを参考に、テスト用ログインメソッドlog_in_asを用意する。


spec/support/application_helper.rb

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


おっけ。じゃあテストといこう。


bash

$ rails g rspec:request users_edit



spec/requests/users_edits_spec.rb

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



失敗時のコントローラを整える

テストが書けたらコントローラの振る舞いを書こう。

成功時の処理は後にする。


app/controllers/users_controller.rb

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モデルが持つエラーを捕えるためにこういうテストを行いたいが、


requests/users_edits_spec.rb

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



bash

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で編集を成功させるとは異なり、リダイレクト先は再びプロフィール編集画面にする。

加えてパスワードを入力しないと変更不可にするので、バリデーションはいじらない。

ただこのままだと利用者が、

「新しいパスワード入力できるの?今までのパスワード入力したらいいの?」

となってしまうので、この問題は別の回で紹介する。

とりあえずテストとコントローラをつくる。


成功時のテストを書く


spec/requests/users_edits_spec.rb

# 中略

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
# 中略


成功時のコントローラを整える


app/controllers/users_controller.rb

# 中略

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
# 中略

成功時↓

lantern_lantern_edit_success.png

失敗時↓

lantern_lantern_edit_failure.png

ちなみにflashとflash.nowの使い分けはこう。


  • リダイレクトあり → flash

  • リダイレクトなし → flash.now

参考にさせていただきました↓

[WIP/初学者]flashとflash.nowの使い分け


ログインユーザだけがプロフィール編集できるようにする


非ログイン時にログインを促す

Tutorial 10.2 認可からプロフィール編集前にログインを要求することができる。

Tutorialでは/user/:idのページに誰でもアクセスできるが、ポートフォリオではここもログインを要求したい。

テスト→コントローラの順で(以下略


ログインを促すテスト

テストを追加、編集する。


bash

$ rails g rspec:request users



spec/requests/users_spec.rb

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



spec/requests/users_edits_spec.rb

# 中略

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を参考につくればよい。


app/controllers/users_controller.rb

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で新たなユーザを生成する。


spec/factories/users.rb

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でテストデータ作成する方法

それではテストを書く。


spec/requests/users_edits_spec.rb

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

この箇所はリファクタリングしておく。


app/helpers/sessions_helper.rb

module SessionsHelper

# 中略
def current_user?(user)
user == current_user
end

よってこれを加味すると以下のコントローラが出来上がる。


app/controllers/users_controller.rb

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


ログイン後は編集ページにリダイレクトしてあげる

編集画面でログインを勧められたのに、ログイン後に別の画面だとユーザが悲しい。

というわけで元いた画面にリダイレクトしよう。


編集ページにリダイレクトするテスト


spec/requests/users_edits_spec.rb

# 中略

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つのメソッドを用意しよう。


app/helpers/sessions_helper.rb

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 → 記憶したリンクか任意のリンクにリダイレクトする

これで準備はできた。

あとはコントローラを編集しよう。


app/controllers/users_controller.rb

class UsersController < ApplicationController

# 中略
def logged_in_user
unless logged_in?
store_location
flash[:warning] = 'ログインしてください'
redirect_to login_url
end
end


app/controllers/sessions_controller.rb

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, アクティベーション編