Chapter 21

4-2. 学習ログ投稿機能作成

Masuyama
Masuyama
2022.10.15に更新

学習ログ投稿機能作成

今回は、学習ログを投稿するページと機能を作成していきましょう。

Controller 作成

rails gコマンドを使用し、Controller ファイル一式を作成します。
まず「投稿」機能を作成するため、new アクションは追加しておきます。

$ bundle exec rails g controller posts new
      create  app/controllers/posts_controller.rb
       route  get 'posts/new'
      invoke  tailwindcss
      create    app/views/posts
      create    app/views/posts/new.html.erb
      invoke  rspec
      create    spec/requests/posts_spec.rb

基本的なファイルは一式で作成されたので、それぞれ編集していきます。

なお、学習ログ関連機能を作る上ではあくまで実装の流れを理解することを優先していただくため、
テストではなく先に実装から作っていくことにします。
(もちろん、テスト駆動開発に慣れるためにテストから先に書いていただいても問題ありません)

実装

ルーティング

rails g実行により追加されたルーティング get 'posts/new' は削除し、
recourcesを使用したルーティングに変更します。

投稿機能では newcreateアクションが必要なので、それぞれを許可するルーティングを設定しましょう。

config/routes.rb

Rails.application.routes.draw do
  # get 'posts/new' # この1行を削除
  devise_for :users
  root 'home#top'

  resources :posts, only: [:new, :create] # 追加
end

この時点で、開発用サーバを起動 (bin/dev)して http://localhost:3000/posts/new へアクセスすると デフォルトのnew.html.erb の中身が表示されます。

image

次は実際に投稿機能を修正していきましょう。

Controller (#new, #create)

PostsController に newcreate アクションを用意してあげます。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :authenticate_user!

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    @post.user_id = current_user.id # ログインユーザのIDを代入
    if @post.save
      flash[:notice] = '投稿しました'
      redirect_to root_path # 一時的にトップページへリダイレクト(要修正)
    else
      flash[:alert] = '投稿に失敗しました'
      render :new
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end

中身としては基本的なものですが、簡単に解説しておきます。

  1. まず before_action でログインしているかどうかを判断する。
    1. ログインしていればそのまま new アクションへ進む。
    2. ログインしていなければログインページへリダイレクトする。
  2. new アクションでは空の Post モデルオブジェクトを用意し、view から渡されたパラメータを受け付ける。
  3. そのユーザー ID はログインしているユーザーの ID をセットする。
  4. Post の保存に成功 or 失敗で処理を分岐する。
    1. 成功すれば、成功時のフラッシュメッセージとともにトップページへリダイレクトする。
    2. 投稿の保存に失敗すれば、失敗時のフラッシュメッセージとともに投稿画面を再表示させる。

最初にログインユーザーかどうかで処理を分岐させていることがポイントです。
before_action :authenticate_user! も、devise を導入していると使える便利なヘルパーメソッドの一つです。

なお、Post の保存に成功した時、本来は学習ログの一覧画面へリダイレクトさせますが、
まだそのページを用意していないため、一時的にトップページへリダイレクトさせています。

次は投稿画面を用意します。

View

new アクション用の view も rails g コマンド実行時に生成されていますので、中身を修正します。
TailwindCSS を用いてデザインを調整した結果がこちらです。

app/views/posts/new.html.erb

<%= form_with model: @post, class: "space-y-6 w-3/4 max-w-lg" do |f| %>
  <label class="block text-xl font-bold text-gray-700">学習ログ投稿</label>
  <div class="mt-1">
    <label class="text-gray-700 text-lg">
      タイトル
    </label>
    <%= f.text_field :title, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm placeholder-gray-400 border border-gray-300 rounded-md", placeholder: "Railsチュートリアル1章を完了" %>
  </div>
  <div class="mt-1">
    <label class="text-gray-700 text-lg">
      本文
    </label>
    <%= f.text_area :content, rows: "5", class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm placeholder-gray-400 border border-gray-300 rounded-md", placeholder: "環境構築を無事に終えることができた!" %>
  </div>
  <p class="mt-2 text-sm text-gray-500">
    学習したこと、開発したことを記録しましょう。<br>
    参考にしたサイトがあればURLを書いておくことをオススメします。
  </p>
  <div class="px-4 py-3 text-right sm:px-6">
    <%= f.submit "ログを記録", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
  </div>
<% end %>

form_with でControllerから渡されたインスタンス変数 @post とフォームを結びつけています。

また、デザインとしてはこのような見た目になっています。

image

翻訳ファイル修正

今回追加した処理に関し、devise の翻訳が必要となる箇所があるので翻訳ファイルに対訳を追加しましょう。

先ほど、学習ログの投稿時にはログインを必要とするよう、PostsController で設定しましたね。

app/controllers/posts_controller.rb (再掲)

class PostsController < ApplicationController
  before_action :authenticate_user!
  ...

これにより、ログイン前に投稿画面にアクセスするとログインを促すフラッシュメッセージが devise の仕組みにより表示されるのですが、
現在はその対訳が存在しないためエラーメッセージが表示されます。

image

そのため、表示されている通りの階層で、翻訳ファイルに対訳を追記してあげましょう。

...
  devise:
    failure:
      invalid: "%{authentication_keys}またはパスワードが違います。"
      user:
        unauthenticated: "ログインしてください。" # 追加
      ...

これにより、先ほどのエラーメッセージが「ログインしてください。」という日本語のメッセージに変わります。

image

ナビゲーションバーにリンク追加

最後に、ナビゲーションバーに投稿ページへのリンクを追加してあげます。
投稿はログイン状態でしか使えないため、ログイン時にのみ表示されるリンクとして追加します。

app/views/shared/_navbar.html.erb

...
        <% if current_user %>
          <%# ここから追加 %>
          <li>
            <%= link_to "ログ投稿", new_post_path, class: "block py-2 pr-4 pl-3 text-gray-400 hover:text-white border-b border-gray-700 hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-blue-white md:p-0" %>
          </li>
          <%# ここまで追加 %>
          <li>
            <%= button_to "ログアウト", destroy_user_session_path, class: "block py-2 pr-4 pl-3 text-gray-400 hover:text-white border-b border-gray-700 hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-blue-white md:p-0", method: :delete %>
          </li>
        <% else %>
        ...

動作確認

では、各種動作を画面で確認していきましょう。

初期表示

開発用サーバを起動し、http://localhost:3000/posts/newにアクセスしてください。
フォームが表示されていれば OK です。

image

投稿成功時

適当なタイトル、本文を入れて「ログを記録」ボタンを選択すると、成功時のフラッシュメッセージとともにトップページへリダイレクトします。

image

投稿失敗時

本文(またはタイトル)を空欄にして「ログを記録」ボタンを選択すると、失敗時のフラッシュメッセージとともに投稿画面が再表示されます。
この時、入力していた内容 (ここではタイトル) は残ったまま再表示されていることに注意してください。

image

ログアウト時

ログアウトしてからhttp://localhost:3000/posts/new に直接アクセスします。
先ほど確認した通り、ログインを促すメッセージと共にログイン画面へリダイレクトします。

image

ここまで画面で確認できれば OK です。

ナビゲーションバー(ログイン時)

ログイン時は「ログ投稿」というリンクが表示されていることを確認します。

image

ナビゲーションバー(ログアウト時)

ログアウト時は「ログ投稿」というリンクが表示されていないことを確認します。

image

テスト

さて、実装は完了したので上記の動作を保証するテストを書いていきましょう。

繰り返しとなりますが、今回は理解のしやすさからテストではなく実装を先に行っています。
そのため動作確認は先ほど終わっており、もはやテストを書く意味がないのでは?と思うかもしれません。
しかし、今後さまざまな変更を加える際、動作を保証するテストがないと変更をするのが不安になりますし、改めて動作確認をするのは手間です。
そのため、後追いでも構わないので必ずテストを書く習慣はつけるようにしましょう。

Request Spec

まずは、新しく作った投稿ページへのアクセスを確認する Request Spec を追加していきます。
Request Spec のテストファイル自体は rails g コマンド実行時に生成されているので、中身を編集するだけで OK です。

spec/requests/posts_spec.rb

require 'rails_helper'

RSpec.describe 'Posts', type: :request do
  before { @user = create(:user) } # 各テストで使用できるユーザーを作成

  describe 'GET /posts/new' do
    context 'ログインしていない場合' do
      it 'HTTPステータス302を返す' do
        get '/posts/new'
        expect(response).to have_http_status(302)
      end

      it 'ログインページにリダイレクトされる' do
        get '/posts/new'
        expect(response).to redirect_to '/users/sign_in'
      end
    end

    context 'ログインしている場合' do
      before { sign_in @user }
      it 'HTTPステータス200を返す' do
        get '/posts/new'
        expect(response).to have_http_status(200)
      end

      it 'ログインページにリダイレクトされない' do
        get '/posts/new'
        expect(response).not_to redirect_to '/users/sign_in'
      end
    end
  end
end

ログイン状態によってリダイレクトされる処理を含め、アクセス性を確認するテストを作成しました。
ここで一旦、テストを実行してみてください。

$ bundle exec rspec spec/requests/posts_spec.rb
...
  1) Posts GET /posts/new ログインしている場合 HTTPステータス200を返す
     Failure/Error: before { sign_in @user }
     
     NoMethodError:
       undefined method `sign_in' for #<RSpec::ExampleGroups::Posts::GETPostsNew::Nested_2 "HTTPステータス200を返す" (./spec/requests/posts_spec.rb:21)>
     # ./spec/requests/posts_spec.rb:20:in `block (4 levels) in <top (required)>'
...

RSpec でヘルパーメソッド sign_in を使えないというエラーが出て、コケました。

これは、以前 rails_helper.rbDevise::Test::IntegrationHelpers を読み込む設定を追加した際、
System Spec でしか使わないという設定をしていたためです。

今回、Request Spec でも sign_in メソッドが必要となったため、以下のように1行を追記してください。

spec/rails_helper.rb

  config.include Devise::Test::IntegrationHelpers, type: :system
  config.include Devise::Test::IntegrationHelpers, type: :request # 追加

もう一度テストを実行しましょう。
今度は通るはずです。

$ bundle exec rspec spec/requests/posts_spec.rb

Posts
  GET /posts/new
    ログインしていない場合
      HTTPステータス302を返す
      ログインページにリダイレクトされる
    ログインしている場合
      HTTPステータス200を返す
      ログインページにリダイレクトされない

Finished in 0.20681 seconds (files took 1.33 seconds to load)
4 examples, 0 failures

これで Request Spec は完了です。
次は System Spec を作成してあげましょう。

System Spec

System Spec は自動では作られていないので、まずはファイル自体を作成します。

$ touch spec/system/posts_spec.rb

作った System Spec の中身は以下のようにします。
基本的にはブラウザで動作確認した時と同一の内容をテストしています。

spec/system/posts_spec.rb

require 'rails_helper'

describe 'Post', type: :system do
  before do
    driven_by :selenium_chrome_headless # ヘッドレスモードで実行
    @user = create(:user) # ログイン用ユーザー作成
  end

  # 投稿フォーム
  let(:title) { 'テストタイトル' }
  let(:content) { 'テスト本文' }

  describe 'ログ投稿機能の検証' do
    # ログ投稿を行う一連の操作を subject にまとめる
    subject do
      fill_in 'post_title', with: title
      fill_in 'post_content', with: content
      click_button 'ログを記録'
    end

    context 'ログインしていない場合' do
      before { visit '/posts/new' }
      it 'ログインページへリダイレクトする' do
        expect(current_path).to eq('/users/sign_in')
        expect(page).to have_content('ログインしてください。')
      end
    end

    context 'ログインしている場合' do
      before do
        sign_in @user
        visit '/posts/new'
      end
      it 'ログインページへリダイレクトしない' do
        expect(current_path).not_to eq('/users/sign_in')
      end

      context 'パラメータが正常な場合' do
        it 'Postを作成できる' do
          expect { subject }.to change(Post, :count).by(1)
          expect(current_path).to eq('/')
          expect(page).to have_content('投稿しました')
        end
      end

      context 'パラメータが異常な場合' do
        let(:title) { nil }
        it 'Postを作成できない' do
          expect { subject }.not_to change(Post, :count)
          expect(page).to have_content('投稿に失敗しました')
        end
        it '入力していた内容は維持される' do
          subject
          expect(page).to have_field('post_content', with: content)
        end
      end
    end
  end
end

System Spec (Home)

今回、ナビゲーションバーにリンクを追加しました。
ナビゲーションバーのリンク表示のテストは Home の System Spec で行っていたため、テストを追加しておきます。

以下では、追記部分を中心に表記しているので注意してください。

spec/system/home_spec.rb

...
  describe 'ナビゲーションバーの検証' do
    context 'ログインしていない場合' do
      ...
      it 'ログ投稿リンクを表示しない' do # 追加
        expect(page).not_to have_link('ログ投稿', href: '/posts/new')
      end

      it 'ログアウトリンクは表示しない' do
        expect(page).not_to have_content('ログアウト')
      end
    ...
    context 'ログインしている場合' do
      ...
      it 'ログ投稿リンクを表示する' do # 追加
        expect(page).to have_link('ログ投稿', href: '/posts/new')
      end

      it 'ログアウトリンクを表示する' do
        expect(page).to have_content('ログアウト')
      end
...

最後に、すべてのテストに通ることを確認しておきます。

$ bin/rspec
...
Finished in 11.06 seconds (files took 0.38787 seconds to load)
48 examples, 0 failures

変更をコミット

ここまでの変更をコミットしておきます。

$ git add .
$ git commit -m "学習ログ投稿機能を作成" 
$ git push

宿題

authenticate_user!

PostsController の一部のアクションについてログインを必須とするよう、
devise のヘルパーメソッド authenticate_user! を設定しました。

このタイミングで devise で使えるヘルパーメソッドの全貌を理解しておきましょう。