Rails Tutorialの第14章にある、メッセージ機能を作る件の続きです。
前回までで表示する画面ができました。作成する画面を作ります。
DMを作成
DMを作成する機能を作ります。micropostを作成するところを参考にします。tutorialのリスト 13.36: を参考にコントローラーにcreateアクションを作ります。
class DmsController < ApplicationController
  before_action :logged_in_user, only: [:index, :create, :destroy]
  
  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
  end
  def create
    @dm = current_user.sent_dms.build(dm_params)
    if @dm.save
      flash[:success] = "DM sent!"
    else
      render 'dms/index'
    end
  end
  private
    def dm_params
      params.require(:dm).permit(:content,:receiver)
    end
end
<section class="micropost_form">
  <%= form_for(@dm) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.label :receiver_name %>
      <%= f.text_field :receiver_name, class: 'form-control' %>
      
      <%= f.text_area :content, placeholder: "new DM..." %>
    </div>
    <%= f.submit "Send", class: "btn btn-primary" %>
  <% end %>   
</section>
rails serverで画面を表示してみます。
エラーになりました。
 Showing /home/ubuntu/environment/sample_app/app/views/dms/index.html.erb where line #13 raised:
First argument in form cannot contain nil or be empty
@dmがnilなのでエラーになったと考えました。リスト 13.40を参考に修正します。
  <%= form_for(@dm) do |f| %>
  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
      @dm = current_user.sent_dms.build if logged_in?
別のエラーになりました。
ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007ff01420e4a8>
Did you mean?  receiver_id
formについて理解が足りないようで、読み直します。
7.2.1でform_forの引数はActive Recordのオブジェクトを取り込むとあります。
一方で、8.1.2ではform_forにActive RecordにはないSessionを扱っています。
リスト8.7で検索をしているところを参考に使えそうです。
form_forの引数にハッシュを使っていることが分かりました。
form_forを修正します。
修正前: <%= form_for(@dm) do |f| %>
修正後: <%= form_for(:dm) do |f| %>
<section class="micropost_form">
  <%= form_for(:dm, url:new_dm_path) do |f| %>
同じエラーです。
ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007f962c6ab110>
      <%= f.text_field :receiver_name, class: 'form-control' %>
メソッドがないというので、モデルにある列名ならよいのかと考え、試しに変えてみます。
変更前:receiver_name
変更後:receiver_id
      <%= f.text_field :receiver_id, class: 'form-control' %>
画面が表示されました。生成されたページのソースを見てみます。
 <form action="/dms/new" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="rSSWCwPnDGlcWrtuhvyk4BZdbOzImu6+XGx9ZVGKqJcUdwsYZdqCxsBOVAPsSHYj0L6plKtBiqAigicYUtCyYA==" />
    <div class="field">
      <label for="dm_receiver_name">Receiver name</label>
      <input class="form-control" type="text" name="dm[receiver_id]" id="dm_receiver_id" />
      
      <textarea placeholder="new DM..." name="dm[content]" id="dm_content">
form_forの引数がActiveRecordと関連していて、列名かどうかをチェックする動きをしています。
試しにdmをdm1と変えてみたところ、列名でなくてもエラーは起きなくなりました。
  <%= form_for(:dm1, url:new_dm_path) do |f| %>
    <%#= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.label :receiver_name %>
      <%= f.text_field :receiver_name, class: 'form-control' %>     
      <%= f.text_area :content, placeholder: "new DM..." %>
    </div>
    <%= f.submit "Send", class: "btn btn-primary" %>
  <% end %>
試しにPOSTを画面で実行
receiver_nameをキーとしてユーザーを見つけ、DMをデータベースに追加します。
リスト12.5を参考にします。
Sendボタンをクリックします。
エラーが起きました。
No route matches [POST] "/dms/new"
ページのソースを調べると
 <form action="/dms/new"
となっています。
urlに指定するのはPOSTのpathだと分かりましたので、修正します。
  <%= form_for(:dm1, url:dms_path) do |f| %>
別のエラーが起きました。
Param is missing or the value is empty: dm
      params.require(:dm).permit(:content,:receiver)
app/controllers/dms_controller.rb:28:in `dm_params'
パラメーターを調べます。
{"utf8"=>"✓", "authenticity_token"=>"QnULF0hViVk9ty/LggtG1fcwzTYNvfp8HMJ6nvq2Z/H9qG/1nSzPgR1+97W7osFg250OdLAO1q8vplnjjIbvNQ==", "dm1"=>{"receiver_name"=>"gege", "content"=>"agege"}, "commit"=>"Send"}
dmをdm1に変更する必要がありそうなので、修正します。
変更前:params.require(:dm).permit(:content,:receiver)
変更後:params.require(:dm1).permit(:content,:receiver)
別のエラーが起きました。
'nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.
 <%= render @dms %>  
@dmsがnilとは?、@dmsを設定していないのでエラーになったと考えます。
createでリストを表示するところに、indexと同じ行をコピーします。重複感があるのですが、まとめるのは後廻しにします。その結果、メッセージが正常に表示されました。
受信者がいないケースのテスト
テストを作ります。
リスト8.9を参考にします。
  test "send dm with invalid receiver" do
    log_in_as(@user)
    get dms_path
    post dms_path, params: { dm1: {receiver_name: "" }}
    assert_template 'dms/index'
    assert_not flash.empty?
  end
リスト13.36を参考に、DMを送るアクションを作ります。
 def create
    @user = current_user
    @dms = @user.sent_dms.paginate(page: params[:page])
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm = @user.sent_dms.build 
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      @dm.save
      flash[:success] = "DM sent!"
      redirect_to dms_path
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end
contentがブランクのテスト
contentをブランクにしてDMを送ってみます。
画面にはエラーが表示されません。コンソールのログを調べます。
   (0.1ms)  rollback transaction
Redirected to https://7eca7b943b584a2296afeb1d7ceb9db2.vfs.cloud9.us-east-2.amazonaws.com/dms
Completed 302 Found in 10ms (ActiveRecord: 0.7ms)
NoMethodError in Dms#index
undefined method `errors' for nil:NilClass
<% if object.errors.any? %>
8章と12章をもう一度読みます。
receiverがnot foundのときを再テストしてみます。
エラーが表示されました。
@dmをbuildしていないので、nilになっているのではと考え、buildしている行をreceiverを探す判定の前に移しました。
 def create
    @user = current_user
    @dms = @user.sent_dms.paginate(page: params[:page])
    @dm = @user.sent_dms.build 
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      if @dm.save
        flash[:success] = "DM sent!"
        redirect_to dms_path
      else
        render 'index'
      end
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end
 end
not foundのメッセージが表示されるようになりました。

無効・有効な送信のテスト
無効・有効な送信のテストを作ります。
リスト 13.55を参考にします。
  test "DM interface" do
    log_in_as(@user)
    get dms_path
    # 無効な送信
    assert_no_difference 'Dm.count' do
      post dms_path, params: { dm1: {receiver_name: @receiver.name,
                                     content: "" } }
    assert_select 'div#error_explanation'
    end
    # 有効な送信
    content = "Dm test content1"
    assert_difference 'Dm.count', 1 do
      post dms_path, params: { dm1: {receiver_name: @receiver.name,
                                     content: content } }
    end
    assert_redirected_to dms_path
    follow_redirect!
    assert_match content, response.body
  end
自分が受信者のDMを表示
自分が受信者のDMも表示するように変更します。
「13.3.3 フィードの原型」を読みます。
Micropostとfeedの関係のように、DMに対してchatとchat_itemsを作ることにします。
  # 試作 chat
  def chat
    Dm.where("sender_id = ?", id)
  end
  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
      @dm = current_user.sent_dms.build 
      @chat_items = @user.chat.paginate(page: params[:page])
  end
<% if @user.sent_dms.any? %>
    <h3>DMs (<%= @user.sent_dms.count %>)</h3>
    <ol class= "microposts">
      <%= render @chat_items %>  
      <%#= render @dms %>  
    </ol>
    <%= will_paginate @chat_items %>
    <%#= will_paginate @dms %>
<% end %>  
receiverをブランクにして送信してみたところエラーになりました。
undefined method `any?' for nil:NilClass
<% if @chat_items.any? %>
chat_itemsを作る行をindexと同じようにcreateにも入れます。
  def index
      @user = current_user
      @dm = @user.sent_dms.build 
      @chat_items = @user.chat.paginate(page: params[:page])
  end
  def create
    @user = current_user
    @dm = @user.sent_dms.build 
    @chat_items = @user.chat.paginate(page: params[:page])
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      if @dm.save
        flash[:success] = "DM sent!"
        redirect_to dms_path
      else
        render 'index'
      end
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end
テストしてすべてGreenです。
所要時間
11/15から11/22までの6.5時間です。

