Railsチュートリアル14　返信機能追加記録

# 概要
この記事は、Rails学習のためにチュートリアル14.4.4の機能拡張にて、返信機能を追加した際の記録です。

# 実装仕様について
- [x] マイクロポスト中に＠記号に続けてユーザ名を入力するとユーザに返信できる。
- [x] 返信は受信者と送信者のフィードにのみ表示される。
- [x] ユーザ名の重なりはユーザ登録の項目に一意のユーザ名(ニックネーム)を追加することで対応する。
- [x] ニックネームは半角英数字と’_’のみの入力を許可する(大文字小文字は区別しない)
- [x] ニックネームがわかるように、フィードのユーザ名の表示はユーザ名＋＠ニックネームとする
- [x] 複数リプライは考慮せず、１つ目の＠記号のみ対応する。

# 実装
## ニックネームの追加
仕様との対応は以下のとおり。

- [x] ユーザ名の重なりはユーザ登録の項目に一意のユーザ名(ニックネーム)を追加することで対応する。
- [x] ニックネームは半角英数字と’_’のみの入力を許可する(大文字小文字は区別しない)

###ユーザモデルにニックネームを追加
マイグレーションを作成した。

```
rails generate migration add_nickname_to_users nick_name:string
```

メモ：クラス名をadd_***_to_@@にすることで自動でカラム追加メソッドがスクリプトに記入される。

マイグレーションファイルに`change_column_null`（NOT NULL制約）と`add_index`（インデックスの追加と一意制約）を追加した。

```ruby:db/migrate/[Timestamp]_add_nickname_to_users.rb
class AddNicknameToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :nick_name, :string
    change_column_null :users, :nick_name, false
    add_index  :users, :nick_name, unique: true
  end
end
```

###ユーザモデルにニックネームを追加
チュートリアル６章を参考に以下のとおり追加した。
ニックネームは正規表現によるチェックをする。

```ruby:app/models/user.rb
  #ニックネームは保存前に小文字にする
  before_save :downcase_nick_name
  #ユーザモデルにニックネームを追加
  VALID_NICKNAME_REGEX = /\A[\w]+\z/i
  validates :nick_name, presence:true, length:{maximum:15},
             format: {with: VALID_NICKNAME_REGEX },
             uniqueness: { case_sensitive: false }
・
・
・
    # ニックネームを全て小文字にする
    def downcase_nick_name
      self.nick_name.downcase!
    end
```

###単体テストの作成
テストモデルの各ユーザーにニックネーム属性を追加。
以下は例。

```ruby:test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true
  activated: true
  activated_at: <%= Time.zone.now %>
  nick_name: Michael
```

バリデーションテストにニックネームを追加した。

```ruby:test/users/user_test.rb
 def setup
    @user = User.new(name: "Example User", email: "user@example.com",
            password: "foobar", password_confirmation: "foobar",
            nick_name: “example" )   
  end

  #ニックネームのバリデーションチェック
  test "nick_name should be present" do
    @user.nick_name = "     "
    assert_not @user.valid?
  end

  #ニックネームの長さチェック
  test "nick_name should not be too long" do
    @user.nick_name = "a" * 16
    assert_not @user.valid?
  end

  #ニックネームのフォーマットチェック
  test "nick_name validation should accept valid name" do
    valid_nick_names = %w[user User1 U_SER U3 1 _U2 _ u]
    valid_nick_names.each do |valid_nick_name|
      @user.nick_name = valid_nick_name
      assert @user.valid?, "#{valid_nick_name.inspect} should be valid"
    end
  end

  test "nick_name validation should reject invalid names" do
    invalid_nick_names = %w[,com .org user.name@
                           ユーザ obar+baz.com foobar..com]
    invalid_nick_names.each do |invalid_nick_name|
      @user.nick_name = invalid_nick_name
      assert_not @user.valid?, "#{invalid_nick_name.inspect} should be invalid"
    end
  end

  #ニックネーム一意性のチェック
  test "nick_names should be unique" do
    duplicate_user = @user.dup
    duplicate_user.nick_name = @user.nick_name.upcase
    @user.save
    assert_not duplicate_user.valid?
  end

  #ニックネームが小文字で保存されることのチェック
  test "nick_names should be saved as lower-case" do
    mixed_case_nick_name = "FooExAMPle_CoM1"
    @user.nick_name = mixed_case_nick_name
    @user.save
    assert_equal mixed_case_nick_name.downcase, @user.reload.nick_name
  end

```

以下でモデルテストを実施した。

```
rails test:models
```

##ニックネーム登録画面
ニックネームはユーザ登録と同時に行うのでユーザ登録画面に入力項目を追加した。

###テストの作成
サインアップ時のニックネームのテストを作成した。

```ruby:test/integration/users_signup_test.rb
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar",
                                         nick_name: "" } }  #ニックネーム追加
    end

  test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password",
                                         nick_name: “example" } }  #ニックネーム追加
```

プロフィールの表示名のテストを作成した。

```ruby:test/integration/users_profile_test.rb
  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: "#{@user.name} @#{@user.nick_name}"
```

セッティング画面でニックネーム変更のテストを作成した。

```ruby:test/integration/users_edit_test.rb
  test "unsuccessful edit" do
    log_in_as(@user)
    #ページにアクセス
    get edit_user_path(@user)
    #目的のページかどうか判定
    assert_template 'users/edit'
    #変更を送信(PATCHメソッドを使う)
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar",
                                              nick_name: "" } }
    #失敗時のページが目的のページかどうか判定
    assert_template 'users/edit'
    #エラーメッセージが表示されている
    assert_select 'div.alert', 'The form contains 6 errors.'

  test "successful edit" do
    log_in_as(@user)
    #ページにアクセス
    get edit_user_path(@user)
    assert_template 'users/edit'
    #name and emailを変更
    name  = "Foo Bar"
    email = "foo@bar.com"
    nick_name = "foobar"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "",
                                              nick_name: nick_name } }
    #フラッシュメッセージがあることを確認
    assert_not flash.empty?
    #最新のユーザ情報を読み出す
    assert_redirected_to @user
    @user.reload
    #変更結果が反映されているか確認
    assert_equal name,  @user.name
    assert_equal email, @user.email
    assert_equal nick_name, @user.nick_name


  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], request.protocol + request.host + edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    assert_nil session[:forwarding_url]
    name  = "Foo Bar"
    email = "foo@bar.com"
    nick_name = "foobar" 
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "",
                                              nick_name: nick_name } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
    assert_equal nick_name, @user.nick_name
```

インデックスページのニックネーム表示テストを作成した。

```ruby:test/integration/site_layout_test.rb
  test "user nick_name test in index" do
    log_in_as(@user)
    get root_path
    assert_select 'h1', text: "#{@user.name}@#{@user.nick_name}"
  end
```

マイクロポストのニックネーム表示テストを作成した。

```ruby:test/integration/microposts_interface_test.rb
  test "micropost interface" do
    log_in_as(@user)
    get root_path 
・
・
・
    # マイクロポストに＠nick_nameがあるかチェック
    assert_select 'h1', text: "#{@user.name} @#{@user.nick_name}"
```

###サインアップ画面の変更
サインアップにニックネームの入力項目を追加した。

```ruby:app/views/users/_form.html.erb
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.label :nick_name %>
  <%= f.text_field :nick_name, class: 'form-control' %>

  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>
```

`user_params`メソッドで`nick_name`の取得を許可した。

```ruby:app/controllers/users_controller.rb
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation, :nick_name)
    end
```

既存ユーザにニックネームを付与した。

```ruby:db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true,
             activated: true,
             activated_at: Time.zone.now,
             nick_name: "example")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password,
               activated: true,
               activated_at: Time.zone.now,
               nick_name: "example_#{n+1}")
end
```

DBをリセットしてseedsを反映した。

```
rails db:migrate:reset
rails db:seeds
```

##ユーザ名の後ろに＠nick_nameの表示を追加
仕様との対応は以下のとおり。

- [x] ニックネームがわかるように、フィードのユーザ名の表示はユーザ名＋＠ニックネームとする

###ビューの変更
ユーザプロフィールビューに＠nick_nameを追加した。

```ruby:app/views/users/show.html.erb
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %> @<%= @user.nick_name %>
```

ユーザ情報にも＠nick_nameを追加する。

```ruby:app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %> @<%= current_user.nick_name %></h1>
```

マイクロポストにも追加する。

```ruby: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 %> @<%= micropost.user.nick_name %></span>
```

##マイクロポストモデルの変更
仕様との対応は下記のとおり。

- [x] マイクロポスト中に＠記号に続けてユーザ名を入力するとユーザに返信できる。
- [x] 複数リプライは考慮せず、１つ目の＠記号のみ対応する。

###返信先/送信元を追加
マイクロポストモデルに返信先・送信元を追加した。

```
rails generate migration add_reply_to_from_to_microposts reply_to:integer reply_from:integer
rails db:migrate
```


```ruby:app/controllers/microposts_controller.rb
  private

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

###テストの作成
モデルのテストを作成した。

```ruby:test/models/micropost_test.rb
  test "input reply_to id" do
    #reply_toに宛先を入れる
    reply_to = @user.microposts.build(content: "This is Reply Test", reply_to: @replyto.id, reply_from: @user.id)
    #宛先が入っているか確認
    assert_equal reply_to.reply_to, @replyto.id
    assert_equal reply_to.reply_from, @user.id
  end

```

##返信宛先の検出
仕様との対応は下記のとおり。

- [x] マイクロポスト中に＠記号に続けてユーザ名を入力するとユーザに返信できる。
- [x] 複数リプライは考慮せず、１つ目の＠記号のみ対応する。

###テストの作成
受信者と送信者が入力され、送信者のフィードに表示されるかを確認するテストを作成した。

```ruby:test/integration/microposts_interface_test.rb
  def setup
    @user = users(:michael)
    @replyto = users(:archer)
    @replyto2 = users(:lana)
  end

  test "micropost reply" do
    log_in_as(@user)
    get root_path
    # 有効な送信
    content = "@#{@replyto.nick_name} @#{@replyto2.nick_name} This micropost is test for reply."
    post microposts_path, params: { micropost: { content: content } }

    get root_path
    #送信者に表示されているかチェック
    assert_match @user.microposts.first.reply_to.to_s, @replyto.id.to_s
    assert_match @user.microposts.first.reply_from.to_s, @user.id.to_s
    assert_match content, response.body
  end
```

###マイクロポストから宛先を検出
マイクロポスト作成時にニックネームを探し、受信者と送信者の`id`を入力するようにコントローラを変更した。

```ruby:app/controllers/microposts_controller.rb
  def create
    @micropost = current_user.microposts.build(micropost_params)

    #正規表現のフォーマット作成
    nickname_regex = /@([\w]{1,15})/i
    #正規表現にマッチするものを探す
    @micropost.content.match(nickname_regex)
    #あったらニックネームからユーザを探す、なければ$1がnilになる
    if $1
      reply_user = User.find_by(nick_name: $1.downcase)
      #ユーザがいたらカラムにセット
      @micropost.reply_to = reply_user.id if reply_user
      @micropost.reply_from = current_user.id
    end
```

以下の記事を参考にさせて頂きました。
- [Railsチュートリアル 第14章 返信機能実装記録](https://qiita.com/johnslith/items/8d931b66f72c97c78199)

##リプライマイクロポストの表示制限
仕様との対応は以下のとおり。

- [x] 返信は受信者と送信者のフィードにのみ表示される。

###テストの作成
送信者と受信者にのみ表示されていることのテストを作成した。

```ruby:test/integration/microposts_interface_test.rb

    # 受信者に表示されているかチェック
    log_in_as(@replyto)
    get root_path
    assert_match content, response.body
  
    # 送信者のフォロワー/受信者のフォロワーに表示されていないかチェック
    log_in_as(@replyto2)
    get root_path
    assert_no_match content, response.body
```

```ruby:test/fixtures/relationships.yml
five:
  follower: archer
  followed: lana
```

###ユーザへの表示制限
マイクロポストの抽出条件を変更した。

```ruby:app/models/user.rb
  def feed
    #スケールしたステータスフィードを返す
    following_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id",user_id: id)
             .where(reply_to: [nil,""])
             .or(Micropost.where(reply_to: id).or(Micropost.where(reply_from: id)))
  end
```

##テスト
テストを実施してGREENになることを確認した。

```
rails test
```

#最後に
チュートリアルにはスコープを使うと書いてあったが理解できなかったので、実装を優先した。
この後、スコープを使った方法に修正したい。