日別アーカイブ: 2021年12月6日

Rails-tutorial(13.4マイクロポストを削除する 主に演習)

13.3から続く

13.3.4 マイクロポストを削除する

最後の機能として、マイクロポストリソースにポストを削除する機能を追加します。
これはユーザー削除と同様に”delete” リンクで実現します。
ユーザーの削除は管理者ユーザーのみが行えるように制限されていたのに対し、
今回は自分が投稿したマイクロポストに対してのみ削除リンクが
動作するようにします。

最初のステップとして、マイクロポストのパーシャルに削除リンクを追加します。作成したコードを示します。

マイクロポストのパーシャルに削除リンクを追加する
app/views/microposts/_micropost.html.erb

<li id="micropost-<%= micropost.id %>">
 <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
 <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
 <span class="content"><%= micropost.content %></span>
 <span class="timestamp">
  Posted <%= time_ago_in_words(micropost.created_at) %> ago.
   <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, method: :delete,
        data: { confirm: "You sure?" } %>
   <% end %>
 </span>
</li>

次にMicropostsコントローラのdestroyアクションを定義しましょう。これも、ユーザーにおける実装とだいたい同じです。
大きな違いは、admin_userフィルターで@user変数を使うのではなく、関連付けを使ってマイクロポストを見つけるようにしている点です。

これにより、あるユーザーが他のユーザーのマイクロポストを
削除しようとすると、自動的に失敗するようになります。
具体的には、correct_userフィルター内でfindメソッドを呼び出すことで、現在のユーザーが削除対象のマイクロポストを保有しているかどうかを確認します。作成したコードを↓に示します。

Micropostsコントローラのdestroyアクション
app/controllers/microposts_controller.rb

before_action :correct_user, only: :destroy
 def destroy
  @micropost.destroy
  flash[:success] = "Micropost deleted"
  redirect_to request.referrer || root_url
 end

private

 def correct_user
 @micropost = current_user.microposts.find_by(id: params[:id])
 redirect_to root_url if @micropost.nil?
 end

destroyメソッドではリダイレクトを使っている点に注目してください。

request.referrerというメソッドを使っています。
このメソッドはフレンドリーフォワーディングのrequest.url変数
と似ていて、一つ前のURLを返します(今回の場合、Homeページになります)

演習

1:マイクロポストを作成し、その後作成したマイクロポストを
削除してみましょう。Railsサーバーのログを見てみて、
DELETE文の内容を確認してみてください。

DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 303]]

2:redirect_to request.referrer || root_urlの行を
redirect_back(fallback_location: root_url)と置き換えても
うまく動くことを、ブラウザを使って確認してみましょう
(このメソッドはRails 5から新たに導入されました)。

redirect_back(fallback_location: root_url)

13.3.5 フィード画面のマイクロポストをテストする

残っている箇所は、Micropostsコントローラの認可をチェックする短いテストとそれらをまとめる統合テストを書くことです。

まずはマイクロポスト用のfixtureに、
別々のユーザーに紐付けられたマイクロポストを追加していきます

別のユーザーに所属しているマイクロポストを追加する
test/fixtures/microposts.yml

ants:
content: "Oh, is that what you want? Because that's how you get ants!"
created_at: <%= 2.years.ago %>
user: archer

zone:
content: "Danger zone!"
created_at: <%= 3.days.ago %>
user: archer

tone:
content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
created_at: <%= 10.minutes.ago %>
user: lana

van:
content: "Dude, this van's, like, rolling probable cause."
created_at: <%= 4.hours.ago %>
user: lana

次に、自分以外のユーザーのマイクロポストは削除をしようとすると、適切にリダイレクトされることをテストで確認します。

間違ったユーザーによるマイクロポスト削除に対してテストする green
test/controllers/microposts_controller_test.rb

require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

test "should redirect destroy for wrong micropost" do
 log_in_as(users(:michael))
 micropost = microposts(:ants)
 assert_no_difference 'Micropost.count' do
  delete micropost_path(micropost)
 end
 assert_redirected_to root_url
end

最後に、統合テストを書きます。今回の統合テストでは、ログイン、
マイクロポストのページ分割の確認、無効なマイクロポストを投稿、
有効なマイクロポストを投稿、マイクロポストの削除、
そして他のユーザーのマイクロポストには [delete] リンクが
表示されないことを確認、といった順でテストしていきます。
いつものように、統合テストを生成するところから始めましょう。

rails generate integration_test microposts_interface

先ほどの順で書いた統合テストは、↓のようになります。書いたコードと、先ほどのステップが結合されている点に注意してください。

マイクロポストのUIに対する統合テスト green
test/integration/microposts_interface_test.rb

require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

def setup
  @user = users(:michael)
end

test "micropost interface" do
 log_in_as(@user)
 get root_path
 assert_select 'div.pagination'
# 無効な送信
 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"
 assert_difference 'Micropost.count', 1 do
 post microposts_path, params: { micropost: { content: content } }
end
 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(:archer))
  assert_select 'a', text: 'delete', count: 0
 end
end

演習

1:4つのコメント(無効な送信など)のそれぞれに対して、
テストが正しく動いているか確認してみましょう。
具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが redになることを確認し、元に戻すとgreenになることを確認してみましょう。

@micropost = @user.microposts.build(content: "")にする
Expected false to be truthy.
test/models/micropost_test.rb:11:in `block in <class:MicropostTest>'

@micropost.destroy
flash[:success] = "Micropost deleted"

"Micropost.count" didn't change by -1.
Expected: 38
Actual: 39

ifの部分を消して誰でも見れるようにする

Expected exactly 0 elements matching "a", found 2..
Expected: 0
Actual: 2

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

サイドバーでマイクロポストの投稿数をテストするためのテンプレート
test/integration/microposts_interface_test.rb

require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest

def setup
  @user = users(:michael)
end
.
.
.
test "micropost sidebar count" do
 log_in_as(@user)
 get root_path
 assert_match "#{FILL_IN} microposts", response.body
# まだマイクロポストを投稿していないユーザー
 other_user = users(:malory)
 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 FILL_IN, response.body
 end
end

@user.microposts.count
“1 micropost”

13.4 マイクロポストの画像投稿

ここまででマイクロポストに関する基本的な操作はすべて実装できました。
この節では、応用編として画像付きマイクロポストを投稿できるようにしてみます。
手順としては、まずは開発環境用のβ版を実装し、その後、
いくつかの改善をとおして本番環境用の完成版を実装します。

画像アップロード機能を追加するためには、2つの視覚的な要素が必要です。1つは画像をアップロードするためのフォーム、もう1つは投稿された画像そのものです。

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

投稿した画像を扱ったり、その画像をMicropostモデルと関連付けするために、今回はCarrierWaveという画像アップローダーを使います。
まずはcarrierwave gemをGemfileに追加しましょう。

gem 'carrierwave', '1.2.2'
gem 'mini_magick', '4.7.0'
group :production do
gem 'fog', '1.42'
end
bundle install

CarrierWaveを導入すると、
Railsのジェネレーターで画像アップローダーが生成できるようになります。早速、次のコマンドを実行してみましょう。

rails generate uploader Picture

(画像のことをimageとすると一般的過ぎるので、
今回はpictureと呼ぶことにします)

picture属性をMicropostモデルに追加するために、マイグレーションファイルを生成し、開発環境のデータベースに適用します。

rails generate migration add_picture_to_microposts picture:string
rails db:migrate

CarrierWaveに画像と関連付けたモデルを伝えるためには、
mount_uploaderというメソッドを使います。このメソッドは、
引数に属性名のシンボルと生成されたアップローダーのクラス名を取ります。

mount_uploader :picture, PictureUploader

(picture_uploader.rbというファイルでPictureUploaderクラスが定義されています。後で修正しますが、今はデフォルトのままで大丈夫です。)
Micropostモデルにアップローダーを追加した結果を↓に示します。

Micropostモデルに画像を追加する
app/models/micropost.rb

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

Homeページにアップローダーを追加するためには、
マイクロポストのフォームにfile_fieldタグを含める必要があります

マイクロポスト投稿フォームに画像アップローダーを追加する
app/views/shared/_micropost_form.html.erb

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture %>
  </span>
<% end %>

Webから更新できる許可リストにpicture属性を追加しましょう。
追加すると、micropost_paramsメソッドは次のようになります。

pictureを許可された属性のリストに追加する
app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  private

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

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

一度画像がアップロードされれば、Micropostパーシャルのimage_tagヘルパーでその画像を描画できるようになります。

画像の無い (テキストのみの) マイクロポストでは画像を表示させないようにするために、picture?という論理値を返すメソッドを使っている点に
注目してください。このメソッドは、画像用の属性名に応じて、
CarrierWaveが自動的に生成してくれるメソッドです。

マイクロポストの画像表示を追加する
app/views/microposts/_micropost.html.erb

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

演習

1:テンプレートを参考に、画像アップローダーをテストしてください。
テストの準備として、まずはサンプル画像をfixtureディレクトリに
追加してください(cp app/assets/images/rails.png test/fixtures/)。

追加したテストでは、Homeページにあるファイルアップロードと、
投稿に成功した時に画像が表示されているかどうかをチェックしています。
なお、テスト内にあるfixture_file_uploadというメソッドは、
fixtureで定義されたファイルをアップロードする特別なメソッドです。
ヒント: picture属性が有効かどうかを確かめるときは、assignsメソッドを使ってください。

このメソッドを使うと、
投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

画像アップロードをテストするためのテンプレートtest/integration/microposts_interface_test.rb

require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    assert_select 'input[type=FILL_IN]'
    # 無効な送信
    post microposts_path, params: { micropost: { content: "" } }
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost:
                                      { content: content,
                                        picture: FILL_IN } }
    end
    assert FILL_IN.picture?
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', '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(:archer))
    assert_select 'a', { text: 'delete', count: 0 }
  end
  .
  .
  .
end

答え:↑FILL_INを↓にする。

file,picture,

assigns(:micropost)

13.5に続く