0
0

More than 3 years have passed since last update.

【Rails】ユーザー情報の編集【Rails Tutorial 10章まとめ】

Last updated at Posted at 2019-12-01

Usersコントローラの編集

Usersコントローラにアクションを追加し、ユーザー情報の編集やユーザー一覧の表示、ユーザーの削除を行えるようにする。

ユーザー情報の編集

editアクションと編集フォーム

Usersコントローラにeditアクションを追加する。
editアクションに対応するURLはusers/:id/edit(名前付きルートはedit_user_path(user))なので、params[:id]を使えば編集したいユーザーを取得できる。

app/controllers/users_controller.rb
  def edit
    @user = User.find(params[:id])
  end

editアクションに対応するeditビューは以下のようになる。

app/views/users/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_for(@user) 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="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

これはユーザー新規登録フォームとほぼ同じである。
編集フォームのform_forは以下のような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" />
  .
  .
  .
</form>

form_forのコードは新規登録フォームと同じなのに、各属性が更新用になっているのは、Railsが新規ユーザーであるか既存ユーザーの編集であるかを自動で判別してくれるからである。

なお、gravatar用のaタグについているtarget="_blank"とrel="noopener"は、リンク先を別のタブで開くためのものである(rel属性はセキュリティ上の問題を解決するためのもの)。

editアクションとビューができたので、レイアウトのヘッダー部分にリンクを貼っておく。

app/views/layouts/_header.html.erb
<% if logged_in? %>
  <li><%= link_to "Users", '#' %></li>
  <li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
      Account <b class="caret"></b>
    </a>
    <ul class="dropdown-menu">
      <li><%= link_to "Profile", current_user %></li>
      <li><%= link_to "Settings", edit_user_path(current_user) %></li>
      <li class="divider"></li>
      <li><%= link_to "Log out", logout_path, method: :delete %></li>
    </ul>
  </li>
<% else %>

editページとsignupページのパーシャル

ユーザー編集フォームと新規登録フォームは、フォームの送信ボタンの文字と、gravatarのリンクしか違いがないので、パーシャルにまとめる。

app/views/users/_form.html.erb
<%= form_for(@user, url: yield(:url)) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

  <%= 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 %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>

signupビュー

app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:url, signup_path) %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>

editビュー

app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %>
<% provide(:url, user_path) %>
<% provide(:button_text, 'Save changes') %>
<h1>Update your profile</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">Change</a>
    </div>
  </div>
</div>

ここでは、2つのprovideメソッドとyieldを使っている。
一つはform_forの送信先である。
新規登録フォームではcreateアクションに送信するので、signup_pathとする。
編集フォームではupdateアクションに送信するので、user_pathとする(引数(user)は不要)。

もう一つはフォーム送信ボタンの文字である。

編集の失敗

編集失敗時の処理

更新フォームの送信先であるupdateアクションを書く。

app/controllers/users_controller.rb
  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end

ユーザーを取得した後update_attributesでその属性を更新する。
更新に失敗した場合は編集画面に戻る。

編集失敗時のテスト

ユーザー編集用の統合テストを作成する。

$ rails generate integration_test users_edit

ユーザー情報の更新に失敗した場合のテストを書く。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }
    assert_template 'users/edit'
  end
end

updateアクションにはPATCHリクエストを送信する点に注意する。

編集の成功

編集成功時の処理

編集に成功した場合は、新規登録のcreateアクションと同様に、フラッシュメッセージを表示してユーザー表示ページに移動する。

app/controllers/users_controller.rb
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

編集成功時のテスト

ユーザー情報の編集に成功した場合のテストを書く。

test/integration/users_edit_test.rb
  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    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
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end

変更したいnameやemailを変数に入れているのは、編集完了後のUserオブジェクトの各属性と比較するためである。
assert_equalでエラーが出れば、編集に成功したはずなのにnameやemailが変わっていないことになる。

ここでテストはREDになるのだが、それはパスワードが有効な値でないためである。
パスワードの変更は必須ではないため、Userモデルのパスワードのバリデーションにallow_nil: trueを追加して、この例外に対応する。

app/models/user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

テストがGREENとなることを確認する。

ユーザーの認可

認可

ユーザーの編集機能が完成したが、この状態では誰もが任意のユーザー情報を変更できてしまう。
そこで、認可(authorization)機能を追加して、特定のページへのアクセスを制限する。

before_actionとlogged_in_userメソッド

before_acitonメソッドを使うことで、各アクションの実行前に特定の操作を行うことができる。

ログインしていないとユーザー情報を編集できないようにする。
editアクションとupdateアクションの前にログインしているかを判定して、ログインしていなければログインページにリダイレクトするlogged_in_userメソッドを定義する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  private
    .
    .
    .
    # beforeアクション
    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

before_actionでは、アクション前に実行するメソッドをハッシュの形(:logged_in_user)で指定する。
これで、ログインしていないユーザーだとeditページにアクセスできなくなる。
また、これが原因でテストがREDになるので修正する。
各テストの前にlog_in_asヘルパーメソッドを使ってログインしておく。

test/integration/users_edit_test.rb
  test "unsuccessful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

logged_in_userのテスト

logged_in_userのテストを書く。

test/controllers/users_controller_test.rb
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end

updateアクションでは、PATCHリクエストでユーザー情報を送信する。
テストの前後でbefore_actionをコメントアウトして、正しくテストできているかを確認しておく。

正しいユーザーを要求する

ユーザーが他のユーザー情報を編集できないようにするために、correct_userメソッドを作成し、before_actionでedit、updateアクションに設定する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      unless @user == current_user
        flash[:danger] = "You have no authority for the page"
        redirect_to(root_url)
      end
    end
end

このメソッドに該当するのはログイン済みのユーザーなので、ログインページではなくルートURLにリダイレクトする。
(ちなみに、チュートリアルではここでフラッシュメッセージを入れてないせいで後のテストがREDになるので勝手に入れてます。)

慣習として、unless @user == current_userの部分をcurrent_user?というヘルパーメソッドにしておく。

app/helpers/sessions_helper.rb
  # 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end
app/controllers/users_controller.rb
    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      unless current_user?(@user)
        flash[:danger] = "You have no authority for the page"
        redirect_to(root_url)
      end
    end

正しいユーザーのテスト

認可機能をテストするために、fixtureファイルに別のユーザーを作成する。

test/fixtures/users.yml
  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') %>

テストを書く。

test/integration/users_edit_test.rb
 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)
    get edit_user_path(@user)
    assert flash.empty?
    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

log_in_asメソッドを使って別のユーザーでログインすることと、ルートURLにリダイレクトすること以外は、非ログイン時のテストと同じである。

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