TojiTech No Programming, No Life.

Rails(6系)でRatyを使った評価機能の実装

背景

  • Rails6では、Rails5と手順が異なる
  • 自分自身の投稿に対しては評価させたくない
  • 何度も同じ生地に評価をさせたくない

サンプルリポジトリ

こちらから、実装済みソースコードが見られます。

注意

サンプルのリポジトリは、Dockerで起動することを前提に作成されています。
Dockerを使用しない場合は、appフォルダ以下で通常のrailsの起動処理を行ってください。

なお、記事ではDockerを使用せず導入する手順で記載していますのでご注意ください。

Deviseの導入手順は、省略していますのでご注意ください。
ユーザー登録が面倒なので、seedにユーザー登録情報を記載しています。

ratyのバージョンについて

現在、ratyの最新版は、4系になっています。
この記事を参考にされる場合は、こちらよりv3.1.1をダウンロードして対応してください。

実装

まずは、モデル・コントローラー類を作成していきましょう。
ついでにjQueryもインストールします。

rails g controller posts
rails g controller reviews
rails g model post
rails g model review
yarn add jquery

続いて、必要なライブラリをダウンロードして各種ディレクトリに保存しましょう

まずは、評価の肝になる星の画像は、こちらよりダウロードしてapp/assets/imagesに保存しましょう。
ダウンロードする画像は、以下の3つのファイルで問題ありません。

  • star-half.png
  • star-on.png
  • star-off.png

次に、Raty本体をこちらよりダウンロードして、app/app/javascript/packsにjquery.raty.jsとして保存しましょう。

jQueryとRatyが使えるように設定を変更していきましょう

const { environment } = require('@rails/webpacker')

// ここから追加
const webpack = require('webpack')
environment.plugins.prepend('Provide',
    new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
    })
)
// ここまで追加

module.exports = environment
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
Turbolinks.start()
ActiveStorage.start()

// ここから追加

// viewからjQueryを読み込めるようにするため追加
window.$ = window.jQuery = require('jquery');

// Ratyを読み込む
require('./jquery.raty')

// ここまで追加

マイグレーションファイルを編集していきましょう

class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.references :user, foreign_key: true, null: false
      t.string :title, null: false
      t.text :content, null: false
      t.timestamps
    end
  end
end
class CreateReviews < ActiveRecord::Migration[6.1]
  def change
    create_table :reviews do |t|
      t.references :user, foreign_key: true, null: false
      t.references :post, foreign_key: true, null: false

      # 0で評価したい人もいることを考えてデフォルト値は0に設定
      t.float :review, default: 0

      t.timestamps
    end
  end
end

モデルを編集していきましょう

class Post < ApplicationRecord
  has_many :reviews, dependent: :destroy
  belongs_to :user
end
class Review < ApplicationRecord
  belongs_to :post
end
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :posts
end

ルートを編集しましょう

Rails.application.routes.draw do
  root to: 'homes#index'
  resources :posts do
    resources :reviews, only: %w[create destroy]
  end

  devise_for :users
end

コントローラーを編集していきましょう

class HomesController < ApplicationController
  def index
    # ログイン済みの場合は、トップページから
    # ポスト一覧にリダイレクト
    redirect_to posts_path if user_signed_in?
  end
end
class PostsController < ApplicationController
  before_action :authenticate_user!

  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])
    # raty.js用のフォーム
    @review = Review.new
    # raty.jsの平均値
    @review_avg = Review.where(post_id: params[:id]).average(:review)
    # すでに評価済みかの確認フラグ
    @review_flg = Review.find_by(user_id: current_user.id, post_id: params[:id])
  end

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to root_path
    else
      render :new
    end
  end

  def edit
    @post = Post.find(params[:id])
  end

  def update
    @post = Post.find(params[:id])
    if @post.update(post_params)
      redirect_to post_path(@post)
    else
      render :edit
    end
  end

  def destroy
    Post.find(params[:id]).destroy()
    redirect_to root_path
  end

  private

  def post_params
    # mergeメソッドでユーザーIDをStrongParameterに追加
    params.require(:post).permit(:title, :content)
          .merge(user_id: current_user.id)
  end
end
class ReviewsController < ApplicationController
  before_action :authenticate_user!

  def create
    @review = Review.new(review_params)
    if @review.save!
      redirect_to post_path(params[:post_id])
    else
      render :edit
    end
  end

  def destroy
    review = Review.find_by(user_id: current_user.id, post_id: params[:post_id])
    # 自分自身の評価のみ削除を許可
    redirect_to posts_path and return unless review.user_id == current_user.id
    review.destroy() # 評価削除
    redirect_to post_path(params[:post_id])
  end

  private

  def review_params
    # mergeメソッドでユーザーID, 投稿_IDをStrongParameterに追加
    params.require(:review).permit(:review)
          .merge(
            user_id: current_user.id,
            post_id: params[:post_id]
          )
  end
end

最後にビューを編集していきましょう

<h1>Homes#index</h1>
<p>Find me in app/views/homes/index.html.erb</p>

<h2>Actions --></h2>
<ul>
  <li><%= link_to 'Log In', new_user_session_path %></li>
  <li><%= link_to 'Sign up', new_user_registration_path %></li>
</ul>
<%= post.label :title, 'タイトル' %><br>
<%= post.text_field :title %><br>
<%= post.label :content, 'コンテンツ' %><br>
<%= post.text_area :content %><br>
<%= post.submit %>
<h1>Posts#edit</h1>
<p>Find me in app/views/posts/edit.html.erb</p>

<h2>Post --></h2>
<%= form_with model: @post do |post| %>
  <%= render 'form', post: post %>
<% end %>
<h1>Posts#index</h1>
<p>Find me in app/views/posts/index.html.erb</p>

<h2>Actions --></h2>
<ul>
  <li><%= link_to 'New Post', new_post_path %></li>
  <li><%= link_to 'Log Out', destroy_user_session_path, method: :delete, data: { confirm: 'ログアウトしますか?' } %></li>
</ul>

<h2>Posts --></h2>
<ul>
  <% @posts.each do |post| %>
    <li><%= link_to post.title, post_path(post) %>
      <ul>
        <li><small><%= post.user.email %></small></li>
      </ul>
    </li>
  <% end %>
</ul>
<h1>Posts#new</h1>
<p>Find me in app/views/posts/new.html.erb</p>

<h2>Actions --></h2>
<%= link_to 'Top', root_path %>

<h2>Post --></h2>
<%= form_with model: @post do |post| %>
  <%= render 'form', post: post %>
<% end %>
<h1>Posts#show</h1>
<p>Find me in app/views/posts/show.html.erb</p>

<h2>Actions --></h2>
<ul>
  <li><%= link_to 'Top', root_path %></li>
  <li><%= link_to 'Edit', edit_post_path(@post) %></li>
  <li><%= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: '削除しますか?' } %></li>
</ul>

<h3>Title --></h3>
<%= @post.title %>

<h3>Content --></h3>
<%# simple_formatは、pタグで囲み、改行コードをbrに変換する %>
<%= simple_format(@post.content) %>

<h3>Review Average --></h3>
<div id="review_avg" data-score="<%= @review_avg %>"></div>
<script>
    $('#review_avg').empty(); // Turbolinksで星が増殖する現象を解消
    $('#review_avg').raty({
        readOnly: true,
        starOff: '<%= asset_path('star-off.png') %>',
        starOn: '<%= asset_path('star-on.png') %>',
        starHalf: '<%= asset_path('star-half.png') %>',
        // divタグのdata-score属性から評価の平均値を呼び出す
        score: function () {
            return $(this).attr('data-score');
        },
    });
</script>

<h3>Review --></h3>
<%# 評価済みの場合は、再評価できないようにする %>
<% if @review_flg.blank? %>

  <%# 自分自身の投稿には評価できないようにする %>
  <% if @post.user_id == current_user.id %>
    <p>自分の投稿には、評価できません。</p>
  <% else %>
    <%= form_with model: @review, url: post_reviews_path(@post) do |review| %>
      <div id="review"></div>
      <%= review.submit %>
    <% end %>

    <script>
        $('#review').empty(); // Turbolinksで星が増殖する現象を解消
        $('#review').raty({
            starOff: '<%= asset_path('star-off.png') %>',
            starOn: '<%= asset_path('star-on.png') %>',
            starHalf: '<%= asset_path('star-half.png') %>',
            // 登録するモデル名とカラム名を記述
            //  送信値として採用される
            scoreName: 'review[review]',
            half: true,
        });
    </script>
  <% end %>

<% else %>
  <p>評価済みです。</p>
  <ul>
    <li>
      <%= link_to '評価を削除', post_review_path(@post), method: :delete, data: { confirm: '評価削除しますか?' } %>
    </li>
  </ul>
<% end %>

最後に

以上で、実装は完了のはずです。うまく動かない場合は、サンプルリポジトリを確認してください。

ぜひ、実装の参考にしてください!

Profile

Yuki Tojima

RubyやPhp、JavaScriptまわりのことを徒然と記録に残す技術ブログです。

至らぬところもあると思いますが、見守っていただけると幸いです。

記事のリクエストや、間違いなどありましたら X (旧 Twitter) のDMなどでお気軽にご連絡ください。

ytojima @TojiTech
プロフィール画像