0
0

More than 3 years have passed since last update.

Railsチュートリアル 第10章

Posted at

ユーザーを更新する

ユーザーの編集ビュー

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>

上記の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>

WebブラウザはネイティブではPATCHリクエストを送信できないので、RailsではPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」している。
また、ユーザー編集(edit)のHTMLはユーザー新規登録(new)のコードと完全に一緒です。Rails では、ユーザーが新規なのか、それとも既存のユーザーであるのかをActive Recordのnew_record?メソッドを使って区別しています。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOST、falseのときはPATCHを使っています。

noopener

target="_blank"の脆弱性対策として、一緒にrel="noopener"もつける。

編集時のテスト

編集の成功と失敗時のテスト

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'
    assert_select "div.alert", "The form contains 4 errors."
  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
end

また、編集時にパスワードを空にしておくと、パスワードのバリデーションに引っかかってしまうので、
allow_nil: trueというオプションをvalidatesに追加する。

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = 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
  .
  .
  .
end

上記のようにすると、新規ユーザー登録時に空のパスワードが有効になってしまうかと思われるかもしれないが、has_secure_passwordでは(追加したバリデーションとは別に)オブジェクト生成時に存在性を検証するようになっているため、空のパスワードが新規ユーザー登録時に有効なることはない。

フレンドリーフォワーディング

フレンドリーフォワーディングとは、ユーザーがログインした後、ログイン直前に閲覧していたページへリダイレクトさせる機能。

フレンドリーフォワーディングのテスト

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)            # ユーザー編集ページにアクセス
    log_in_as(@user)                 # ログイン 
    assert_redirected_to edit_user_url(@user)    # ログイン直前にアクセスしたページにリダイレクトされているか確認
    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
end

フレンドリーフォワーディングの実装

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

store_locationメソッド

リクエストが送られたURLをsession変数の:forwarding_urlキーに格納する。
ただし、GETリクエストの場合のみ。

redirect_back_orメソッド

session変数に格納されているURLがnilでなければ、session[:forwarding_url]を使い、そうでなければデフォルトのURLを使う。
この場合、redirect文を実行しても、次のセッション削除もされます。明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しない。

app/controllers/uses_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location           ⇦追加 GETリクエストが送られたURLを格納
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    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] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

サンプルユーザー作成

Faker gemを使い、テストユーザーを一気に作成する。

Gemfile
source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3'
.
.
.
$ bundle install

サンプルユーザーを生成するRubyスクリプトを追加する。
Rails ではdb/seed.rbというファイルを標準として使う。

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

create!は基本的にはcreateメソッドと同じですが、ユーザーが無効な場合に falseを返すのではなく、
例外を発生させます。こうしておくことで、見過ごしやすいエラーを回避できます。

Railsタスクを実行(db:seed)

$ rails db:migrate:reset
$ rails db:seed

ページネーション

Gemfileにwill_paginate gemとbootstrap-will_paginate gemを追加する。

source 'https://rubygems.org'

gem 'rails',                   '5.1.6'
gem 'bcrypt',                  '3.1.12'
gem 'faker',                   '1.7.3'
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
.
.
$ bundle install

ビューにwill_paginateメソッドを追加する。

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

will_paginateメソッドの特性として、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成します。

paginateを使うことで、ユーザー一覧のページネーションを行えるようになります。
paginateメソッドの引数にpageパラメータにはparams[:page]が使われているが、これはwill_paginateによって自動的に生成されている。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
end

ユーザー一覧のテスト

ページネーションを確認するのに多くのユーザーが必要なので、fixtureにユーザーを追加する。

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

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

ページネーションを含めた一覧ページのテスト

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination', count: 2
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

一覧ページのリファクタリング

元のテンプレート

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

ユーザーのliをrender呼び出しに置き換える

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

ここでは、renderをパーシャル(ファイル名の文字列)に対してではなく、Userクラスのuser変数に対して実行しています。
この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しに行くので、下記のパーシャルを作成する必要があります。

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

さらに、renderを@users変数に対して直接実行します。

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>

Railsは@usersをUserオブジェクトのリストであると推測します。
さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力します。

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