0
0

More than 3 years have passed since last update.

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

Posted at

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

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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の内容は以下のようになります。

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文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

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

スクリーンショット 2020-01-03 14.04.18.png

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

スクリーンショット 2020-01-03 14.04.37.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?」という確認メッセージが表示されます。

スクリーンショット 2020-01-03 14.29.03.png

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

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

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

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

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

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

ヒント: リスト 13.57を参考にしてみてください。

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

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

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

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

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

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

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


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

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