マイクロポストリソースのルーティング
Micropostsコントローラーの内容を実装する前提条件として、まずは必要なルーティングをconfig/routes.rb
に記述していかなければなりません。但し、「Micropostリソースのインターフェースへのアクセスは、プロフィールページ・Homeページを介して行われる」という前提があるため、Mircopostsコントローラーにnew
やedit
のようなアクションは必要とされません。実際に必要とされるアクションは、create
とdestroy
の2つのみとなります。
以上を踏まえて、config/routes.rb
に対して実際に行う変更の内容は、以下のようになります。
Rails.application.routes.draw do
get 'password_resets/new'
get 'password_resets/edit'
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
post '/signup', to: 'users#create'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
+ resources :microposts, only: [:create, :destroy]
end
上述config/routes.rb
を前提とした場合、Micropstsリソースが提供するRESTfulルートの内訳は以下のようになります。
HTTPリクエスト | URL | アクション | 名前付きルート |
---|---|---|---|
POST | /microposts | create | microposts_path |
DELETE | /microposts/1 | destroy | micropost_path(micropost) |
マイクロポストのアクセス制御
長くなりましたので、別記事で解説します。
演習 - マイクロポストのアクセス制御
1. なぜUsersコントローラ内にあるlogged_in_user
フィルターを残したままにするとマズイのでしょうか? 考えてみてください。
「スーパークラスで定義された名前と同じ名前のメソッドをサブクラスで定義すると、当該サブクラスにおいては、当該メソッドの定義は上書きされる」というのが、オブジェクト指向一般のルール1です。そして、UsersController
はApplicationController
のサブクラスです。
また、今回の実装においては、ApplicationController#logged_in_user
メソッドの実装をUsersController
で上書きしなければならない理由はありません。そのような状況で無駄にlogged_in_user
メソッドの実装を上書きすることは、以下のような問題があります。
- 存在しなくてもいい重複したコードがプログラム中に存在することになる
- 無駄なコードは、無駄にバグを埋め込む原因となる
マイクロポストの作成・フィードの実装・マイクロポストの削除・フィード画面のマイクロポストに対するテスト
「テストから先に書き、テストの内容を網羅していく」という方法を取った結果、内容が非常に長くなりました。別記事で解説します。
演習 - マイクロポストを作成する
1. Homeページをリファクタリングして、if-else
文の分岐のそれぞれに対してパーシャルを作ってみましょう。
まずはログイン済みユーザーのHomeページに対応するパーシャルを作成します。ファイル名はapp/views/shared/_home_logged_in.erb
とします。内容は以下のようになります。
<divclass="row"><asideclass="col-md-4"><sectionclass="user_info"><%=render'shared/user_info'%></section><sectionclass="micropost_form"><%=render'shared/micropost_form'%></section></aside><divclass="col-md-8"><h3>Micropost Feed</h3><%=render'shared/feed'%></div></div>
続いて、未ログインユーザーのHomeページに対応するパーシャルを作成します。ファイル名はapp/views/shared/_home_not_logged_in.erb
とします。内容は以下のようになります。
<divclass="center jumbotron"><h1>Welcome to the Sample App</h1><h2>
This is the home page for the
<ahref="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</h2><%=link_to"Sign up now!",signup_path,class: "btn btn-lg btn-primary"%></div><%=link_toimage_tag("rails.png",alt: "Rails logo"),'http://rubyonrails.org/'%>
続いて、新たに作成したパーシャルを元々のHomeページのビューで使うようにします。新たなapp/views/static_pages/home.html.erb
の内容は以下のようになります。
<%provide(:title,"Home")%><%iflogged_in?%><%=render'shared/home_logged_in'%><%else%><%=render'shared/home_not_logged_in'%><%end%>
最後に、テストスイート全体を実行して問題がないことを確認しておきましょう。
# rails test
Running via Spring preloader in process 419
Started with run options --seed 35036
60/60: [=================================] 100% Time: 00:00:14, Time: 00:00:14
Finished in 14.10136s
60 tests, 323 assertions, 0 failures, 0 errors, 0 skips
演習 - フィードの原型
1. 新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT
文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。
まずは、新たに実装したマイクロポストの投稿フォームに文字を入力し、実際に投稿してみます。
「Post」ボタンを押すと、以下のような画面が出力されます。
「Micropost created!」というフラッシュメッセージもきちんと表示されていますね。
このときのPOST
リクエストに対して出力されたRailsサーバーのログは、以下のようになっています。
Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:04:24 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
Parameters: {...略, "micropost"=>{"content"=>"Est vitae enim nihil qui voluptates nemo quia."}, "commit"=>"Post"}
User Load (7.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.2ms) begin transaction
SQL (15.6ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Est vitae enim nihil qui voluptates nemo quia."], ["user_id", 1], ["created_at", "2020-01-03 05:04:24.981222"], ["updated_at", "2020-01-03 05:04:24.981222"]]
(13.1ms) commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 55ms (ActiveRecord: 36.7ms)
特にSQLのINSERT
文のみを見ると、以下のようになっています。
INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Est vitae enim nihil qui voluptates nemo quia."], ["user_id", 1], ["created_at", "2020-01-03 05:04:24.981222"], ["updated_at", "2020-01-03 05:04:24.981222"]]
2. コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、Micropost.where("user_id = ?", user.id)
とuser.microposts
、そしてuser.feed
をそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。
ヒント:
==
で比較すると結果が同じかどうか簡単に判別できます。
# rails console
Running via Spring preloader in process 454
Loading development environment (Rails 5.1.6)
>> user = User.first
User Load (2.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
>> Micropost.where("user_id = ?", user.id) == user.microposts
Micropost Load (9.4ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC [["user_id", 1]]
Micropost Load (3.3ms) SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
>> Micropost.where("user_id = ?", user.id) == user.feed
=> true
>> user.microposts == user.feed
Micropost Load (6.6ms) SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
Micropost.where("user_id = ?", user.id) == user.microposts
Micropost.where("user_id = ?", user.id) == user.feed
user.microposts == user.feed
以上3つの条件文は、いずれもtrue
を返しています。
演習 - マイクロポストを削除する
1. マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。
マイクロポストの「delete」リンクをクリックすると、まず以下のように「You sure?」という確認メッセージが表示されます。
そこで「OK」をクリックすると、マイクロポストが実際に削除されます。
「Micropost deleted」というフラッシュメッセージも表示されています。
Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:28:46 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
Parameters: {...略, "micropost"=>{"content"=>"Fantastic Wooden Shirt"}, "commit"=>"Post"}
User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.2ms) begin transaction
SQL (25.1ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Fantastic Wooden Shirt"], ["user_id", 1], ["created_at", "2020-01-03 05:28:46.381435"], ["updated_at", "2020-01-03 05:28:46.381435"]]
(14.8ms) commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 72ms (ActiveRecord: 43.8ms)
...略
Started DELETE "/microposts/304" for 172.17.0.1 at 2020-01-03 05:29:04 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#destroy as HTML
Parameters: {"authenticity_token"=>"7eAZU7veT9D+/vpKM8o9evxc9AF0EKF1aMXomohNHiPbnBWkcEODxVMeIlQ2ThxDQn2AQWcW+aBy0cE3CTsMbA==", "id"=>"304"}
User Load (5.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Micropost Load (2.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["user_id", 1], ["id", 304], ["LIMIT", 1]]
(0.2ms) begin transaction
SQL (33.6ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]]
(16.6ms) commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 73ms (ActiveRecord: 58.4ms)
削除リンクのhref
属性には、例えば /microposts/304 のようなパスへのリンクが設定されています。
SQLのDELETE
文の内容のみを見ると、以下のようになります。
DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]]
なお私は、以下のtypoにより、最初「deleteリンクをクリックしたときに、確認メッセージが表示されることなくマイクロポストが削除されてしまう」という不具合を発生させてしまいました。
- <%= link_to "delete", micropost, method: :delete, data: { confierm: "You sure?" } %>
+ <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
2. redirect_to request.referrer || root_url
の行をredirect_back(fallback_location: root_url)
と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう。
(このメソッドはRails 5から新たに導入されました)
まずは、app/controllers/microposts_controller.rb
の内容を以下のように変更してみます。
class MicropostsController < ApplicationController
...略
def destroy
@micropost.destroy
flash[:success] = "Micropost deleted"
- redirect_to request.referrer || root_url
+ redirect_back(fallback_location: root_url)
end
...略
end
テストスイート全体が正常に完了することを確認します。
# rails test
Running via Spring preloader in process 492
Started with run options --seed 51584
60/60: [=================================] 100% Time: 00:00:10, Time: 00:00:10
Finished in 10.53625s
60 tests, 323 assertions, 0 failures, 0 errors, 0 skips
テストスイート全体が正常に完了しましたね。
続いて、ブラウザでマイクロポストの新規投稿・削除操作を行い、そのときのRailsサーバーのログを確認してみます。
Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:28:46 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
Parameters: {...略, "micropost"=>{"content"=>"Fantastic Wooden Shirt"}, "commit"=>"Post"}
User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.2ms) begin transaction
SQL (25.1ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Fantastic Wooden Shirt"], ["user_id", 1], ["created_at", "2020-01-03 05:28:46.381435"], ["updated_at", "2020-01-03 05:28:46.381435"]]
(14.8ms) commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 72ms (ActiveRecord: 43.8ms)
...略
Started DELETE "/microposts/304" for 172.17.0.1 at 2020-01-03 05:29:04 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#destroy as HTML
Parameters: {...略, "id"=>"304"}
User Load (5.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Micropost Load (2.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["user_id", 1], ["id", 304], ["LIMIT", 1]]
(0.2ms) begin transaction
SQL (33.6ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]]
(16.6ms) commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 73ms (ActiveRecord: 58.4ms)
マイクロポストの投稿・削除ともに正常に行えているようですね。
演習 - フィード画面のマイクロポストをテストする
1. リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストがred
になることを確認し、元に戻すとgreen
になることを確認してみましょう。
長くなりましたので、別記事で解説します。
2. サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。
ヒント: リスト 13.57を参考にしてみてください。
test/integration/microposts_interface_test.rb
の変更内容は以下のようになります。
require 'test_helper'
class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:rhakurei)
+ @other_user = users(:skomeiji)
end
...略
+
+ test "micropost sidebar count" do
+ log_in_as(@user)
+ get root_path
+ assert_match "#{@user.microposts.count} microposts", response.body
+ # まだマイクロポストを投稿していないユーザー
+ log_in_as(@other_user)
+ get root_path
+ assert_match "0 microposts", response.body
+ @other_user.microposts.create!(content: "A micropost")
+ get root_path
+ assert_match /1 micropost(?!s)/, response.body
+ end
end
なお、以下の正規表現は、「"1 micropost"には一致するが、"1 microposts"には一致しない」という動作を想定しています。
/1 micropost(?!s)/
ここまでの実装内容に問題がなければ、現時点でこのテストは成功するはずです。
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 707
Started with run options --seed 57726
2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.85516s
2 tests, 21 assertions, 0 failures, 0 errors, 0 skips
サイドバーで単数形と複数形が正しく表示されていない場合
全て単数形で表示されている場合
app/views/shared/_user_info.html.erb
の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %><span>
- <span><%= pluralize(current_user.microposts.count, "micropost" %></span>
+ <span><%= "#{current_user.microposts.count} micropost") %>
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 733
Started with run options --seed 53872
FAIL["test_micropost_sidebar_count", MicropostsInterfaceTest, 3.921162500002538]
test_micropost_sidebar_count#MicropostsInterfaceTest (3.92s)
Expected /34\ microposts/ to match "...略
<span>34 micropost\n</span>
...略".
test/integration/microposts_interface_test.rb:45:in `block in <class:MicropostsInterfaceTest>'
私の環境では、現時点でtest/integration/microposts_interface_test.rb
の45行目は以下のようになっています。
assert_match"#{@user.microposts.count} microposts",response.body
(@user
の対象となるユーザーは複数のマイクロポストに紐付けされているのに)「microposts」という文字列がHTML中にない、ということですね。想定通りの失敗です。
全て複数形で表示されている場合
今度は、app/views/shared/_user_info.html.erb
の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %><span>
- <span><%= pluralize(current_user.microposts.count, "micropost" %></span>
+ <span><%= "#{current_user.microposts.count} microposts") %>
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 940
Started with run options --seed 25315
FAIL["test_micropost_sidebar_count", MicropostsInterfaceTest, 3.437450199999148]
test_micropost_sidebar_count#MicropostsInterfaceTest (3.44s)
Expected /1 micropost(?!s)/ to match "...略
<span>1 microposts\n</span>
...略".
test/integration/microposts_interface_test.rb:52:in `block in <class:MicropostsInterfaceTest>'
2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.44661s
2 tests, 21 assertions, 1 failures, 0 errors, 0 skips
私の環境では、test/integration/microposts_interface_test.rb
の52行目は以下のようになっています。
assert_match/1 micropost(?!s)/,response.body
「"1 microposts"でない"1 micropost"という文字列が見つからない」という趣旨のメッセージが出てテストが失敗している、ということになります。想定通りの失敗ですね。
こうしたルールを利用してメソッドの定義を上書きすることを「オーバーライド」と言います。 ↩