patorashのブログ

方向性はまだない

d3.js, c3.jsで描いたチャートを印刷する

d3.jsを皆さまはご存知でしょうか?data-driven-documentの頭文字をとって、d3.js。JavaScriptsvgでチャートを書くためのライブラリです。

d3js.org

そして、d3.jsをベースにチャートを生成しやすくしたライブラリがc3.js。

c3js.org

c3.jsで描いたチャートを印刷したかったのですが、d3.js・c3.js共に印刷とかファイルへの保存とかには対応していないみたいだったので、自分でなんとかしてみました。 元々は、highchartsというライブラリを使っていたのですが、有償ということもあって、別にものに変えたかったというのがありました。こちらは、印刷とかpngやpdfにして保存という機能が元々あって便利です。

www.highcharts.com

方針

チャートだけ印刷したかったので、以下の手順で実現することにしました。

  1. ポップアップウィンドウで別ウィンドウを開く。
  2. そこに親ウィンドウのsvgタグの内容をマルッとコピーして突っ込む。
  3. 印刷を起動する。
  4. 印刷が終わったらポップアップウィンドウを自動で閉じる。

とりあえずは、これでやりたいことは実現できました。

ハマったこと

IE11と旧Edgeの対応具合が悪い

d3.jsでチャートを書くときに、IE11でエラーが起きていました。d3.jsのバージョン5だと、fetchやpromiseを使っているので、それが存在しないということでエラーになっていたので、polyfillを入れました。 また、svgタグの内容をinnerHTMLで取得していたのですが、IE11だとsvgタグ内の内容をinnerHTMLで取得できないという仕様のようでした😨 それもまた、Googleが作っているinnersvg-polyfillというライブラリを入れることで解決しました。

最初はjQueryのouterHTMLを使っていましたが、jQueryに依存した状態になるのも微妙なので、XMLSerializerでNodeを文字列に変換するのがよいという記事を見かけたのでそうすることにしました。

c3.jsのcssを読み込まなければならない

c3.jsはチャートの表示の整形にcssを使っているので、それをポップアップウィンドウ側でも読み込まなければなりません。

IE11がafterprintイベントを拾わない

イベントリスナを定義して、afterprintでポップアップウィンドウを閉じていたのですが、閉じてくれないケースがありました。 かなり悩んだのですが、setTimeoutで閉じるという技を見かけたので、これを採用しました。

上記の問題を解決したコード

CoffeeScriptですが、以下のような感じになりました。

# self.svgには、d3.jsのSelectionオブジェクトが入っている
printWindow = window.open('', 'PrintChart', "width=#{self.svg.attr('width')}, height=#{self.svg.attr('height')}")
printWindow.document.writeln('<head><link rel="stylesheet" href="/css/c3.css" media="all" /></head>')
printWindow.document.writeln("<body class='c3'>#{new XMLSerializer().serializeToString(self.svg.node())}</body>")
printWindow.document.close()
printWindow.print()
setTimeout ->
  printWindow.close()
, 1000

動かしてみる

実際に開いた印刷画面はこちら。(Chromeにて)

f:id:patorash:20200603181202p:plain
c3.jsのチャートをポップアップウィンドウで表示して印刷

とりあえず印刷できるところまではいけたのでOK!!👍

他にもボチボチc3.jsのノウハウが溜まってきたので書いていこうと思います。

LenovoのPCのバッテリー消費が激しいのは高速スタートアップが原因だった

現在は個人PCはLenovo L380 Yogaを使っているのですが、電源を切っているにも関わらず翌日起動したらバッテリーの残量が30%近く減っていて物凄く驚きました。しかし普通に使う分には結構バッテリーも持つので、バッテリーの不具合ではないだろうとは思っていたのだけれど、それでも電源を切って数日でバッテリーが激減しているのはずっとストレスでした😥

まぁそんなにACアダプタの使えない場所で作業することもなかったので、1年以上我慢していたのですが、原因が分からずにずっと悩んでいたので、久々にググってみたところ、高速スタートアップが原因じゃないか?というのを見かけました。

高速スタートアップは、次回のパソコンの起動を速くするために、CPU・メモリの状態を保存しておく仕組みらしいです(それってシャットダウンか…?🤔) デフォルトで高速スタートアップは有効になっているらしいですが、正直デフォルトは無効にしろよ…と思います。バッテリーの初期不良かと思ったじゃないか…😡

無効にする方法を調べたところ、Lenovoのサイトでも紹介されていました。

support.lenovo.com

無効にしたところ、確かに起動は少々遅くなりましたが、バッテリーは数%程度しか減っていませんでした👍これで安心して持ち運ぶことができます!!

SwitchBotのキャンペーンに当選した

Twitterで行われていたSwitchBotのキャンペーンに申し込んでいたら、なんと当選してSwitchBotを送って頂きました!

www.switchbot.jp

うまく動かないケース

付けてみたんだけれど、ボタンが押しきれない…。

そうしたら、Panasonicのボタンは押せないケースがあるというアドバイスを頂きました。本家のサポートに記事が掲載されていました。

support.switch-bot.com

ようやく動いた!

まんまですが、これで動きました。

レビュー

いい点

寝ながらライトを消せるようになったのはよかったです。布団に入る前に消すと布団に入るまでが真っ暗になるので、微妙に思っていたのでそれが解消されました。

また、次の動作時間を決めることができるので、1時間後に消灯するとか、8時間後に点灯することなどできるようになりました。よく読書しながらライトつけっぱなしで寝落ちしていたので、1時間後に消灯するようにしておいたらうまくいきました。

微妙な点

SwitchBot単体ではGoogleアシスタントで操作できません…。希望は音声操作できたらいいなと思っているのですが、SwitchBotだけだとスマホでの操作しかできず、音声で操作したければSwitchBot Hubが必要になります。 SwitchBot HubはSwitchBot製品をインターネット越しに操作できたり、シーンを定義して一斉に操作したりできるので、買ってもいいかな~と思えるのですが、似たような製品のNature Remoを既に家で使っているため、わざわざ買うのがなぁ~…という気持ちで、なかなか更なるスマート家電化に踏み切れません…。

また、モーター音が結構大きいので、寝落ちしていたときに消灯タイマーが作動したときに作動音で起きてしまいました(起きたと同時に消灯…)

総評

赤外線で操作できるものはNature Remoで操作できているから、使いどころがなかなか難しいですが、換気扇のタイマーによさそうとかいう話もTLで出ていたので、使い方次第ではよさそうです。赤外線も使えないアナログなものをIoTにできるので、使い道は色々あるんだろうなぁ~とは思います。でもそんなに家にないんですよねー…。やはりライトくらい。 Nature Remoをまだ使ってないご家庭であれば、SwitchBot Hubを導入してSwitchBotに統一するとめちゃくちゃ便利になると思います!

gem doorkeeperの承認画面に遷移しない場合があった

gem doorkeeperで連携アプリケーションにログインをさせようとしたところ、認証後に権限の承認画面に遷移せずに普通に認証アプリケーションのログイン後の画面に遷移してしまいました。本来ならば、連携アプリケーションにログインしてほしいのに…。

原因

deviseによって使われるメソッドである、after_sign_in_path_forをオーバーライドしていたからでした。

after_sign_in_path_forメソッドは、ログイン後に遷移させる場所を定義するためのメソッドなのだが、これを上書きしていたため、リダイレクト先が認証アプリケーションのログイン後の画面のみになっていました。

before

def after_sign_in_path_for(resource)
  root_path
end

after

stored_location_forというメソッドがあるみたいだったので、これがあるときはこちらを使う、という設定に変更しました。

def after_sign_in_path_for(resource)
  stored_location_for(resource) || root_path
end

Qiitaにも記事がありました。

qiita.com

GETメソッドのFormでの画面遷移をTurbolinksに委ねる

今作ってるアプリケーションではTurbolinksを効かせてて、かなりいい感じだな〜と思ってるのですが、一覧で検索した結果や表示件数を変更した場合に、ページの更新が発生してサクサク移動してたのに、ここでもたつくなぁ…と課題に感じていました。

なんとなくググってみたところ、TurbolinksのTipsとしてissueに上げられていたのでこりゃいい!と思ったので、やり方を書いておきます。

元ネタはここです。

github.com

元ネタのほうだと、jQueryを使っていたりしたので依存度を下げたい。また、data-remote=trueを対象にしていたりして、なんかよくわかりません・・・。data-remote=trueはもともとAjaxで値を取得しにいくから、ダメじゃないの?って思うんですが…。画面の一部を書き換えるときとかならわかるのですが、全体を書き換えたいのを対象にするのであれば、上記の一番最初のサンプルコードのように、data-remote=falseを対象とするべきでしょう。しかし、form_withヘルパーメソッドでlocal=trueにした場合、data-remote=falseという属性は出力されていないので、data-remote=trueを除外する、というアプローチをとります。

コードを紹介

gistを作っておいたのでそちらを見てみましょう。

Support GET from with Turbolinks.

解説

やっていることは以下の通り。

  1. turbolinks:loadイベントで、Turbolinksに委ねてもいいフォームを抽出する
  2. フォームのsubmitイベントを拾うよう設定する
  3. 本来のsubmitイベントをキャンセルする
  4. 現在のページと遷移先にページが同じ場合、perパラメータを引き継ぐ(kaminariの表示件数)
  5. フォームに入力されたものをクエリ文字列に変換する
  6. Turbolinks.visitで移動する

これの肝はクエリセレクタでうまいこと対象のフォームだけを抽出することです。

document.querySelectorAll('form[method=get]:not([data-remote=true])')で、GETメソッドだけどdata-remote=trueのものは除外しています。

また、ページングのgemであるkaminariを使っているので、表示件数をコントロールできるようにするため、pathnameに変更がない場合はperパラメータは引き継ぐようにしました。

まとめ

これで、検索フォームも超速くなりました🚀

WSL2 + DockerでRailsの変更を検知しないのを修正した。

表題の通りなのですが、ファイルの変更を行ってからブラウザをリロードしても変更が反映されませんでした。反映しなおすには、Railsサーバを再起動しなければならず、非常に面倒です。

調べたところ、Vagrantでの開発とかでも同様のことが起きたりしているぽかったのですが、Railsのissueに直し方が書いてありました。

github.com

config/environments/development.rbで、file_watcherを修正すればよいとのこと。

# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
config.file_watcher = ActiveSupport::FileUpdateChecker

ホストOSと共有しているファイルだからか、ファイルアップデートイベントを検知できないようです。FileUpdateCheckerにすれば、変更をポーリングでチェックしているから、検知できる模様。 また、EventedFileUpdateCheckerを使わないのであれば、listen gemを削除できるらしいです。ならば、消してもいいかも…。

deviseで論理削除を実装するときの手順をまとめておく

新しいRailsアプリを作るたびにやっている気がするので、一旦まとめておこうと思います。

今回公開する手順は、削除済みのメールアドレスで再登録可能な論理削除の実装方法です。

Deviseとは?

Deviseはアカウント認証管理のgemです。Webアプリケーションにほぼ必須である認証機能をほぼ網羅しています。

  • アカウントの新規作成
  • メールアドレスの確認
  • パスワード忘れの問い合わせ
  • パスワード間違えすぎたらアカウントロック
  • その他諸々

github.com

論理削除とは?

論理削除とは、レコードは物理的に削除しないけれど、削除済ということにする、ということです。 物理的に削除してしまうと、そのアカウントに紐づいたデータが全て削除されてしまったり、データに紐づいていたユーザが誰だったかわからなくなり、困ったことになります。 そこで、論理削除することで、ログインはできないけれど関連データを表示するときには使いたい、などがあります。

Deviseで論理削除するには?

Wikiに書いてある方法だと再登録不可能

Deviseは非常に便利なのですが、デフォルトでは論理削除に対応していないため、カスタマイズする必要があります。

一応、GitHubリポジトリWikiには、論理削除のやり方が書いてあります。

How to: Soft delete a user when user deletes account · heartcombo/devise Wiki · GitHub

それを翻訳してくださっている記事がQiitaにあります。

【翻訳】deviseで論理削除を実装する方法 - Qiita

ただし、この方法だと削除済みのメールアドレスで再登録しようとしたらできません。

そこで、今回は削除済みのメールアドレスで再登録可能な論理削除の実装方法をまとめておきます。

実装手順

deviseとkakurenbo-putiをインストールする。

Gemfileにdeviseとkakurenbo-putiを追加します。kakurenbo-putiは、論理削除機能を追加するためのgemです。論理削除機能を追加するgemは多々あるのですが、default scopeを上書きする等して論理削除済アカウントを除外するような動作をするものや、Railsのアップデートに伴って動作しなくなるものがあり、非常に辛いことになるのですが、kakurenbo-putiはdefault scopeを上書きせずにあくまでも機能を追加するだけなので副作用の心配がありません。

github.com

では、追加しましょう。

gem 'devise'
gem 'devise-i18n' # <-手軽に日本語化したければ…
gem 'kakurenbo-puti'

bundle install します。

deviseをセットアップする

deviseをセットアップします。今回はUserモデルとします。論理削除のみに集中するので、他の設定については公式を参照してください。

$ bin/rails generate devise:install
$ bin/rails generate devise User

これで、deviseの各種ファイルとUserモデルのファイルとマイグレーションファイルが追加されました。まだマイグレーションしないでください。

マイグレーションファイルを修正する

deviseのデフォルトのマイグレーションだと、論理削除を考慮した構成になっていませんので、論理削除用のカラムの追加と、インデックスの設定の修正を行います。

具体的には、以下を追加します。

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      # 略
      # kakurenbo-puti
      t.datetime :soft_destroyed_at
      # 略
    end

    # 元々あるemailをユニークにするindexはコメントアウトor削除する
    # add_index :users, :email, unique: true

    # 代わりに、soft_destoryed_atがNULLであることを条件にした部分indexを追加する
    add_index :users, :email, unique: true, where: '(soft_destroyed_at IS NULL)'
    add_index :users, :soft_destroyed_at
  end
end

修正が終わりましたら、マイグレーションを実行します。

$ bin/rails db:migrate

kakurenbo-putiをセットアップする

Userモデルでkakurenbo-putiの機能が使えるようにするため、修正します。

メールアドレスで再登録可能な実装をする場合、deviseのvalidatableはemailのみでユニークであることを検証しようとするため、コメントアウトして、代わりに自分でvalidationを追加する必要があります。

class User < ApplicationRecord

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable,
         :registerable,
         :recoverable,
         :rememberable,
         # :validatable, # <- emailのみでユニーク制約を検証してしまうのでコメントアウトする
         :confirmable,
         :lockable,
         :timeoutable

  soft_deletable # <- kakurenbo-putiを使えるようにする

  # 論理削除に対応するため、validationをカスタマイズする
  validates :email, presence: true, length: { maximum: 255 }
  validates_uniqueness_of :email, scope: :soft_destroyed_at
  validates_format_of :email, with: Devise.email_regexp, if: :will_save_change_to_email?
  validates :password, presence: true, confirmation: true, length: { in: Devise.password_length }, on: :create
  validates :password, confirmation: true, length: { in: Devise.password_length }, allow_blank: true, on: :update

  # 略
end

これで、Userモデルに論理削除用の機能が追加されました。

kakurenbo-putiで追加されるメソッド

論理削除機能が追加されることで、以下のようなメソッドが使えます。

ActiveRecord::Relation
User.all # 論理削除済ユーザも含んだ全てのユーザを取得
User.without_soft_destroyed # 論理削除済ユーザを除外して取得
User.only_soft_destroyed # 論理削除済ユーザのみ取得
Userモデル
user = User.without_soft_destroyed.first
user.soft_destroyed? # => false
user.soft_destroy # => 論理削除を実行
user.soft_destroyed? # => true
user.restore # => 復元を実行
user.soft_destroyed? # => false

他にも、callbackや破壊的メソッドなどもありますので詳しくはkakurenbo-putiのページをご参照ください。

ログイン時の処理で論理削除済ユーザを除外する

あとは、実際にログイン処理を行うときに利用されるメソッドであるfind_for_database_authenticationを上書きします。

参考になる公式のWikiはこちら。こちらはユーザ名でログインの実装なのですが、やっていることはfind_for_database_authenticationの上書きです。

How To: Allow users to sign in using their username or email address · heartcombo/devise Wiki · GitHub

再び、Userモデルを修正します。やることは、without_soft_destroyedを挟むことで、論理削除済ユーザを除外することです。

class User < ApplicationRecord
  # 略

  # データベース認証時に使われるメソッドを上書きして、
  # without_soft_destroyedを追加する
  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    self.without_soft_destroyed.where(conditions.to_h).first
  end

  # 略
end

アカウント削除のactionを上書きする

あとは、アカウント削除処理を上書きしてdestroyする箇所をsoft_destroyにする必要があります。

まず、上書き用のコントローラーを作成しておきます。

$ rails generate devise:controllers users

これで、app/controllers/users配下にコントローラーができました。まだこれは使われる状態ではありません。config/routes.rbに設定する必要があります。

では、config/routes.rbでdeviseのregistrationsのコントローラーに先ほど生成されたコントローラーを指定します。

Rails.application.routes.draw do
  # 略
  devise_for :users, controllers: {
      registrations:      'users/registrations',
  }
  # 略
end

最後に、app/controllers/users/registrations_controller.rbを編集します。

class Users::RegistrationsController < Devise::RegistrationsController
  # 略
  
  # DELETE /resource
  def destroy
    resource.soft_destroy # <- 論理削除を実行
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    set_flash_message :notice, :destroyed
    yield resource if block_given?
    respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
  end

  # 略
end

まとめ

これで、deviseに論理削除を実装することができました。 deviseとkakurenbo-putiを合わせて使うと、論理削除の実装の手間がかなり軽減されます。 それでは、よい論理削除ライフを。