10章
edit、update、index、destroyアクションを加え、RESTアクションを完成させる
ユーザーが自分のプロフィールを自分で更新できるようにし
ユーザーを削除し、データベースから完全に消去する機能も追加していく
10章 10.1ユーザーを更新する
ユーザー情報を編集するパターンは、新規ユーザーの作成と極めて似通っている。
newアクションと同じようにeditアクションを追加していく。
POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成。
10.1.1編集フォーム
Userコントローラーにeditアクションを追加しビューを追加していく。
前提としてユーザー情報を読み込む必要がある。
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
を追加
<% 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パーシャルが使えるので再利用する。
<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に分岐し編集ページをレンダリング
#ユーザーの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で編集を成功させる
投稿編集フォームを動作させる。
この編集の失敗に対するテストを参考にしていく。
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メソッドで最新のユーザー情報をデータベースから再度読み込むことで、更新内容が正しいことを確認
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は失敗とのこと。
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
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の購入ページなんかをイメージしました。
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
テストデータを設定
module SessionsHelper
.
.
.
# アクセスしようとしたURLを保存する
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
ユーザーがGETリクエストを送信した際にのみ、そのリクエストのURLを:forwarding_urlキーに保存します。これにより、ログインしていない状態でPOST、PATCH、またはDELETEリクエストが送信された場合に、不適切なリダイレクトを防ぐことができる。
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
を追加
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ページはログインしたユーザーにしか見えないようにし、未登録で登録ができるユーザーは制限をかける。
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
ログインしていない状態でリダイレクトするかどうかのテストを記述
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はログインしているかどうかを事前に確認する。
つまりログインしてないとアクセスができないってことですね。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all
end
.
.
.
end
全ユーザーを保存する変数を作成して、ユーザー一覧を表示するindexビューを実装。
<% 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ヘルパーに引数を追加
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
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
CSSを整える
<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属性を追加する。
$ 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
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ページの各ユーザーに削除用のリンクを追加し
続いて管理ユーザーへのアクセスを制限
<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>
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を追加する必要がある。
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リクエストで取得するよう指示するために使用されます。
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の部分はあらかじめどういった状態であるのか等想定して
ユーザーアクションを考えて機能をどういうふうにするか考えなければいけませんね。
構築部分は基本的には一緒なのですが、細かい仕様やセキュリティ的なところを
しっかり踏まえてやっていく必要があると思いました。
引き続き頑張ります。