saran12345678 / RubyonRailsTutorial

0 stars 1 forks source link

第14章 ユーザーをフォローする #14

Open saran12345678 opened 2 months ago

saran12345678 commented 2 months ago

図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。(ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。)

ID:1がフォローしているユーザのID一覧が出力される

図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? 想像してみてください。また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

IDが1のユーザ情報を出力する 1が出力される ※ユーザID:2をフォローしているのはID:1のみであるため

saran12345678 commented 2 months ago

コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。

irb(main):003> User.first.active_relationships.create(followed_id: User.second.id)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
  TRANSACTION (0.0ms)  begin transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Relationship Create (0.4ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2024-09-05 12:12:00.773049"], ["updated_at", "2024-09-05 12:12:00.773049"]]
  TRANSACTION (30.5ms)  commit transaction
=> #<Relationship:0x000076f5624f28a0 id: 1, follower_id: 1, followed_id: 2, created_at: Thu, 05 Sep 2024 12:12:00.773049000 UTC +00:00, updated_at: Thu, 05 Sep 2024 12:12:00.773049000 UTC +00:00>

先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

ID1がID2をフォローしているとわかる

irb(main):009> User.first.active_relationships
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Relationship Load (0.1ms)  SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> [#<Relationship:0x000076f561a0a780 id: 1, follower_id: 1, followed_id: 2, created_at: Thu, 05 Sep 2024 12:12:00.773049000 UTC +00:00, updated_at: Thu, 05 Sep 2024 12:12:00.773049000 UTC +00:00>]
saran12345678 commented 2 months ago

リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5以降は必須ではなくなりました。ここでは念のためこのバリデーションを省略していませんが、このバリデーションが省略されているのを見かけるかもしれないので、覚えておくと良いでしょう。)

validatesをコメントアウトした状態で、テストに成功することを確認

class Relationship < ApplicationRecord
  # belongs_to :follower, class_name: "User"
  # belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end
95 runs, 359 assertions, 0 failures, 0 errors, 0 skips
saran12345678 commented 2 months ago

コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。

エラーになるため、firstとsecondを使用した

irb(main):005> michael = User.first
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 
#<User:0x00007ee16818ce50
...

irb(main):006> archer = User.second
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> 
#<User:0x00007ee16805d840
...

irb(main):011> michael.follow(archer)
  TRANSACTION (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Relationship Create (0.3ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2024-09-05 12:51:27.654311"], ["updated_at", "2024-09-05 12:51:27.654311"]]
  TRANSACTION (25.6ms)  commit transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 
[#<User:0x00007ee16805d840
  id: 2,
  name: "Zula Goodwin",
  email: "example-1@railstutorial.org",
  created_at: Thu, 05 Sep 2024 07:48:18.581206000 UTC +00:00,
  updated_at: Thu, 05 Sep 2024 07:48:18.581206000 UTC +00:00,
  password_digest: "[FILTERED]",
  remember_digest: nil,
  admin: false,
  activation_digest: "$2a$12$5Ldvj.V4yWA5m46e1kwC6uK3tNX6hTO2Vbr87G5q6Zq3DRGoTh8ae",
  activated: true,
  activated_at: Thu, 05 Sep 2024 07:48:18.351068000 UTC +00:00,
  reset_digest: nil,
  reset_sent_at: nil>]

irb(main):013> michael.following?(archer)
=> true

irb(main):014> michael.unfollow(archer)
  TRANSACTION (0.1ms)  begin transaction
  Relationship Delete All (0.4ms)  DELETE FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ?  [["follower_id", 1], ["followed_id", 2]]
  TRANSACTION (26.0ms)  commit transaction
=> 
[#<User:0x00007ee16805d840
  id: 2,
  name: "Zula Goodwin",
  email: "example-1@railstutorial.org",
  created_at: Thu, 05 Sep 2024 07:48:18.581206000 UTC +00:00,
  updated_at: Thu, 05 Sep 2024 07:48:18.581206000 UTC +00:00,
  password_digest: "[FILTERED]",
  remember_digest: nil,
  admin: false,
  activation_digest: "$2a$12$5Ldvj.V4yWA5m46e1kwC6uK3tNX6hTO2Vbr87G5q6Zq3DRGoTh8ae",
  activated: true,
  activated_at: Thu, 05 Sep 2024 07:48:18.351068000 UTC +00:00,
  reset_digest: nil,
  reset_sent_at: nil>]

irb(main):015> michael.following?(archer)
=> false

irb(main):016> michael.follow(michael)
=> nil

irb(main):017> michael.following?(michael)
=> false

先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。

followではInsert unfollowではdelete それ以外ではSelect文が実行されている

saran12345678 commented 2 months ago

コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?

irb(main):007> user1.followers.map(&:id)
  User Load (0.2ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [2, 3]

上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。

一致していることを確認

irb(main):008> user1.followers.count
  User Count (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 2

user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか?(ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。)

SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]

user.followers.to_a.countとの違いはない。

saran12345678 commented 2 months ago

コンソールを開いて、User.first.followers.countの結果がリスト 14.14で期待される結果と一致していることを確認してみましょう。

期待される結果と一致している

irb(main):001> User.first.followers.count
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Count (0.1ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38

上の演習と同様に、User.first.following.countの結果も一致していることを確認してみましょう。

一致している

irb(main):002> User.first.following.count
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Count (0.1ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49
saran12345678 commented 2 months ago

ブラウザから/users/2にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5では[Unfollow]ボタンが表示されているはずです。さて、/users/1にアクセスすると、どのような結果が表示されるでしょうか?

user2の場合はSeedのデータを見る限りフォローしていないため、フォローボタンが表示される user5の場合は、同じくSeedのデータにてフォローが行われているため、アンフォローボタンが表示される user1の場合は、current_userが@userではないかの分岐があるため、ボタンは非表示になるように実装されている スクリーンショット 2024-09-05 23 57 08 スクリーンショット 2024-09-06 0 00 22 スクリーンショット 2024-09-06 0 00 34

ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。

正しく数値が表示されている スクリーンショット 2024-09-06 0 03 37

Homeページに表示されている統計情報に対してテストを書いてみましょう。同様にして、プロフィールページにもテストを追加してみましょう。(ヒント: リスト 13.29で示したテストに追加してみてください。)

to_sにて文字列に変換しないとテストに失敗する

    assert_match @user.followers.count.to_s, response.body
    assert_match @user.following.count.to_s, response.body
saran12345678 commented 2 months ago

ブラウザで/users/1/followersと/users/1/followingを開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像のリンクが正常に機能していることも確認してみましょう。

スクリーンショット 2024-09-06 0 27 28 スクリーンショット 2024-09-06 0 27 47 サイドバーの画像リンクが機能していることも確認

リスト 14.29のassert_selectのテストが正しく動作することを、関連するアプリケーションのコードをコメントアウトして確認してみましょう。

2箇所コメントアウト 1箇所ではアイコン下もしくはフォロー(フォロワー)一覧のリンクが残るため、テストに成功してしまう。

<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><strong>Microposts:</strong> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%# <%= link_to gravatar_for(user, size: 30), user %> %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%# <%= render @users %> %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>
  1) Failure:
FollowPagesTest#test_following_page [/workspaces/sample_app/test/integration/following_test.rb:19]:
Expected at least 1 element matching "a[href="/users/409608538"]", found 0..
Expected 0 to be >= 1.

  2) Failure:
FollowPagesTest#test_followers_page [/workspaces/sample_app/test/integration/following_test.rb:29]:
Expected at least 1 element matching "a[href="/users/409608538"]", found 0..
Expected 0 to be >= 1.

100 runs, 380 assertions, 2 failures, 0 errors, 0 skips
saran12345678 commented 2 months ago

ブラウザ上から/users/2を開き、[Follow]と[Unfollow]を実行してみましょう。うまく機能しているでしょうか?

スクリーンショット 2024-09-06 8 12 27 スクリーンショット 2024-09-06 8 12 38

先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?

フォロー時

  Relationship Create (3.6ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2024-09-05 23:12:22.526405"], ["updated_at", "2024-09-05 23:12:22.526405"]]

アンフォロー時

Relationship Delete All (0.3ms)  DELETE FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ?  [["follower_id", 1], ["followed_id", 2]]
saran12345678 commented 2 months ago

フォロー機能やフォロー解除機能を「標準の方法」または「Turboによる方法」で実現する方法の違いは、Turboは標準の方法よりもサーバーリクエストが1回多いという点しかありません。このため、Turboが確実に動作しているかどうかを判定するのが難しくなることがあります。リスト 14.35およびリスト 14.36にあるフォロワー数の後ろに一時的に「Turbo利用中」という文字を追加し、次に[Follow]ボタンや[Unfollow]ボタンをクリックして、Turboのテンプレートが確実にレンダリングすることを確認してみてください。確認が終わったら元に戻しておきましょう。

スクリーンショット 2024-09-06 9 23 29 スクリーンショット 2024-09-06 9 23 34

saran12345678 commented 2 months ago

リスト 14.34のcrete、destroyメソッドでrespond_toブロック内のformat.html、format.turbo_streamを順にコメントアウトしていき、それぞれテストの結果がどうなるか確認してみましょう。

createアクションのformat.htmlのみをコメントアウト

  1) Error:
FollowTest#test_should_follow_a_user_the_standard_way:
ActionController::UnknownFormat: ActionController::UnknownFormat
    app/controllers/relationships_controller.rb:7:in `create'
    test/integration/following_test.rb:39:in `block (2 levels) in <class:FollowTest>'
    test/integration/following_test.rb:38:in `block in <class:FollowTest>'

106 runs, 392 assertions, 0 failures, 1 errors, 0 skips

createアクションのformat.turbo_streamのみをコメントアウト

  1) Error:
FollowTest#test_should_follow_a_user_with_Hotwire:
ActionController::UnknownFormat: ActionController::UnknownFormat
    app/controllers/relationships_controller.rb:7:in `create'
    test/integration/following_test.rb:46:in `block (2 levels) in <class:FollowTest>'
    test/integration/following_test.rb:45:in `block in <class:FollowTest>'

106 runs, 394 assertions, 0 failures, 1 errors, 0 skips

destroyアクションのformat.htmlのみをコメントアウト

  1) Error:
UnfollowTest#test_should_unfollow_a_user_the_standard_way:
ActionController::UnknownFormat: ActionController::UnknownFormat
    app/controllers/relationships_controller.rb:16:in `destroy'
    test/integration/following_test.rb:65:in `block (2 levels) in <class:UnfollowTest>'
    test/integration/following_test.rb:64:in `block in <class:UnfollowTest>'

106 runs, 391 assertions, 0 failures, 1 errors, 0 skips

destroyアクションのformat.turbo_streamのみをコメントアウト

  1) Error:
UnfollowTest#test_should_unfollow_a_user_with_Hotwire:
ActionController::UnknownFormat: ActionController::UnknownFormat
    app/controllers/relationships_controller.rb:16:in `destroy'
    test/integration/following_test.rb:73:in `block (2 levels) in <class:UnfollowTest>'
    test/integration/following_test.rb:72:in `block in <class:UnfollowTest>'

106 runs, 394 assertions, 0 failures, 1 errors, 0 skips
saran12345678 commented 2 months ago

マイクロポストのidが数字の小さい順に並び、数字が大きいほど新しいと仮定すると、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。(ヒント: 13.1.4の実装で使ったdefault_scopeを思い出してください。)

IDが大きいほど最新であるため、IDの降順で取得されるはずである。 これはマイクロポストの際にも同様であったためそのように定義する。 user.feed.map(&:id)ではIDを取得しようとしているため、図14.22を参考にすると、 10,9,7,5,4,2,1の順で取得できるはずである。

saran12345678 commented 2 months ago

リスト 14.41において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?

OR user_id = ?", following_ids, idを取り除けばフォローしている人の投稿のみが表示されるはず

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

このテストが失敗するはず

    # フォロワーがいるユーザー自身の投稿を確認
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end

失敗することを確認

  4) Failure:
UserTest#test_feed_should_have_the_right_posts [/workspaces/sample_app/test/models/user_test.rb:111]:
Expected false to be truthy.

リスト 14.41において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?

user_id IN (?) OR の条件を取り除けば良い

    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

リスト 14.41において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?(ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。)

Micropost.whereを使用しなければ、Where句の条件が指定されてなくなるため全件表示される。

  Micropost.all
saran12345678 commented 2 months ago

Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
  end

リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています(このメソッドは11.2.3で扱ったCGI.escapeと密接に関連します)。このコードでHTMLをエスケープする必要がある理由を考えてみてください。(ヒント: 試しにエスケープ処理を削除して、得られるHTMLのソースコードと一致しないマイクロポストのコンテンツがないかどうか、注意深く調べてみてください。また、ターミナルの検索機能Cmd-FもしくはCtrl-Fで「sorry」を検索すると、原因の究明に役立つでしょう。)

CGI.escapeHTMLを行わないとテストに失敗する。 test/fixtures/microposts.ymlのテストデータ

"I'm sorry. Your words made sense, but your sarcastic tone did not."

テスト失敗時のエラー文抜粋

/I'm\ sorry\.\ Your\ words\ made\ sense,\ but\ your\ sarcastic\ tone\ did\ not\.

演習にて「「sorry」を検索すると、原因の究明に役立つでしょう。」とあることから、 シングルクォーテーションがエスケープされているため、assert_matchに失敗していると思う。

test/fixtures/microposts.ymlにてシングルクォーテーションを全て取り除いた上で、 再度テストを実施すると、テストに成功するため合っていると判断

@saran12345678 ➜ /workspaces/sample_app (main) $ rails db:seed
@saran12345678 ➜ /workspaces/sample_app (main) $ rails t
Run options: --seed 11162

# Running:

............................................................................................................

Finished in 7.006843s, 15.4135 runs/s, 70.2171 assertions/s.

リスト 14.44のコードは、実はRailsのleft_outer_joinsメソッドを使うと、いわゆるLEFT OUTER JOINで直接表現できます。リスト 14.50のコード23 を適用してテストを実行し、このコードが返すフィードがテストでパスすることを確かめてみましょう。 残念ながら、テストがパスするにもかかわらず、実際のフィードにはユーザー自身のマイクロポストがいくつも重複表示されている(図 14.2524 ので、次はリスト 14.51のテストを使ってこのエラーをキャッチしてください。このテストで使っているdistinctは、コレクション内の要素を重複抜きで返します。エラーをキャッチできたら、クエリにdistinctメソッドを追加したコード(リスト 14.52)に置き換えるとテストが green になることを確かめてください。次は、生成されたSQLを直接調べて、DISTINCTという語がクエリ自身に含まれていることを確認してください。これは、DISTINCTを指定した要素がアプリケーションのメモリ上ではなく、データベース上で効率よくSELECTされていることを示しています。(ヒント: SQLを直接調べるには、RailsコンソールでUser.first.feedを実行します。)

このコードが返すフィードがテストでパスすることを確かめてみましょう。

108 runs, 492 assertions, 0 failures, 0 errors, 0 skips

次はリスト 14.51のテストを使ってこのエラーをキャッチしてください。 下記テストを追加

      assert_equal michael.feed.distinct, michael.feed

テストが失敗し、重複をキャッチできていることを確認

108 runs, 458 assertions, 1 failures, 0 errors, 0 skips

エラーをキャッチできたら、クエリにdistinctメソッドを追加したコード(リスト 14.52)に置き換えるとテストが green になることを確かめてください。 distinctを追加することでテストに成功するようになった

108 runs, 528 assertions, 0 failures, 0 errors, 0 skips

コンソールにて、feed実行時にDISTINCTが使用されていることを確認

  Micropost Count (3.5ms)  SELECT COUNT(DISTINCT "microposts"."id") FROM "microposts" LEFT OUTER JOIN "users" ON "users"."id" = "microposts"."user_id" LEFT OUTER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" LEFT OUTER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" WHERE (relationships.follower_id = 1 or microposts.user_id = 1)
  ↳ app/views/shared/_feed.html.erb:1
  Micropost Load (1.3ms)  SELECT DISTINCT "microposts".* FROM "microposts" LEFT OUTER JOIN "users" ON "users"."id" = "microposts"."user_id" LEFT OUTER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" LEFT OUTER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" WHERE (relationships.follower_id = 1 or microposts.user_id = 1) ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 30], ["OFFSET", 0]]
  ↳ app/views/shared/_feed.html.erb:3