LoginSignup
0
0

More than 3 years have passed since last update.

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

Posted at

概要

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

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

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

画像をアップロードするためのフォームには、「Upload Image」というボタンからアクセスすることとします。Railsチュートリアル本文では、図 13.18に、「Upload Image」ボタンと画像つきマイクロポストを含むモックアップが示されています。

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

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

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

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

1. 画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。

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

スクリーンショット 2020-01-07 7.50.28.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に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。

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

リスト 13.63で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです。

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

変更対象のファイルはtest/integration/microposts_interface_test.rbです。変更内容は以下のようになります。

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が含まれていない」場合です。

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つの不具合があります。その内容解説と解消については、別記事で解説します。

画像の検証

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

演習 - 画像の検証

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

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

スクリーンショット 2020-01-09 20.41.08.png

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

スクリーンショット 2020-01-09 20.45.33.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ブラウザでアップロードするファイルを選択しようとした際にグレーアウトして選択できないようになっています。

スクリーンショット 2020-01-10 7.42.03.png

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

- <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ブラウザで無効な形式のファイルを選択することが可能になります。

スクリーンショット 2020-01-10 7.48.24.png

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

スクリーンショット 2020-01-10 7.56.41.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)

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

画像のリサイズ

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

演習 - 画像のリサイズ

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

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

スクリーンショット 2020-01-10 18.40.32.png

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

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

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

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

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

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を作成しても、エラーが解消できませんでした。

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

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

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

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

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

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

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

スクリーンショット 2020-01-18 22.37.22.png

スクリーンショット 2020-01-18 22.39.52.png

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0