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
テストが通ることを確認。

Rspec実行時におけるエラー

エラー内容

system specを用いて新規作成機能のコードをテストした際、
「Failure/Error: fill_in '名称', with: task_name」と
「Capybara::ElementNotFound: Unable to find field "タイトル"」エラーがでた。

エラー画面

f:id:meo2:20210406035329p:plain

「名称」は無効になっているので使用することはできないということから、テストコードに書かれているfieldの名称が異なっていることがわかる。

エラー解決手順

chromeデベロッパーツールを使用しfieldの名称を確かめる。

検証画像 f:id:meo2:20210406040511p:plain テストするタスク新規登録画面のフォーム入力のところにカーソルを近づけ調べるとfill_inのname属性と異なっていることがわかった。’名称’ではなく、'task[name]'であった。

fill_inについて

fill_inはフォーム入力を行うことができる「fill_in 'フォーム名' , with: フォームへ入力する文字列」と記述する。フォーム名にはname属性を記述される。

②name属性が違っていたので、テストコードを正しい記述へ変更する。

変更前のテストコード

describe '新規作成機能' do
    let(:login_user) { user_a }

    before do
      visit new_task_path
      fill_in '名称', with: task_name</span>
      click_button '登録する'
    end

    context '新規作成画面で名称を入力したとき' do
      let(:task_name) { '新規作成のテストを書く' }
      it '正常に登録される' do
        expect(page).to have_selector '.alert-success', text: '新規作成のテストを書く'
      end
    end

    context '新規作成画面で名称を入力しなかったとき' do
      let(:task_name) { '' }

      it 'エラーとなる' do
        within '#error_explanation' do
          expect(page).to have_content '名称を入力してください'</span>
        end
      end
    end
  end

変更後

describe '新規作成機能' do
    let(:login_user) { user_a }

    before do
      visit new_task_path
      fill_in 'task[name]', with: task_name
      click_button '登録する'
    end

    context '新規作成画面で名称を入力したとき' do
      let(:task_name) { '新規作成のテストを書く' }
      it '正常に登録される' do
        expect(page).to have_selector '.alert-success', text: '新規作成のテストを書く'
      end
    end

    context '新規作成画面で名称を入力しなかったとき' do
      let(:task_name) { '' }

      it 'エラーとなる' do
        within '#error_explanation' do
          expect(page).to have_content 'Nameを入力してください'
        end
      end
    end
  end

再度テストを実行し確認。

f:id:meo2:20210406044159p:plain 無事にエラーが解決。

追記

bundle exec rspecについて

Rspecには表示を詳細に出力する--format documentationというものがある。上記画像のテストコードは-fdオプションで実行している。

-fdオプションの使い方

テスト実行時に以下のように指定

$ bundle exec rspec -fd

または、.rspecに下記の記述を追加する。

--format documentation

どのように表示が変わるか

通常時 bundle exec rspec では . で表示される。

$ bundle exec rspec spec/system/tasks_spec.rb 
2021-04-06 04:42:19 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.
Capybara starting Puma...
* Version 3.12.6 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:61777
.....

Finished in 3.44 seconds (files took 1.82 seconds to load)
5 examples, 0 failures

-fdオプションを指定するとテストコードのcontextとitを表示してくれる。

$ bundle exec rspec -f d  spec/system/tasks_spec.rb
2021-04-06 04:07:10 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

タスク管理機能
  一覧表示機能
    ユーザーAがログインしているとき
      behaves like ユーザーAが作成したタスクが表示される
Capybara starting Puma...
* Version 3.12.6 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:53137
        is expected to have text "最初のタスク"
    ユーザーBがログインしているとき
      ユーザーAが作成したタスクが表示されない
  詳細表示機能
    ユーザーAがログインしているとき
      behaves like ユーザーAが作成したタスクが表示される
        is expected to have text "最初のタスク"
  新規作成機能
    新規作成画面で名称を入力したとき
      正常に登録される
    新規作成画面で名称を入力しなかったとき
      エラーとなる

Finished in 3.52 seconds (files took 1.8 seconds to load)
5 examples, 0 failures

ActiveRecordエラー

ActiveRecord::StatementInvalid
SQLite3::SQLException: no such column: tasks.user_id:

error文に書いてあるとおりtasksテーブルにuser_idカラムがないというエラーである。 データベース上でUserとTaskを紐付ける際に発生した。 まず、ジェネレータでマイグレーションを作成し、

$rails g migration AddUserIdToTasks 

マイグレーションファイルを以下のように編集しマイグレーションを実行した。

class AddUserIdToTasks < ActiveRecord::Migration[5.2] 
  def up
    execute 'DELETE FROM tasks;'
    add_reference :tasks, :user, null: false, index: true
  end

  def down
    remove_reference :tasks, :user, index: true
  end
end

$bin/rails db:migrate

データベースの状態を確認したところ、マイグレーションを実行したにもかかわらず、データベースにuser_idカラムはつくられていないことがわかった。

#データベースへ接続
$rails db
=>SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
#データベースの一覧を確認
sqlite>.database
#テーブル一覧を取得
sqlite>.tables 
ar_internal_metadata  tasks               
schema_migrations     users               
#テーブルの中のカラムを確認(今回はtasksテーブル)
sqlite> PRAGMA TABLE_INFO(tasks);
0|id|integer|1||1
1|name|varchar|1||0
2|description|text|0|NULL|0
3|created_at|datetime|1||0
4|updated_at|datetime|1||0
sqlite> .exit

結論

ジェネレータの書き方が間違っていた。マイグレーションファイルにchangeメソッドがなかった。書籍では、bin/rails g migration AddUserIdToTasksで実行しているが、 「rails g migration Addカラム名Toテーブル名 カラム名:カラムのデータ型」このように、カラム名とカラムのデータ型を追加したら Tasksテーブルにuser_idカラムを追加することができた。マイグレーションをやり直すために、最初に作成したファイルを削除する必要がある。

#まず、ステータスを確認する。
$rails db:migrate:status
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20210323195016  Create tasks
   up     20210329092418  Change tasks name not null
   up     20210329175420  Create users
   up     20210329190039  Add admin to users
  down    20210331174646  Add user id to tasks

#upの場合、「$bin/rails db:migrate:down VERSION=migration_file_id」を実行し、downにする。
#downは、まだデータベースへ反映されていないので、削除しても問題ない。削除を実行。
$rm db/migrate/20210331174646_add_user_id_to_tasks.rb

#削除できたかステータスを確認。
$rails db:migrate:status
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20210323195016  Create tasks
   up     20210329092418  Change tasks name not null
   up     20210329175420  Create users
   up     20210329190039  Add admin to users

削除が確認できたらマイグレーションを「rails g migration Addカラム名Toテーブル名 カラム名:カラムのデータ型」で作成。

$bin/rails g migration AddUserIdToTasks user_id:integer
Running via Spring preloader in process 65692
      invoke  active_record
      create    db/migrate/20210401072016_add_user_id_to_tasks.rb

マイグレーションファイルを以下のように編集し、マイグレーションを実行。 マイグレーションファイルでは、changeメソッドの中でテーブルが作成される。

class AddUserIdToTasks < ActiveRecord::Migration[5.2]
  def change
    add_column :tasks, :user_id, :integer
  end

  def up
    execute 'DELETE FROM tasks;'
    add_reference :tasks, :user, null: false, index: true
  end

  def down
    remove_reference :tasks, :user, index: true
  end
end

$bin/rails db:migrate
== 20210401072016 AddUserIdToTasks: migrating =================================
-- add_column(:tasks, :user_id, :integer)
   -> 0.0010s
== 20210401072016 AddUserIdToTasks: migrated (0.0011s) ========================

#ステータスを確認
$bin/rails db:migrate:status
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20210323195016  Create tasks
   up     20210329092418  Change tasks name not null
   up     20210329175420  Create users
   up     20210329190039  Add admin to users
   up     20210401072016  Add user id to tasks

user_idカラムが作成されているかデータベースを確認。

$rails db
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.

sqlite> .databases
sqlite> .tables
ar_internal_metadata  tasks               
schema_migrations     users               

sqlite> PRAGMA TABLE_INFO(tasks);
0|id|integer|1||1
1|name|varchar|1||0
2|description|text|0|NULL|0
3|created_at|datetime|1||0
4|updated_at|datetime|1||0
5|user_id|integer|0||0

sqlite> .exit

無事にuser_idカラムが作成され、解決。