概要
マイクロポストの投稿に関する基本的な機能は、第13章ここまでの学習で一通り実装が完了しました。続いては「画像つきマイクロポストを投稿できるようにする」という項です。
「画像をアップロードしてマイクロポストに関連付ける」という機能を実装するためには、以下のリソースが必要となります。
- 画像をアップロードするためのフォーム
- 投稿された画像そのもの
画像をアップロードするためのフォームには、「Upload Image」というボタンからアクセスすることとします。Railsチュートリアル本文では、図 13.18に、「Upload Image」ボタンと画像つきマイクロポストを含むモックアップが示されています。
まずは開発環境にて、画像アップロード機能のβ版を実装していくこととします。
基本的な画像アップロード
長くなりましたので、別記事で解説します。
演習 - 基本的な画像アップロード
1. 画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。
実際に画像付きのマイクロポストを投稿した結果です。
サーバー側における、マイクロポスト投稿時の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
文の中身」それぞれ見てみましょう。
"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">
}
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
です。変更内容は以下のようになります。
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
が含まれていない」場合です。
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以上の画像ファイルを選択した時点で、まず警告メッセージが出ます。
それでも送信を強行しようとすると、以下のようなエラーメッセージが出て、マイクロポストの送信が強制的に中止されます。
「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ブラウザでアップロードするファイルを選択しようとした際にグレーアウトして選択できないようになっています。
続いて、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ブラウザで無効な形式のファイルを選択することが可能になります。
そのままマイクロポストを投稿してみます。
エラーメッセージが出て、マイクロポストの送信が強制的に中止されました。
「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. 解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?
ここまでの実装を終えた時点で、改めて大きなサイズの画像をアップロードしてみましょう。
いい感じにリサイズされた上でアップロードされるようになりました。長方形の画像でも、アスペクト比がおかしくなるようなことはないですね。
なお、Railsサーバーのログには、画像のリサイズに関するログは残りませんでした。
2. 既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせるとエラーメッセージが表示されるようになるはずです。このエラーを取り除いてみましょう。
ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。
下記のようなassigns
を使ったController Specのテストの場合、画像のリサイズ処理を実装すると、テストが失敗するようになります。
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
を作成しても、エラーが解消できませんでした。
if Rails.env.test?
CarrierWave.configure do |config|
config.enable_processing = false
end
end
なお、以下のようなRequest Specのテストを行っている場合、上記のエラーは発生しません。
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. 本番環境で解像度の高い画像をアップロードし、適切にリサイズされているか確認してみましょう。長方形の画像であっても、適切にリサイズされていますか?
適切に画像はリサイズされているようです。