Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの画像投稿

# 概要

マイクロポストの投稿に関する基本的な機能は、第13章ここまでの学習で一通り実装が完了しました。続いては「画像つきマイクロポストを投稿できるようにする」という項です。

「画像をアップロードしてマイクロポストに関連付ける」という機能を実装するためには、以下のリソースが必要となります。

* 画像をアップロードするためのフォーム
* 投稿された画像そのもの

画像をアップロードするためのフォームには、「Upload Image」というボタンからアクセスすることとします。Railsチュートリアル本文では、[図 13.18](https://railstutorial.jp/chapters/user_microposts?version=5.1#fig-micropost_image_mockup)に、「Upload Image」ボタンと画像つきマイクロポストを含むモックアップが示されています。

まずは開発環境にて、画像アップロード機能のβ版を実装していくこととします。

# 基本的な画像アップロード

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

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - 基本的な画像アップロード](https://qiita.com/rapidliner00/items/a6733ce24f77b3eef3df)

# 演習 - 基本的な画像アップロード

## 1. 画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の[13.4.3](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-image_resizing)でこの問題を直します)。

実際に画像付きのマイクロポストを投稿した結果です。

<img width="1168" alt="スクリーンショット 2020-01-07 7.50.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/fd145fb2-0bf0-e525-ecd7-3bb233219cf2.png">

サーバー側における、マイクロポスト投稿時の`POST`リクエストに対して記録されたログは以下の通りです。

```
Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"LGTM!", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00005592590299f0 @tempfile=#<Tempfile:/tmp/RackMultipart20200106-15302-4935p1.png>, @original_filename="lgtm1.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"lgtm1.png\"\r\nContent-Type: image/png\r\n">}, "commit"=>"Post"}
  User Load (2.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (20.4ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES (?, ?, ?, ?, ?)  [["content", "LGTM!"], ["user_id", 1], ["created_at", "2020-01-06 22:48:18.966477"], ["updated_at", "2020-01-06 22:48:18.966477"], ["picture", "lgtm1.png"]]
   (10.2ms)  commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 278ms (ActiveRecord: 36.3ms)
```

目新しいのは、「`POST`リクエストに含まれる`picture`パラメータ」と「SQLの`INSERT`文に含まれる`picture`属性」です。以下、わかりやすいように改行を入れつつ、「`POST`リクエストのパラメータの中身」と「SQLの`INSERT`文の中身」それぞれ見てみましょう。

```:POSTリクエストのパラメータの中身
"micropost"=>{
  "content"=>"LGTM!",
  "picture"=>#<ActionDispatch::Http::UploadedFile:0x00005592590299f0
    @tempfile=#<Tempfile:/tmp/RackMultipart20200106-15302-4935p1.png>, 
    @original_filename="lgtm1.png",
    @content_type="image/png",
    @headers="Content-Disposition: form-data;
      name=\"micropost[picture]\"; filename=\"lgtm1.png\"\r\nContent-Type:image/png\r\n">
}
```

```:SQLのINSERT文の中身
INSERT INTO "microposts"
  ("content", "user_id", "created_at", "updated_at", "picture")
  VALUES (?, ?, ?, ?, ?)  [
    ["content", "LGTM!"],
    ["user_id", 1],
    ["created_at", "2020-01-06 22:48:18.966477"],
    ["updated_at", "2020-01-06 22:48:18.966477"],
    ["picture", "lgtm1.png"]
  ]
```

## 2. [リスト 13.63](https://railstutorial.jp/chapters/user_microposts?version=5.1#code-image_upload_test)に示すテンプレートを参考に、[13.4](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-micropost_images)で実装した画像アップローダーをテストしてください。

> テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: `cp app/assets/images/rails.png test/fixtures/`)。

> [リスト 13.63](https://railstutorial.jp/chapters/user_microposts?version=5.1#code-image_upload_test)で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にある`fixture_file_upload`というメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです。

> ヒント: `picture`属性が有効かどうかを確かめるときは、11.3.3で紹介した`assigns`メソッドを使ってください。このメソッドを使うと、投稿に成功した後に`create`アクション内のマイクロポストにアクセスするようになります。

変更対象のファイルは`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 interface" do
      log_in_as(@user)
      get root_path
      assert_select 'img.gravatar'
      assert_select 'h1', @user.name
      assert_select 'span', /#{@user.microposts.count}/
      assert_select 'form[action="/microposts"]'
      assert_select 'textarea'
      assert_select 'div.pagination'
+     assert_select 'input[type="file"]'
      # 無効な送信
      assert_no_difference 'Micropost.count' do
        post microposts_path, params: { micropost: { content: "" } }
      end
      assert_select 'div#error_explanation'
      # 有効な送信
      content = "This micropost really ties the room together"
      picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
      assert_difference 'Micropost.count', 1 do
-       post microposts_path, params: { micropost: { content: content } }
+       post microposts_path, params: { micropost: { content: content, picture: picture } }
      end
+     assert assigns(:micropost).picture?
      assert_redirected_to root_url
      follow_redirect!
      assert_match content, response.body
      # 投稿を削除する
      assert_select 'a', text: 'delete'
      first_micropost = @user.microposts.paginate(page: 1).first
      assert_difference 'Micropost.count', -1 do
        delete micropost_path(first_micropost)
      end
      # 違うユーザーのプロフィールにアクセス（削除リンクがないことの確認）
      get user_path(users(:mkirisame))
      assert_select 'a', text: 'delete', count: 0
    end

    ...略
  end
```

現時点で、上記テストは問題なく成功します。

```
# rails test test/integration/microposts_interface_test.rb
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
Running via Spring preloader in process 15350
Started with run options --seed 7126

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.57526s
2 tests, 23 assertions, 0 failures, 0 errors, 0 skips
```

### どのような場合に上記テストが失敗するのか

例えば、以下のように`app/controllers/microposts_controller.rb`を書き換えるとどうなるでしょうか。「MicropostsコントローラーのStrong Parametersで、Webからの変更を受理する属性に`picture`が含まれていない」場合です。

```diff:app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
    ...略

    private

      def micropost_params
-       params.require(:micropost).permit(:content, :picture)
+       params.require(:micropost).permit(:content)
      end

      ...略
  end
```

以下のようなメッセージを出力してテストが失敗するようになります。

```
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15402
Started with run options --seed 13967

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.093530500002089]
 test_micropost_interface#MicropostsInterfaceTest (4.09s)
        Expected false to be truthy.
        test/integration/microposts_interface_test.rb:30:in `block in <class:MicropostsInterfaceTest>'

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.10118s
2 tests, 17 assertions, 1 failures, 0 errors, 0 skips
```

## このテストの不具合

実は、このテストには1つの不具合があります。その内容解説と解消については、別記事で解説します。

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像アップローダーのテストに存在する一つの不具合と、その解消策](https://qiita.com/rapidliner00/items/6972f55f2c4137943eb1)

# 画像の検証

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

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像の検証 - Qiita](https://qiita.com/rapidliner00/items/351b9b08e91c61237de2)

# 演習 - 画像の検証

## 1. 5MB以上の画像ファイルを送信しようとした場合、どうなりますか?

5MB以上の画像ファイルを選択した時点で、まず警告メッセージが出ます。

<img width="1168" alt="スクリーンショット 2020-01-09 20.41.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/46f3f73f-0b5b-df7e-cca6-e24404e53904.png">

それでも送信を強行しようとすると、以下のようなエラーメッセージが出て、マイクロポストの送信が強制的に中止されます。

<img width="1168" alt="スクリーンショット 2020-01-09 20.45.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/c95a3bd3-6ff0-6c9c-2e5a-7160031e532d.png">

「Picture should be less than 5MB」というメッセージは、確かにバリデーションで定義したとおりですね。「Picture」というのは、`errors.add`の第1引数として与えたシンボル名を元に、Railsによって自動補完されたものです。

### 5MB以上の画像ファイルを送信しようとした場合にRailsサーバーが返すログの内容

このとき、当該マイクロポストの`POST`リクエストに対してRailsサーバーが返すログの内容は以下のようになります。

```
Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"25MB picture uploading", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd15dac96e0 @tempfile=#<Tempfile:/tmp/RackMultipart20200109-15302-g9g2gb.jpg>, @original_filename="48965744982_478d5a648c_o.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"48965744982_478d5a648c_o.jpg\"\r\nContent-Type: image/jpeg\r\n">}, "commit"=>"Post"}
  User Load (2.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
  Rendering static_pages/home.html.erb within layouts/application
   (3.6ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 1]]
  Rendered shared/_user_info.html.erb (8.3ms)
  Rendered shared/_error_messages.html.erb (1.1ms)
  ...略
Completed 200 OK in 3238ms (Views: 507.8ms | ActiveRecord: 24.8ms)
```

```
rollback transaction
```

上記のように、RDBへの保存が取り消されています。

```
Rendered shared/_error_messages.html.erb (1.1ms)
```

上記のように、エラーメッセージのパーシャルが描画されています。

## 2. 無効な拡張子のファイルを送信しようとした場合、どうなりますか?

標準の状態では、`input`要素の`accept`属性にない形式のファイルは、Webブラウザでアップロードするファイルを選択しようとした際にグレーアウトして選択できないようになっています。

<img width="1168" alt="スクリーンショット 2020-01-10 7.42.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/4f63f5cf-2cb7-ef55-4d30-7c37c4a28368.png">

続いて、Webブラウザのインスペクター機能を用いて、`input`要素の`accept`属性を無理やり削除してみます。

```diff:
- <input accept="image/jpeg,image/gif,image/png" type="file" name="micropost[picture]" id="micropost_picture">
+ <input type="file" name="micropost[picture]" id="micropost_picture">
```

`input`要素の`accept`属性を削除すると、以下のように、Webブラウザで無効な形式のファイルを選択することが可能になります。

<img width="1168" alt="スクリーンショット 2020-01-10 7.48.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/4c52c923-cc5f-907d-c147-11bda43a61a7.png">

そのままマイクロポストを投稿してみます。

<img width="1168" alt="スクリーンショット 2020-01-10 7.56.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/f573b880-6e92-7f07-29cb-22ba469c00ca.png">

エラーメッセージが出て、マイクロポストの送信が強制的に中止されました。

「Picture You are not allowed to upload "pu" files, allowed types: jpg, jpeg, gif, png」というエラーメッセージも自動生成されます。

### 無効な拡張子のファイルを送信しようとした場合にRailsサーバーが返すログの内容

このとき、当該マイクロポストの`POST`リクエストに対してRailsサーバーが返すログの内容は以下のようになります。

```
Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"invalid format file", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd16c06a398 @tempfile=#<Tempfile:/tmp/RackMultipart20200109-15302-164lc9d.pu>, @original_filename="Class.pu", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"Class.pu\"\r\nContent-Type: application/octet-stream\r\n">}, "commit"=>"Post"}
  User Load (2.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
  Rendering static_pages/home.html.erb within layouts/application
   (3.3ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 1]]
  Rendered shared/_user_info.html.erb (6.5ms)
  Rendered shared/_error_messages.html.erb (0.7ms)
  Rendered shared/_micropost_form.html.erb (20.7ms)
  Rendered shared/_feed.html.erb (0.5ms)
  Rendered shared/_home_logged_in.erb (85.2ms)
  Rendered static_pages/home.html.erb within layouts/application (103.8ms)
  Rendered layouts/_rails_default.erb (289.3ms)
  Rendered layouts/_shim.html.erb (0.3ms)
  Rendered layouts/_header.html.erb (1.0ms)
  Rendered layouts/_footer.html.erb (0.5ms)
Completed 200 OK in 572ms (Views: 529.8ms | ActiveRecord: 6.4ms)
```

```
rollback transaction
```

上記のように、RDBへの保存が取り消されています。

```
Rendered shared/_error_messages.html.erb (0.7ms)
```

上記のように、エラーメッセージのパーシャルが描画されています。

# 画像のリサイズ

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

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像のリサイズ - Qiita](https://qiita.com/rapidliner00/items/c5cd115ac71075c56a80)

# 演習 - 画像のリサイズ

## 1. 解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?

ここまでの実装を終えた時点で、改めて大きなサイズの画像をアップロードしてみましょう。

<img width="1168" alt="スクリーンショット 2020-01-10 18.40.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/048ffa0c-71be-f0bb-85be-d391347bac44.png">

いい感じにリサイズされた上でアップロードされるようになりました。長方形の画像でも、アスペクト比がおかしくなるようなことはないですね。

なお、Railsサーバーのログには、画像のリサイズに関するログは残りませんでした。

## 2. 既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせるとエラーメッセージが表示されるようになるはずです。このエラーを取り除いてみましょう。

> ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。

下記のような`assigns`を使ったController Specのテストの場合、画像のリサイズ処理を実装すると、テストが失敗するようになります。

```ruby: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 interface" do
    # ...略
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content, picture: picture } }
    end
    assert assigns(:picture).picture?
    # ...略
  end
end
```

具体的には、以下のようなエラーが発生するようになります。

```
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15789
Started with run options --seed 59918

ERROR["test_micropost_interface", MicropostsInterfaceTest, 4.157690599997295]
 test_micropost_interface#MicropostsInterfaceTest (4.16s)
NoMethodError:         NoMethodError: undefined method `picture?' for nil:NilClass
            test/integration/microposts_interface_test.rb:30:in `block in <class:MicropostsInterfaceTest>'

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.43253s
2 tests, 16 assertions, 0 failures, 1 errors, 0 skips
```

`assigns`絡みでエラーが発生しているのはわかるのですが、以下の内容の`config/initializers/skip_image_resizing.rb`を作成しても、エラーが解消できませんでした。

```ruby:config/initializers/skip_image_resizing.rb
if Rails.env.test?
  CarrierWave.configure do |config|
    config.enable_processing = false
  end
end
```

なお、以下のようなRequest Specのテストを行っている場合、上記のエラーは発生しません。

```ruby: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 interface" do
    log_in_as(@user)
    get root_path
    assert_select 'img.gravatar'
    assert_select 'h1', @user.name
    assert_select 'span', /#{@user.microposts.count}/
    assert_select 'form[action="/microposts"]'
    assert_select 'textarea'
    assert_select 'div.pagination'
    assert_select 'input[type="file"]'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content, picture: picture } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    assert_select "img[src*='#{picture.original_filename}']"
    # ...略
  end
```

# 本番環境での画像アップロード

開発環境における画像アップロード機能のβ版は、これにて完成となりました。今度は本番環境に画像アップロード機能を実装していきます。長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第13章 ユーザーのマイクロポスト - 本番環境での画像アップロード - Qiita](https://qiita.com/rapidliner00/items/a10806a6f5365d3ead31)

# 演習 - 本番環境での画像アップロード

## 1. 本番環境で解像度の高い画像をアップロードし、適切にリサイズされているか確認してみましょう。長方形の画像であっても、適切にリサイズされていますか?

<img width="1161" alt="スクリーンショット 2020-01-18 22.37.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/2d779c55-be1c-e8ff-9f4a-9150fd62e5d1.png">

<img width="1161" alt="スクリーンショット 2020-01-18 22.39.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/04d765db-d187-4604-c566-7358c92d3d37.png">

適切に画像はリサイズされているようです。