Ruby on Railsチュートリアル第6版 追加第3章

Ruby on Railsチュートリアル第6版が終わり、そこで作ったサンプルアプリケーションの修正や機能追加をやってます。

追加第3章では、追加第2章で実装した返信機能をバージョンアップさせます。

追加第3章 返信機能のバージョンアップ

追加第3章では、追加第2章で実装した返信機能をバージョンアップさせます。これは追加第2章で諦めたものです。

VU版返信機能の要件と実装案

追加第3章で実装する機能要件はこんな感じです。

VU版返信機能の要件
  1. フィードで@user_nameにリンクを貼る
  2. replyリンクをreply or showに変更
  3. showページでは宛先のマイクロポストを表示
  4. ユーザーが削除されたらin_reply_to_userからも削除

すぐにできるものから、難しそうなものまであります。2番目については、純粋にshowページを表示するリンクを作ろうと思ったところ、showページはreplyリンクから行けるので、名前を変更するだけになりました。

追加第2章と同じように、実装案を先に考えておきます。

VU版返信機能の実装案
フィードで@user_nameにリンクを貼る
  • link_user_nameメソッドを利用
  • showページで使ったlink_user_nameをフィードでも使えるようにする
replyリンクをreply or showに変更
  • aタグのテキストを変更

showページで宛先のマイクロポストを表示
  • in_reply_to_micropostカラムから宛先のマイクロポストのidを取得
  • idを使ってマイクロポストを取り出す
  • 取り出したマイクロポストをshowマイクロポストの上に表示
  • if文で宛先があるときだけ表示
ユーザーが削除されたらin_reply_to_userカラムからも削除
  • 削除するユーザーのuser_nameでin_reply_to_userを検索
  • in_reply_to_userカラムの全user_nameを配列に入れ、対象user_nameだけを削除
  • in_reply_to_userにカンマ区切りで保存(カンマ区切りにするメソッドを追加?)

特に理由はありませんが、上から順番に実装していきました。

フィードで@user_nameにリンクを貼る

追加第2章で大変そうだから実装をやめたものです。やってみたら、思ってたよりも簡単でした。

フィードが表示されるページはstaticPagesコントローラのhomeアクションなので、そこに変更を加えます。

フィードの@user_nameにリンクがあるかのテストを書く

期待されるものがはっきりしてるので、今回はテストから書きました。テストコードはshowページで使ったものをそのまま使いました。

@user_nameがいくつかあるマイクロポストを返信に使ってるので、それに対して正しくリンクが貼られているか確認するコードです。

フィードの@user_nameにリンクを貼る

テストが書けたので、それに合うコードを書いていきます。

@feed_itemsに表示するマイクロポストが集められてます。ここは、追加第2章で実装したフィードに返信マイクロポストを表示する機能で修正した場所です。あれは苦労しました。

@user_nameにリンクを貼るコードはlink_user_nameという名前でメソッド化してありますが、それはMicropostsコントローラの中にあります。このままではStaticPagesコントローラでは使えません。

そこで、link_user_nameメソッドをApplicationコントローラに格上げしました。そうすることで、継承を通してMicropostsコントローラでも、StaticPagesコントローラでも使えるようになります。

StaticPagesコントローラのhomeアクションでは、@feed_itemsの各要素に対して、link_user_nameメソッドを使っていきます。

@feed_items.map { |feed_item| link_user_name(feed_item) }

mapメソッドを使って、link_user_nameメソッドの結果を@feed_itemsに入れます。こうすることによって、フィードでも@user_nameにリンクが貼られました。

しかし、テストはREDです。

テストの修正

投稿内容をcontentに代入して、その変数を使って投稿するという流れのコードにして、そのcontentが投稿後に表示されてるかをテストしました。

フィードでも@user_nameにリンクを貼ったことで、contentと実際の表示が変わってます。aタグが挿入されてますからね。それなのに、contentがあるかをチェックしてたので、REDだったんです。

これがわかった理由は、ブラウザでソースコードを見たときに、aタグ内に余計なスペースが入ってるのを見つけたのがきっかけです。リンクを貼るコードからスペースを削除したりしながら、「もしかしたらaタグが問題なんじゃないか」と考えました。

そこで、assert_matchでaタグ入りの文字列を入れてみたところ、GREENになりました。これで確定です。

この問題を解決するために、リンクを貼るメソッドを使おうとしましたが、うまく使えませんでした。そこで、マイクロポストにはidがあり、そのidを使ってHTMLに「id=”micropost-〇〇”」と入れてることを使いました。

最新の投稿のidをもつHTMLのidがあるかを確認することにしました。

assert_select "li#micropost-#{Micropost.first.id}", count: 1

マイクロポストはliタグで表示されてて、そのliタグのidに「micropost-〇〇」があります。「micropost-(最新の投稿のid)」があれば、ちゃんと表示されてるというわけです。

これを投稿後に表示されるページと、宛先になったユーザーのフィードにあるかを確認しました。宛先になったユーザーは投稿したユーザーをフォローしてないので、自分宛てのマイクロポストをフィードに表示できなければREDになるというわけです。

フィードで@user_nameにリンクを貼る修正をする前はcontentがあるかをチェックするだけでよかったんですが、それができなくなったので、テストを変更することになりました。

そう考えるとshowページでcontent自体の確認を忘れてたということになるので、ここは反省点ですね。

テストがGREENになり、フィードでも@user_nameにリンクを貼ることができました。

replyリンクをreply or showに変更

これはテキストを変更するだけです。それに合わせてテストも変更してあります。

showページで宛先のマイクロポストを表示

これに関してはいろんな問題が絡み合って苦労しました。途中でわけがわからなくなり、Gitで元に戻そうかと思ったんですが、上の修正後にコミットしてないことに気づき、そのまま修正を続けました。

showページのテストを書き直す

ここもテストから書きましたが、結果的にそのテストが間違ってることが判明しました。

いろいろと直しまくったので、最初のテストがどうだったかがわからなくなってしまいました。本当は何が悪かったかをちゃんと把握したかったんですが、一部しか把握できない状況です。

テストに関しては、後でまた戻ってきます。

showページに宛先のマイクロポストを表示する

まず、showページに宛先のマイクロポスト用のHTMLを追加しました。基本的に使い回しですが。CSSを少し変更して背景色を変え、margin-rightを設定して、showマイクロポストと区別できるようにしました。

マイクロポストの返信

フロントエンドの技術は今後の課題なので、とりあえず区別ができればいいという感じです。本当はもっとステキな感じにしたいんですけどね。

<% if @replied_user %>
  <li id="micropost-<%= @replied_micropost.id %>" class="replied_micropost">
    <%= link_to gravatar_for(@replied_user, size: 50), @replied_user %>
    <span class="user">
      <%= link_to "#{@replied_user.name} (@#{@replied_user.user_name})",
                  @replied_user %>
    </span>
    <span class="content">
      <%= @replied_micropost.content.html_safe %>
      <%= image_tag micropost.display_image if micropost.image.attached? %>
    </span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(micropost.created_at) %> ago.
      <% if current_user %>
        <%= link_to "reply or show", micropost_path(@replied_micropost) %>
      <% end %>
      <% if current_user?(micropost.user) %>
        <%= link_to "delete", micropost, method: :delete,
                                         data: { confirm: "You sure?" } %>
      <% end %>
    </span>
  </li>
<% end %>

(app/views/microposts/_micropost.html.erb)

最終的に出来上がったコードがこれです。@replied_userなどのreplied系変数はMicropostsコントローラで設定してあります。

if @micropost.in_reply_to_micropost
  @replied_micropost = Micropost.find(@micropost.in_reply_to_micropost)
  @replied_user = User.find(@replied_micropost.user_id)
end

(app/controllers/microposts_controller.rb)

showアクションにこれを追加しました。表示するマイクロポストのin_reply_to_micropostカラムに何かしらの値が入ってたら(特定のマイクロポストに対しての返信だったら)、宛先のマイクロポストと宛先になったユーザーを取り出します。

ビュー側でそれを表示するという流れです。

サンプルデータにマイクロポストに対する返信になってるデータを入れ、表示できることを確認しました。それなのに、テストでREDです。テストコードが間違ってるはずなので、変更しまくりましたが、REDのままです。

どこの処理がうまくいってないのか確認するためにテストコードを追加してみると、in_reply_to_micropostにマイクロポストのidが保存されてないことがわかりました。バリデーションでin_reply_to_micropostのpresenceをtrueにしてみると、投稿自体が保存されず、返信マイクロポストの投稿のところのテストコードでREDになりました。

ということで、createアクションが間違ってる可能性が出てきました。

in_reply_to_micropost属性は追加第2章で追加したので、ちゃんと動いてるつもりでいましたが、動いてなかったようです。追加第2章では使わなかったので、気づかなかったということなのかもしれません。

hidden_fieldでマイクロポストのidを渡すようにしてありましたが、debuggerで見てみると受け取れてないことがわかりました。受け取るところのコードはメソッド化してあります。

if @micropost.content.include?("@")
  replied_users = []
  split_microposts = @micropost.content.split(" ")
  split_microposts.each do |split_micropost|
    split_micropost = split_micropost.scan(/@(.*)/)
    replied_users << split_micropost if User.find_by(user_name: split_micropost)
  end
  @micropost.in_reply_to_user = replied_users.join(",")
  @micropost.in_reply_to_micropost = params[:micropost_id]
end

最後の1文ですね。ここが間違ってたようです。

ビュー側は、

<%= hidden_field_tag :micropost_id, params[:id] %>

こうなってました。

いろいろ調べながら変更して確認したので、ぐちゃぐちゃしてしまったんですが、とりあえずカラム名に合わせて、micropost_idからin_reply_to_micropostに変更したりしてます。

最終的にはこんな感じです。

micropost.in_reply_to_micropost = params[:in_reply_to_micropost]
<%= hidden_field_tag :in_reply_to_micropost, params[:id] %>

hidden_field_tagとhidden_fieldで違いが出るようですが、ここではhidden_field_tagを使ってます。

このように変更することで、in_reply_to_micropostが無事保存されるようになりました。でも、テストはREDのままです。

テストの修正

記事にまとめると1つずつ解決した感じになってますが、実際はテストコードとアプリのコードの両方を修正しながら、原因を特定していった感じです。

content = "@archer1 test@test foobar@malory1 @michael1foobar @lana1"
assert_difference 'Micropost.count', 1 do
  post microposts_path, params: { micropost:{ content: content,
                                in_reply_to_micropost: microposts(:replied_micropost).id } }
end

テストではこんな感じで、テストデータ(replied_micropost)に対して返信してます。この投稿自体は成功してるのでこの部分はパスしてます。

でも、宛先のマイクロポストは表示されてないようです。いろいろと試行錯誤した結果、上でも書いたようにhidden_field_tagを使ったデータの受け渡しに問題があることがわかりました。

アプリのコードでは、params[:in_reply_to_micropost]での受け渡しですが、テストコードではparams[:micropost][:in_reply_to_micropost]での受け渡しになってます。

テストだとin_reply_to_micropostが空のままになってしまうので、宛先のマイクロポストが表示されないのは当たり前ですね。

form_with内でhidden_fieldを使うとparams[:micropost][:in_reply_to_micropost]での受け渡しになり、hidden_field_tagを使うとparams[:in_reply_to_micropost]になるようです。

試しに、hidden_fieldに変更し、params[:micropost][:in_reply_to_micropost]で受け渡しをするようにしてみたら、上のテストコードでGREENになりました。

どっちがいいのかわかりませんが、hidden_field_tagで実装しました。テストコードはこんな感じに変更です。

content = "@archer1 test@test foobar@malory1 @michael1foobar @lana1"
assert_difference 'Micropost.count', 1 do
  post microposts_path, params: { micropost:{ content: content },
                      in_reply_to_micropost: microposts(:replied_micropost).id }
end

波カッコの位置をいるとわかると思いますが、in_reply_to_micorpostはmicropostの外にあります。こうすることで、params[:in_reply_to_micropost]でのデータの受け渡しができるようになります。

このような修正をして、やっとテストでGREENになりました。

ユーザーが削除されたらin_reply_to_userからも削除

micropostsテーブルのin_reply_to_userカラムには、宛先になってるユーザーのuser_nameが保存されてます。存在しないuser_nameは保存しないようにしたので、ユーザーが削除されたらin_reply_to_userからも削除しようと考えました。

でも、@user_nameにリンクを貼るときに、存在するユーザーを条件にしてるので、そのままでもユーザーが削除されたらリンクは貼られないんですよね。

ということは、どちらでも表示という意味では変わらないことになります。データベースへの問い合わせの問題はありますが。

そんなことを考えて、この機能は実装しないことにしました。本気でTwitterライクなアプリケーションを作るのであれば、削除されたユーザーのuser_nameを再利用できる仕様の場合は、in_reply_to_userからuser_nameを削除しておいた方が混乱が少ないだろうなと思います。

バグ修正

追加第2章で実装したものも含めて、マイクロポストのshowページ関係のバグが存在してます。これは追加第3章をやり始めてから気づきました。

  1. 宛先のマイクロポストが削除されてるとエラーになる
  2. showページからマイクロポストを削除すると、そのshowページにリダイレクトされてエラーになる

この2つのバグは結局のところ同じですね。存在しないマイクロポストIDのページにアクセスしたときにエラーになるということです。これは第11章で気づいた存在しないユーザーIDのページにアクセスしたらエラーになる問題とも同じですね。

第11章で修正するのは苦労しましたが、今回は簡単にできました。コードを比べてみて、その理由がわかりました。

存在しないマイクロポストのページにアクセスしたときにリダイレクト

まずMicropostsコントローラのshowアクションから。

if @micropost = Micropost.find_by(id: params[:id])
  link_user_name(@micropost)
else
  redirect_to root_url and return
end

params[:id]を使ってマイクロポストを取り出し、@micropostに代入する式をif文にしてあります。マイクロポストが存在すればそのマイクロポストが代入され、存在しなかったらnilが代入されます。

Railsコンソールで試すと、こんな感じです。

> @micropost = Micropost.find_by(id: 1000)
   (省略)
 => nil 
> @micropost
 => nil 

if文の条件式がnilなら処理がスキップされるので、link_user_name(@micorpost)は実行されず、else文の処理に移ります。

else文ではroot_urlにリダイレクトするように設定してあるので、homeページにリダイレクトされることになります。

ここで「and return」が活躍するわけです。これがないと、その下にあるコードが実行されてしまい、それが@micropostを使う処理なのでエラーになります。

「and return」は第11章でも出てきていて、その意味はまだ十分に理解できてませんが、returnでメソッドを抜けられるらしいので、root_urlにリダイレクトされた時点でその下にある処理は実行されないということなんだと思います。

なぜここでif文を使うことを思いついたかというと、もともとの処理の流れ的に必要だったからです。でも、最初に書いたコードは違います。

もともとの処理は、マイクロポストを取り出して、返信用の@reply(空のマイクロポスト)を用意して、という感じでした。

そういう一連の流れとは違う流れでリダイレクトがあるので、条件分岐で最初はコードを書きました。つまり、その後の処理もif文の中に入れたということです。

なので、わざわざ変数に値を入れて、それの真偽を確かめて、というコードにするより、それらをまとめた方がいいというのをパッと思いつくことができました。ちょっとは成長してるようです。

そのままでもよかったんですが、問題としてはif文の中にifが入り、さらに後置if文というややこしい感じになってました。

テストがらみで試行錯誤してるときにたまたま上に書いたコードにして、テストの問題を解決することができたのでそのままにしてあります。試してみましたが、最初のコードでもテストはGREENになりました。

if文の入れ子構造より読みやすいかなと思って、今のコードにしてあります。

リダイレクトのテスト

またまたテストで苦労しました。

今回はshowページでマイクロポストを削除したときのリダイレクト問題だったので、リダイレクトをテストすればいいということはわかってましたが、問題がありました。

その問題は、「destroyアクションでリダイレクトされて、showアクションで再度リダイレクトされる」というものです。

2段階のリダイレクトをテストでどう書くかがわかりませんでした。

最初は削除するマイクロポストのページに行き、そこでDELETEリクエストを送って削除という順番でテストを書きました。リダイレクト先はroot_urlです。

これがGREENなんですよね。

実はこのテストコードを最初に書いてたんですが、実際に試してみると「and return」なしだったのでエラーになってました。それなのにGREENです。

何かがおかしいということで、テストコードとアプリのコードを修正しながら、「and return」にたどり着いて上のコードになったわけです。

でも、リダイレクト先をabout_pathに変えてもGREENのまま。

Railsチュートリアルで書いたマイクロポストの削除のテストでは、リダイレクトについての記述はありません。見落としてなければ。

destroyアクションでは、1つ前のページかroot_urlにリダイレクトされるように設定されてます。showページから削除した場合はそのshowページにリダイレクトされるので、showアクションが反応するはずです。

それなのに、root_urlにリダイレクトされてるかを確認するとGREENになります。

この動きから、テストでDELETEリクエストを送るというのは直接DELETEリクエストを送ってて、特定のページから送ってるわけではないということが推測できます。

そうなるとリダイレクト先はroot_urlになるので、showアクションでどこにリダイレクトさせるようにしてもGREENなわけです。

ということで、仕方ないので、削除後に削除したマイクロポストのページに行き、そこからroot_urlにリダイレクトされるかを確認するようにしました。

showアクションでリダイレクト先を変えるとRED、リダイレクト先をroot_urlにするとGREENになりました。成功です。

まとめ

追加3章では、追加第2章で実装した返信機能をバージョンアップさせました。さらに、追加第2章で見逃してたバグも修正しました。

テスト関係で苦労しましたが、少しずつレベルアップできてる気がしてます。まだまだ満足できるほどではありませんが、「継続は力なり」を実感し始めてる感じです。

作りたいアプリケーションを考えると、次はメッセージ機能か検索機能かなと考えてましたが、もっと優先した方がいいことがありました。

1つはI18n対応です。もっとわかりやすく言えば日本語化ですね。日本人向けのアプリケーションを作ろうと思ってるので、エラーメッセージとかを日本語化する必要があります。

もう1つはログイン/認証システムに関するものです。Railsチュートリアルはゼロから実装しましたが、普通はDeviseなるものを使うらしいので、それに置き換えようと考えてます。

まずは日本語化からやってく予定です。

Ruby on Rails チュートリアル:実例を使って Rails を学ぼう
SNS 開発を題材にした大型チュートリアル。プロダクト開発の 0→1 を創りながら学びます。電子書籍や解説動画、質問対応、法人向けサービスも提供しています。