Open7

レビューサービスの機能を学ぼう

かいかい

課題 7d: 5 段階評価機能の実装

何が必要か?

自分で考えてみる

評価という数値を新しく扱うことになるので、新しいモデルscoreを作成して、bookと関連付けする

必要な事の答え

結論:新しいモデルは不要
bookに新しいカラムでscoreを作成する。

「5段階評価の実装方法は様々な手法がありますが、
今回はレーティング(格付け)を星型のアイコンで表示及び入力できる
JavaScriptで作成されたプラグインであるRatyを使用します。」
と指定されているので、これに対応させる準備が必要

以上の2つが必要

追記
uninitialized constant Book::Rating
というエラーが出たら
Ratingモデルが存在しているか確認する
無い場合は以下のコマンドで作成する

rails generate model Rating score:integer book:references
rails db:migrate

bookに新しいカラムでscoreを作成する方法

1.score カラムを books テーブルに追加する

score を Book モデルの属性として使いたい場合は、
マイグレーションを作成して books テーブルに score カラムを追加する必要があります。
以下のコマンドを実行してマイグレーションを作成します。

rails generate migration AddScoreToBooks score:float

その後、マイグレーションを実行します。

rails db:migrate

BooksController の book_params メソッドを確認し、以下のように :score を追加してください。

private

def book_params
  params.require(:book).permit(:title, :body, :score)  # :scoreを追加
end

この変更を加えることで、フォームから送信される :score パラメータが許可

Ratyの実装方法

Ratyは、JavaScriptで星型の評価(レーティング)を簡単に実装できるプラグインです。以下は、Ratyの使い方について詳しく説明します。

1. Ratyのインストール

Ratyの利用手順

githubよりダウンロード(https://github.com/wbotelhos/raty)
画像を app/assets/imagesに配置
src/raty.jsをapp/javascriptにコピー
app/javascript/packs/application.jsに下記を追加

2. JavaScriptの設定

Ratyを初期化するためのJavaScriptコードを追加します。
DOMContentLoadedイベントを使用して、
DOMが完全に読み込まれた後に実行されるようにします。

application.js

import Raty from "raty.js"
window.raty = function(elem,opt) {
  let raty =  new Raty(elem,opt)
  raty.init();
  return raty;
}

//Ratyのインポート: raty.jsライブラリをインポートしています。
//raty関数: この関数は、指定した要素(elem)とオプション(opt)を使ってRatyを初期化し、インスタンスを返します。


document.addEventListener('DOMContentLoaded', function() {
  const elem = document.querySelector('#post_raty');
  
  // 要素が存在する場合にのみRatyを初期化
  if (elem) {
    const opt = {
      starOn: "/assets/star-on.png", // 画像パスは適宜変更
      starOff: "/assets/star-off.png", // 画像パスは適宜変更
      starHalf: "/assets/star-half.png", // 画像パスは適宜変更
      half: true,
      scoreName: 'score',
      click: function(score) {
        document.getElementById('rating-score').value = score; // 選択したスコアを隠しフィールドに設定
      }
    };
    window.raty(elem, opt); // Ratyを初期化
  } else {
    console.warn('Raty element not found.');
  }
});

3. オプションの設定

Ratyでは様々なオプションを設定できます。以下は主要なオプションです。

  • starOn: 選択された星の画像のパスを指定します。
  • starOff: 未選択の星の画像のパスを指定します。
  • starHalf: 半星の画像のパスを指定します。
  • half: 半星評価を有効にする場合はtrueを設定します。
  • scoreName: サーバに送信する際のパラメータ名を指定します(デフォルトはscore)。
  • score: 初期表示するスコアを指定します。
  • click: ユーザーが星をクリックした際に実行される関数を指定します。

4. フォームの統合

評価をフォームで送信したい場合、隠しフィールドを追加して評価結果を送信します。

<!-- 現在の評価が存在するか確認 -->
  <% if @book.score.present? %>
    <h4>現在の評価</h4>
    <p>評価: <%= @book.score %> / 5</p>
    <div class="ratings">
      <% (1..5).each do |i| %>
        <%= image_tag(i <= @book.score ? 'star-on.png' : 'star-off.png') %>
      <% end %>
    </div>
  <% else %>
    <h4>評価</h4>
    <div id="post_raty"></div>
    <%= f.hidden_field :score, id: 'rating-score' %> <!-- 隠しフィールドを追加 -->
  <% end %>

これを新規投稿のフォームの中に入れる
(すでにあるフォームの中なのでサブミットボタンやフォームの記述は省略)

<script>をつかってHTMLの中に書き込むと、ぺージ移動するたびに発火するので、注意。
application.jsに書いておいた方が良い

6. 画像の準備

star-on.pngstar-off.pngstar-half.pngの画像ファイルを用意し、適切なディレクトリに配置します。Railsプロジェクトの場合、app/assets/imagesに配置し、image_pathヘルパーを使って画像のパスを指定できます。

7. 結果の確認

全ての設定が完了したら、ブラウザでページをリロードして評価機能が正しく動作するか確認します。星をクリックすると、選択した評価がコンソールに表示され、フォームが送信されるとサーバにスコアが送信されます。

以上がRatyの使い方の基本的な流れです。評価機能を追加するためのカスタマイズも行いやすく、多くのオプションが用意されていますので、ぜひ色々試してみてください。

リロードしないと表示されない・・・

application.jsの'DOMContentLoaded'を'turbolinks:load'に変更する。
これによって処理が高速化されてリロードしなくても読み込みをやってくれる。

turbolinkのgemはrailsには最初から搭載されてることが多いので基本は変えるだけでOK

そもそも・・・
テンプレの中にテンプレは可能?
⇒可能だがルートパスを注意する。layoutsに入れた方が管理しやすい

かいかい

課題 8d: 本の一覧ページの並び替え機能

何が必要か?

お気に入り順で並び替えた時のsortを使えばよい?
今回は新しいコントローラーやモデルの作成は不要のはず

必要なことの答え

考え方は正解。
正確にはbooks_controller.rbのindexアクションを編集する
その後、切り替えに対応したビューのコードを追加する。

テンプレに切り替えコードを入れても良いが、切り替えボタンはルートパスを使うので、
ユーザー詳細画面のbookのindexが対応しなくなるので、テンプレのindexには変更はなく
books/index.html.erbでのテンプレの上に入れておく

細かいコードの説明は以降に・・・

indexアクションを編集する
books_controller.rb
def index
    to = Time.current.at_end_of_day
    from = (to - 6.day).at_beginning_of_day
    
    # 並び替え条件に応じて異なる並び替えを実行
    @books = case params[:sort]
             when "rating"
               Book.order(score: :desc)
             when "new"
               Book.order(created_at: :desc)
             else
               # お気に入り順(デフォルト)
               Book.includes(:week_favorites).sort_by { |book| -book.week_favorites.count }
             end
#ここまでが変更箇所

    @book = Book.new
  end
case params[:sort]って何?

case params[:sort] は、URL のクエリパラメータ sort の値を基に条件分岐を行うコードです。params[:sort] の値に応じて、異なる並び替えを実行できるようになっています。

詳細な動作の解説

  1. params[:sort]:これは、リクエストされたURLからsortパラメータの値を取得します。たとえば、URL が /books?sort=rating なら、params[:sort]"rating" という値を持ちます。

  2. case params[:sort]case 文を使って、params[:sort] の値を基に条件分岐を行います。以下のような処理を行います:

    • when "rating"params[:sort]"rating" の場合、本を score の降順で並び替えます(評価の高い順)。
    • when "new"params[:sort]"new" の場合、本を created_at の降順で並び替えます(新着順)。
    • else:それ以外の場合、お気に入り順(week_favorites.count の多い順)に並び替えます。

具体例

たとえば、次のように sort を指定してページにアクセスしたときに、並び替えが切り替わります:

  • /books?sort=rating:評価の高い順
  • /books?sort=new:新着順
  • /bookssort パラメータなし):お気に入り順

つまり分岐を受け取って、それに対応したソートを選んでくれるやつ

orderってなに?

order メソッドは、ActiveRecord のクエリメソッドの一つで、
データベースからレコードを取得する際に特定の順序で並び替えをするために使用されます。
例えば、created_at などの日時や score のような数値を基にして、
昇順または降順で並び替えることが可能です。

orderはallが取得して並び替えてくれるバージョンみたいなかんじ

基本的な使い方

  • 昇順 (ASC) で並び替える場合:

    Book.order(:created_at) # 日付の古い順に並び替え
    
  • 降順 (DESC) で並び替える場合:

    Book.order(created_at: :desc) # 日付の新しい順に並び替え
    

複数のカラムでの並び替え

複数のカラムを基に並び替えることもできます:

Book.order(score: :desc, created_at: :desc) # scoreで降順、同じscoreならcreated_atで降順

このケースでの使い方

以下のコードでは、score カラムの降順で並び替えます。

Book.order(score: :desc)

このコードは、評価が高い順(score が高い順)に本のリストを表示するために役立ちます。

切り替えに対応したビューのコードを追加する
index.html.erb
        <%= link_to "新着順", books_path(sort: "new"), class: "btn btn-primary" %>
        <%= link_to "評価順", books_path(sort: "rating"), class: "btn btn-primary" %>
        <%= link_to "お気に入り順", books_path, class: "btn btn-primary" %>

これを追加するだけ

ルートパスを作成して、新着と評価に関しては、パスの中にsort:で指定することで、
indexアクションのcase params[:sort]が受け取りwhenで分岐する
お気に入りだけは普通な理由はアクションの内でデフォルトに設定されているから
(elseの後に書かれているので、新着と評価の2つ以外⇒お気に入り順ってこと)

かいかい

課題 9d: 本へのタグ付け機能の実装

何が必要か?

タグは本(投稿)に結び付いたものになるから、Bookに新しい「タグ」というカラムを追加すればよい?
タグの検索機能はJavaScriptを使って可能?
タグをクリックして、対処のタグで検索されるにはJavaScriptの代入するやつを使う?

必要なものは・・・

結論⇒acts-as-taggable-onというGemを追加する

なんで?新しいカラム追加はダメなの?

Bookモデルに「タグ」カラムを追加する方法も検討できますが、
以下のような理由から、タグ専用のGemを使用した実装がより柔軟で拡張性が高いとされています。

1. 複数のタグを扱うための管理が簡単

単一の「タグ」カラムをBookモデルに追加する場合、複数のタグを持たせるにはカンマ区切りで保存するなどの工夫が必要です。しかし、これには以下のような課題があります:

  • タグの追加・削除が手動で行う必要があり、操作ミスが発生しやすい。
  • タグの一括検索や効率的な検索処理が難しい。
  • タグの重複が発生しやすい(例えば、「Ruby」と「ruby」は別のタグとして扱われるなど)。

acts-as-taggable-onなどのGemを利用すると、複数のタグを簡単に管理でき、各投稿に対して柔軟にタグを追加・削除できます。また、Gemが提供するメソッドを活用することで、検索機能の実装も簡単です。

2. 拡張性と将来の変更対応

Bookモデルに「タグ」カラムを追加する方法だと、タグが増えた際の対応が難しくなります。たとえば、今後、ユーザーが追加したタグごとの使用頻度をカウントしたり、人気タグの一覧を作成したい場合、個別のカラムでは対応しづらくなります。

acts-as-taggable-onはこうした機能の拡張にも対応しているため、アプリの成長に合わせて機能を追加するのが容易です。

3. データベースパフォーマンスの向上

タグを別テーブルで管理することで、重複したタグ情報を持たずに済み、データベースの容量を節約できる場合があります。また、タグの検索やフィルタリングをデータベース側で効率的に行えるようになります。

acts-as-taggable-onって何ですか?

acts-as-taggable-onは、Railsで簡単にタグ機能を実装できるGemです。このGemを使うと、モデルにタグを設定する機能が追加され、複数のタグを持たせたり、タグによる検索や分類が簡単にできるようになります。

主な機能

acts-as-taggable-onを使うと、以下のような機能が利用できます:

  1. タグの複数設定:例えば「Ruby」「Rails」「Web開発」といった複数のタグを1つの投稿に設定できます。
  2. タグによる検索:特定のタグがついた投稿を検索できるようになります。たとえば、「Ruby」のタグがついた投稿だけを一覧で表示させることができます。
  3. 柔軟なタグ管理:複数のタグセットを作成したり(例:tagsskillsのようにタグの種類を分ける)、特定のタグに基づいてグループ化や分類が簡単に行えます。

使用方法

  1. Gemのインストール
    Gemfileに以下のコードを追加し、インストールします。

    gem 'acts-as-taggable-on'
    
  2. マイグレーションの実行
    タグを保存するためのテーブルを生成するマイグレーションファイルを作成し、マイグレーションを実行します。

    rails acts_as_taggable_on_engine:install:migrations
    rails db:migrate
    
  3. モデルへのタグ機能追加
    タグを追加したいモデル(例えばBookモデル)にacts_as_taggable_on :tagsと記述することで、タグ機能を簡単に利用できるようになります。

    class Book < ApplicationRecord
      acts_as_taggable_on :tags
    end
    
  4. タグ入力と表示
    投稿フォームでタグを入力できるようにしたり、タグごとに投稿を検索・表示する機能を作成できます。

メリット

  • タグ機能を自分で一から作る必要がなく、既存のコードで実装できます。
  • 拡張性が高く、今後タグの種類を増やしたり、検索機能を強化するのも簡単です。
  • 「タグ検索」や「人気タグ表示」などのよくある機能もサポートされているため、柔軟にアプリを成長させやすいです。

このように、acts-as-taggable-onを使うと、タグ機能を簡単に、そして柔軟に実装できるので、多くのRailsアプリで利用されています。

Gemの追加
かいかい

タグ機能を実装するには、以下の手順で進めると良いでしょう。
今回はacts-as-taggable-onというGemを使って、タグの作成と検索をシンプルに実現します。

1. acts-as-taggable-onのインストール

Gemfileに以下を追加し、インストールします。

gem 'acts-as-taggable-on'

インストール後、次のコマンドを実行してデータベースにタグ用のテーブルを追加します。

bundle install

これでGemの読み込みは終わり

rails acts_as_taggable_on_engine:install:migrations

これを実行すると大量のマイグレーションファイルが作成される

このままrails db:migrateをするとエラーが出るのでマイグレーションファイルの編集をする
編集をするマイグレーションファイルは

  • _add_missing_unique_indices.acts_as_taggable_on_engine
  • _add_missing_taggable_index.acts_as_taggable_on_engine

    この二つの中のコードをコメントアウトにする
    その後にマイグレーションファイルを読み込む
rails db:migrate

2. モデルにタグ機能を追加

Bookモデルでタグを使えるように設定します。

class Book < ApplicationRecord
  acts_as_taggable_on :tags
end

3. タグ入力のインターフェースを作成

フォームでタグを入力できるように、bookのフォームにタグ入力フィールドを追加します。

<%= form_with model: @book, local: true do |f| %>
  <!-- その他のフォームフィールド -->
<div class="form-group form-group_tags">
  <%= f.label :tag_list, "Tag", class: "form-label" %>
  <%= f.text_field :tag_list, value: @book.tag_list.join(","), class: "form-control", data: { role: "tagsinput" } %>
</div>
<% end %>

投稿する内容に新しい要素が足されたので、それを許可する様に修正がいる!
レビューの時と同じ

books_controller
  def book_params
    params.require(:book).permit(:title, :body, :score, :tag_list)
  end

4. タグでの検索機能を実装

コントローラーにタグ検索用のアクションを追加し、タグで検索できるようにします。

app/controllers/books_controller.rb
  def index
         @book = Post.find(params[:id])
    //省略
         @tags = @book.tag_counts_on(:tags)
  end
end

次に、BooksControllerにtagアクションを追加して、
指定されたタグに関連する本(または投稿)を表示できるようにします。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  #他のアクション

  def tag
    @tag = params[:tag]
    @books = Book.tagged_with(@tag)
  end

コントローラーに新しいアクションが追加されたら
新しいルーティングを用意する

config/routes.rb
  # 他のルート
  resources :books do
    get 'tags/:tag', to: 'books#tag', as: :tag, on: :collection
  end

5. タグのリンクを表示

各投稿の詳細ページや一覧ページでタグをクリックして検索できるようにリンクを設定します。

<div class="tags">
  <% if @tags.present? %>
    <div class="d-flex flex-wrap">
      <% @tags.each do |tag| %>
	<span class="badge badge-info mr-2 mb-2">
	  <%= link_to "#{tag.name}(#{tag.taggings_count})", tag_books_path(tag.name), class: "text-white" %>
	</span>
      <% end %>
    </div>
  <% else %>
    <p>登録されているタグはありません</p>
  <% end %>
</div>

これで、自分で入力したタグを作成し、そのタグを使った検索も可能になります。
acts-as-taggable-onは柔軟で、複数タグやカテゴリ分けなどにも対応しているので、
必要に応じて追加の設定も可能です。

かいかい

タグごとのページはどのように作成しますか?

app/views/booksディレクトリにtag.html.erbという名前のテンプレートファイルを作成し、
@booksに含まれる投稿や本を一覧表示するコードを追加します。

app/views/books/tag.html.erb
<h1><%= @tag %>に関連する本</h1>

<% if @books.any? %>
  <% @books.each do |book| %>
    <div class="book-item">
      <h2><%= link_to book.title, book_path(book) %></h2>
      <p><%= book.body %></p> <!-- descriptionをbodyに変更 -->
      <!-- 他に表示したい情報を追加 -->
    </div>
  <% end %>
<% else %>
  <p>このタグに関連する本はありません。</p>
<% end %>


かいかい

投稿の一覧ページでもタグを表記させたい

ビューのコードの編集

このコードにタグ表示を追加するには、各bookに関連付けられたタグリストをテーブルに追加しましょう。book.tag_listでタグの配列が取得できるので、それをeachでループして表示します。

以下のように、タグのカラムを新しく追加し、タグを表示するコードを組み込みます。

<table class='table table-hover table-inverse'>
  <thead>
    <tr>
      <th></th>
      <th>Title</th>
      <th>Opinion</th>
      <th>Tags</th> <!-- タグ表示用のカラム -->
      <th colspan="3"></th>
    </tr>
  </thead>
  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td>
          <%= link_to(book.user) do %>
            <%= image_tag book.user.get_profile_image, size: '50x50' %>
          <% end %>
        </td>
        <td><%= link_to book.title.truncate(5), book %></td>
        <td><%= book.body.truncate(5) %></td>
        
        <!-- タグの表示部分 -->
        <td>
          <% book.tag_list.each do |tag| %>
            <span class="badge badge-info mr-1">
              <%= link_to tag, tag_books_path(tag), class: "text-white" %>
            </span>
          <% end %>
        </td>
        
        <td id="favorite_btn_<%= book.id %>">
          <%= render 'favorites/favorite-btn', book: book %>
        </td>
        <td>
          <p>コメント数:<%= book.book_comments.count %></p>
        </td>
        <td>
          <p>閲覧数: <%= book.impressionist_count %></p>
        </td>
        <td>
          <%= render 'layouts/score', book: book %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

変更点の説明

  • <th>Tags</th>: ヘッダーにTagsカラムを追加しました。
  • <td>内でbook.tag_listeachでループし、各タグをバッジとして表示しています。tag_books_path(tag)へのリンクも追加し、タグをクリックするとタグごとのページに遷移できるようにしています。

このコードで、各本のタグがテーブル内に表示されるようになります。

アクションの編集

現在のindexアクションは並び替え機能を持っており、@booksに並び替え済みのBookオブジェクトが含まれています。この状態でタグの情報も併せて取得するために、Bookモデルにtag_listが含まれるように調整します。

コントローラーの修正

indexアクションで取得するBooktagsも含めるようにincludesメソッドを使用しましょう。これにより、タグ情報を効率的に取得できます。

def index
  to = Time.current.at_end_of_day
  from = (to - 6.day).at_beginning_of_day
  
  # 並び替え条件に応じて異なる並び替えを実行
  @books = case params[:sort]
           when "rating"
             Book.includes(:tags).order(score: :desc)
           when "new"
             Book.includes(:tags).order(created_at: :desc)
           else
             # お気に入り順(デフォルト)
             Book.includes(:tags, :week_favorites).sort_by { |book| -book.week_favorites.count }
           end

  @book = Book.new
end

説明

  • Book.includes(:tags)で各Bookに関連するタグを事前に読み込んでいます。これにより、ビューでbook.tag_listを使ってタグを表示する際に効率が向上します。

これで、indexページでbook.tag_listを使ってタグを表示できるようになります。

かいかい

タグを入力して検索したい
今回は一覧ページのアクションを変更して検索システムを導入する
ビューのコードは以下の通り

<%= form_with url: books_path, method: :get, local: true do |f| %>
  <div class="form-group">
    <%= f.label :tag_name, "タグで絞り込む", class: "form-label" %>
    <%= f.text_field :tag_name, value: params[:tag_name], class: "form-control", placeholder: "タグを入力" %>
    <%= f.submit "検索", class: "btn btn-primary mt-2" %>
  </div>
<% end %>

このフォームで一覧表示させるときの条件を絞っている

それに対応させるコントローラーが以下の通り

def index
  to = Time.current.at_end_of_day
  from = (to - 6.day).at_beginning_of_day

  # タグ検索機能
  if params[:tag_name].present?
    @books = Book.tagged_with(params[:tag_name])
  else
    # 並び替え条件に応じて異なる並び替えを実行
    @books = case params[:sort]
             when "rating"
               Book.includes(:tags).order(score: :desc)
             when "new"
               Book.includes(:tags).order(created_at: :desc)
             else
               # お気に入り順(デフォルト)
               Book.includes(:tags, :week_favorites).sort_by { |book| -book.week_favorites.count }
             end
  end

  @book = Book.new
end

これで検索すると、いつもの一覧ページを作成して、検索結果を表示させれる