LoginSignup
0
0

Railsチュートリアル第10章まとめ

Last updated at Posted at 2024-05-06

10章

edit、update、index、destroyアクションを加え、RESTアクションを完成させる
ユーザーが自分のプロフィールを自分で更新できるようにし
ユーザーを削除し、データベースから完全に消去する機能も追加していく

10章 10.1ユーザーを更新する

ユーザー情報を編集するパターンは、新規ユーザーの作成と極めて似通っている。
newアクションと同じようにeditアクションを追加していく。

POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成。

10.1.1編集フォーム

Userコントローラーにeditアクションを追加しビューを追加していく。
前提としてユーザー情報を読み込む必要がある。

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      reset_session
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new', status: :unprocessable_entity
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  private

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

userコントローラーに

  def edit
    @user = User.find(params[:id])
  end

を追加

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_with(model: @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="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

ユーザーeditのViewを作成
error_messagesパーシャルが使えるので再利用する。

app/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>
      <div class="navbar-header">
        <button id="hamburger" type="button" class="navbar-toggle collapsed">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      </div>
      <ul id="navbar-menu"
          class="nav navbar-nav navbar-right collapse navbar-collapse">
        <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="#" id="account" class="dropdown-toggle">
              Account <b class="caret"></b>
            </a>
            <ul id="dropdown-menu" 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,
                                       data: { "turbo-method": :delete } %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ヘルパーメソッドを使ってsettingsに追加。
edit_user_pathという名前付きルーティングと、 current_userを使う。

<%= link_to "Settings", edit_user_path(current_user) %

10.1.2編集の失敗

編集に失敗した場合について着手していく。
updateアクションの作成から進めていく。

update→params→無効な情報が送信された場合→falseが返され、elseに分岐し編集ページをレンダリング

app/controllers/users_controller.rb
#ユーザーのupdateアクションの初期実装
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      reset_session
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new', status: :unprocessable_entity
    end
  end

  def edit
    @user = User.find(params[:id])
  end

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

  private

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

render 'edit', status: :unprocessable_entity
これにより編集に失敗した場合はレンダリングを行い処理に問題があった場合は送信できないようにしているようです。

10.1.4TDDで編集を成功させる

投稿編集フォームを動作させる。

この編集の失敗に対するテストを参考にしていく。

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

・ユーザー情報を更新する正しい振る舞いをテストで定義
・フラッシュメッセージが空でないかどうか、プロフィールページにリダイレクトされるかどうかをチェック
・データベース内のユーザー情報が正しく変更されたかどうかも検証
・@user.reloadメソッドで最新のユーザー情報をデータベースから再度読み込むことで、更新内容が正しいことを確認

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

これは編集が成功したメッセージで、ユーザーに戻るようです。
しかしこのままではtestは失敗とのこと。

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: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  .
  .
  .
end

空のパスワードで更新されてもエラーをでるようにしたので正常に動作することができます。

10.2.1ユーザーにログインを要求する

今回はユーザーにログインを要求するために、logged_in_userメソッドを定義してbefore_action :logged_in_userという形式で使う。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  private

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

    # beforeフィルタ

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

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

    # beforeフィルタ

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

onlyをつけることでeditとupdateの前にbefore actionを発動する。

# beforeフィルタ

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

で logged_in_userの中身
ユーザーログインの確認でログインしていなかった場合は警告メッセージを表示
さらにログインしていないユーザーに対してログインページを出す。

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

保護されたページにユーザーがアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまう。
ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にその編集ページにリダイレクトするのが望ましい動作。

保護されたページにアクセス→ログイン→ログイン認証OK→アクセスしようとした保護されたページにログインしたユーザーとしてリダイレクトされる。

これが正常な動作ってことですかね?Amazonの購入ページなんかをイメージしました。

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 store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

ユーザーがGETリクエストを送信した際にのみ、そのリクエストのURLを:forwarding_urlキーに保存します。これにより、ログインしていない状態でPOST、PATCH、またはDELETEリクエストが送信された場合に、不適切なリダイレクトを防ぐことができる。

app/controllers/users_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
        flash[:danger] = "Please log in."
        redirect_to login_url, status: :see_other
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url, status: :see_other) unless current_user?(@user)
    end
end

store_locationを追加

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])
      forwarding_url = session[:forwarding_url]
      reset_session
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      log_in user
      redirect_to forwarding_url || user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end
  .
  .
  .
end

forrowdingURLを実装。 ある場合はそこへ転送し、nilであればユーザープロフィールへ戻す。

すべてのユーザーを表示する

すべてのユーザーを一覧表示する、表示する為にindexアクションに追加を行い
ページの分割を行う必要がある。

10.3.1ユーザーの一覧ページ

セキュリティモデルを確定させる。
・showページについては、ユーザーがログインしているかどうかに関わらず、
サイトにアクセスするすべてのユーザーから見えるようにする
・indexページはログインしたユーザーにしか見えないようにし、未登録で登録ができるユーザーは制限をかける。

test/controllers/users_controller_test.rb
require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end

  test "should get new" do
    get signup_path
    assert_response :success
  end

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

ログインしていない状態でリダイレクトするかどうかのテストを記述

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

before_action :logged_in_user, only: [:index, :edit, :update]
このbefore_actionを追加することでindexとeditはログインしているかどうかを事前に確認する。
つまりログインしてないとアクセスができないってことですね。

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

全ユーザーを保存する変数を作成して、ユーザー一覧を表示するindexビューを実装。

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

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

gavitorヘルパーに引数を追加

app/helpers/users_helper.rb
module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    size         = options[:size]
    gravatar_id  = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end
app/assets/stylesheets/custom.scss
.
.
.
/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

CSSを整える

app/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>
      <div class="navbar-header">
        <button id="hamburger" type="button" class="navbar-toggle collapsed">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      </div>
      <ul id="navbar-menu"
          class="nav navbar-nav navbar-right collapse navbar-collapse">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", users_path %></li>
          <li class="dropdown">
            <a href="#" id="account" class="dropdown-toggle">
              Account <b class="caret"></b>
            </a>
            <ul id="dropdown-menu" 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,
                                       data: { "turbo-method": :delete } %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

user pathを整える。
これでindexはページが完全に動くようになる。

10.4ユーザーを削除する

ユーザーを削除するためのリンクを追加する。
admin属性を追加する。
スクリーンショット 2024-05-05 22.57.04.png

$ rails generate migration add_admin_to_users admin:boolean

マイグレーションしてadmin属性を追加/boolean型という。

adminカラムにuserテーブルが追加された

class AddAdminToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

デフォルトで管理者になれない設定を追加

$ rails db:migrate

マイグレートを実行

疑問符付きのadmin?メソッドも利用可能に

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

10.4.2destroyアクション

ユーザーindexページの各ユーザーに削除用のリンクを追加し
続いて管理ユーザーへのアクセスを制限

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, data: { "turbo-method": :delete,
                                          turbo_confirm: "You sure?" } %>
  <% end %>
</li>
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url, status: :see_other
  end

  private
  .
  .
  .
end

deleteリンクを動作させるにはdestroyを追加する必要がある。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url, status: :see_other
  end

  private
  .
  .
  .
end

ユーザーを削除するにはログイン済みであることが必須なので、
destroyアクションもlogged_in_userというbeforeフィルターに追加する。

上記コードをchatGPTでまとめた。
User.find(params[:id]).destroy:
User.find(params[:id])は、URLから渡されたidパラメータを使って、Userモデル(データベーステーブル)から該当するユーザーを検索します。
検索されたユーザーに対して.destroyメソッドを呼び出し、そのユーザーをデータベースから削除します。
flash[:success] = "User deleted":
この行は、ユーザーが正常に削除されたことを示すメッセージをフラッシュメッセージに設定します。このメッセージは、次に表示されるページでユーザーに表示される一時的な通知です。
redirect_to users_url, status: :see_other:
redirect_to users_urlは、リクエストをユーザーリストページ(users_urlで指定されるURL)にリダイレクトします。
status: :see_otherは、HTTPステータスコード303 See Otherを指定します。これは、DELETEリクエストの結果としてブラウザに新しいURL(ここではユーザーリストページ)をGETリクエストで取得するよう指示するために使用されます。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url, status: :see_other) unless current_user.admin?
    end
end

user_controlllerのbefore_actiionにdestroyアクションを追加。

10.5.1本章のまとめ(引用)

・ユーザーは、編集フォームからPATCHリクエストをupdateアクションに対して送信し、情報を更新する
・Strong Parametersを使うことで、Web経由で安全にデータを更新できるようになる
・beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出せる
・beforeフィルターを使って、認可(アクセス制御)を実現した
・認可に対するテストでは、特定のHTTPリクエストを直接送信するシンプルなテストと、ブラウザの操作をシミュレーションする複雑なテスト(統合テスト)の2つを利用した
・フレンドリーフォワーディングとは、ログイン成功時に元々表示したかったページに転送する機能である
・ユーザー一覧ページでは、すべてのユーザーをページ単位に分割して表示する
・rails db:seedコマンドは、db/seeds.rbにあるサンプルデータをデータベースに流し込む
・render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する
・boolean型のadmin属性をUserモデルに追加すると、admin?という論理オブジェクトを返すメソッドが自動的に追加される
・管理者が削除リンクをクリックすると、DELETEリクエストがdestroyアクションに送信され、該当するユーザーが削除される
・fixtureファイル内でERBを使うと、テストユーザーを多数作成できる

感想

基本的なところは過去やった流れを踏襲してるというイメージでした。
ただセキュリティ的な概念や補足を作る時にこれらのアクセス権限を踏まえた実装をしなければいけないので
あらゆる角度の面から俯瞰して物事を考えて機能を実装しなければいけないと痛感しました。
特にbefore_actionの部分はあらかじめどういった状態であるのか等想定して
ユーザーアクションを考えて機能をどういうふうにするか考えなければいけませんね。

構築部分は基本的には一緒なのですが、細かい仕様やセキュリティ的なところを
しっかり踏まえてやっていく必要があると思いました。

引き続き頑張ります。

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