こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#15 投稿機能, Active Storage編
次回:#17 VPC環境構築編
<追記:2020年2月20日>
記録時間をリセットする実装を追加しました。
データベースを初期化するDatabase Cleanerを追加しました。
今回の流れ
- 完成のイメージを理解する
- Chartkickを理解・導入する
- 各メソッドを作る
- コントローラを編集する
- ビューを作る
- 計測時間をリセットする画面を編集する
- テストを書く
この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回は入力したフォームからグラフを表示します。
完成のイメージを理解する
フォームに入力した時間を日付別に取り出し、合計したものを**「週間・月間・年間」でグラフに表示**できるようにします。
そのためにはグラフを描画するgem、Chartkickを使用します。
「週間・月間・年間」の切り替えには、ドロップダウンからクエリパラメータを送信・取得します。
最後に、以前プロフィール編集画面で実装予定だった、計測時間をリセットするボタンを実装します。
Chartkickを理解・導入する
ここでの手順は以下の通りです。
- Chartkickの動作を理解する
- Chartkickを導入する
Chartkickの動作を理解する
Chartkickはグラフを描画するgemです。
設定を済ませると、下の記述だけでグラフを描けます。
@chart = { "2020-01-01" => 100, "2020-01-02" => 80, "2020-01-03" => 110, "2020-01-04" => 130, "2020-01-05" => 90 }
<%= line_chart @chart %>
Chartkickを導入する
早速、Chartkickを導入しましょう。
そのためにはインストールと設定が必要です。
+ gem 'chartkick'
$ bundle install
ここからはRails5か6で手順が異なります。
間違えないよう、気をつけましょう。
Rails 5以下
//= require chartkick
//= require Chart.bundle
Rails 6以上
yarn add chartkick chart.js
require("chartkick")
require("chart.js")
導入完了です。
あとはオプションを設定するためのファイルを作ります。
(Rails5、6ともに必要な手順です。)
$ touch config/initializers/chartkick.rb
Chartkick.options = {
curve: false,
messages: {empty: "データがありません"}
}
- 線グラフを曲線でなく直線にする
- 空の場合のメッセージを加える
というオプションを追加しました。
補足:DBにSQLite3を使う場合は、Chartkickの日付管理に便利なGroupdateが使えない
Groupdateというgemを使うと、モデル(データベース)から期間ごとに取り出せるメソッドが使えるのですが、SQLite3には対応していません。
(当ポートフォリオも、SQLite3を使っています)
詳しくは、公式のドキュメントをご参照ください。
Chartkick(公式)
Groupdate(公式)
各メソッドを作る
ここでの手順は以下の通りです。
- 日付と日ごとの合計時間をハッシュで出力するメソッドを作る
- 期間内の合計時間(分、時間)を出力するメソッドを作る
日付と日ごとの合計時間をハッシュで出力するメソッドを作る
各ユーザの日付と日ごとの合計時間をハッシュで出力する、microposts_periodメソッドを作ります。
仕組みは以下の通りです。
- 各期間の始めと終わりを引数で指定する
- Active Recordで指定した期間を使い、micropostを取得する
- ループ処理で日ごとの動画視聴時間を合計する
- 日付と日ごとの合計時間をハッシュに代入する
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入門
Rails で1日前とか1週間前とか
商品の合計金額のようにModelの数値属性の合計値を求める方法
期間内の合計時間(分、時間)を出力するメソッドを作る
ビューで期間内の合計時間を表示したいので、そのためのsum_times、to_hourメソッドを作ります。
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を描画するハッシュの変数です。
class UsersController < ApplicationController
# 中略
def show
# 中略
@period = params[:period]
@chart = @user.microposts_period(@period)
end
# 中略
変数periodの値にはパラメータを使います。
このパラメータはビューから飛ばす予定のクエリパラメータです。
というわけで、次からはビューを作ります。
ビューを作る
パーシャルを用いてビューを作りましょう。
$ touch 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を使用しています。
重要なのは、ドロップダウン内のリンクによるクエリパラメータです。
<%= 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アクションのインスタンス変数に使用しましたね。)
計測時間をリセットする画面を編集する
計測時間のリセットする画面のひな形は、#11で作成済みです。
削除系なので、チェックを入れるとボタンが押せるようにします。
その実装に、jQureyを使います。
チェックオフ時:変更ボタンが押せない
チェックオン時:変更ボタンが押せる
コードは次のようになります。
<% 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>
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
# 中略
$(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)を確認しよう
jQuery:チェックされたらor入力されたら送信可能にするボタンを実装する方法
テストを書く
グラフ関係のテストにはModel/Request Specを使います。
プロフィール編集画面のテストにはSystem Specを使います。
(足りないテストは準備次第、追加します。)
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チートシート
【Rails】Selenium/RSpecでconfirmダイアログのテストをする
補足:テストごとにデータベースを初期化する
データベースのデータが残ることで、テストが失敗する場合があります。
テストごとにデータベースを初期化するよう、Database Cleanerを使います。
いつも通りGemを加え、設定をします。
group :development, :test do
# 中略
+ gem 'database_cleaner'
end
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に突っ込んだデータを消したい
長らく続いた第一部は、以上です。
次回、第二部ではAWSを学習します。