1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】Railsチュートリアル10章を読み終えて

Posted at

はじめに

こんにちは。アメリカに住みながら独学でエンジニアを目指している taira です。
本日はRailsチュートリアル10章を読み終えたので、その内容について自分なりに整理してみようと思いました

ユーザーを更新する

9章はcookieを使用したRemember me機能の実装でしたが、10章は今まで未実装だったUsersリソース用のRESTアクション等を完成させる章でした

ユーザーを更新するためにはeditアクションとupdateアクションを作っていきます

class UsersController <ApplicationController

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

  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

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

editアクション、updateアクションは上記のように作成しました
updateがうまくいったら、user_path(@user)にリダイレクトし、失敗したら、editのviewを再び呼び起こします
updateメソッドはStrong Parametersを使って、マスアサインメントの脆弱性を防止しています

<% provide(:title,"Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6col-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" rel = "noopener">change</a>
    </div>
  </div>
</div>

上記はedit.html.erbです。new.html.erbと同様に<%= form_with(model:@user)do |f| %>を使用していますが、ActiveRecordのnew_record?によって@userが新規ユーザーか否かを見極めて新規作成か編集なのかを見極めています。

target="_blank"だけだと古いブラウザなどでは、リンク先のHTMLのwindowオブジェクトを扱うことができてしまい、フィッシングサイトのような悪意のあるリンクに書きかえられる可能性があります。
そこでrel = noopenerを入れることで、それを防ぐことができます。

認可

上記でeditとupdateアクションの大まかな実装はできたのですが、このままでは、ログインしていないユーザー(誰でも)が情報を書きかえることができてしまいます。
また、ログインしてもほかのユーザーが同様に編集できてしまってはまずいので、それについての実装も行いました
さらに、ユーザー体験をあげるために、フレンドリーフォワーディングも実装しました

ログイン済み・正しいユーザーのみが使用できるようにする

この仕組みを実装するためにbefore_actionメソッドをUsersコントローラー内で実装しました。
これはアクションを行う前に、メソッドを指定することができます。

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


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

  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

    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

edit・updateアクションを実施する前に、logged_in_userによってログイン済みか否かを確認、correct_userメソッドによって、ログイン済みのユーザーとcurrent_userが一致するか確認しています

class UsersControllerTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end

  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

  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
end

上から順番に以下のテストをしています

  1. ログインしていないユーザーが編集した場合
  2. ログインしていないユーザーが更新した場合
  3. ログイン済みの違うユーザーが編集した場合
  4. ログイン済みの違うユーザーが更新した場合

フレンドリーフォワーディングについては以前記事を書いたのでそちらに詳しくまとめています

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

ここでは、すべてのユーザーを表示するindexアクションを実装し、gemのfakerとpagenateを実装して効率よくユーザー一覧を表示させます
基本的なコントローラーとビューの実装ではなくfakerとページネーションについて説明していきます

fakerを使用してユーザーを量産する

gemfileにfakerを実装します。後の説明に使用するページネーションのgemも併せて追加しておきます

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.3"

gem "rails", " 7.0.4.3"
gem "bcrypt", "3.1.18"
gem "faker", "2.21.0" # 追加
gem "will_paginate", "3.3.1" # 追加
gem "bootstrap-will_paginate", "1.0.0" # 追加

bundle installで変更を反映させたのち、db/seeds.rbに以下のようにユーザーを作成します

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

先ほどGemでインストールしたFakerは名前をランダムで作成してくれます。
データベースをリセットして、上記変更を反映させます。

rails db:migrate:reset
rails db:seed

これで簡単にユーザーを100人作成することができました

ページネーション

indexの画面にユーザーが100人一気に表示されたらとても見ずらいと思います。
また、User.allでフルスキャンしていることからその数をできるだけ少なくしたほうがパフォーマンスも向上します。
ページネーションの実装はそこまで難しくありません

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

<%= will_paginate %>

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

<%= will_paginate %>
 def index
    @users = User.paginate(page: params[:page])
  end

will_pagenateを上下に入れて、コントローラーでpagenateメソッドを使用することで簡単にページネーションを実装することができました

ユーザー一覧のテスト

ここまで実装できたら、indexの統合テストを実施します

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'
    User.paginate(page:1).each do |user|
      assert_select 'a[href=?]',user_path(user), text:user.name
    end
  end
end

User.pagenate(page:1)によって30人のユーザー情報があるのでそれをブロック処理してassert_selectでテストしています

ユーザーを削除する

最後にdestroyアクションを実装します。むやみあたらにほかのユーザーの情報を削除できないように管理者のみが削除できるように設定します。
管理ユーザー実装のために、Usersテーブルにadmin属性(boolean)を追加します

rails generate migration add_admin_to_users admin:boolean

ここでbooleanはtrueかfalseを値として取ります。これによりadmin?メソッドによって管理ユーザーか否かを確認できるようになりました

次にdestroyアクションを実装します。

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

  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url, status: :see_other
  end

  private

  # ログイン済みユーザーかどうか確認
  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

  # 管理者かどうか確認
  def admin_user
    redirect_to(root_url, status: :see_other)unless current_user.admin?
  end
end

destroyアクションもログイン済みのユーザーかつ、管理者でないとできないようにしたいのでbefore_actionで設定します

また、destroyのテストも実施します

require "test_helper"

class UsersIndexTest < ActionDispatch::IntegrationTest
  
  def setup
    @admin = users(:michael)
    @non_admin = users(:archer)
  end

  test 'index including pagination and delete links' do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination',count:2
    first_page_of_users = User.paginate(page:1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]',user_path(user), text:user.name
      unless user == @admin
        assert_select 'a[href=?]',user_path(user), text:'delete'
      end
    end
    assert_difference 'User.count',-1 do
      delete user_path(@non_admin)
      assert_response :see_other
      assert_redirected_to users_url
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count:0 # <a[href]>にdeleteという文字が存在しない
  end
end

管理ユーザーがdestroyアクションを呼び出したら、ユーザーを削除することができ、そうでなければ削除できないのをテスト上で表現しています

まとめ

10章はほかの画面の実装部分だったため、9章よりはかなり理解しやすかったです。
残すところ4つとなりとても感慨深いですが、これからも着実に進めていきたいと思います

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?