ユーザーの更新・表示・削除 | rails チュートリアル 16

Ruby on Rails チュートリアル:実例を使って Rails を学ぼう第10章 ユーザーの更新・表示・削除の勉強メモです。

ユーザーを更新する

ユーザー情報を更新することができるようにする。

viewを実装

headerのリンクを変更する

<header class="site_header">
  <div class="container">
    <small><%= link_to "site title", root_path, id: "logo" %></small>
    <nav>
      <ul>
        <li><%= link_to "About", about_path %></li>
        <li><%= link_to "Contact", contact_path %></li>
        <li><a href="http://news.railstutorial.org/">News</a></li>
        <% if logged_in? %>
          <li><%= link_to "Profile", current_user %></li>
          <li><%= link_to "Settings", edit_user_path(current_user) %></li>
          <li><%= link_to "Log out", logout_path, method: :delete %></li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

次に、編集フォームを作成。

controllerを修正し、

class UsersController < ApplicationController
.
.
.
  def edit
    @user = User.find(params[:id])
  end
.
.
.
end
$ touch app/views/users/edit.html.erb

form部分が、アカウント作成部分とにているため、共通化していく。

<%= f.submit "Save changes", class: "btn btn-primary" %>
<%= f.submit yield(:button_text), class: "btn btn-primary" %>

このパーシャルを読み込んでいるviewに、yield(:button_text)の部分を操作できるように、html.erbを修正

<% provide(:button_text, 'Create my account') %>
.
.
.
<%= render 'form', user: @user, url: signup_path %>
<% provide(:button_text, 'Save changes') %>
.
.
.
<%= render 'form', user: @user, url: user_path %>

生成された編集ページのformには、以下のような隠しinputが挿入されいる。

<input type="hidden" name="_method" value="patch">

編集の成功、失敗

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

パスワードが空のままでも更新できるように

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

エラー時のrender後のurlバーの文字列が気に食わない

submitする前には、「/users/:id/edit」なのに、エラーを起こしていると、urlバーの文字列が、「/users/:id」になっているのが違和感。

なので、以下の様に修正した。

  resources :users
  patch  '/users/:id/edit', to: 'users#update'
<%= render 'form', user: @user, url: edit_user_path %>

これでユーザー情報の編集がエラーを起こしても、formのaction先が、「/users/:id/edit」になっているため、urlバーの文字列は「/users/:id/edit」のままになる。

このままエラーがなければ、ひとまず、これで進める。

認可

いまのままでは、ログインをしていなくても、どのユーザー情報を編集出来てしまう。

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

before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みを利用する。

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

確認をして、ログインを要求するだけでは十分ではない。
このままでは、ログインをしているだけで、他人の情報も編集できてしまう。

一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装します

module SessionsHelper
.
.
.
  # 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end

  # 記憶トークン (cookie) に対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
.
.
.
end

helperで作ったメソッドをcontrollerのアクションで使用する。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
.
.
.
  private
.
.
.
    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      unless  current_user?(@user)
        flash[:danger] = "操作が受け付けられませんでした。"
        redirect_to(root_url)
      end
    end
end

これで、ログインをしているユーザーは、自分自身のユーザー編集ページ以外は見れなくなった。

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

ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作である。

urlを覚えておくことと、そのurlへリダイレクトをするメソッドを作成して、

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を覚えるようにメソッドを設置し、

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

ログイン後のリダイレクトの部分を修正する

  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

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

すべてのユーザーを一覧表示できるようにする。

ユーザーの一覧ページ

User.allを使ってデータベース上の全ユーザーを取得する

それをindexアクションに含める

全ユーザー表示は、ログインを要求する。

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

  def index
    @users = User.all
  end
end

viewファイルを作成する

$ touch app/views/users/index.html.erb

headerにリンクを追加する

<header class="site_header">
  <div class="container">
    <small><%= link_to "site title", root_path, id: "logo" %></small>
    <nav>
      <ul>
        <li><%= link_to "About", about_path %></li>
        <li><%= link_to "Contact", contact_path %></li>
        <li><a href="http://news.railstutorial.org/">News</a></li>
        <% if logged_in? %>
          <li><%= link_to "Users", users_path %></li>
          <li><%= link_to "Profile", current_user %></li>
          <li><%= link_to "Settings", edit_user_path(current_user) %></li>
          <li><%= link_to "Log out", logout_path, method: :delete %></li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

最後に、index.html.erbを修正

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

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

サンプルのユーザーを作成

一覧が出来たので、ユーザーが多くなった場合のことも想定する。
その際、ブラウザからユーザー登録ページへ行って手作業で1人ずつ追加するという方法もできますが、せっかくなのでRubyを使ってユーザーを一気に作成してみる。

fakerというgemを使う。

gem 'faker'
$ bundle install --path vendor/bundle

つぎに、サンプルユーザーを生成するRubyスクリプトを修正する。

User.create!(name:  "Example User",
             email: "example@email.com",
             password:              "foobar",
             password_confirmation: "foobar")

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

用意ができたら、サンプルユーザーを作成する。

# データをresetする。
$ bundle exec rails db:migrate:reset
# サンプルデータを作成
$ bundle exec rails db:seed

ページャーを作成

次に、1つのページに一度に30人だけユーザーを表示

gemを追加して

gem 'will_paginate'

インストール

$ bundle install --path vendor/bundle

viewとcontrollerを修正する

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

<div class="container">
  <div>
    <br>
    <br>
    <h1 class="title_signup">All users</h1>
    <%= will_paginate %>
    <br>
    <ul class="users">
      <% @users.each do |user| %>
        <li>
          <%= gravatar_for user, size: 50 %>
          <%= link_to user.name, user %>
          <br><br>
          <%= user.inspect %>
        </li>
      <% end %>
    </ul>
    <br>
    <%= will_paginate %>
    <br>
    <br>
  </div>
</div>
  def index
    # @users = User.all
    @users = User.paginate(page: params[:page])
  end

こうすることで、ページャーが作成される。

<div class="pagination">
  <span class="previous_page disabled">← Previous</span>
  <em class="current">1</em>
  <a rel="next" href="/users?page=2">2</a>
  <a href="/users?page=3">3</a>
  <a href="/users?page=4">4</a>
  <a class="next_page" rel="next" href="/users?page=2">Next →</a>
</div>

ユーザーを削除する

RESTに準拠した正統なアプリケーションにするためには、のこりdestroyアクションの実装です。

管理ユーザー

削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成しましょう。

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加する。

$ bundle exec rails generate migration add_admin_to_users admin:boolean
Running via Spring preloader in process 80072
      invoke  active_record
      create    db/migrate/20170719211446_add_admin_to_users.rb

このadmin:boolean は、defaultではnil、、、すなわち falseのため、ユーザーは管理者としては認識されないが、明示的にfalseという、引数を与え、Railsと開発者に意図を明確に示すため、mggrateファイルを修正する

class AddAdminToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

その後、migrate

$ bundle exec rails db:migrate
== 20170719211446 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.1005s
== 20170719211446 AddAdminToUsers: migrated (0.1007s) =========================

schema.rbを確認する

ActiveRecord::Schema.define(version: 20170719211446) do

  create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "password_digest"
    t.string "remember_digest"
    t.boolean "admin", default: false
  end

end

では、seeds.rbを更新し、データベースをリセットして、サンプルデータを再度生成する。

password = "Password"
User.create!(name:  "Example User",
             email: "example@email.com",
             password:              password,
             password_confirmation: password,
             admin: true)

99.times do |n|
  name  = Faker::Name.name
  email = "example#{n+1}@email.com"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end
$ bundle exec  rails db:migrate:reset
Dropped database 'witharea_development'
Dropped database 'witharea_test'
Created database 'witharea_development'
Created database 'witharea_test'

$ bundle exec rails db:seed

ユーザーを削除できる設定ができるようになったことで、外部からの攻撃も考慮して行かなければならない。
つまり、編集してもよい安全な属性だけを更新することが重要になる。

users_controller.rbの user_paramsを見てみると、

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

:name, :email, :password, :password_confirmation といった属性は、記述されているが、adminは、記述されていません。
これで、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できます。

destroyアクション

実際に、viewを修正して、deleteボタンを表示してみる。

      <% @users.each do |user| %>
        <li>
          <%= gravatar_for user, size: 50 %>
          <%= link_to user.name, user %>
          <% if current_user.admin? && !current_user?(user) %>
            | <%= link_to "delete", user, method: :delete,
            data: { confirm: "You sure?" },
            class: "btn btn-ss" %>
          <% end %>
          <br><br>
          <%= user.inspect %>
        </li>
      <% end %>

つぎに、この削除リンクが動作するための、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
  end

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

herokuにup

$ git push heroku master
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
$ heroku restart
$ heroku open

メモ

チュートリアルは残り4つ

並び替え機能や、検索機能などは、チュートリアル外で作っていくしかないなぁ。

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。


  1. KATOON.NET
  2. TRASH
  3. ユーザーの更新・表示・削除 | rails チュートリアル 16