Ruby on Railsチュートリアル第6版が終わり、そこで作ったサンプルアプリケーションの修正や機能追加をやってます。
今回はその追加第1章です。ここでは第13章で気づいたリロード問題を解決します。
追加第1章 リロード問題の解決

リロード問題とは、第13章で気づいた問題で、マイクロポストの投稿がバリデーションに引っかかった後にリロードするとエラーになるというものです。
これはマイクロポストの投稿だけでなく、ユーザー登録、パスワードリセットでも起こります。
これを修正したいと思います。
リロード問題の原因
リロード問題の原因は簡単です。例としてマイクロポストの投稿を挙げます。
マイクロポストの投稿はHomeページで行うようになってます。そのURLは「hogehoge.com/」というようなルートURLです。
マイクロポストを投稿するとMicropostsコントローラのcreateアクションにPOSTリクエストが投げられます。POSTリクエストは「hogehoge.com/microposts」に送られます。
マイクロポストが空欄とかでエラーだと「static_pages/home」がレンダリングされます。URLは「hogehoge.com/microposts」です。
この状態でリロードすると、「hogehoge.com/microposts」にGETリクエストが送られますが、それに対応するアクションは作っていません。ということで、エラーになるわけです。
renderからredirect_toに書き直す
「現場から学ぶ、RailsのControllerアンチパターン」によると、POSTした後にrenderするのは良くないということだったので、redirect_toで書き直す必要がありそうです。
これ自体はそこまで難しくありません。マイクロポストはルートURLから投稿するので、ルートURLにリダイレクトするだけです。
問題はエラーメッセージです。
リダイレクトすると「_error_messages.html.erb」というパーシャルで設定したエラーメッセージが表示されません。そこにエラーメッセージを渡そうと思いましたが、できませんでした。
さっき紹介した記事ではflashを利用するとなってたので、flashにエラーメッセージを入れました。
flash[:danger] = @micropost.errors.full_messages
こうすると、dangerをキーとしたハッシュになります。ハッシュからメッセージ部分だけを取り出すようにコードを書けば、エラーメッセージは表示されます。
ただ、フォームの上だけでなく、ページ上部にもエラーメッセージが表示されます。flashの挙動ですね。それを消すことを考えましたが、他のflashに影響を与えない方法がわかりませんでした。
さらに、投稿が成功したときはflashに直接メッセージを入れてるので、ハッシュからメッセージを取り出す処理でエラーが出るというおまけ付きです。
パスワードリセットのリダイレクト
実際に修正した順番は前後しますが、この方法はパスワードリセットの場合だけうまくいきました。
そもそもパスワードリセットのエラーメッセージはflashを使ってたので、エラーメッセージ問題は回避できたからです。
render 'new'
こうなってたところを、
redirect_to new_password_reset_path
こう書き直すだけです。
でも、テストはRED。よくよく見てみると、テストではテンプレートが「password_resets/new」であることを確認してます。
リダイレクトに書き換えたので、new_password_rest_pathにリダイレクトされてるかの確認をする必要があります。
そこで、
assert_redirected_to new_password_reset_path
テストコードをこのように書き換えました。これでGREENになりました。
ルーティングの変更
とりあえず、できたところまで記事にしてみようと思って書いてたら、別の方法を思いつきました。それがルーティングの変更です。「リロード問題の原因」を書いてるときに思いつきました。
POSTリクエストが送られるURLが問題なのであれば、問題のないURLに送ればいいという発想です。
マイクロポストのルーティング変更
マイクロポストのルーティングは、
resources :microposts, only: [:create, :destroy]
という感じで、resourcesを使って設定してます。このうち、createアクションのルーティングを変更したいわけです。
だとしたら、resourcesで設定するのをdestroyアクションだけにすればいいということですね。それに加えて、createアクションを別に設定すれば解決するはずです。
post '/', to: 'microposts#create', as: 'microposts'
resources :microposts, only: [:destroy]
これが修正したルーティングです。POSTリクエストをルートに送って、Micropostsコントローラのcreateアクションが反応するようにしてあります。
「as:」を使って名前付きルートがmicroposts_pathになるようにしました。この名前付きルートを使ってたか記憶にありませんが、使ってた場合にエラーが出ないはずです。
テストの通って修正完了です。
ユーザー登録のルーティング変更
ユーザー登録も同じようにすればうまくいくはずと思って修正しました。
resources :users, only: [:index, :show, :new, :edit, :update, :destroy] do
member do
get :following, :followers
end
end
post '/signup', to: 'users#create', as: 'users'
こうすると、ルーティング設定のエラーになりました。何やら「users」はすでに使われてるとのことです。確かにusersはGETとPOSTで設定するので、resourcesでGETのusersは使われてます。
もしかしたら順番が関係してるかもしれないと思って、
post '/signup', to: 'users#create', as: 'users'
resources :users, only: [:index, :show, :new, :edit, :update, :destroy] do
member do
get :following, :followers
end
end
このように書き換えたら、ルーティングの設定は問題なくなりました。実際に動かしてみても、期待通りに動きます。
でも、テストはREDです。2つ失敗です。
test "should redirect index when not logged in" do
get users_path
assert_redirected_to login_url
end
1つ目はここで、ログインしてない状態でusers_pathにアクセスしてもリダイレクトされないというもの。users_pathを「/signup」にしたから当たり前ですね。
もう1つは、
test "layout links when not logged in" do
get root_path
.
.
.
assert_select "a[href=?]", users_path, count: 0
.
.
.
ここです。これも同じで、ログインしてない状態でusers_path、つまり「/signup」へのリンクが存在してるというものです。
これ自体は正しいテストコードに書き換えればいいんですが、users_pathが以前のように機能してないのが問題ですね。おそらく、GETリクエストとPOSTリクエストで同じ「/users」だったことが関係してるんだと思います。
ということは、このようなルーティングの設定による変更は間違ってる可能性が高そうです。
リダイレクト再び
ルーティングの変更はおそらく間違ってると思われるので、リダイレクトで可能性を探ることにしました。
最初に試した通り、flashは使えないことがわかってます。では、flashに変わる何かが必要となります。
まず思いついたのが、エラーメッセージをインスタンス変数に入れてリダイレクト先で使う方法です。結果としては失敗なんですが、リダイレクトするとインスタンス変数が使えないようです。
再度「現場から学ぶ、RailsのControllerアンチパターン」を見てみると、POSTした値の保持にsessionを使うということが書いてありました。
使い方として正しいかわかりませんが、エラーメッセージをsessionに入れたらリダイレクト先でも使える可能性があります。
ということで、実際にやってみると、うまくいきました。でも、sessionは保持され続けるようで、明示的に削除する必要がありました。
マイクロポストのリロード問題をリダイレクトで解決
まずMicropostsコントローラのcreateアクションを見てみましょう。
def create
@micropost = current_user.microposts.build(micropost_params)
@micropost.image.attach(params[:micropost][:image])
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
@feed_items = current_user.feed.paginate(page: params[:page])
render 'static_pages/home'
end
end
(app/controllers/microposts_controller.rb)
if文の最初の条件を満たした場合は問題ありません。elseの処理を変更します。
else
@feed_items = current_user.feed.paginate(page: params[:page])
session[:error_message] = @micropost.errors.full_messages
redirect_to root_url
end
このようにすることによって、session[:error_message]にエラーメッセージを入れて、root_urlにリダイレクトすることができます。
ただ、これだけではエラーメッセージが表示されません。エラーメッセージのパーシャルを変更する必要があります。
<% if object.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(object.errors.count, "error") %>.
</div>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
(app/views/shared/_error_messages.html.erb)
これがエラーメッセージのパーシャルです。第13章で理解に苦労したf.objectですね。これを変更します。
<% object = session[:error_message] %>
<% if !object.nil? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(object.count, "error") %>.
</div>
<ul>
<% object.each do |msg| %>
<%# message.each do |msg| %>
<li><%= msg %></li>
<%# end %>
<% end %>
</ul>
</div>
<% end %>
<% session[:error_message] = nil %>
せっかくなので、object変数をそのまま活用して、session[:error_message]をobjectに入れました。
if文のところを「object.any?」にしましたがエラーになったので、「!object.nil?」に書き換えてあります。あとはerrorsメソッドが使えなくなってるのでそこの変更です。
最後にsession[:error_message]の中身を削除しないとずっとエラーメッセージが表示されることになるので、nilを入れて削除してます。
ユーザー登録のリロード問題をリダイレクトで解決
ユーザー登録も基本的には同じような修正です。エラーメッセージのパーシャルは直してあるので、修正は不要になってます。
修正したのはUsersコントローラです。
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:success] = "Please check your email to activate your account."
redirect_to root_url
else
session[:error_message] = @user.errors.full_messages
redirect_to signup_path
end
end
session[:error_message]にエラーメッセージを入れて、signup_pathにリダイレクトしてます。
ユーザー情報の編集のリロード問題をリダイレクトで解決
これはテストを走らせて気づいたんですが、ユーザー情報の編集でもリロード問題が出てました。
修正したのはUsersコントローラです。
def update
if @user.update(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
session[:error_message] = @user.errors.full_messages
redirect_to edit_user_path(@user)
end
end
ユーザー登録と同じ感じですね。
パスワードリセットのリロード問題をリダイレクトで解決
パスワードリセットについては、PasswordResetsコントローラのcreateアクションとupdateアクションの2つを直しました。
createアクションはrenderをredirect_toに直すだけなので省略します。
あとは同じようにやればいいと思っていたら、updateアクションの修正で苦労しました。最終的に完成したのがこれです。
def update
if params[:user][:password].empty?
session[:error_message] = @user.errors.add(:password, :blank)
redirect_back_or root_url
elsif @user.update(user_params)
log_in @user
@user.update_attribute(:reset_digest, nil)
flash[:success] = "Password has been reset."
redirect_to @user
else
session[:error_message] = @user.errors.full_messages
redirect_back_or root_url
end
end
ここで何が問題になったかというと、リダイレクト先です。editアクションにリダイレクトすると、そのままroot_urlにリダイレクトされてしまいます。
before_actionで有効なユーザーか確認して、有効じゃなかったらroot_urlにリダイレクトされるようにしてるからです。そこで確認してるのはURLに含まれるidなので、それを引き継ぐ必要があります。
renderメソッドで問題ないかなと思ったら、URLからメールアドレスが抜けていて、リロードでエラーが出ます。
Railsチュートリアルの中で、アクセスしようとしたURLを覚えておいて、そのURLにリダイレクトするというのをやった記憶がよみがえり、探してみました。
第10章の「10.2.3 フレンドリーフォワーディング」です。
これを使ってアクセスしたURLにリダイレクトできるように修正しました。
迷ったのが、アクセスしようとしたURLを覚えておくコードをどこに入れるかです。第10章ではbefore_actionで実行されてたので、それに倣うことにしました。
Usersコントローラにあるvalid_userメソッドに入れました。
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
else
store_location
end
end
もともとelseがないメソッドで、有効じゃないユーザーだったらroot_urlにリダイレクトするものでしたが、有効なユーザーだったときにURLを覚えるようにしました。
こうすることでredirect_back_orメソッドが使えるようになります。このメソッドは、覚えておいたURLにリダイレクトするか、それがなかったら指定したURLにリダイレクトするものです。後者のリダイレクト先はroot_urlにしておきました。
リロード問題解決に対するテスト
これで問題解決としたいところですが、やっぱりテストは大事ですよね。
実際の修正はテストを走らせながら試行錯誤でしたが、最終的なテストの変更は修正したところに「follow_redirect!」を入れて、テンプレートの確認を追加したくらいです。
まとめ
Ruby on Railsチュートリアル第6版 追加第1章として、リロード問題の解決を選びました。
最初は簡単にできるかなと思ってましたが、思った以上に苦労しました。
プログラミングに関しては答えのある問題を解く形で学んできたので、今回のプログラミングチャレンジで初めて「答えのない問題を解く」ことをやったことになります。Railsチュートリアルの演習は、公式ではないとはいえ、ググれば解答が出てきますからね。
今回の修正方法に問題があるかもしれませんが、自分の力で解決できて満足してます。
