Ruby
Rails
ransack
RubyOnRailsTutorial

Ruby on Rails チュートリアル 機能拡張2(マイクロポスト検索)

Ruby on Rails チュートリアルについて

Ruby on Railsを勉強したいというと、まず紹介される有名なRailsのチュートリアル。
内容はハードですが、無料でRailsによるWebアプリケーション開発を楽しく学べます。

Ruby on Rails チュートリアル
https://railstutorial.jp/

Sample Appの拡張

チュートリアルの最後には、作成したSampleAppの拡張機能についていくつかのヒントが記載されています。
その中の以下の機能を順に実装していきます(途中で挫折するかも。。。)。
1. ユーザー検索
2. マイクロポスト検索
3. フォロワーの通知
4. 返信機能
5. メッセージ機能

マイクロポスト検索

今回は、2つ目のマイクロポスト検索の実装を行います。
マイクロポストは、ログイン時のホーム画面およびプロフィール画面の二つの画面で表示されるので、検索フォームもホーム画面とプロフィール画面の両方に実装します。

前回(ユーザー検索)はこちら

環境と準備

マイクロポスト検索(および前回のユーザー検索)では、Rails用の検索機能を簡単に実装できるRansack Gemを利用することにします。
Ransackは他のGemと同様に、Gemfileに追加し、bundle installします。

モジュール バージョン
Rails 5.1.2
Ruby 2.3.1
Ransack 1.8.4

実装

まず、マイクロポスト検索用のフォームを作成します。

app/views/shared/_microposts_search_form.html.erb
<%= search_form_for @q, url: @url do |f| %>
  <%= f.label :content_cont, 'Micoropost Search' %>
  <div class="input-group">
    <%= f.text_field :content_cont, placeholder: "Enter keyword...",
                      class: 'form-control' %>
    <span class="input-group-btn">
     <%= f.submit 'Go', class: "btn btn-primary" %>
    </span>
  </div>
<% end %>

今回の実装ではここがいきなり肝になりました。というか、ハマりました。。。
検索条件のcontent_contはMicropostのcontentからの検索ということで、難しくはないと思います。
前回のユーザー検索フォームとの大きな違いは、url: @urlの部分になります。
ユーザー検索時は、urlをわざわざ指定せずとも上手く動きましたが、
マイクロポスト検索ではここでurlを以下のように指定(上の実装では各controllerから与えています)しなければ、検索実行時に「get /microposts(= index)」のrouteが見つからない!とエラーになります。
routeエラー.png

理由は、search_form_forのデフォルトの送信先が検索モデルのindexアクションだからです。(当たり前ですね。。。)
そのため、今回は明示的にフォームの送信先を指定してやる必要があります。
インスタンス変数で渡しているのは、このフォームをホーム画面/プロフィール画面の両方で使うためです。

検索フォームができたので、次にcontrollerの実装を行います。
まずは、ホーム画面のフィードの検索のため、StaticPagesContorollerのhomeアクションに処理を加えます。

app/controllers/static_pages_controller
def home
    if logged_in?
      @micropost = current_user.microposts.build
      if params[:q] && params[:q].reject { |key, value| value.blank? }.present?
        @q = current_user.feed.ransack(microposts_search_params)
        @feed_items = @q.result.paginate(page: params[:page])
      else
        @q = Micropost.none.ransack
        @feed_items = current_user.feed.paginate(page: params[:page])
      end
      @url = root_path
    end
  end
~
省略
~

ここの処理も前回のユーザー検索とほぼ同じですね。
個人的に驚いたのは、current_user.feed.ransackと書けるところです。
Micropost.ransack(〜)とfeedメソッドでやっていることを、ransackの条件としてだらだら実装しないといけないと思っていましたが、そんな必要ありませんでした。(でもDBアクセスとかぜんぜん考えてない。。。)

あとは、@qが必要なのは当然として、検索しない場合はnilとかじゃなくてModel名.none.ransackと書かないといけないんですね。
検索フォーム作成の部分でも記載しましたが、ここでは@urlroot_pathを指定しています。

続けて、UsersControllerのshowアクションに処理を追加します。

app/controllers/users_controller
~
省略
~
def show
    redirect_to root_url and return unless @user.activated?
    if params[:q] && params[:q].reject { |key, value| value.blank? }.present?
      @q = @user.microposts.ransack(microposts_search_params)
      @microposts = @q.result.paginate(page: params[:page])
    else
      @q = Micropost.none.ransack
      @microposts = @user.microposts.paginate(page: params[:page])
    end
    @url = user_path(@user)
  end
~
省略
~

特に気になるところはないと思います。
@urlにはuser_path(@user)を代入しています。

そういえば、microposts_search_paramsは、UsersControllerとStaticPagesControllerの両方から利用されるため、ApplicationControllerに設置しました。

app/controllers/application_controller
private
    def microposts_search_params
      params.require(:q).permit(:content_cont)
    end

忘れかけていましたが、MicropostControllerのcreateアクションに@qransackの処理を追加しないといけません。

ruby
def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @q = Micropost.none.ransack
      @feed_items = current_user.feed.paginate(page: params[:page])
      render 'static_pages/home'
    end
  end

追加するのは、マイクロポストの投稿が失敗した際の処理のみです。
失敗した場合のみ、MicropostContorollerからホームのviewをrender(redirect_toではなく)しているため、@qが必要になります。
renderとredirectの違いについては、こちらにとてもわかりやすくまとめられています。

最後にviewに戻って、最初に作った_microposts_search_form.html.erbをそれぞれのviewに挿入します。

app/views/static_pages/_logged_in_home.html.erb
<% provide(:title, @user.name) %>
~
省略
~
  <div class="col-md-8">
    <div class="row">
      <div class="col-md-4">
        <h3>Micropost Feed</h3>
      </div>
      <div class="search_form">
        <%= render 'shared/microposts_search_form' %>
      </div>
    </div>
    <%= render 'shared/feed' %>
  </div>
</div>
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
~
省略
~
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
    <div class="row">
      <div class="col-md-4">
        <h3>Microposts (<%= @user.microposts.count %>)</h3>
      </div>
      <div class="search_form">
        <%= render 'shared/microposts_search_form' %>
      </div>
    </div>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

scssですが、前回のユーザー検索時に設定した値で十分のため、今回は何も加えません。

見た目はこんな感じです。
ホーム画面
ホーム画面.png

プロフィール画面
プロフィール画面.png

テスト

最後にテストについて記載します。
今回は簡単に、検索後に期待したマイクロポストが画面上に表示されているかを、ホーム画面とプロフィール画面の両方に対してテストします。
テストはチュートリアルで作成していた、users_profile_test.rbに追記しました。

test/integration/user_profile_test.rb
~
省略
~
test "profile display" do
    ~
    省略
    ~
    # Micropost Search
    get user_path(@user), params: {q: {content_cont: "a"}}
    q = @user.microposts.ransack(content_cont: "a")
    q.result.paginate(page:1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end

  test "home profile display" do
    ~
    省略
    ~
    # Micropost Search
    get root_path, params: {q: {content_cont: "a"}}
    q = @user.feed.ransack(content_cont: "a")
    q.result.paginate(page:1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end

テストが通ったことを確認して終了です。

最後に

Rails歴1週間もなく、以上の実装/テストも私の環境で動いたということに過ぎません。
修正点や指摘等ございましたら、ぜひコメントお願いいたします。

参考記事

マイクロポスト検索では、下記のページを参考にさせていただきました。

GitHub
https://github.com/activerecord-hackery/ransack/issues/21
https://github.com/activerecord-hackery/ransack/issues/170