0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-08-24

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#10.5 RSpecでTutorialのテストを書き直す
次回:#12 ActionMailer, アクティベーション編

こんなことが分かる

  • プロフィール編集画面の例
  • Bootstrap4のCardの使用例
  • FactoryBotで複数ユーザを生成する方法
  • before_actionによる強制リダイレクトする方法
  • 直前のリンクを記憶しリダイレクトする方法

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

今回の流れ

  1. プロフィール編集のフォームをつくる
  • プロフィール編集失敗/成功時を考える
  • ユーザだけがプロフィール編集できるようにする
  • ログイン後は編集ページにリダイレクトさせる

本ポートフォリオは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, アクティベーション編

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?