Rails 掲示板一覧の作成

モデルの作成

  • Boardモデルの作成しtitle,bodyカラムを追加する。
  • Boardモデルにuser_idを外部キーとして設定する。
    紐づくモデル名+「_id」で外部キーと呼ぶ。

reference型を使いモデルと同時に作成。

$ rails g model board title:string body:text user:references
      invoke  active_record
      create    db/migrate/20210506055856_create_boards.rb
      create    app/models/board.rb

マイグレーションファイルとモデルファイルができる。

UserモデルとBoardモデルにアソシエーションを設定し、バリデーションを追加。

class Board < ApplicationRecord
  validates :title, presence: true, length: { maximum: 200 }
  validates :body, presence: true, length: { maximum: 60_000 }

  belongs_to :user
end

dependent: :destroyオプション

has_manyにdependent: :destroyを追加すると、親モデル(user)を削除した時に、その親モデルに紐づく子モデル(board)も同時に削除されるようになる。これを定義しないとUserを削除したときに、boardに投稿した内容が残ってしまうので必須である。

class User < ApplicationRecord
  authenticates_with_sorcery!
  validates :password, length: { minimum: 3 }, if: -> 
  #...
  has_many :boards, dependent: :destroy
end

データベースに未入力データの登録を排除するための NOT NULL 制約を追加。

class CreateBoards < ActiveRecord::Migration[5.2]
  def change
    create_table :boards do |t|
      t.string :title, null: false
      t.text :body, null: false
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

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

$ rails db:migrate

gem Fakerで作成したダミーデータをデータベースへ投入する。

$ rails db:seed

掲示板一覧画面の作成

ルーティングを定義

Rails.application.routes.draw do
  root to: 'users#index'
  get '/login', to: 'user_sessions#new'
  post '/login', to: 'user_sessions#create'
  post '/logout', to: 'user_sessions#destroy'
  resources :users
  resources :boards
end

コントローラを作成しindexアクションを定義

$ rails g controller boards
      create  app/controllers/boards_controller.rb
      invoke  erb
      create    app/views/boards
      invoke  decorator
      create    app/decorators/board_decorator.rb

includesメソッドとN+1問題

N+1問題とは、データベースからデータを取得する際、必要以上にSQLが発行されてしまいパフォーマンスが低下してしまう問題である。アソシエーションが定義されている場合に発生する。 @boards = Board.allと定義した場合、掲示板一覧ページ遷移時(1回)にユーザーとその掲示板の数(N回)だけSQLが発行されてしまう。
以下のようになる

対策

includesメソッドで関連するテーブルをまとめて取得。
orderメソッドは昇順 ASC、降順 DESCの並び替えができる。
created_atは、作成された日時なので 降順で新しい投稿が上にくるようにできる。
descはdescendingの略で降順という意味。

class BoardsController < ApplicationController
  skip_before_action :require_login, only: %i[index create]
  def index
    if logged_in?
      @boards = Board.all.includes(:user).order(created_at: :desc)
    else
      redirect_to login_path, danger: 'ログインしてください'
    end
  end

viewの作成。

パーシャルを使用し掲示板一覧画面を表示させる。
1.eachを使う場合

<%= @boards .each do |board| %>
 <div class="col-sm-12 col-lg-4 mb-3">
  <div id="board-id-<%= board.id %>">
   <div class="card">
    <%= image_tag 'board_placeholder.png', class: 'card-img-top', size: '300x200' %>
#...
<% end %> 

パーシャルをindex.htmlで読み込む。 部分テンプレート内のboardという変数に@boardが代入される。localsオプションが省略されてる。
<%= render partial: 'board', locals: {board: @board} %>
*localsオプションを使用した場合、partialは省略できない。

#...
<% if @boards.present? %>
  <%= render 'board', {board: @board} %> 
<%else%>
  <p><%= '掲示板がありません' %></p>
<%end%>

2.eachを使わない場合

 <div class="col-sm-12 col-lg-4 mb-3">
  <div id="board-id-<%= board.id %>">
   <div class="card">
    <%= image_tag 'board_placeholder.png', class: 'card-img-top', size: '300x200' %>
#... 
Railsでは、@boardsという変数からよしなに「_board.html.erb」を探してくれる。
@boardsにrenderメソッドを使うことで「_board.html.erb」をboardの数だけ繰り返し表示することができる。<%= render @boards %>は、collectionオプションが省略されている。
<%= render partial: 'board', collection: @boards %>
省略できる条件

  • 呼び出す部分テンプレートがviewフォルダ内のboardsフォルダに存在する
  • 部分テンプレート名が_board.html.erbである
  • 部分テンプレート内で使う変数がboardである

#...
<% if @boards.present? %>
  <%= render @boards %>
<%else%>
  <p><%= '掲示板がありません' %></p>
<%end%>
<%= render @boards %>
このコードは以下の処理とまったく同じである。
<%= render partial: 'board', collection: @boards %>
<!--下のコードと全く同じ -->
<% @boards.each do |board| %>
  <%= render partial: 'board', board: board %>
<% end %>

タイムゾーンを日本時間にする。

 config.time_zone = 'Tokyo'
 config.active_record.default_timezone = :local

Fakerを利用しダミーデータを作成

掲示板の一覧にダミーデータを投入する。

gem Fakerのインストール。
Fakerはdevelopmentとtest環境にインストールする。

group :development, :test do
#...
  gem 'faker'
#...
end

bundle installで完了。

データベース上にダミーデータを生成

今回はBoardモデルのダミーデータを20個作る。 データは公式のREADMEからとってくる。

20.times do
  title = Faker::Games::Pokemon.name
  body = Faker::Games::Pokemon.move
  user = User.offset(rand(User.count)).first
  Board.create!(
    title: title,
    body: body,
    user: user
  )
end

seedに書いた内容をデータベースへ反映。

$ rails db:seed

データが作成されたか確認。 f:id:meo2:20210509035116p:plain

なぜcreate!を使うのか。

createとcreate!の違い
createの場合はユーザーが無効なときfalseを返す。一方、create!は例外を発生させる(例外が発生した時点で処理が止まる)。よって、デバッグが安易になるのでcreate!を使用したほうがいい。

user = User.offset(rand(User.count)).first

について

  • モデル.offset(取得開始位置) 指定位置からレコードを取得。
  • rand 引数に整数を渡したら0以上指定した整数未満の整数を返す。
  • first 最初のレコードを取得。

20個のダミーデータから0〜19個間でランダムに数字を生成して、最初のレコードを取得している。 User.offset(18).firstなら18以降から最初のUserを取得する。

Decoratorの導入

Decoratorについて

Decoratorとはソフトウェアのデザインパターンのひとつであり、モデルとviewの間に追加する。 「モデル→Decorator→view」のようにviewを装飾するメソッドを追加するときはモデルではなく、デコレーターへ追加する。
Decoratorを導入すればviewにロジックを記述しないことができる。そうすることで、 モデルにはデータベース関連の処理のみを記載し、モデルの肥大化を防ぐことができる。

gem Draper

railsでデコレーター層(プレゼンテーション層)を導入するにはDraperかActive_Decoratorを使用するのが一般的である。
今回はDraperを利用しデコレーター層を導入する。

導入

gem 'Draper' とGemfile記述しbundle installする。bundle後、

$ rails g draper:install
create  app/decorators/application_decorator.rb

を実行。これでインストールが完了しジェネレーターコマンドが使えるようになる。
UserモデルのDecoratorを生成する。「rails g decorator (モデル名)」

$ rails g decorator User
create  app/decorators/user_decorator.rb

appにdecoratorsディレクトリとuser_decorator.rbが作成される。

Decorator内にメソッドを追加

class UserDecorator < Draper::Decorator
  delegate_all
  def full_name
    "#{last_name} #{first_name}"
  end
end

delegate_allという記述はUserモデルすべてのメソッドを呼び出せるようにし、UserDecoratorクラスでも使えるようにしている。
これがあることで、Userモデルが所有するメソッド(last_name,first_name)を定義したfull_nameメソッドで使うことができる。

view側でdecoratorメソッドを呼び出す。

<%= current_user.decorate.full_name %>

反映にはサーバーの再起動が必要。

Bootstrapでflashメッセージを実装

概要

以下のflashメッセージをbootstrapで表示させる。

  • ログイン成功時・・・「ログインしました」
  • ログイン失敗時・・・「ログインに失敗しました」
  • ログアウト時・・・「ログアウトしました」
  • ユーザー登録時・・・「ユーザー登録が完了しました」
  • ユーザー登録失敗時・・・「ユーザー登録に失敗しました」

定義の追加

flashオブジェクトはハッシュ形式で保存されており、viewにメッセージを表示している。デフォルトではnoticeとalertキーしかない。bootstrapを使用する場合、下図のようなキーが使用できる。 f:id:meo2:20210502184841p:plain
これらのキーを使用するにはキーを許可する宣言をしなければならない。 application_controller.rbに以下の記述をする。

add_flash_types :success, :info, :warning, :danger

すると、success, :info, :warning, :dangerのキーが使用可能になる。

flashメッセージの表示をviewに追加

flashメッセージは複数のviewで使用するのでパーシャルを使う。
_flash_message.html.erbを作成しapplication_controllerで読み込む。

<% flash.each do |key, value| %>
  <div class="alert alert-<%= key %>"><%= value %></div>
<% end %>

keyとvalueはコントローラーから指定する。
例1

redirect_back_or_to root_path, success: 'ログインしました'

「success」がkeyで「ログインしました」はvalueで取得される。
例2

flash.now[:danger] = 'ログインに失敗しました'

「danger」がkeyで「ログインに失敗しました」がvalue

flashflash.nowの違い

flash[:danger]の場合は次のアクションまでメッセージを表示させる。 flash.now[:danger]とすると、次のアクションに遷移した時点でメッセージが消える。

パーシャルを読み込む

#...
   <body>
   <% if logged_in? %>
     <%= render 'shared/header'%>
   <% else %>
     <%= render 'shared/before_login_header'%>
   <% end %>
     <%= render 'shared/flash_message'%>
   <%= yield %>
     <%= render 'shared/footer'%>
  </body>
</html>

ログイン・ログアウト時にflashメッセージを表示させる

ログイン成功、ログアウト時はsuccessキーで表示。 ログイン失敗時はdangerキーで表示させる。

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[create new]
  def new; end

  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to root_path, success: 'ログインしました'
    else
      flash.now[:danger] = 'ログインに失敗しました'
      render :new
    end
  end

  def destroy
    logout
    redirect_to root_path, danger: 'ログアウトしました'
  end
end

renderとredirect_toの違い

上記を例とすると、
ログインできた場合
users#createアクション→root_path(users#index)→index.html.erbが描画される。
ログインに失敗した場合
users#createアクション→new.html.erbが描画される。という処理が流れる。 この処理から、redirect_to は、view の表示には直接は関係なく、新たな HTTPリクエストが発行される。*GETのみ(下図参照)
renderはcreateアクションに指定した値を直接表示させることがわかる。 f:id:meo2:20210502201821p:plain

ユーザー登録時にflashメッセージを表示させる。

ユーザー登録に成功したときと失敗したときに、メッセージを表示させる。

#...
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to login_path, success: 'ユーザー登録が完了しました'
    else
      flash.now[:danger] = 'ユーザー登録に失敗しました'
      render :new
    end
  end
#...

追記

ログイン前のヘッダーにあるログインボタンのhttpリクエストをgetにする。 ここがpostとかになっているとヘッダーのログインボタンからログインページにいったときにflashメッセージが表示されてしまう。

<%= link_to 'ログインする', login_path, method: :get, class: 'nav-link' %>

link_toメソッド

Railsでは主にviewファイルで使用され、引数を指定することでリンクを生成する。 <%= link_to 'リンクの文言', 'パス', class: 'クラス名', method: :HTTPリクエスト %>

methodでリンク先のルーティングを指定できる。

i18nによる日本語化

i18nを使用しユーザー登録画面とログイン画面を日本語化する。
i18n国際化(Internationalization)はRailsにおいて、言語の翻訳機能があるgemである。
実行環境

  • Rails5.2.3
  • Ruby2.6.4

gem 'rails-i18n'をGemfileに記述し、bundleする。

# rails5系なら以下を記述
gem 'rails-i18n', '~> 5.1' 

# rails4系なら以下を記述
gem 'rails-i18n', '~> 4.0'

config/application.rbにデフォルトの言語を日本語にする設定とロケールファイルを読み込むPATHを設定。

config.i18n.default_locale = :ja

config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

config/locales以下にロケールファイルを作成する。 モデル名、モデル属性に関する日本語化はactiverecord/ja.ymlに記述。
ビューに関する日本語化はviews/ja.ymlに記述。

モデル側の日本語化

モデルは全て activerecord 以下に記述する。 models以下に日本語化するモデルを指定、attributes以下にはモデルの属性を指定する。

ユーザー登録画面 modelに関する翻訳ファイル

ja:
 activerecord:
   models:
    user: 'ユーザー'
   attributes:
     user:
       last_name: '姓'
       first_name: '名'
       email: 'メールアドレス'
       password: 'パスワード'
       password_confirmation: 'パスワード確認'

Modelに紐づくform_withのlabelはRailsがロケートファイルの翻訳を適応してくれる。 一方でform_withの引数にModelを渡してないときはモデルと紐付いていないので、label自身に翻訳を適応する記述をする必要がある。また、国際化の際はlazy lookup機能を使用する。

     <%= form_with model:@user, local: true do |f| %>
        <div class="form-group">
          <%= f.label :last_name %>
          <%= f.text_field :last_name, class: 'form-control', id: 'user_last_name' %>
        </div>
        <div class="form-group">
          <%= f.label :first_name %>
          <%= f.text_field :first_name, class: 'form-control', id: 'user_first_name' %>
        </div>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.text_field :email, class: 'form-control', id: 'user_email' %>
        </div>
        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: 'form-control', id: 'user_password' %>
        </div>
        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: 'form-control', id: 'user_password_confirmation' %>
        </div>
        <%= f.submit (t 'defaults.register'), class: 'btn btn-primary'%>
        <%end%>
      <div class='text-center'>
      <%= link_to (t 'defaults.to_login_page'), login_path%>

view側の日本語化

ログイン画面 viewに関する翻訳ファイル

ja:
  defaults:
    login: 'ログイン'
    register: '登録'
    title: 'ユーザー登録'
    to_login_page: 'ログインページへ'
  user_sessions:
     new:
       Back: '戻る'
       title: 'ログイン'
       email: 'メールアドレス'
       password: 'パスワード'
       to_register_page: '登録ページへ'

ログイン画面はform_withの引数がurlでモデルと紐付いていないのでlabelに翻訳を適応させる記述をする。 Model.human_attribute_name(:カラム名)で属性名を取得する。

<%= link_to (t '.Back'), users_path %>
<div class="container">
 <div class="row">
   <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
     <h1><%= t '.title' %></h1>
     <%= form_with url: login_path, local: true do |f| %>
       <div class="form-group">
         <%= f.label :email, User.human_attribute_name(:email) %>
         <%= f.text_field :email, class: "form-control" %>
       </div>
       <div class="form-group">
         <%= f.label :password, User.human_attribute_name(:password) %>
         <%= f.password_field :password, class:"form-control" %>
       </div>
       <div class="actions">
         <%= f.submit (t 'defaults.login'), class: "btn btn-primary" %>
       </div>
     <% end %>
     <div class='text-center'>
       <%= link_to (t '.to_register_page'), new_user_path %>

gem sorceryを使用し、ユーザー登録、ログイン機能を実装

概要

sorceryとは、railsで作成されたアプリケーションに対し、ログイン機能を実装させるgem。
認証用gemであるdeviseと比較すると、sorceryはある程度のコードは自分で書かなければいけなく、deviseよりコードを書く量が多いがその分カスタマイズ性があり、エラーが発生したときなど自分で書いたコードなのでエラーの特定がしやすい。

導入

sorcery wikiを参考にし、gemの導入からユーザー登録、ログイン、ログアウト機能を実装する。
実行環境

  • Rails 5.2.3
  • Ruby 2.6.4
  • bootstrap 4.3.1
  • node 12.14.0

まずは、Gemfileにgem 'sorcery'を追加し、bundle installする。

gem 'sorcery'

sorceryによって追加されたジェネレータを実行しrails db:migrateすると、 userモデルとマイグレーションファイルが作成され、rails db:migrateによりusersテーブルが作成される。

$ rails g sorcery:install
      create  config/initializers/sorcery.rb
    generate  model User --skip-migration
      invoke  active_record
      create    app/models/user.rb
      insert  app/models/user.rb
      insert  app/models/user.rb
      create  db/migrate/20210429103839_sorcery_core.rb

$rails db:migrate
== 20210429103839 SorceryCore: migrating ======================================
-- create_table(:users)
   -> 0.0016s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0006s
== 20210429103839 SorceryCore: migrated (0.0024s) =============================

今回は、ユーザ登録に姓名、email、password、password confirmationを入力できるようにしたいので、データベースにlast_name、first_nameカラムをstring型で追加する。

$ rails g migration AddLastNameToUsers last_name:string
#マイグレーションファイルを編集
  def change
    add_column :users, :last_name, :string
  end
rails db:migrate  #first_nameも同様に作成する。

SQLで作成されているか確認。

sqlite> PRAGMA TABLE_INFO(users);
0|id|integer|1||1
1|email|varchar|1||0
2|crypted_password|varchar|0|NULL|0
3|salt|varchar|0|NULL|0
4|created_at|datetime|1||0
5|updated_at|datetime|1||0
6|last_name|varchar|0||0
7|first_name|varchar|0||0

ユーザーの作成

ジェネレータを使用し、usersコントローラとビューの作成。

$rails g scaffold_controller user email:string crypted_password:string salt:string
     create  app/controllers/users_controller.rb
      invoke  erb
      create    app/views/users
      create    app/views/users/index.html.erb
      create    app/views/users/edit.html.erb
      create    app/views/users/show.html.erb
      create    app/views/users/new.html.erb
      create    app/views/users/_form.html.erb

viewの作成

form_withについて

form_withとは、モデルオブジェクトを使ってhtmlのフォームを作成するためのヘルパーメソッドである。 form_withにはmodel:とurl:オプションがあり使い分ける必要がある。 入力された情報がデータベースへ保存しない(モデルとフォームが紐付かない)場合はurl:オプションを使う。

<%= form_with url: "パス" do |form| %>
<% end %>

データベースへ保存するときは、model:オプションを使う。

<%= form_with model: モデルクラスのインスタンス do |form| %>
<% end %>

今回は、新規登録内容をデータベースへ保存する必要があるのでmodel:オプションを使用する。form_withにmodel: @userという引数を渡すことで、モデルとフォームを対応づける。また、form_withはデフォルトでajax通信になるのでlocal: trueをつける。

<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1>ユーザー登録</h1>
      <%= form_with model:@user, local: true do |f| %>
        <div class="form-group">
          <%= f.label :last_name %>
          <%= f.text_field :last_name, class: 'form-control', id: 'user_last_name' %>
        </div>
        <div class="form-group">
          <%= f.label :first_name %>
          <%= f.text_field :first_name, class: 'form-control', id: 'user_first_name' %>
        </div>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.text_field :email, class: 'form-control', id: 'user_email' %>
        </div>
        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: 'form-control', id: 'user_password' %>
        </div>
        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: 'form-control', id: 'user_password_confirmation' %>
        </div>
        <%= f.submit '登録する', class: 'btn btn-primary'%>
        <%end%>
      <div class='text-center'>
      <%= link_to 'ログインページへ', login_path%>     
      </div>
    </div>
  </div>
</div>

ログイン状態に応じてヘッダー表示を切り替える。

logged_in?メソッドを使用する。 ログイン前はログインボタン、ログイン後は一覧やログアウトボタンを表示するようになる。

#...
   <body>
   <% if logged_in? %>
     <%= render 'shared/header'%>
   <% else %>
     <%= render 'shared/before_login_header'%>
   <% end %>
   <%= yield %>
   <%= render 'shared/footer'%>
  </body>
</html>

_form.html.erbにパスワードを保持する仮想フィールドを追加

<div class="field">
   <%= form.label :password %><br />
   <%= form.password_field :password %>
</div>
<div class="field">
   <%= form.label :password_confirmation %><br />
   <%= form.password_field :password_confirmation %>
</div>

controllerの作成

ユーザー登録後はログイン画面へリダイレクトするようにし、ストロングパラメータでは、last_name,first_name,password,password_confirmationを受け取れるようにする。

ストロングパラメータについて

Rails4から導入された「Mass-assignment」という脆弱性へのセキュリティ対策である。テーブルに登録されるすべてのデータはストロングパラメータによる検証をクリアしなければ登録できなくなっている。例えば、@user =User.new(user_params)を(params[:user])に変更して実行すると「ActiveModel::ForbiddenAttributesError 」という例外が発生してしまう。user_paramsメソッドに記載されたコードは「paramsが:userというキーを持っていて、かつparams[:user]が、:email, :last_name, :first_name, :password, :password_confirmation, :crypted_password, :saltというキーを持つハッシュである」ことを確認し、登録できるカラムを特定し、それ以外のカラムへのアクセスを禁止している。よって、許可されたカラムしか登録、更新ができなくなる。

class UsersController < ApplicationController
  before_action :set_user, only: %i[show edit update destroy]
  skip_before_action :require_login, only: %i[new create index]
#...
  def new
    @user = User.new 
#インスタンス変数にuser#newアクションからviewに渡したいデータを入れる
  end

  def create
    @user = User.new(user_params) 
#user#createアクションで@userにuser_paramsに入ったデータを代入。
    if @user.save #データを保存
      redirect_to login_path, notice: 'success'
#login_path ヘルパーメソッドを使用。保存が成功したらログインページへ
    else
      render :new #保存が失敗したら登録画面へ
    end
  end
#...

  private

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

  def user_params
    params.require(:user).permit(:email, :last_name, :first_name, :password, :password_confirmation, :crypted_password, :salt)
  end
end

Modelの作成

モデルファイルにバリデーションを設定する。authenticates_with_sorcery!はuserモデルにsorceryによる認証機能をもたせている。 passwordとpassword_confirmationはuserモデルのカラムには存在しない。crypted_passwordカラムの仮想属性である。sorceryによってcrypted_passwordカラムの内容であると認識される。

class User < ApplicationRecord
  authenticates_with_sorcery!
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
#ここでcrypted_passwordとして認識される。
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, presence: true, uniqueness: true
  validates :last_name, presence: true, length: { maximum: 250 }
  validates :first_name, presence: true, length: { maximum: 250 }
end

ログイン、ログアウト機能の実装

ジェネレータでuser_sessions_controllerを作成する。

$ rails g controller UserSessions new create destroy 
      create  app/controllers/user_sessions_controller.rb
      invoke  erb
      create    app/views/user_sessions
      create    app/views/user_sessions/new.html.erb
      create    app/views/user_sessions/create.html.erb
      create    app/views/user_sessions/destroy.html.erb
Routerを定義する。

ルーティングを以下のように定義。

Rails.application.routes.draw do
  root to: 'users#index'
  get '/login', to: 'user_sessions#new', as: :login
  post '/login', to: 'user_sessions#create'
  post '/logout', to: 'user_sessions#destroy', as: :logout
  resources :users
end

controllerの作成

redirect_back_or_toメソッドはsorceryでフレンドリーフォーワーディングを実現するための機能である。 ログインが必要なページに未ログイン状態でアクセスした場合、ログイン画面に遷移させてログイン後はログインが必要なページへ飛ばす。

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new; end

  def create
    @user = login(params[:email], params[:password])
#emailによるユーザ検索、passwordの検証を行いUserレコードのid値をセッションに格納。
    if @user
      redirect_back_or_to root_path, notice: 'Login successful!!!'
#ログイン後はトップページへリダイレクト
    else
      flash.now[:alert] = 'Login failed'
      render action: 'new'
    end
  end

  def destroy
    logout
    redirect_to(root_path, notice: 'Logged out!')
#ログアウト後トップページへリダイレクト
  end
end

セッションviewの作成

ログインフォームはモデルと紐付かないので、form_withのオプションはurl:を使用する。フォーム送信先は/loginなのでlogin_pathとする。

<%= link_to 'Back', users_path %>
<div class="container">
 <div class="row">
   <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
     <h1>ログイン</h1>
     <%= form_with url: login_path, local: true do |f| %>
       <div class="form-group">
         <%= f.label :email %>
         <%= f.text_field :email, class: "form-control" %>
       </div>
       <div class="form-group">
         <%= f.label :password %>
         <%= f.password_field :password, class:"form-control" %>
       </div>
       <div class="actions">
         <%= f.submit "ログイン", class: "btn btn-primary" %>
       </div>
     <% end %>
     <div class='text-center'>
       <%= link_to '登録ページへ', new_user_path %>
     </div>
   </div>
 </div>
</div>

これでユーザー登録、ログイン・ログアウト機能の完成。

テストバグ修正

環境

  • rbenv local 2.6.6
  • nodenv local 12.14.0

rails test:systemでテストを実行したところ
sign up success
login
logout
login faild not exist user
visit about pageができない

sign upは検証ツールで確かめると、viewからフォーム送信はできていることがわかる。 f:id:meo2:20210419205722p:plain

users_controller.rbのcreateアクションを確認しbinding.irbをいれたあとブラウザで再度パスワードを入力

def create
    @user = User.new(user_params)
    binding.irb
    if @user.save
      log_in @user
      flash[:success] = "成功"
      redirect_to @user
    else
      render :new
    end
  end

paramsで検証ツールでみたようにパスワードが送れてることが確認する。

irb(#<UsersController:0x00007fa315031c58>):001:0> params
=> <ActionController::Parameters {"authenticity_token"=>"dewo4jc5+wMJcaAOyaReiUi+UIiYyCTQe4rrq8Kg3HIYD03MBpptBSOXbOONtPckOTnX5d1SA/u6SZXhM5X9Og==", "user"=><ActionController::Parameters {"name"=>"meo", "email"=>"meo@gmail.com", "password"=>"1111", "password_confirmation"=>"1111"} permitted: false>, "commit"=>"Create my account", "controller"=>"users", "action"=>"create"} permitted: false>

@userが作れてるか確認するとpassword_digestがnilになっているので@user = User.new(user_params)が間違っている

irb(#<UsersController:0x00007fa315031c58>):002:0> @user
=> #<User id: nil, name: "meo", email: "meo@gmail.com", created_at: nil, updated_at: nil, password_digest: nil>

irb(#<UsersController:0x00007fa315031c58>):003:0>  @user = User.new(user_params)
Unpermitted parameter: :password

パスワードが許可されてないとでるのでuser_paramsをさらに確認するとusers_controller.rbのuser_paramsアクションにpasswordが抜けていることがわかる。

passwordを追加。

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

irbをexitで終了し再度ブラウザでPWを入力し@userを実行すると、[FILTERED]と表示されているので値がはいってることがわかる。

irb(#<UsersController:0x00007fa310d78510>):001:0> @user
=> #<User id: nil, name: "meo", email: "meo@gmail.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>

@user.valid?でバリデーションを通るか確認するとtrueになったのでうまくいっている。

irb(#<UsersController:0x00007fa3114c6bc8>):001:0> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "meo@gmail.com"], ["LIMIT", 1]]
  ↳ (irb):1:in `create'
=> true

exitで終了するとCompleted 500 Internal Server Error in 30608ms (ActiveRecord: 3.2ms | Allocations: 67204)NoMethodError (undefined method `log_in' for #<UsersController:0x00007fa3114c6bc8> Did you mean? login_url):というエラーが出る。

これは新規作成はできたがlog_inというメソッドがないエラーであり、エディターでlog_inで検索するとsessions_helper.rbに書かれている。 ヘルパーはclass UsersController < ApplicationControllerのどちらかで読み込まれてることが多い application_controller.rbを見ると何も書かれていないので include SessionsHelperを追加しヘルパーを読み込ませる。 再度ブラウザから入力し@userとuser.valid?とlog_inをirbで通ることを確認してexitするとログインした状態になる。

binding.rbを削除しサーバーを再起動 メールアドレスとパスワードを入力しログインできることを確認してテストを実行rails test:system f:id:meo2:20210420023217p:plain sign up successとlogin logoutは解決した。

login faild not exist user

存在しないユーザーでログインすると失敗する emailとパスワードを適当にいれてログインすると以下のエラーになる。 f:id:meo2:20210420023657p:plain
authenticateメソッドはユーザーを認証するときのメソッドである authenticateメソッドは定義されてないとでるので調べるとnilとでてくる

>>  user.authenticate(params[:session][:password])
NoMethodError: undefined method `authenticate' for nil:NilClass
    from /Users/owner/exam1/Exam_RailsBasic_01/app/controllers/sessions_controller.rb:6:in `create'
>> user
=> nil

>>  user = User.find_by(email: params[:session][:email].downcase)
=> nil

>> params[:session][:email].downcase
=> "admin@example.com"

find byメソッドは存在しない場合nilをかえす。nilに対してメソッドを実行しようとしているのでエラーになっている。 また、findメソッドの場合はactiverecord例外が発生して500エラーになる。 →ぼっち演算子を使う。 &.(ぼっち演算子)はレシーバーであるオブジェクトに対してあるメソッドを実行した時、そのオブジェクトがnilの場合、nilを返すことでエラーを出さなくする

>>  user&.authenticate(params[:session][:password])
=> nil

visit about page aboutページへいくとエラーになる

f:id:meo2:20210420024827p:plain
ルーティングエラーなのでrails routesで調べると f:id:meo2:20210420024902p:plain

static_pages_about GET /static_pages/about(.:format)
static_pages#aboutが間違っている。

localhost:3000/static_pages/aboutへアクセスしてみると f:id:meo2:20210420025127p:plain static_pages_controllerにaboutアクションがないとでるのでstatic_pages_controller.rbにaboutアクションを作る

class StaticPagesController < ApplicationController
  def top; end

  def about; end

end

※中身のないメソッドについて
def about; end

def about
end

def about
render :about
end

これらはすべて同じ書き方でrenderは省略されている。

aboutアクション作成後、再度アクセスするとaboutアクションはあるがそれに対応するテンプレートがないとでた。 f:id:meo2:20210420030051p:plain viewsを調べたらファイル形式が違っていて認識されていないことがわかった。

owner@ownernoMacBook-Pro Exam_RailsBasic_01 % ls app/views/static_pages
about.rb        top.html.erb

about.rb→about.html.erbに修正。

あとはlocalhost:3000/static_pages/aboutからlocalhost:3000/aboutへルーティングを変更するだけ get 'static_pages/about’からget '/about', to: 'static_pages#about'へ変更

f:id:meo2:20210420030447p:plain
テストが通ることを確認。