Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #16 線グラフ, Chartkick編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#15 投稿機能, Active Storage編](https://qiita.com/aokyo17/items/8769ecff3b7c08dca70b)
次回：[#17 VPC環境構築編](https://qiita.com/aokyo17/items/405d8eda3b6108c7af05)

**<追記：2020年2月20日>**
記録時間をリセットする実装を追加しました。
データベースを初期化するDatabase Cleanerを追加しました。

# 今回の流れ
1. 完成のイメージを理解する
* Chartkickを理解・導入する
* 各メソッドを作る
* コントローラを編集する
* ビューを作る
* 計測時間をリセットする画面を編集する
* テストを書く

この記事は、**動画を観た時間を記録するアプリのポートフォリオ**です。
今回は**入力したフォームからグラフを表示**します。

# 完成のイメージを理解する
フォームに入力した時間を日付別に取り出し、合計したものを**「週間・月間・年間」でグラフに表示**できるようにします。

そのためにはグラフを描画するgem、Chartkickを使用します。
**「週間・月間・年間」の切り替えには、ドロップダウンからクエリパラメータを送信・取得**します。

最後に、以前プロフィール編集画面で実装予定だった、**計測時間をリセットするボタンを実装**します。

完成するとこんな感じです↓
![lantern_figure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/71b53b76-8781-5709-573a-f51b88234ba8.png)
それでは手順通りに進めます。

# Chartkickを理解・導入する
ここでの手順は以下の通りです。

* Chartkickの動作を理解する
* Chartkickを導入する

## Chartkickの動作を理解する
Chartkickはグラフを描画するgemです。
設定を済ませると、下の記述だけでグラフを描けます。

```rb:hoge_controller.rb
@chart = { "2020-01-01" => 100, "2020-01-02" => 80, "2020-01-03" => 110, "2020-01-04" => 130, "2020-01-05" => 90 }
```

```rb:hoge.html.erb
<%= line_chart @chart %>
```

<img width="539" alt="lantern_chart.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/5a7666ab-cee9-326c-bb35-b73457079d41.png">

## Chartkickを導入する
早速、Chartkickを導入しましょう。
そのためにはインストールと設定が必要です。

```diff:Gemfile
+ gem 'chartkick'
```

```bash:bash
$ bundle install
```

ここからは**Rails5か6で手順が異なります**。
間違えないよう、気をつけましょう。

#### Rails 5以下
```js:app/assets/javascripts/application.js
//= require chartkick
//= require Chart.bundle
```

#### Rails 6以上
```bash:bash
yarn add chartkick chart.js
```

```js:app/javascript/packs/application.js
require("chartkick")
require("chart.js")
```
導入完了です。

あとはオプションを設定するためのファイルを作ります。
**（Rails5、6ともに必要な手順です。）**

```bash:bash
$ touch config/initializers/chartkick.rb
```

```rb:config/initializers/chartkick.rb
Chartkick.options = {
  curve: false,
  messages: {empty: "データがありません"}
}
```

* 線グラフを曲線でなく直線にする
* 空の場合のメッセージを加える

というオプションを追加しました。

## 補足：DBにSQLite3を使う場合は、Chartkickの日付管理に便利なGroupdateが使えない
Groupdateというgemを使うと、モデル（データベース）から期間ごとに取り出せるメソッドが使えるのですが、**SQLite3には対応していません。**
（当ポートフォリオも、SQLite3を使っています）

詳しくは、公式のドキュメントをご参照ください。
[Chartkick（公式）](https://chartkick.com/)
[Groupdate（公式）](https://github.com/ankane/groupdate)

# 各メソッドを作る
ここでの手順は以下の通りです。

* 日付と日ごとの合計時間をハッシュで出力するメソッドを作る
* 期間内の合計時間（分、時間）を出力するメソッドを作る

## 日付と日ごとの合計時間をハッシュで出力するメソッドを作る
各ユーザの日付と日ごとの合計時間をハッシュで出力する、microposts_periodメソッドを作ります。
仕組みは以下の通りです。

* 各期間の始めと終わりを引数で指定する
* Active Recordで指定した期間を使い、micropostを取得する
* ループ処理で日ごとの動画視聴時間を合計する
* 日付と日ごとの合計時間をハッシュに代入する

```rb:app/models/user.rb
class User < ApplicationRecord
# 中略
  def microposts_period(period)
    current = Time.current.beginning_of_day
    case period
    when "week"
      start_date = current.ago(6.days)
    when "month"
      start_date = current.ago(1.month - 1.day)
    when "year"
      start_date = current.ago(1.year - 1.day)
    else
      start_date = current.ago(6.days)
    end
    end_date = Time.current
    dates = {}
    (start_date.to_datetime...end_date.to_datetime).each do |date|
      microposts = self.microposts.where(created_at: date.beginning_of_day...date.end_of_day)
      sum_times = microposts.sum(:time)
      dates.store(date.to_date.to_s, sum_times)
    end
    return dates
  end
# 中略
```

（Active Recordを呼び出しすぎて非効率かもしれません。）
（より良い方法が分かる方、ご教授を願います。）

参考にさせていただきました↓
[ActiveRecord入門](https://qiita.com/tfrcm/items/80625a75959591c2b7cd)
[Rails で1日前とか1週間前とか](https://yatta47.hateblo.jp/entry/2018/02/24/221927)
[商品の合計金額のようにModelの数値属性の合計値を求める方法](http://rails.takayukikoyama.com/model%e9%96%a2%e9%80%a3/model-sum-total-price/)

## 期間内の合計時間（分、時間）を出力するメソッドを作る
ビューで期間内の合計時間を表示したいので、そのためのsum_times、to_hourメソッドを作ります。

```rb:app/helpers/users_helper.rb
module UsersHelper
  def sum_times
    @chart.values.inject(:+) unless @chart.nil?
  end
  
  def to_hour
    hour = sum_times / 60.0
    hour.round(2)
  end
end
```

以上で必要なメソッドが揃いました。

# コントローラを編集する
user_pathにグラフを描画するので、usersコントローラのshowアクションを編集します。
具体的には2つのインスタンス変数、periodとchartを追加します。

periodはmicroposts_periodメソッドの引数に入れる変数です。
chartはChartkickを描画するハッシュの変数です。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
# 中略
  def show
    # 中略
    @period = params[:period]
    @chart = @user.microposts_period(@period)
  end
# 中略
```
変数periodの値にはパラメータを使います。
このパラメータはビューから飛ばす予定のクエリパラメータです。
というわけで、次からはビューを作ります。

# ビューを作る
パーシャルを用いてビューを作りましょう。

```bash:bash
$ touch app/views/layouts/_figure.html.erb
```

```erb:app/views/layouts/_figure.html.erb
<div class="container figure-container">
  <h1 class="title">グラフ</h1>
  <div class="figure-sum-and-button row">
    <span class="figure-sum-text col-sm">
      <div>合計：<em class="figure-sum"><%= sum_times %></em> 分</div>
      <div>（<em class="figure-sum"><%= to_hour %></em> 時間）</div>
    </span>
    <span class="dropdown col-sm">
      <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        <%= @user.microposts_period(@period).keys.first %> - <%= @user.microposts_period(@period).keys.last %>
      </button>
      <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
        <%= link_to "週間", user_path(@user, period: "week"), class: "dropdown-item" %>
        <%= link_to "月間", user_path(@user, period: "month"), class: "dropdown-item" %>
        <%= link_to "年間", user_path(@user, period: "year"), class: "dropdown-item" %>
      </div>
    </span>
  </div>
  <%= line_chart @chart %>
</div>
```

レイアウトにはBootstrap4を使用しています。
重要なのは、ドロップダウン内のリンクによるクエリパラメータです。

```erb:
<%= link_to "週間", user_path(@user, period: "week"), class: "dropdown-item" %>
<%= link_to "月間", user_path(@user, period: "month"), class: "dropdown-item" %>
<%= link_to "年間", user_path(@user, period: "year"), class: "dropdown-item" %>
```

これにより、params[period]の形で値を取得できるようになります。
（showアクションのインスタンス変数に使用しましたね。）

ドロップダウンから期間を選択すると、グラフが変化します。
![lantern-dropdown.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/8da73f59-3a4f-aa29-e96e-89a0d50a9323.png)
![lantern_month.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/5f02ae0b-fc8f-36c0-98ea-357152b49de5.png)

# 計測時間をリセットする画面を編集する
計測時間のリセットする画面のひな形は、[#11](https://qiita.com/aokyo17/items/0a7d7dcfc28e705f5610)で作成済みです。
削除系なので、チェックを入れるとボタンが押せるようにします。
その実装に、jQureyを使います。

**チェックオフ時**：変更ボタンが押せない
![lantern_check_off.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/6685c1a0-818a-5bee-0015-10edf7eb646e.png)
**チェックオン時**：変更ボタンが押せる
![lantern_ckeck_on.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/3cb96548-ca41-fc1a-e6ac-73a111cdc4fe.png)

コードは次のようになります。

```erb:app/views/users/edit.html.erb
<% provide(:title, 'プロフィール編集') %>
<div class="container edit-container">
  <div class="edit-titles">
    <%= image_tag 'profile.png', class: 'edit-img' %>
    <h1 class="title edit-title">プロフィール編集</h1>
  </div>
  <div class="card-deck">
    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header title form-title edit-user-card-title">アカウント情報変更</h2>
        <%= form_with(model: @user, url: user_path(@user), local: true) do |form| %>
        <div class="card-body">
          <%= render 'shared/error_messages', object: form.object %>

          <div class="form-group">
            <%= form.text_field :name, class: 'form-control', placeholder: "名前" %>
          </div>
          <div class="form-group">
            <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード（再入力）" %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
          </div>
        </div>
        <% end %>
      </section>
    </div>

    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header title form-title edit-user-card-title">記録時間リセット</h2>
        <%= form_with(scope: :microposts, url: user_path(@user), method: :delete, data: { confirm: "本当に削除しますか？" },  local: true) do |form| %>
        <div class="card-body">
          <div class="form-group form-group-reset_time">
            <%= form.check_box :reset_time, class: 'form-check-input' %>
            <%= form.label :reset_time, '記録時間をリセットする（時間がかかる場合があります）', class: 'form-check-label' %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit', id: 'microposts_reset_time_submit' %>
          </div>
        </div>
        <% end %>
      </section>
    </div>
  </div>
</div>
```

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
# 中略
  def destroy
    @user = User.find(params[:id])
    if params[:microposts][:reset_time] == '1'
      @user.microposts.destroy_all
      flash[:success] = '記録時間とメモをリセットしました'
      redirect_to edit_user_path(@user)
    end
  end
# 中略
```

```js:app/assets/javascripts/application.js
$(function() {
  $('#microposts_reset_time_submit').attr('disabled', true);

  $('#microposts_reset_time').click(function() {
    if ( $(this).is(':checked') ){
      $('#microposts_reset_time_submit').attr('disabled', false);
    } else {
      $('#microposts_reset_time_submit').attr('disabled', true);
    }
  });
});
```

というわけでアプリの全機能が整いました。

参考になりました↓
[【Rails入門】データを削除する方法（destroy/delete）を確認しよう](https://www.sejuku.net/blog/63480)
[jQuery：チェックされたらor入力されたら送信可能にするボタンを実装する方法](https://www.nxworld.net/tips/jquery-switch-disable-input-button.html)

# テストを書く
グラフ関係のテストにはModel/Request Specを使います。
プロフィール編集画面のテストにはSystem Specを使います。
（足りないテストは準備次第、追加します。）

```rb:spec/systems/edit_user_spec.rb
require 'rails_helper'

RSpec.describe "EditUser", type: :system do
  
  let(:user) { create(:user) }
  
  def valid_information(remember_me = 0)
    fill_in 'メールアドレス', with: user.email
    fill_in 'パスワード', with: 'password'
    check 'session_remember_me' if remember_me == 1
    find(".form-submit").click
  end

  describe "GET /users/:id/edit" do
    it "can press submit button when check button is pressed" do
      visit login_path
      valid_information
      visit edit_user_path(user)
      check 'microposts_reset_time'
      click_on 'microposts_reset_time_submit'
      page.driver.browser.switch_to.alert.accept
      expect(page).to have_selector '.alert-success'
    end
  end
end
```

参考になりました↓
[Capybaraチートシート](https://qiita.com/morrr/items/0e24251c049180218db4#%E8%A6%81%E7%B4%A0%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8Bcapybaranode)
[【Rails】Selenium/RSpecでconfirmダイアログのテストをする](https://qiita.com/at-946/items/403d85d45cb02615c323)

# 補足：テストごとにデータベースを初期化する
データベースのデータが残ることで、テストが失敗する場合があります。
テストごとにデータベースを初期化するよう、Database Cleanerを使います。
いつも通りGemを加え、設定をします。

```rb:Gemfile
group :development, :test do
  # 中略
+ gem 'database_cleaner'
end
```

```rb:spec/spec_helper.rb
require 'database_cleaner'

RSpec.configure do |config|
  # 中略 
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end
```

参考になりました↓
[RSpecでテストDBに突っ込んだデータを消したい](https://qiita.com/tak-msk/items/ea29455212f2e5e71e0b)

長らく続いた第一部は、以上です。
次回、第二部ではAWSを学習します。

---
前回：[#15 投稿機能, Active Storage編](https://qiita.com/aokyo17/items/8769ecff3b7c08dca70b)
次回：[#17 VPC環境構築編](https://qiita.com/aokyo17/items/405d8eda3b6108c7af05)