学習ログ投稿機能作成
今回は、学習ログを投稿するページと機能を作成していきましょう。
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
を使用したルーティングに変更します。
投稿機能では new
と create
アクションが必要なので、それぞれを許可するルーティングを設定しましょう。
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
の中身が表示されます。
次は実際に投稿機能を修正していきましょう。
Controller (#new, #create)
PostsController に new
と create
アクションを用意してあげます。
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
中身としては基本的なものですが、簡単に解説しておきます。
- まず
before_action
でログインしているかどうかを判断する。- ログインしていればそのまま new アクションへ進む。
- ログインしていなければログインページへリダイレクトする。
- new アクションでは空の Post モデルオブジェクトを用意し、view から渡されたパラメータを受け付ける。
- そのユーザー ID はログインしているユーザーの ID をセットする。
- Post の保存に成功 or 失敗で処理を分岐する。
- 成功すれば、成功時のフラッシュメッセージとともにトップページへリダイレクトする。
- 投稿の保存に失敗すれば、失敗時のフラッシュメッセージとともに投稿画面を再表示させる。
最初にログインユーザーかどうかで処理を分岐させていることがポイントです。
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
とフォームを結びつけています。
また、デザインとしてはこのような見た目になっています。
翻訳ファイル修正
今回追加した処理に関し、devise の翻訳が必要となる箇所があるので翻訳ファイルに対訳を追加しましょう。
先ほど、学習ログの投稿時にはログインを必要とするよう、PostsController で設定しましたね。
app/controllers/posts_controller.rb (再掲)
class PostsController < ApplicationController
before_action :authenticate_user!
...
これにより、ログイン前に投稿画面にアクセスするとログインを促すフラッシュメッセージが devise の仕組みにより表示されるのですが、
現在はその対訳が存在しないためエラーメッセージが表示されます。
そのため、表示されている通りの階層で、翻訳ファイルに対訳を追記してあげましょう。
...
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-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 です。
投稿成功時
適当なタイトル、本文を入れて「ログを記録」ボタンを選択すると、成功時のフラッシュメッセージとともにトップページへリダイレクトします。
投稿失敗時
本文(またはタイトル)を空欄にして「ログを記録」ボタンを選択すると、失敗時のフラッシュメッセージとともに投稿画面が再表示されます。
この時、入力していた内容 (ここではタイトル) は残ったまま再表示されていることに注意してください。
ログアウト時
ログアウトしてからhttp://localhost:3000/posts/new
に直接アクセスします。
先ほど確認した通り、ログインを促すメッセージと共にログイン画面へリダイレクトします。
ここまで画面で確認できれば OK です。
ナビゲーションバー(ログイン時)
ログイン時は「ログ投稿」というリンクが表示されていることを確認します。
ナビゲーションバー(ログアウト時)
ログアウト時は「ログ投稿」というリンクが表示されていないことを確認します。
テスト
さて、実装は完了したので上記の動作を保証するテストを書いていきましょう。
繰り返しとなりますが、今回は理解のしやすさからテストではなく実装を先に行っています。
そのため動作確認は先ほど終わっており、もはやテストを書く意味がないのでは?と思うかもしれません。
しかし、今後さまざまな変更を加える際、動作を保証するテストがないと変更をするのが不安になりますし、改めて動作確認をするのは手間です。
そのため、後追いでも構わないので必ずテストを書く習慣はつけるようにしましょう。
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.rb
に Devise::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 で使えるヘルパーメソッドの全貌を理解しておきましょう。