Rails 総復習1ヶ月チャレンジ 4日目(Railsチュートリアル編)

チャレンジ4日目です!今日はまたRailsチュートリアルの続きからやっていきます!

4日目(Ruby on Rails チュートリアル 10章ユーザーの更新・表示・削除)

allow_nil

ユーザー情報編集の際に名前やemail情報だけを変更しようとしてもUserモデルにパスワードの長さに対するバリデーションを設定している為、このバリデーションに引っかかってしまいます。そういう場合はallow_nil :trueを設定しパスワードのバリデーションが空だった場合の例外処理を加えます。

#app/models/user.rb

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  .
  .
  .
end

新規ユーザー登録時に空のパスワードが有効になってしまうのではないか?という心配が出てきますが、これはhas_secure_passwordが解決しています。今回追加したバリデーションとは別にhas_secure_passwordがオブジェクト生成時に存在性を検証するようになっている為、空のパスワード(nil)が新規ユーザー登録時に有効になることはありません。

認可(ログインしているユーザーかつユーザー本人のみ編集を許可する)

認可のシステムを実装します。 まずはログインしているユーザーのみ編集できる状況を作るため、ユーザーにログインを要求する仕組みを実装します。

#app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: %i[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
      end
    end
end

before_actionメソッド使い、privateの中で定義したlogged_in_userメソッドを呼び出します。使いたい場面は編集時なので、only: %i[edit update]でedit,updateアクション呼び出し前に実行するようにします。

続いてユーザー本人のみが情報を編集できるようにします。まずはSessionsヘルパーでユーザーがcurrent_userであればtrueを返すcurrent_user?メソッドを定義します。

#app/helpers/sessions_helper.rb

module SessionsHelper
    .
    .
    .
  # 渡されたユーザーがカレントユーザーであればtrueを返す
  def current_user?(user)
    user && user == current_user
  end
    .
    .
    .
  end
  .
  .
  .
end

なお下記のコードはuser == current_userでもほぼ問題なく動くと思いますが、userがnilになってしまったレアケースもキャッチするためにuser &&を記載しています。

user && user == current_user

そしてUsersコントローラーにて正しいユーザーかどうか確認するcorrect_userをprivate内に定義し、before_actionでedit,updateのアクション前に呼び出すようにします。

#app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: %i[edit update]
  before_action :correct_user,   only: %i[edit update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private
  .
  .
  .
    # ログイン済みユーザーかどうか確認
    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])
      redirect_to(root_url) unless current_user?(@user)
    end
end

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

認可システムによって保護されたページにアクセスしようとするとこちらで指定されたページに移動させるようになっていますが、ユーザーが元々行きたかったページに移動させるのが望ましいです。今回はその実装をやっていきます。
流れとしてはざっくりこのような形です。

  • (アクセス権限のない)ユーザーがアクセスしようとしたURLを記憶 ↓
  • ユーザーがアクセス権限を取得した後、記憶したURLにリダイレクトさせる

では実装を行なっていきます!
まずはSessionsヘルパーに必要なメソッドを定義します。

#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

まず転送先のURLを保存する仕組みはユーザーログインさせた時と同様、session変数を使います。 - stroe_locationメソッド
リクエストが送られたURL(request.original_urlでリクエスト先が取得できます)をsession変数の:forwarding_urlキーに格納しています。if request.get?部分でGETリクエストが送られた時だけ格納するようにしています。

  • redirect_back_orメソッド
    リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトします。
session[:forwarding_url] || default

上記コードで値がnilでなければsession[:forwarding_url]を評価し、nilの場合はdefault URLにリダイレクトさせるようにしています。その後の行session.delete(:forwarding_url)で転送用のURLを削除しています。これをしないと次回ログイン時に保護されたページに転送されてしまいます。

まずstore_locationメソッドをUsersコントローラーに追加します。

#app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_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?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
   .
   .
   .
end

続いてredirect_back_orメソッドをSessionsコントローラーに追加します。

#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

これで実装完了です!

パーシャルのrender

ユーザー一覧ページでパーシャルのrenderを実装します。
まず現状のコードはこのようになっています。

#app/views/users/index.html.erb

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

一旦下記にリファクタリングします。

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

これはrenderでパーシャルに対してではなく、Userクラスのuser変数に対して実行しています。この場合はRailsは自動的に_user.html.erbという名前のパーシャルを探しにいくようにしています。(パーシャルは作成しておく必要があります)
ただこれはさらにリファクタリングできます。

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

renderを@users(この場合indexアクション内でUser.allを格納している変数)に対して直接実行しています。Railsではモデルインスタンスの集合体をrenderの引数に渡すとパーシャル(今回は_user.html.erb)をレンダリングします。

boolean

管理ユーザーカラムを設定する際の属性の型として使用する。

#ターミナル
$ rails generate migration add_admin_to_users admin:boolean
#db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

booleanを設定することにより、カラム?(admin?)、toggle(デフォルトでfalseをtrueにする)メソッドが使えるようになる。

今回は以上です!