[rails初心者]複数権限でのログインを使ったレビューサイトの作成（基本）

<h2>初めに

ポートフォリオテーマとして業者のレビューサイトを作成しています。<br>以下の画像のようなレビューサイトを作っていこうと思います。

![top画面１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/75cffe6d-1714-fc3c-9ad8-068addf06205.png)

※自分用でまとめてますので、分かり辛かったらすいません。<br>また理解不足・誤りがあったらコメント頂けますと幸いです。m(__)m<ﾖﾛｼｸｵﾈｶﾞｲｼﾏｽ

<h2>前提条件</h2>

<p>①ER図に載っているモデル(※レビューモデルは無くても大丈夫です→準備段階で作ります)が作成されていること<br>
②ログイン機能が実装済み(複数権限でのログイン機能を実装済みの体で進めます)<br>
③jQuery & Raty導入済み<br>
④Bootstrap導入済み<br>
⑤今回の記事では基本機能のみ※管理者機能や平均値算出などは次回の記事にて紹介いたします<br>
</p>


<details><summary>ER図について</summary>

今回は評価される側（業者:trader）と評価する側（顧客:client）の間に、中間テーブルとしてレビューがいる位置関係になります。</details>
<details><summary>raty参考資料について</summary>

[[Rails 6] Raty.jsを使った星型レビュー](https://qiita.com/nozomi_nozomi/items/eb6848c6025ca63af1c4)
[【Rails】raty.jsを用いた星5レビュー機能の実装](https://qiita.com/kcl215/items/e226ac58d14360fd1b8e)
[【Rails+jQuery Raty】レビュー用の星★の評価を実装する(入力、保存、表示)](https://qiita.com/yuki_0920/items/a966d9fa2bdb621f805d)
[公式Github](https://github.com/wbotelhos/raty)
</details>


![ER図_PF材料屋レビューサイトVer1.0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/ca3c2cb8-716d-8b25-cbf9-88cd20dcf08f.png)

<h2>今回作成するレビュー機能について</h2>

・評価データの新規登録機能＆編集機能
・登録時のヴァリデーション
・一覧ページに表示


<h2>主なやること</h2>

・コントローラやモデルの作成（下準備）
・ルーティングの記述
・コントローラの記述
・モデルの記述
・ビューの作成＆編集

<h2>開発環境</h2>
・rails 6.1.7<br>
・ruby 3.1.2 <br>
・raty 3.1.1<br>
・jquery 3.6.1<br>
・bootstrap 4.5

<h2>コントローラやモデルの作成（下準備）</h2>

まずはコントローラの作成

```:ターミナル上で実行
  rails g conntroller client/reveiews new show edit
  #レビュー状況の一覧ページを表示したいのでindexでも可※今回showで作ってしまったので、showで進めます
```
```:ターミナル上で実行
  rails g conntroller trader/client_reveiews index 
  #コントローラ名がclient側と同じ場合(reviews)、エラーの原因になるので「client_reviews」にしましょう
```
:::note info
・評価する側(client) → 評価結果を新規登録したい or 更新したい → new edit の２つ +＠index
・評価される側(trader) → 自社の評価結果を一覧で表示したい → indexのみ
:::

次にモデルの作成
```:ターミナル上で実行
  rails g model Review
```
:::note info
・モデルの命名規則は、単数形・先頭は英大文字
:::


<h5>rails db:migrateはちょっと待った！！</h5>
次にカラムの追加をしていきます ※もう「rails db:migrate」をしてしまった方は下のプルダウンのところを参照してみてください <br>
以下の通りに編集します。<br>

```:○○○○_create_reviews.rb
  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
```
:::note info
ポイント①評価は１～５の整数で行う　→　**integer型**を採用
ポイント②`client_id`と`trader_id`は外部キー　→　**references**を採用
:::

<details><summary>migrateファイルの編集前に「rails db:migrate」をしてしまった場合</summary>

①ターミナル上で「**rails db:migrate:status**」を実行

```: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ファイルを直で編集してＯＫです。
</details>

追加作業が終わったら「**rails db:migrate**」を実行

<h2>ルーティングの記述</h2>

ルーティングの記述は以下の通りです
```:アプリ名/config/routes.rb
  ##顧客側
  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
```
:::note info
ポイント①**namespace**を使って業者・顧客の２つに分ける
ポイント②顧客側＝`レビューする`→最低限**create**や**update**等を使えるようにすればOKです
ポイント③業者側＝`レビュー結果を見る`→**index**またはshowがあればよい※今回はindexのみ
ポイント④`reviews`は**ネスト**すること→詳しくは次で解説します
:::

<h3>rails routes にて確認

こんな感じになればOK

```:ターミナル上で rails routes | grep review
                   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
```
<h3>ネストさせた理由について３点</h3>

:::note info
ポイント①ネストさせることで`client_id`や`trader_id`をURL上に付け足すことができるため
:::
まず`client_id`は**clientの主キー**が入ります。ということは、**何番目の顧客(client)なのか**を特定できることになります。
これは業者(trader)側も同じです。この考え方を踏まえて次のポイントが実装可能になります。

:::note info
ポイント②顧客側でログイン時、特定の業者のレビュー状況を確認できる
:::
次の画像は、顧客側でログインしたときの画面になります。
特定の業者(trader)のレビュー状況を確認できる一覧が表示できました。
![評価一覧client側１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/4b45e408-2914-b26e-3b4c-9f0b9d6fe413.png)

:::note info
ポイント③業者側でログイン時、特定の顧客のレビュー状況を確認できる
:::
次に業者側でログインしたときの画面になります。
こちらは逆に顧客(client)のレビュー状況を確認できる一覧が表示できました。
![評価一覧trader側2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/a235aa0c-4460-1058-84cc-b1e3a514c58b.png)

<h2>コントローラの記述</h2>
<h3>まずはclient側</h3>

```:/app/controller/client/reviews_controller.rb
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

```

<h3>次にtrader側</h3>

```: /app/controller/trader/client_reviews_controller.rb
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

```

<h3>最後にTopページ</h3>

```: /app/controller/homes_controller.rb
class HomesController < ApplicationController
  def top
    @traders = Trader.page(params[:page]).per(10)
    @clients = Client.page(params[:page]).per(10)
    #動的ヘッダーのごとく、Viewページを作成するため２つ宣言 → 説明だと分かり辛いと思うので、Viewページ作成のところを参考にしてください
  end
  def about
  end
end
```

<details><summary>whereについて</summary>

[where | railsドキュメント](https://railsdoc.com/page/model_where)
</details>

<h2>モデルの記述</h2>

アソシエーションとバリデーションの記述をしていきます。

```: /app/models/client.rb
class Client < ApplicationRecord
  has_many :reviews
end
```
```: /app/models/trader.rb
class Trader < ApplicationRecord
  has_many :reviews
end
```
```: /app/models/review.rb
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
```
:::note info
ポイント①reviewは中間テーブルに位置づけされている → 序盤のER図を参照
ポイント②starカラムはnull falseにしているため、ヴァリデーションをかける
:::

<h2>viewの記述(client側)</h2>
<h3>1.newページ</h3>

```: /app/reviews/client/reviews/new.rb
<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>
```
<h3>完成イメージ</h3>

![評価新規登録client側2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/7d0df341-7ea2-6dec-ba5f-9b7a5047dde8.png)

:::note info
実際に新規登録できればOK
:::
:::note warn
コメントにはバリデーションをかけていないので、空白でも登録できます
:::

<h3>2.editページ</h3>

```: /app/reviews/client/reviews/edit.rb
<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>
```
<h3>完成イメージ</h3>

![評価編集client側2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/49164f72-b330-443f-0e8e-36b1f2d460d0.png)

:::note info
ポイント①：登録済みのコメント(nilの場合は表示されません)が表示されていればOK
ポイント②：更新処理ができればOK
:::


<h3>3.showページ※indexでも可</h3>
※コントローラ作成時indexならindexに記述

```: /app/reviews/client/reviews/show.rb
<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>
```
<h3>完成イメージ</h3>

![評価一覧client側１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/7c91f01c-5a55-b2ff-6305-cad4b43816a6.png)

:::note info
ポイント：指定した業者(今回は、テスト電気材料1号店さん)のレビュー結果を一覧表示できていればOK
:::

<h2>viewの記述(trader側)</h2>
<h3>1.indexページ</h3>

```: /app/reviews/trader/reviews/index.rb
<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>
```

<h3>完成イメージ</h3>

![評価一覧trader側2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/d7ece9cf-8424-7f43-b0d1-e7d7210bdb14.png)

:::note info
ポイント：指定した顧客(今回は、一般職１太郎さん)のレビュー結果を一覧表示できていればOK
:::

<h2>viewの記述(Top側)</h2>
<h3>topページ</h3>

```: /app/reviews/homes/top.rb
<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>
```

<h3>完成イメージ(clientログイン時)</h3>

![top画面１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/25538118-8caa-f947-1e63-e048867f84c0.png)


<h3>完成イメージ(traderログイン時)</h3>

![top画面2(trader側).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822721/02f55816-d134-a704-f2fb-82462b3aa626.png)


:::note info
ポイント：ログイン状態によって表示が変わっていればOK
:::
:::note warn
警告
・評価平均値や評価回数は、次回紹介するのでコメントアウトしています
・client側のコントローラ作成時に「indexアクション」を選んだ場合、リンクのパス名が違うのでエラーが発生します。
:::

<h2>さいごに</h2>

今回PF作成のテーマとしてレビューサイトを作ってみました。
まだ学習中の身なので理解不足・誤りがあったらコメント頂けますと幸いです。m(__)m<ﾖﾛｼｸｵﾈｶﾞｲｼﾏｽ


<h2>感想</h2>

今回のPF作成では、複数権限でのログインを使ったレビューサイトを作りました。
それによってコントローラやViewファイルを２つ以上作らないといけないことに閲覧者の方も気付いたと思います。なので正直混乱しましたw
というのも今回アプリ開発にあたって、アプリケーション詳細設計書(ルーティングをまとめた設計書)を作成せずに進めました。その結果、レビュー関係のコントローラだけで最初５つ作成し、うち２つは要らないorクラス名のエラーによって使えないことが発覚 → コントローラ作成からやり直す事態に遭いました(;^ω^)
なので複数権限でのログインを使用するときは、`設計書の作成`が重要だとご理解いただけたと思います。
これを最後まで読んでくださった方は、アプリケーション詳細設計書を作成してからぜひやってみてください。
<h2>参考サイトまとめ</h2>

[[Rails 6] Raty.jsを使った星型レビュー](https://qiita.com/nozomi_nozomi/items/eb6848c6025ca63af1c4)
[【Rails】raty.jsを用いた星5レビュー機能の実装](https://qiita.com/kcl215/items/e226ac58d14360fd1b8e)
[【Rails+jQuery Raty】レビュー用の星★の評価を実装する(入力、保存、表示)](https://qiita.com/yuki_0920/items/a966d9fa2bdb621f805d)
[公式Github](https://github.com/wbotelhos/raty)
[where | railsドキュメント](https://railsdoc.com/page/model_where)