学習ログ投稿機能を作成しよう
- 学習の目標
- PostController 作成
- ルーティングの修正
- 動作確認
- 投稿機能の実装
- 投稿画面の修正
- 翻訳ファイル修正
- ナビゲーションバーにリンク追加
- 動作確認
- テスト作成
- 変更をコミット
- 補足:authenticate_user!
今回は、TechLog アプリケーションの中心的な機能である学習ログを投稿するページと機能を作成していきます。機能追加に伴い学ぶことも多いですが、一歩ずつ確実に進めていきましょう。
学習の目標
本章では、以下の内容を学習します。
- 学習ログを投稿するためのページを作成する
- 投稿機能を実装する
- 投稿機能のテストを作成する
- ナビゲーションバーに投稿ページへのリンクを追加する
PostController 作成
まずは、rails g コマンドを使用して、投稿機能に必要なコントローラーファイルを生成します。今回は、よく使われる基本的なアクションとして、index(一覧表示)、new(新規作成フォーム)、create(データ保存)、show(詳細表示)、destroy(削除)のアクションを用意しましょう。
$ bin/rails g controller posts index new create show destroy
      create  app/controllers/posts_controller.rb       route  get "posts/index"              get "posts/new"              get "posts/create"              get "posts/show"              get "posts/destroy"      invoke  tailwindcss      create    app/views/posts      create    app/views/posts/index.html.erb      create    app/views/posts/new.html.erb      create    app/views/posts/create.html.erb      create    app/views/posts/show.html.erb      create    app/views/posts/destroy.html.erb      invoke  rspec      create    spec/requests/posts_spec.rb      create    spec/views/posts      create    spec/views/posts/index.html.tailwindcss_spec.rb      create    spec/views/posts/new.html.tailwindcss_spec.rb      create    spec/views/posts/create.html.tailwindcss_spec.rb      create    spec/views/posts/show.html.tailwindcss_spec.rb      create    spec/views/posts/destroy.html.tailwindcss_spec.rb上記のコマンドによって、コントローラーファイル、ビューファイル、テストファイルなどが生成されました。ただし、生成されたファイルの中には使用しないものもあります。以下のビューファイルは不要なので削除しておきましょう。
app/views/posts/index.html.erbapp/views/posts/destroy.html.erbまた、ビューのテストファイルも今回は使用しないので、以下のコマンドで削除します。
$ rm -rf spec/viewsこれで基本的なファイルが生成されました。次は、これらのファイルを編集していきます。
テスト駆動開発(TDD)のアプローチは、ユーザー認証機能の実装で既に体験したので、今回は先に機能の実装を進めていきます。これにより、実装の流れがより理解しやすくなるでしょう。
ルーティングの修正
rails g コマンドで自動生成されたルーティングは、RESTfulなルーティングとしては適切ではありません。Rails では、基本的なCRUD操作のためのルーティングを resources メソッドで簡単に設定できます。
config/routes.rb ファイルを以下のように修正しましょう。
Rails.application.routes.draw do  devise_for :users  root 'home#top'  resources :posts, only: [:index, :new, :create, :show, :destroy] # 追加endここで使用している resources メソッドは、RESTfulなルーティングを一括で設定するための便利な方法です。標準では、index、show、new、create、edit、update、destroy の7つのアクションに対応するルーティングが生成されます。
しかし、今回の TechLog アプリケーションでは投稿の編集機能は必要ないため、only オプションを使って必要なアクションだけを指定しています。これにより、edit と update アクションに関連するルーティングは生成されません。
動作確認
この時点で開発用サーバを起動(bin/dev)して、http://127.0.0.1:3000/posts/new にアクセスすると、自動生成された new.html.erb の内容が表示されるはずです。次は、実際の投稿機能を実装していきましょう。
投稿機能の実装
まずは、PostsController を以下のように修正します。このコントローラーは投稿の作成や表示などの処理を担当します。
app/controllers/posts_controller.rb
class PostsController < ApplicationController  before_action :authenticate_user!, only: [:new, :create] # ログインしているかどうかを判断
  def index  end
  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
  def show  end
  def destroy  end
  private
  # ストロングパラメータで許可するカラムを指定  def post_params    params.require(:post).permit(:title, :content) # title と content のみ許可  endendこのコントローラーには、いくつかの重要な機能が実装されています。
before_action
before_action :authenticate_user!, only: [:new, :create] の行によって、new と create アクションが実行される前に、ユーザーがログインしているかどうかをチェックしています。
before_action は、指定したアクションが実行される前に、指定したメソッドを実行する仕組みです。ここでは、Devise が提供する authenticate_user! というヘルパーメソッドを使用しています。このメソッドは、ユーザーがログインしていない場合に自動的にログインページへリダイレクトします。
この設定により、ログインしていないユーザーは投稿機能を使用できなくなります。ただし、index と show アクションはこの制約から除外されているため、誰でもアクセスできます。
new アクション
new アクションでは、新しい投稿を作成するためのフォームで使用する空の Post オブジェクトを準備しています。@post = Post.new によって作成された変数は、ビュー(new.html.erb)で使用されます。
create アクション
create アクションは投稿を実際にデータベースに保存する処理を行います。ここでは以下のステップを実行しています:
- ストロングパラメータによって安全に処理されたフォームデータから新しい Postオブジェクトを作成
- 現在ログインしているユーザーの ID を投稿に関連付け
- 投稿の保存を試みる
- 保存に成功したら成功メッセージを表示して、トップページにリダイレクト
- 保存に失敗したら失敗メッセージを表示して、投稿フォームを再表示
ストロングパラメータは、フォームから送信されるデータを制限するセキュリティ機能です。ここでは、title と content のパラメータのみを許可しています。
投稿画面の修正
次に、投稿画面を用意します。rails g コマンドで自動生成された new.html.erb ファイルを以下のように修正しましょう。
app/views/posts/new.html.erb
<%= form_with model: @post, class: "space-y-6 w-3/4 max-w-lg bg-white p-6 rounded-lg shadow" 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 py-1 px-2 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 py-1 px-2 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">    学習したこと、開発したことを記録しましょう。  </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 hover:cursor-pointer" %>  </div><% end %>このフォームには、以下の要素が含まれています:
- form_withヘルパーメソッドを使用して、- @postモデルとフォームを関連付け
- タイトル入力用の text_field
- 本文入力用の text_area
- 投稿ボタン(submit)
また、TailwindCSS を使ってスタイリングを適用しています。これにより、見た目の良いフォームが作成されます。
実際にブラウザで確認すると、以下のような画面が表示されるはずです:

なお、投稿機能はログインしている状態でなければ使用できないので、ログインしていない状態ではアクセスできないことも確認しておきましょう。
翻訳ファイル修正
投稿機能では、ログインを必要とする設定をしているため、ログインしていない状態で投稿ページにアクセスしようとすると、ログインを促すメッセージが表示されます。このメッセージの日本語表示を設定するために、翻訳ファイルを修正します。
config/locales/devise.ja.yml に以下の行を追加しましょう:
...  devise:    failure:      invalid: "%{authentication_keys}またはパスワードが違います。"      user: # 追加        unauthenticated: "ログインしてください。" # 追加      ...これにより、ログインしていない状態で投稿ページにアクセスしようとすると、「ログインしてください。」という日本語のメッセージが表示されるようになります。

ナビゲーションバーにリンク追加
次に、ナビゲーションバーに投稿ページへのリンクを追加します。投稿機能はログインしているユーザーのみが使用できるため、このリンクもログイン状態のときだけ表示されるようにします。
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-200 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-200 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://127.0.0.1:3000/posts/new にアクセスします。ログインしていれば、投稿フォームが表示されます。

投稿成功時
適当なタイトルと本文を入力して「ログを記録」ボタンをクリックすると、成功メッセージとともにトップページにリダイレクトされます。

投稿失敗時
タイトルまたは本文を空欄にして「ログを記録」ボタンをクリックすると、失敗メッセージとともに投稿フォームが再表示されます。入力していた内容(この場合はタイトル)は保持されています。

これは、render メソッドを使用しているためです。render メソッドは、新しくアクションを実行せずに、指定したビューを表示します。そのため、@post インスタンス変数に保存されている入力内容がそのまま使用されます。
ログアウト時
ログアウトした状態で http://127.0.0.1:3000/posts/new に直接アクセスすると、ログインを促すメッセージとともにログイン画面にリダイレクトされます。

ナビゲーションバー(ログイン時)
ログイン時には、ナビゲーションバーに「ログ投稿」リンクが表示されています。

ナビゲーションバー(ログアウト時)
ログアウト時には、「ログ投稿」リンクは表示されていません。

テスト作成
機能の実装が完了したので、次はこれらの動作を確認するためのテストを作成しましょう。
Request Spec
まずは、投稿ページへのアクセスを確認する Request Spec を作成します。このテストでは、ログイン状態によってアクセス可否が異なることを確認します。
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  endendテストを実行すると、次のようなエラーが発生する可能性があります:
$ bin/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)>'...これは、Request Spec で sign_in メソッドを使用するための設定が不足しているためです。spec/rails_helper.rb ファイルに以下の行を追加しましょう:
  config.include Devise::Test::IntegrationHelpers, type: :system  config.include Devise::Test::IntegrationHelpers, type: :request # 追加endこの設定により、Request Spec でも sign_in メソッドが使用できるようになります。再度テストを実行してみましょう:
$ bin/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 を作成します。まずはファイルを生成しましょう:
$ bin/rails g rspec:system posts      create  spec/system/posts_spec.rb生成された spec/system/posts_spec.rb ファイルを以下のように編集します:
require 'rails_helper'
describe 'Post', type: :system do  before do    driven_by :rack_test    @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  endendこのテストでは、以下の点を検証しています:
- ログインしていない場合、投稿ページへのアクセスはログインページにリダイレクトされる
- ログインしている場合:- 投稿ページにアクセスできる
- 正常なパラメータで投稿を作成できる
- 異常なパラメータでは投稿を作成できない
- 投稿に失敗しても入力内容は維持される
 
System Spec で効率的にテストするために、フォーム入力と送信の操作を subject にまとめています。
System Spec(ナビゲーションバー)
最後に、ナビゲーションバーのリンク表示に関するテストを追加します。既存の 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!
この実装では、投稿機能に authenticate_user! というメソッドを使用してログイン必須の制限を設けました。このメソッドは Devise が提供するヘルパーメソッドの一つです。
Devise には他にも便利なヘルパーメソッドが多数あります。さらに詳しく知りたい方は、以下の記事が参考になります:
今回は投稿機能の基本的な実装を行いました。次回は投稿の一覧表示や詳細表示などの機能を追加していきます。
Basicプランでより詳しく学習
この先のコンテンツを読むにはBasicプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。