noggy’s blog

自分用の備忘録です。。。

10.1 ユーザーを更新する

ユーザーの情報を編集するためのeditアクションを実装する。

10.1.1 編集フォーム

ここではUsesコントローラにeditアクションを追加し、対応するeditビュー(ユーザーの編集フォーム)を作成する。これはユーザーの新規作成と似ている。

editアクションの追加

ユーザーのidはparams[:id]で取り出すことができるので、次を追加する。
controllers/users_controller.rb

  def edit
    @user = User.find(params[:id])                                             
  end
対応するビューを作成

views/users/edit.html.erb

<% provide(:title, "Ebit 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.text_field :email, class: 'form-control' %>

     <%= f.label :password %>
     <%= f.password_field :password, class: 'form-control' %>

     <%= f.label :password_contfirmation, "Confirmation" %>
     <%= f.password_field :password_confirmation, class: "form-control" %>

     <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
    <div class="grabatar_edit">
      <%= gravatar_for @user %>
      <a fref="http://gravatar.com/emails" target="_blank">編集</a>←②
    </div>
  </div>
</div>

①:error_messagesパーシャルを再利用している。
②:Gravatarへのリンクでtarget="_blank"が使われているが、これによりリンク先を新しいタブ(またはウインドウ)で開くようになる。

ここで、NameやEmailの部分を見ると、名前やメールアドレスのフィールドに値が自動的に入力されていることが分かる。
これらの値は、@userから取り出しているからである。

editフォームのhtml
 <form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
<input type="hidden" name="_method" value="patch" />

webブラウザはネイティブではPATCHリクエストを送信できない。
よって、POSTリクエストの隠しinputフィールドを利用してPATCHリクエストを「偽造」!うーん、すごい!

ナビゲーションバー

ナビゲーションバーにあるユーザー設定へのリンクを更新。edit_user_pathという名前つきルートと、current_userというヘルパーメソッドを使うと便利
views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% 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 %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>  
      </ul>
    </nav>  
  </div>
</header> 

10.1.2 編集の失敗

まずはupdateアクションの作成から。

update_attributes(複数形)を使って、editフォームから送信されたparamsハッシュを受け取り、更新したい。
更新に成功時にはtrue、失敗時にはfalseを返すので、次のようになる。

  def update
    @user =User.find(params[:id])
    if @user.update_attributes(user_params) ←①
      #更新に成功した場合
      redirect_to @user
    else  
      #更新に失敗した場合 
      render 'edit'
    end
  end

update_attributesへの呼び出し①で、user_paramsを使っている点に注目。
第7章で扱ったStrong Parametersを使ってマスアサインメントの脆弱性を防止している。

10.1.3 編集失敗時のテスト

統合テストをかく。まずは生成。

$ rails g 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

①:編集ページにアクセス
②:assert_template(expected, message = nil):そのアクションで指定されたテンプレートが描写されているかをバリデーション
すなわと、editビューが描画できてたらtrueを返す。
③:無効なユーザー情報を送信
④:editビューが再描画できてたらtrueを返す。

無効なユーザー情報のため、edit画面にレンダ―されるので、テストは成功する。

$ rails test
10.1.4 TDDで編集の成功

今度は編集フォームが動作するようにする。

プロフィール画像の編集

プロフィール画像の編集は画像のアップロードをGravatarに任せてあるので、既に動作するようになっている。
changeボタンをクリックすれば、Gravatarを編集できる。

それ以外の機能の実装

より快適にテストするには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利。

編集の成功に対するテスト
  def setup
    @user = users(:michael)
  end
  :
  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

①: michaelのuserIDを取得
②:editビュー(入力フォーム)が描画できたらtrue
③:user情報を送信。ここで、パスワードとパスワード確認が空であることに注目。
ユーザー名やメールアドレスを編集する時に毎回パスワードを入力するのは不便なので、
わざとパスワードを入力せずに更新している。
④:flashメッセージが空でなければ(何らかのエラーメッセージが出ていれば)true
⑤:michaelのプロフィールページにリダイレクトされればtrue
⑥:DBから最新のユーザー情報を読み込み直して、正しく更新されたかチェック

※こうした正しい振る舞いをというのは一般に忘れがちですが、受け入れテストでは先にテストを書くので、効果的なユーザー体験について考えるようになる。

updateアクション

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

ただし、テストは失敗する。なぜなら、パスワードの長さに対するバリデーションがあるので、
パスワードやパスワード確認の欄を空にしておくとこれに引っかかるから。

allow_nilオプション

パスワードのバリデーションに対して、空だったときの例外処理を加える必要がある、それがallow_nil: true

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { email.downcase! }                                             
  validates :name, presence: true, length: { maximum: 50 }                      
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i                      
  validates :email, presence: true, length: { maximum: 255 },                   
                    format: { with: VALID_EMAIL_REGEX },                        
                    uniqueness: { case_sensitive: false }                       
  has_secure_password                                                           
  validates :password, presence: true,length: { minimum: 6 }, allow_nil: true ←追加