初めに
ポートフォリオテーマとして業者のレビューサイトを作成しています。
以下の画像のようなレビューサイトを作っていこうと思います。
※自分用でまとめてますので、分かり辛かったらすいません。
また理解不足・誤りがあったらコメント頂けますと幸いです。m(__)m<ヨロシクオネガイシマス
前提条件
①ER図に載っているモデル(※レビューモデルは無くても大丈夫です→準備段階で作ります)が作成されていること
②ログイン機能が実装済み(複数権限でのログイン機能を実装済みの体で進めます)
③jQuery & Raty導入済み
④Bootstrap導入済み
⑤今回の記事では基本機能のみ※管理者機能や平均値算出などは次回の記事にて紹介いたします
ER図について
今回は評価される側(業者:trader)と評価する側(顧客:client)の間に、中間テーブルとしてレビューがいる位置関係になります。
raty参考資料について
[Rails 6] Raty.jsを使った星型レビュー
【Rails】raty.jsを用いた星5レビュー機能の実装
【Rails+jQuery Raty】レビュー用の星★の評価を実装する(入力、保存、表示)
公式Github
今回作成するレビュー機能について
・評価データの新規登録機能&編集機能
・登録時のヴァリデーション
・一覧ページに表示
主なやること
・コントローラやモデルの作成(下準備)
・ルーティングの記述
・コントローラの記述
・モデルの記述
・ビューの作成&編集
開発環境
・rails 6.1.7・ruby 3.1.2
・raty 3.1.1
・jquery 3.6.1
・bootstrap 4.5
コントローラやモデルの作成(下準備)
まずはコントローラの作成
rails g conntroller client/reveiews new show edit
#レビュー状況の一覧ページを表示したいのでindexでも可※今回showで作ってしまったので、showで進めます
rails g conntroller trader/client_reveiews index
#コントローラ名がclient側と同じ場合(reviews)、エラーの原因になるので「client_reviews」にしましょう
・評価する側(client) → 評価結果を新規登録したい or 更新したい → new edit の2つ +@index
・評価される側(trader) → 自社の評価結果を一覧で表示したい → indexのみ
次にモデルの作成
rails g model Review
・モデルの命名規則は、単数形・先頭は英大文字
rails db:migrateはちょっと待った!!
次にカラムの追加をしていきます ※もう「rails db:migrate」をしてしまった方は下のプルダウンのところを参照してみてください以下の通りに編集します。
class CreateReviews < ActiveRecord::Migration[6.1]
def change
create_table :reviews do |t|
t.references :client, null: false, foreign_key: true
t.references :trader, null: false, foreign_key: true
t.integer :star, null: false
t.string :review_comment
t.timestamps
end
end
end
ポイント①評価は1~5の整数で行う → integer型を採用
ポイント②client_id
とtrader_id
は外部キー → referencesを採用
migrateファイルの編集前に「rails db:migrate」をしてしまった場合
①ターミナル上で「rails db:migrate:status」を実行
Status Migration ID Migration Name
--------------------------------------------------
up ○○○○00404○○○○ Create reviews
②「Migration ID
」の部分をコピー
③ターミナル上にて「rails db:migrate:down VERSION=○○○○00404○○○○(さっきコピーした数字)」を実行
④再度ターミナル上で「rails db:migrate:status」を実行
Status Migration ID Migration Name
--------------------------------------------------
down ○○○○00404○○○○ Create reviews
down状態が確認できたらmigrateファイルを直で編集してOKです。
追加作業が終わったら「rails db:migrate」を実行
ルーティングの記述
ルーティングの記述は以下の通りです
##顧客側
namespace :client do
#業者情報
resources :traders, only: [:show] do
#レビュー
resources :reviews, only: [:new,:create,:show,:edit,:update,:destroy]
end
end
##店側
namespace :trader do
#顧客情報
resources :clients, only: [:show] do
#レビュー
resources :client_reviews, only: [:index]#特定のclientの評価状況を一覧で表示する
end
end
ポイント①namespaceを使って業者・顧客の2つに分ける
ポイント②顧客側=レビューする
→最低限createやupdate等を使えるようにすればOKです
ポイント③業者側=レビュー結果を見る
→indexまたはshowがあればよい※今回はindexのみ
ポイント④reviews
はネストすること→詳しくは次で解説します
rails routes にて確認
こんな感じになればOK
client_trader_reviews POST /client/traders/:trader_id/reviews(.:format) client/reviews#create
new_client_trader_review GET /client/traders/:trader_id/reviews/new(.:format) client/reviews#new
edit_client_trader_review GET /client/traders/:trader_id/reviews/:id/edit(.:format) client/reviews#edit
client_trader_reviews GET /client/traders/:trader_id/reviews/:id(.:format) client/reviews#show
PATCH /client/traders/:trader_id/reviews/:id(.:format) client/reviews#update
PUT /client/traders/:trader_id/reviews/:id(.:format) client/reviews#update
DELETE /client/traders/:trader_id/reviews/:id(.:format) client/reviews#destroy
trader_client_reviews GET /trader/clients/:client_id/client_reviews(.:format) trader/client_reviews#index
ネストさせた理由について3点
ポイント①ネストさせることでclient_id
やtrader_id
をURL上に付け足すことができるため
まずclient_id
はclientの主キーが入ります。ということは、何番目の顧客(client)なのかを特定できることになります。
これは業者(trader)側も同じです。この考え方を踏まえて次のポイントが実装可能になります。
ポイント②顧客側でログイン時、特定の業者のレビュー状況を確認できる
次の画像は、顧客側でログインしたときの画面になります。
特定の業者(trader)のレビュー状況を確認できる一覧が表示できました。
ポイント③業者側でログイン時、特定の顧客のレビュー状況を確認できる
次に業者側でログインしたときの画面になります。
こちらは逆に顧客(client)のレビュー状況を確認できる一覧が表示できました。
コントローラの記述
まずはclient側
class Client::ReviewsController < ApplicationController
before_action :authenticate_client! #client側でログインした時のみ、このコントローラの処理を許可する
before_action :find_params, only: [:edit, :update, :destroy] #似たような処理はbefore_actionでまとめるのがおすすめ
before_action :get_params, only: [:new, :create]
def new
@review = Review.new #newページを表示させるための空の変数
end
def show
@reviews=Review.where(trader_id: @trader.id)
end
def create
@review = Review.new(review_params)
#review_paramsで取得したカラムにそれぞれのデータを新規登録する宣言※ここでは、まだ登録処理はされません
@review.trader_id = @trader.id
#trader_idカラムに登録する業者IDは誰のIDにするかを宣言
@review.client_id = current_client.id
#client_idカラムに登録する顧客IDは誰のIDにするかを宣言
if @review.save #登録処理&成功したかどうかをif文にかける
redirect_to root_path, notice: "レビューを保存しました" #成功時,topページに戻る&フラッシュメッセージ表示
else
render :new, notice: "レビューの登録が失敗しました"#失敗時,新規登録ページに戻る&フラッシュメッセージ表示
end
end
def edit
#before_actionに全てが記述されているため、記述無し
end
def update
if @review.update(review_params) #更新処理&成功したかどうかをif文にかける
redirect_to client_trader_review_path(@trader.id,@review.id), notice: "レビューの更新が成功しました"
else
render :edit, notice: "レビューの更新が失敗しました"#編集ページに戻る
end
end
def destroy
@review.destroy #削除処理
redirect_to client_user_path(current_client) #削除後どこにリダイレクトするかを指定
end
private
def review_params
params.require(:review).permit(:client_id,:trader_id,:star,:review_comment)
#params.require(モデル名).permit(キー1, キー2, ...)
end
def find_params #before_actionの処理内容を記述
@trader = Trader.find(params[:trader_id])
#URL上の「trader_id」の部分の番号を取得 → URLについては「rails routesにて確認」のところを確認してください
@review = Review.find(params[:id])
#URL上の「:id」の部分の番号を取得 → つまりReviewの主キー
@client = @review.client
#@clientには「cilent_id」カラムの情報を入れたいので、@reviewに紐づけすれば取得可能
end
def get_params #before_actionの処理内容を記述
@trader = Trader.find(params[:trader_id])
end
end
次にtrader側
class Trader::ClientReviewsController < ApplicationController
def index
@client = Client.find(params[:client_id])
#URL上の「client_id」の部分の番号を取得 → URLについては「rails routesにて確認」のところを確認してください
@reviews = Review.where(client_id: @client.id)
#記述「モデル.where(条件..)」→ 条件に当てはまるレコードを全て取得※今回はclient_id
end
end
最後にTopページ
class HomesController < ApplicationController
def top
@traders = Trader.page(params[:page]).per(10)
@clients = Client.page(params[:page]).per(10)
#動的ヘッダーのごとく、Viewページを作成するため2つ宣言 → 説明だと分かり辛いと思うので、Viewページ作成のところを参考にしてください
end
def about
end
end
whereについて
モデルの記述
アソシエーションとバリデーションの記述をしていきます。
class Client < ApplicationRecord
has_many :reviews
end
class Trader < ApplicationRecord
has_many :reviews
end
class Review < ApplicationRecord
belongs_to :client
belongs_to :trader
# 星の評価の空を禁止する、且つ1以上、5未満
validates :star, numericality: {
less_than_or_equal_to: 5,
greater_than_or_equal_to: 1,
}, presence: true
end
ポイント①reviewは中間テーブルに位置づけされている → 序盤のER図を参照
ポイント②starカラムはnull falseにしているため、ヴァリデーションをかける
viewの記述(client側)
1.newページ
<div class="container">
<div class="row">
<%= form_with model: [@trader,@review], url: client_trader_reviews_path(@trader.id), local:true, method: :post do |f| %>
<!--評価-->
<div class="form-group row" id="star">
<%= f.label :star,'評価 ', class:'col' %>
<%= f.hidden_field :star, id: :review_star %>
</div>
<!-- 評価javascript -->
<script>
$('#star').empty(); //ブラウザバックで星が増え続ける不具合防止。
$('#star').raty({
size : 36,
starOff: '<%= asset_path('star-off.png') %>',
starOn : '<%= asset_path('star-on.png') %>',
scoreName: 'review[star]',
half: false,
});
</script>
<!--コメント-->
<div class="form-group">
<%= f.label :"コメント" %>
<%= f.text_area :review_comment,placeholder: "コメントをここに", class: 'w-100 comment-textarea' %>
</div>
<div class="form-group">
<%= f.submit '保存', class: 'btn btn-success' %>
</div>
<% end %>
</div>
</div>
完成イメージ
実際に新規登録できればOK
コメントにはバリデーションをかけていないので、空白でも登録できます
2.editページ
<div class="container">
<div class="row">
<!--コメント入力Form-->
<%= form_with(model:[@trader, @review],url: client_trader_review_path(@trader.id,@review.id), method: :patch, local: true) do |f| %>
<!--評価-->
<div class="form-group row" id="star">
<%= f.label :star,'評価 ', class:'col-md-3 col-form-label' %>
<%= f.hidden_field :star, id: :review_star %>
</div>
<!-- 評価javascript -->
<script>
$('#star').empty(); //ブラウザバックで星が増え続ける不具合防止。
$('#star').raty({
size : 36,
starOff: '<%= asset_path('star-off.png') %>',
starOn : '<%= asset_path('star-on.png') %>',
scoreName: 'review[star]',
half: false,
});
</script>
<!--コメント-->
<div class="form-group">
<%= f.label :"コメント" %>
<%= f.text_area :review_comment, rows:'5',placeholder: "コメントをここに", class: "w-100 comment-textarea" %>
</div>
<div class="form-group">
<%= f.submit '保存', class: 'btn btn-success' %>
</div>
<% end %>
</div>
</div>
完成イメージ
ポイント①:登録済みのコメント(nilの場合は表示されません)が表示されていればOK
ポイント②:更新処理ができればOK
3.showページ※indexでも可
※コントローラ作成時indexならindexに記述<div class="container">
<div class="row">
<div class="col">
<h2 class="head-line text-center title_h3">
<%= @trader.name %>さん レビュー情報
<%= @trader.review_average(@trader) %>
</h2>
<table class="table table-bordered table-condensed" >
<thead>
<tr>
<th>顧客氏名</th>
<th>点数</th>
<th colspan="3">コメント</th>
</tr>
</thead>
<tbody>
<% @reviews.each do |review| %>
<tr>
<td><%= review.client.name %></td>
<td><%= review.star %></td>
<td><%= review.review_comment %></td>
<% if review.client == current_client %>
<td><%= link_to "編集",edit_client_trader_review_path(@trader.id,review.id) %></td>
<td><%= link_to "削除",client_trader_review_path(@trader.id,review.id),method: :delete %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
完成イメージ
ポイント:指定した業者(今回は、テスト電気材料1号店さん)のレビュー結果を一覧表示できていればOK
viewの記述(trader側)
1.indexページ
<div class="container">
<div class="row">
<div class="col">
<h3 class="head-line text-center title_h3">
<%= @client.name %>さん レビュー情報
</h3>
<table class="table table-bordered table-condensed" >
<thead>
<tr>
<th>会社名</th>
<th>点数</th>
<th>コメント</th>
</tr>
</thead>
<tbody>
<% @reviews.each do |review| %>
<tr>
<td><%= link_to review.trader.name,trader_user_path(review.trader.id) %></td>
<td><%= review.star %></td>
<td><%= review.review_comment %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
完成イメージ
ポイント:指定した顧客(今回は、一般職1太郎さん)のレビュー結果を一覧表示できていればOK
viewの記述(Top側)
topページ
<div class="container px-5 px-sm-0">
<% if client_signed_in? %><!--clientでログインした場合-->
<div class="row d-flex">
<div class="col">
<div><!--%= render 'genreのフォルダ先を入力' %--></div>
</div>
</div>
<div class="row mt-3">
<div class="col">
<strong style="font-size:25px;">業者一覧</strong>
<table class='table table-hover table-inverse'>
<thead>
<tr>
<th></th>
<th>業者名</th>
<th>メールアドレス</th>
<th>連絡先</th>
<th>ジャンル</th>
<th colspan="2">評価結果(平均値)</th>
</tr>
</thead>
<tbody>
<% @traders.each do |trader| %>
<tr>
<td><%= image_tag(trader.get_profile_image(100,100)) %></td>
<td><%= link_to trader.name,client_trader_path(trader.id) %></td>
<td><%= trader.email %></td>
<td><%= trader.telephone_number %></td>
<td><%= trader.genre.name %></td>
<td>評価平均:<!--%= trader.review_average(trader) %--></td>
<td>
<%= link_to " 口コミ状況", client_trader_review_path(trader.id,trader.reviews), class: "btn btn-success" %>
<%= link_to "評価する",new_client_trader_review_path(trader.id), class:"btn btn-warning" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate @traders %>
</div>
</div>
<% elsif trader_signed_in? %><!--traderでログインした場合-->
<div class="row d-flex">
<div class="col-md-3">
<div><!--%= render 'genreのフォルダ先を入力' %--></div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-9">
<strong style="font-size:25px;">顧客一覧</strong>
<table class='table table-hover table-inverse'>
<thead>
<tr>
<th></th>
<th>氏名</th>
<th>メールアドレス</th>
<th>連絡先</th>
<th>職業</th>
<th>評価</th>
</tr>
</thead>
<tbody>
<% @clients.each do |client| %>
<tr>
<td><%= image_tag(client.get_profile_image(100,100)) %></td>
<td><%= link_to client.name,trader_client_path(client.id) %></td>
<td><%= client.email %></td>
<td><%= client.telephone_number %></td>
<td><%= client.work_i18n %></td>
<td>評価回数:<!--%= client.review_count(client) %--></td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate @clients %>
</div>
</div>
<% else %><!--ログアウトした場合-->
<div class="row d-flex">
<div class="col-md-2">
<div><!--%= render 'genreのフォルダ先を入力' %--></div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-10">
<strong style="font-size:25px;">業者一覧</strong>
<table class='table table-hover table-inverse'>
<thead>
<tr>
<th></th>
<th>業者名</th>
<th>メールアドレス</th>
<th>連絡先</th>
<th>ジャンル</th>
<th>評価結果(平均値)</th>
</tr>
</thead>
<tbody>
<% @traders.each do |trader| %>
<tr>
<td><%= image_tag(trader.get_profile_image(100,100)) %></td>
<td><%= trader.name %></td>
<td><%= trader.email %></td>
<td><%= trader.telephone_number %></td>
<td><%= trader.genre.name %></td>
<td>評価平均:<!--%= trader.review_average(trader) %--></td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate @traders %>
</div>
</div>
<% end %>
</div>
完成イメージ(clientログイン時)
完成イメージ(traderログイン時)
ポイント:ログイン状態によって表示が変わっていればOK
警告
・評価平均値や評価回数は、次回紹介するのでコメントアウトしています
・client側のコントローラ作成時に「indexアクション」を選んだ場合、リンクのパス名が違うのでエラーが発生します。
さいごに
今回PF作成のテーマとしてレビューサイトを作ってみました。
まだ学習中の身なので理解不足・誤りがあったらコメント頂けますと幸いです。m(__)m<ヨロシクオネガイシマス
感想
今回のPF作成では、複数権限でのログインを使ったレビューサイトを作りました。
それによってコントローラやViewファイルを2つ以上作らないといけないことに閲覧者の方も気付いたと思います。なので正直混乱しましたw
というのも今回アプリ開発にあたって、アプリケーション詳細設計書(ルーティングをまとめた設計書)を作成せずに進めました。その結果、レビュー関係のコントローラだけで最初5つ作成し、うち2つは要らないorクラス名のエラーによって使えないことが発覚 → コントローラ作成からやり直す事態に遭いました(;^ω^)
なので複数権限でのログインを使用するときは、設計書の作成
が重要だとご理解いただけたと思います。
これを最後まで読んでくださった方は、アプリケーション詳細設計書を作成してからぜひやってみてください。
参考サイトまとめ
[Rails 6] Raty.jsを使った星型レビュー
【Rails】raty.jsを用いた星5レビュー機能の実装
【Rails+jQuery Raty】レビュー用の星★の評価を実装する(入力、保存、表示)
公式Github
where | railsドキュメント