Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストを操作する

# マイクロポストリソースのルーティング

Micropostsコントローラーの内容を実装する前提条件として、まずは必要なルーティングを`config/routes.rb`に記述していかなければなりません。但し、「Micropostリソースのインターフェースへのアクセスは、プロフィールページ・Homeページを介して行われる」という前提があるため、Mircopostsコントローラーに`new`や`edit`のようなアクションは必要とされません。実際に必要とされるアクションは、`create`と`destroy`の2つのみとなります。

以上を踏まえて、`config/routes.rb`に対して実際に行う変更の内容は、以下のようになります。

```diff: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)`|

# マイクロポストのアクセス制御

長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストのアクセス制御を、テスト駆動開発で実装する](https://qiita.com/rapidliner00/items/6691882a5f25a964330b)

# 演習 - マイクロポストのアクセス制御

## 1. なぜUsersコントローラ内にある`logged_in_user`フィルターを残したままにするとマズイのでしょうか? 考えてみてください。 

「スーパークラスで定義された名前と同じ名前のメソッドをサブクラスで定義すると、当該サブクラスにおいては、当該メソッドの定義は上書きされる」というのが、オブジェクト指向一般のルール[^オーバーライド]です。そして、`UsersController`は`ApplicationController`のサブクラスです。

また、今回の実装においては、`ApplicationController#logged_in_user`メソッドの実装を`UsersController`で上書きしなければならない理由はありません。そのような状況で無駄に`logged_in_user`メソッドの実装を上書きすることは、以下のような問題があります。

* 存在しなくてもいい重複したコードがプログラム中に存在することになる
* 無駄なコードは、無駄にバグを埋め込む原因となる

[^オーバーライド]: こうしたルールを利用してメソッドの定義を上書きすることを「オーバーライド」と言います。

# マイクロポストの作成・フィードの実装・マイクロポストの削除・フィード画面のマイクロポストに対するテスト

「テストから先に書き、テストの内容を網羅していく」という方法を取った結果、内容が非常に長くなりました。別記事で解説します。

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの作成・削除に関する機能を、テスト駆動開発で実装する](https://qiita.com/rapidliner00/items/dee95f75f445bae13bb6)

# 演習 - マイクロポストを作成する

## 1. Homeページをリファクタリングして、`if-else`文の分岐のそれぞれに対してパーシャルを作ってみましょう。

まずはログイン済みユーザーのHomeページに対応するパーシャルを作成します。ファイル名は`app/views/shared/_home_logged_in.erb`とします。内容は以下のようになります。

```erb:app/views/shared/_home_logged_in.erb
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= render 'shared/user_info' %>
    </section>
    <section class="micropost_form">
      <%= render 'shared/micropost_form' %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3>Micropost Feed</h3>
    <%= render 'shared/feed' %>
  </div>
</div>
```

続いて、未ログインユーザーのHomeページに対応するパーシャルを作成します。ファイル名は`app/views/shared/_home_not_logged_in.erb`とします。内容は以下のようになります。

```erb:app/views/shared/_home_not_logged_in.erb
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a href="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_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %>
```

続いて、新たに作成したパーシャルを元々のHomeページのビューで使うようにします。新たな`app/views/static_pages/home.html.erb`の内容は以下のようになります。

```erb:app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<% if logged_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`文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

まずは、新たに実装したマイクロポストの投稿フォームに文字を入力し、実際に投稿してみます。

<img width="1168" alt="スクリーンショット 2020-01-03 14.04.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/67caa834-3337-43bc-e311-dc374adcdbe1.png">

「Post」ボタンを押すと、以下のような画面が出力されます。

<img width="1168" alt="スクリーンショット 2020-01-03 14.04.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/7f70f275-c5f7-56a9-5b36-9c6e710c2fb4.png">

「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?」という確認メッセージが表示されます。

<img width="1168" alt="スクリーンショット 2020-01-03 14.29.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/d82ea1bc-8cf8-0c91-65fe-58eed141e730.png">

そこで「OK」をクリックすると、マイクロポストが実際に削除されます。

<img width="1168" alt="スクリーンショット 2020-01-03 14.39.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/611593d4-8332-34ef-5151-4ce923ef1996.png">

「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リンクをクリックしたときに、確認メッセージが表示されることなくマイクロポストが削除されてしまう」という不具合を発生させてしまいました。

```diff:
- <%= 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`の内容を以下のように変更してみます。

```diff: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](https://railstutorial.jp/chapters/user_microposts?version=5.1#code-microposts_interface_test)で示した４つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが`red`になることを確認し、元に戻すと`green`になることを確認してみましょう。

長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - ユーザーのフィード画面に対するテストは正しく動いているか](https://qiita.com/rapidliner00/items/73978178088c66867b5e)

## 2. サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。

> ヒント: [リスト 13.57](https://railstutorial.jp/chapters/user_microposts?version=5.1#code-sidebar_micropost_count)を参考にしてみてください。

`test/integration/microposts_interface_test.rb`の変更内容は以下のようになります。

```diff: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"には一致しない」という動作を想定しています。

```ruby:
/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`の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。

```diff: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行目は以下のようになっています。

```ruby: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`の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。

```diff: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行目は以下のようになっています。

```ruby:test/integration/microposts_interface_test.rb
assert_match /1 micropost(?!s)/, response.body
```

「"1 microposts"でない"1 micropost"という文字列が見つからない」という趣旨のメッセージが出てテストが失敗している、ということになります。想定通りの失敗ですね。