0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

railsチュートリアル 第十章

Last updated at Posted at 2019-03-25

##はじめに
railsチュートリアルで理解しにくいところや、詰まったところを書いていく記事になります。
なので、手順を示す記事とはなっていません。

のはずが、気づけばほぼ手順を示すようやくした記事になっていました\\\٩( 'ω' )و ////

##ユーザーの更新

ユーザー情報を編集する工程は新規ユーザー更新と似ているところが多い。

ビューファイルは newアクション のような editアクション を作成し、POSTリクエストに応答する createアクション のようにPATCHリクエストに応答する updateアクション を作成する。

違いとしては更新の場合はそのユーザー自身に限られるというところになる。

###編集フォーム
editアクション を定義する。

ユーザーのid情報を取り出す@userを定義しておきたいから以下のようになる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
  def edit
      @user = User.find(params[:id])
  end
end

ビューファイル(edit.html.erb)はほとんどcreateアクションのもの(new.html.erb)と同様。

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

名前やアドレスは @user の変数の属性情報から引き出される。

form_for(@user) のコードは new.html.erbファイル と同じだが、railsがユーザーが新規なのか、それともデータベースに存在する既存のユーザーであるかを、Active Recordのnew_record?論理値メソッドを使って区別するから問題ない。

ここまで完了すると、あとはheaderにリンクを追加すればいい。
<%= link_to "Settings", edit_user_path(current_user) %>

##updateアクション
次に、updateアクション を定義するが、これはcreateアクションと非常に似通ったものになる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
#省略
def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
end

 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

編集の場合はパスワードが空でも編集できるように、例外処理を加える必要がある。
編集の例外処理↓

app/models/user.rb
# allow_nil: trueを追加
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

このバリーデーションを追加しても、has_secure_password によりオブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはない。

##認可 (authentication)
認証 (authentication) はサイトのユーザーを識別することであり、認可 (authorization) はそのユーザーが実行可能な操作を管理すること。

このままだと、どのユーザーでもあらゆるアクションにアクセスできるため、誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまう。

よって、ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する必要がある。

###ユーザーにログインを要求する
ユーザーにログインを要求するための logged_in_userメソッド を定義し、before_actionメソッド を利用してedit,updateアクションの処理が行われる前にログインを要求する。

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

###正しいユーザーを要求
ログインの要求は完了したから、次にユーザーが自分の情報だけを編集できるようにする。

そのためには current_userメソッド を作成し、beforeフィルターからこのメソッドを呼び出すようにする。

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?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

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

上記のように、correct_userメソッド@user を定義しているから、edit,updateアクションでは定義する必要はなくなる。

また、フィルタリングとして、一般的には
current_user?
という論理値を返すメソッドが作られることが多い。

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

よって、correct_userメソッド をコンパクトにすると以下のようになる。

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

##フレンドリーフォワーディング
認可機能はこれで大体揃った。
しかし、保護されたページにアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまう。

ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましいから、フレンドリーフォワーディングをここで実装する。

ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。

この動作を store_locationredirect_back_or の2つのメソッドを使って実装することができる。

これらのメソッドは 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

store_locationメソッド では、GETリクエストが送られたURLsession変数:forwarding_urlキー に格納している。

定義した store_locationメソッド を使って、早速 logged_in_user を修正。

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

フォワーディング自体を実装するには、redirect_back_orメソッド を使う。

リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトするような処理にすればいい。

session[:forwarding_url] || default

というredirect_back_orメソッド内の一文は、

「値がnilでなければsession[:forwarding_url]を代入し、そうでなければデフォルトのURLを使う。」

という意味となる。

また、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまう、という問題を解決するために、

session.delete(:forwarding_url)

という行も追加しておく。

ちなみに明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しない。
よってredirect文の後にあるコードでも、そのコードは実行されることとなる。

redirect_back_orメソッド を使い、createアクション を修正していく。

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

##ユーザーの一覧
今回のセキュリティモデルでは、ユーザーの showページ については、今後も (ログインしているかどうかに関わらず) サイトを訪れたすべてのユーザーから見えるようにしておき、ユーザーの indexページ はログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限する。

まず、beforeフィルターのlogged_in_userに indexアクション を追加して、このアクションを保護する。

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
  #全ユーザーを取得する
  @users = User.all
  end
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>

次にusers_pathを用いてユーザー一覧用のリンク(users)を使い追加する。

 <% if logged_in? %>
          <li><%= link_to "Users", users_path %></li>

##ページネーション
ユーザーは表示できたが、このままではユーザーが増えると1つのページに大量のユーザーの表示されてしまう。

これを解決するために**ページネーション (pagination)**というものを利用する。

今回は will_paginateメソッド を使用。
以下のように Gemfile に追加する。

gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
$ bundle install

ページネーションが動作するには対応するアクションとビューに、コードを追加しなければならい。

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

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

一覧の上下にページネーションを表示。

この will_paginateメソッド は、usersビューのコードの中から @usersオブジェクト を自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成する。

ただこのままでは@users変数にはUser.allが定義されていて、will_paginateでは paginateメソッド を使った結果が必要となる。

よって、アクションの方を修正する必要が出てくる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    #修正(@user = User.allから)
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

これにより、ページネーションが正常に動作するようになる。

##ユーザーの削除

削除を実行できるのは管理者だけとして、まず管理ユーザー定義する。

###管理ユーザー
特権を持つ管理ユーザーを識別するために、論理値をとる admin属性 をUserモデルに追加する。
こうすると自動的に admin?メソッド (論理値を返す) も使える。

$ rails generate migration add_admin_to_users admin:boolean
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

default: false という引数をadd_columnに追加する。
これは、デフォルトでは管理者になれないということを示す。

マイグレーションを実行する。

$ rails db:migrate

これでadmin属性が追加されたので、今回は最初のユーザーを管理者にすることとする。

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             #追加
             admin: true)

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

###destroyアクション
destroyアクションでは、該当するユーザーを見つけてActive Recorddestroyメソッド
を使って削除し、最後にユーザーindexに移動する。

ユーザーを削除するためにはログインが必要になるから、:destroyアクションもlogged_in_userフィルターに追加しておく。

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
    #2つのメソッドを連結
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

  private
  .
  .
  .
end

また、beforeフィルターを使って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]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

仕上げにユーザー一覧に管理者だけに表示する削除リンクを追加する。

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, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

##さいごに
第10章では、ユーザーに関する機能(編集、一覧、削除など)とそれを実装するのに必要なアクションやメソッド(before、fixture)を見てきた。
比較的理解しやすく、安心。

次↓
https://qiita.com/jonnyjonnyj1397/items/108d088ea4274b05d8c6

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?