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 %>
最後に
以上で、実装は完了のはずです。うまく動かない場合は、サンプルリポジトリを確認してください。
ぜひ、実装の参考にしてください!