【Rails6】5段階評価の星レビュー機能を実装する手順（raty.js）

# 概要
* ユーザーが任意のitemに対して5段階評価機能の星評価（非同期通信）ができるように実装したので、その手順について記す。
    * Userは、itemの詳細ページ（/show）にいくと、5段階評価の☆評価（0.5単位）とコメントを行い、レビューを送信する（/form）。
    * トップ画面(/index)にitemが一覧化されており、itemごとに☆評価の内の総合評価の平均点（関数で切り上げ）が表示される。
    * itemの詳細ページ（/show）にも、総合評価の平均点（関数で切り上げ）が表示される。

# 前提
* Ruby on Rails6で実装。
    * Rails 6.0よりJavaScriptの扱いが変わっているため、Rails5などを使っている方は、別記事の参照を推奨。
* raty.jsを利用して実装する。
* 参考にした記事：
    * [Railsで「Raty」を使った星機能をつける](https://qiita.com/busitora2/items/5b59d1ea9e90c1b016b4)
    * [[Rails]星評価の実装方法](https://qiita.com/iwkmsy9618/items/b3fa6a0854172797aad2)
* 画面のリロード等行わずとも、ホバリングしたら色が変わる挙動にする。（非同期通信）
* 5段階評価機能の対象項目は5つ。テーブルとの関係性などは詳細は以下データベース設計を参照。
    * all_rating（総合評価項目）
    * rating1（評価項目1）
    * rating2（評価項目2）
    * rating3（評価項目3）
    * rating4（評価項目4）
* コメント機能はここでは割愛。別記事にしています
    * [【Ruby on Rails】indexページにitem一覧、それぞれのshowページでコメントできるようにする手順](https://qiita.com/Ryo-0131/items/33a443a41bda68a706a0)

# データベース設計
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/7e4bbe3b-3ae8-6940-8acf-bbaea53047f0.png)

# 星評価ができるraty.jsについて
* ソース：https://github.com/wbotelhos/raty
* Jqueryコンソールで`Uncaught TypeError: $(...).raty is not a function`
というエラー表示についてはこちらで解決：
    * [[Rails 6] Raty.jsを使った星型レビュー
](https://qiita.com/nozomi_nozomi/items/eb6848c6025ca63af1c4)

# jQueryとRatyの導入
* `yarn add jquery`
    * Rails5以前の導入方法ではjquery-railsというGemをインストールするのが基本のよう
    * 今回はWebpackerで管理するのでyarnコマンドを使用してインストール。
    * Rails6では、webpackで複数のjsコンパイルして出力する。JavaScriptの標準バンドラーは Webpackerになっている。
* webpackerの設定ファイルで、jQueryを管理下に追加するための追記を行うため、以下を追記
```config/webpack/environment.js
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)
```
* なお、jQueryでは、`$: ‘jquery’`と`jQuery: ‘jquery’`どちらの書き方もできるため、どちらも書いている。 WordPress では後者らしい。

* `application.js`に以下を追記。
    * このあと作成する外部ファイル`raty.js`についても記載しておく
```app/javascript/packs/application.js
require("jquery")  
###
window.$ = window.jQuery = require('jquery');
require('packs/raty')
```

* turbolinksについて
    * `// require("turbolinks").start()`でturbolinksは無効化しておいた方がいいかもしれない。turbolinksとはRails4から正式導入された画面遷移を高速化させるgemライブラリ。js, cssの読み込みを初回時に行い、次回以降の読み込み処理を省略することで高速化する役割があるが、Javascriptのイベント発生のタイミングが違ってくることがあるため、無効化しておくのが良いかと。ただ、メリットとデメリットがあるし、部分的な無効化もできるらしく完全な悪者ではないので、各自で調べてみてください。要は、ページが読み込まれた時に発火するloadイベント（JavaScript）と、turbolinksの画面遷移時に高速化する機能がぶつかってしまうことがある、ということですね。

* `app/javascript/packs` にjsファイル`raty.js`を新規作成。
* 上記記載のraty.jsの[Githubソース](https://github.com/wbotelhos/raty)からraty.jsを探し、中身にコピペする。（定期的に更新？されているみたいです）

# Ratyの星評価画像の準備
* 上記記載の`raty.js`の[Githubソース](https://github.com/wbotelhos/raty)にあるimagesフォルダの画像を`app/assets/images`に保存。今回の場合は、以下の画像を保存。
    * app/assets/images/cancel-off.png
    * app/assets/images/cancel-on.png
    * app/assets/images/star-half.png
    * app/assets/images/star-off.png
    * app/assets/images/star-on.png

# モデル側を設定する
* migrateでratingカラムたちを追加。floatにしているのは、トップビューと詳細ページで平均点を出す際に、小数点にする必要があるため。

```ruby
# frozen_string_literal: true

class CreateReviews < ActiveRecord::Migration[6.0]
  def change
    create_table :reviews do |t|
      t.text        :comment, null: false
      t.float       :all_rating, null: false, default: 0
      t.float       :rating1, null: false, default: 0
      t.float       :rating2, null: false, default: 0
      t.float       :rating3, null: false, default: 0
      t.float       :rating4, null: false, default: 0
      t.references  :user, null: false, foreign_key: true
      t.references  :item, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```

* reviewモデルにvalidation追記。ここで星評価の1~5を設定できる。
```app/models/review.rb
  validates :all_rating, numericality: {
    less_than_or_equal_to: 5,
    greater_than_or_equal_to: 1}, presence: true

```

* reviewsコントローラーのストロングパラメーターへratingカラムたちを追加する。
```app/controllers/reviews_controller.rb 
  def review_params
    params.require(:review).permit(:comment, :all_rating, :rating1, :rating2, :rating3, :rating4).merge(
      user_id: current_user.id, item_id: params[:item_id]
    )
  end
```

# 送信フォーム(/form)と詳細ページ(/show)のViewを編集する
* 5つの評価項目と数が多いので、部分テンプレートpartial（form_rateとshow_rate）をつかって管理する。リファクタリング！

* `_form_rate.html.erb`にjquery発火イベントを追記する。reviewカラムに保存するので、`review[all_rating]`とする。
    * 0.5単位なので`half: true`としている

```erb:app/views/items/_form_rate.html.erb
<%# jquery発火イベントを記載%>
  <script>
    $('#star').raty({
      size     : 36,
      starOff:  '<%= asset_path('star-off.png') %>',
      starOn : '<%= asset_path('star-on.png') %>',
      starHalf: '<%= asset_path('star-half.png') %>',
      scoreName: 'review[all_rating]',
      half: true,
    });
  </script>
<%# 同様に、rating1なども記載する%>
```


* `_show_rate.html.erb`にもjquery発火イベントを追記する。

```erb:app/views/items/_show_rate.html.erb
<div class="starrate">
<%# 平均点を算出し、round関数で切り上げ %>
<%=@all_rating %>：<%= @reviews.average(:all_rating).to_f.round(1) %>
<%# 評価数を星に置き換える %>
<div class="average-review-rating" data-score=<%= @reviews.average(:all_rating) %>></div>
<%# jquery発火イベントを記載%>
<script>
  $('.average-review-rating').raty({
    readOnly: true,
    starOn: "<%= asset_path('star-on.png') %>",
    starOff: "<%= asset_path('star-off.png') %>",
    starHalf: "<%= asset_path('star-half.png') %>",
    score: function() {
      return $(this).attr('data-score')
    }
  });
</script>
<br>
<%# 同様に、rating1なども記載する%>
</div>
```

* `_form.html.erb`に以下を追加する。これで、上でつくった部分テンプレートのスクリプトを呼び出せる

```erb:app/views/items/_form.html.erb
<%# 平均点を算出し、round関数で切り上げ %>
<%=@all_rating %>：<%= @reviews.average(:all_rating).to_f.round(1) %>
<%# 評価数を星に置き換える %>
 <div class="average-review-rating" data-score=<%= @reviews.average(:all_rating) %>></div> 

<th class="table__col1">総合評価</th>
 <td class="table__col1">  <div class="field" id="star"><% f.label :all_rating %></div></td>
</tr>
<%# 同様に、rating1なども記載する%>

<%= render partial: "items/form_rate", locals: { item: @item } %>
```

* `show.html.erb`に以下を追加する。これで、上でつくった部分テンプレートのスクリプトを呼び出せる
```erb:app/views/items/show.html.erb
<%= render partial: "items/show_rate", locals: { item: @item } %>
```

* メモ：エラー`ActionView::Template::Error (Missing partial...`について
    * show.html.erbにて、"show_rate"から"items/show_rate"にしたことで解決。reviewsコントローラーのcreate発動によってreviewsフォルダの中にありませんよーという意味になっていたのか？詳細は不明。


# トップビュー(/index)に平均点を表示する
* `items_controller.rb`に以下を追加する。
```app/controllers/items_controller.rb 
def index
    @reviews = Review.all
    @all_rating = '総合評価'
    @rating1 = '評価1'
    @rating2 = '評価2'
    @rating3 = '評価3'
    @rating4 = '評価4'
end
```

* `_item.html.erb`(部分テンプレートにしていない場合は、`items/index.html.erb`)に以下を追加する。
* 平均点はround関数で切り上げる
* トップビューに表示するのは、all_rating（総合評価）のみ
```erb:app/views/items/_item.html.erb
<div class="average-review-rating" data-score=<%= item.reviews.average(:all_rating) %>></div>
　<%=@all_rating %>：<%= item.reviews.average(:all_rating).to_f.round(1) %>
</div>
```

* 平均点の表示について、ちょっと躓いたので、一応メモ。
　⭕️　`<%= item.reviews.average(:all_rating).to_f.round(1) %>`　→ itemに紐づくreviewsを一括表示させる
　❌　`<%= @reviews.average(:all_rating) %>`　→ reviewsの全平均がどのitemにも同じように表示されてしまうのでNG
　❌　`<%= @item.reviews.count %>`　→ reviewの個数が表示されてしまうのでNG