とりあえずコード書けよ

技術的なことの備忘録。

Railsのindex_errorsでi18nを適用する際にハマったのと、それを調べる過程について

要約

  • index_errorsでi18nを使うときは config.active_model.i18n_customize_full_message = true を指定すること
  • 一次情報が大事。Qiita等を見るのはそのあと。
  • 困ったときはとりあえずprintデバッグから始めてみる。順を追って行けばそれなりに読める。

本文

Railsで1対多の関係(has_many)において、どの子要素でエラーが出たのかを載せるindex_errorsという機能がある。
index_errorsについては下記参照。
github.com

qiita.com

しかし、この機能困ったことにエラーのキーに上記を見ると分かるが、エラーのキーにindexが入ってくるため、i18nでエラーメッセージを変換するのがなかなか難しいのが個人的に難点だった。

だが、Rails6.0ではこれに対し、indexを除いた値をi18nで参照するような修正がなされているということで、おお素晴らしいとアップデートの日を心待ちにしていた。
github.com

しかし、アップデートしたもののどうだろうか、エラー表示は変わらなかった。
しかも、特に調べてみてもその辺りに関する言及は出てこなかった。
(accepts_nested_attributes_forが市民権を得てないからかな?笑)

どうやらデフォルトでONになるわけではないようである。

仕方ないので、binding.pryしながらコードを読み進めていくと、下記のコードでindexを削るか否かの判定をしていることが分かった。

if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)

https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/activemodel/lib/active_model/errors.rb#L416

@base.class.respond_to?はtrueだったが、self.class.i18n_customize_full_messageはfalseだったので、処理が走らないという状況だった。
(ちなみに、self.classはActiveModel::Errors、@base.classはフォームで操作しているオブジェクト)

では、このi18n_customize_full_messageをどこで操作しているのか。
まずActiveModel::Errors内でi18n_customize_full_messageを見ると、デフォルトでfalseがセットされているっぽいことが分かった。

class << self
  attr_accessor :i18n_customize_full_message # :nodoc:
end
self.i18n_customize_full_message = false

https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/activemodel/lib/active_model/errors.rb#L65-L68

そこで次はrailsリポジトリ内でi18n_customize_full_messageを検索してみる。 そうすると、testファイルを除いてrailtie.rbとconfigure.mdがヒットした。

railtie.rbを見ると、どうやらconfigで何か指定しなくてはならないことが読み解ける。

 initializer "active_model.i18n_customize_full_message" do
   ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
 end

そして、configure.mdを見るとバッチリ書いてあった。

config.active_model.i18n_customize_full_message is a boolean value which controls whether the full_message error format can be overridden at the attribute or model level in the locale files. This is false by default.

https://github.com/rails/rails/blob/439d4995c1dab475b576fcb19ea95ae37e0ed222/guides/source/configuring.md#configuring-active-model

というわけで、config/application.rbに下記の内容を追加する。

module Aftshop
  class Application < Rails::Application
    # other config..
    config.active_model.i18n_customize_full_message = true
  end
end

すると、indexを除いたi18n_keyの内容を無事、参照してくれるようになった。

教訓

一次情報をよく見るべし、とつよつよエンジニアの方々はよく言うが、今回は身を以て経験した感じ。
そして、改めて大事だなと思ったのは2つ。
1. 公式リポジトリを見る習慣 2. rails等のフレームワークのコードも、順に追っていけば案外読める

Railsのコードも、一つの巨大な塊と考えてしまうと、どこからアプローチしていけばいいのか分かりにくい。
でも、binding.pry等を駆使して、順に処理を追って行けば、意外と読めるなという感覚。

どんなに優れたコードとて、所詮は同じ人間が書いたもの。
そんな感じで気楽に構えたらいいのかなーと思った。

Outlookのメールをcsvに変換する方法

めちゃニッチな話だけど、調べても特に情報が出て来なかったので、万が一誰かの役に立つかもしれないから残しておく。
ちなみにoffice for macの話です。

事のいきさつとしては、定期購入で商品を買っているんだけど、商品を発送しましたとかって定型の通知メールが色々届くんですが、いままでいつ何が送られて来たのかをデータとして管理したかった。
(残念ながらCSV出力等の機能はそちらにはなかった。)

なので、メールだけが頼りなんだけど、それをさすがに一個一個調べていってcsvで管理とかは無駄無駄無駄無駄ァ!って感じなので避けたかったんですよね。

まず考えたのはメールソフトでCSV出力できないの?だった。 で、調べたらどうやらOutlookがいけるらしいぞ、というのを下をみて思ったわけですよ。
jp.tipsandtricks.tech

さすがMicrosoft、俺たちにできない事を平然とやってのけるっ!と思って、早速Outlookを導入。
(ちなみに、わざわざOffice365を契約した(1ヶ月無料なので、まだダメージはない))

で、いざツール→エクスポートって進んでみてもないんですよね。CSV出力なんてものは。

そもそも↑のサイトもOffice for macは.olmしかないよ!って書いてあるんですよね。
マニュアルはよく読もう。

.olmをハックする

仕方がないので、.olmというこの謎のファイルをどうにかしようと思ったわけです。
で、olm to csv とかで調べてみると、一応↓のようにソフト自体は存在することが分かった。

www.adviksoft.com

ためしに使ってみたものの、フリーだと25件しか解凍できないとのことで、ライセンスも$39だし、ピンポイントにしか需要がないのでそこに$39も払うのもなーという感じ。
そこでふと、エンジニアの血が騒いでしまった。
いうてエンジニアの端くれなんだし、なんか面白そうだし、いじってみよと。
大人になってから夏休み終了間際(日付的に。)に自由研究を嬉々としてやることになるとは思わなかった。

.olmとはなにものか

まず.olm自体がなんなのかよく分からなかったので、とりあえずvimで開いてみた(雑)。 まぁ案の定読めなかったんですけど、エンコードの問題かなーとか思ったんで、エンコード調べよ!って思ったらfileに--mimeっていう素晴らしいオプションがあるんですね。全然知らんかった。

# --mimeでエンコードも出力される。
file --mime hoge.olm
→hoge.olm:: application/zip; charset=binary

ってなったんで、いや、っていうかそもそもお前zipだったんか、っていうところでエンコードどうこうよりも、とりあえず解凍しようと思ったわけです。

How to Unzip

普段The Unarchiver使ってるんで(そもそも使う機会ほぼないですが)、突っ込んでみたら「ファイルが壊れています」でNG。
次はアーカイブユーティリティに突っ込んでみてもこれまたNG。
で、最終兵器unzipに突っ込んだんですけど、まさかのなんとかしてくれた。unzip最強説。

unzip hoge.olm
# Illegal byte sequenceとかでちょこちょこエラー出ますけどメッセージの取り出し自体は問題はなさげ

で、いざ解凍してみるとこんなディレクトリ構成になりました。

Accounts  
∟hoge@xxx.ne.jp  
 ∟com.microsoft.__Messages  
  ∟INBOX  
   ∟message_00000.xml
   ∟message_00001.xml
   ∟...大量のxml

メッセージはそれぞれxmlことが分かりました。
ここまできたらもう勝ったも同然。

xmlをパースする

とりあえずディレクトリ構成を解凍した状態から若干変更します。
適当な作業ディレクトリ ∟INBOX
| ∟message_00000.xml
| ∟message_00000.xml

xml_to_csv.rb 以下xml_to_csv.rbの中身。

# frozen_string_literal: true

# REXML::Documentを使ってパースしていく
require 'rexml/document'
require 'csv'

# CSVファイルの生成
CSV.open('./output.csv', 'w+', col_sep: "\t") do |csv|
  # ヘッダ行の設定
  csv << %w[subject sent_at body]
  # Dir.glob使って、解凍したxmlファイル名を読み込み
  Dir.glob('./**/*.xml').each do |xml_file|
    # 読み込んだxml_file名からfileを開く
    xml_doc = REXML::Document.new(File.new(xml_file))
    # 件名の読み取り
    subject = xml_doc.elements['emails/email/OPFMessageCopySubject'].text
    # 送信日時の読み取り
    sent_at = xml_doc.elements['emails/email/OPFMessageCopySentTime'].text
    # 本文の読み取り
    body = xml_doc.elements['emails/email/OPFMessageCopyBody'].text
  end
end

あとは本文(↑でいうbodyに格納されてる内容)から正規表現等々を駆使して自分で欲しい情報を取り出していけばお好みのメールcsvが作れます。

まとめ

  • .olmはunzipで解凍できる。
  • メッセージはxmlでできている。
  • あとはxmlをパースしましょう。

めでたしめでたし。

Otemachi.rb#20でLTしてきた

概要

Otemachi.rb#20で「突撃!隣のフォームオブジェクト」という話でLTしてきました。
タイトルは某番組をインスパイアしたものの、内容はこちらのフォームオブジェクトに関する取り組みについてなので、完全にタイトル詐欺である。

スライド

speakerdeck.com

内容について

責務がごっちゃでどこに何書いてるかわけわからんカオスなプロダクトを、制度変更に際して色々と整理している。
その中でフォームオブジェクトを採用しており、それをActiveTypeというgemで対応した。

ActiveTypeについて

めちゃくちゃざっくり言うと非ARモデルをARオブジェクトっぽく扱えるようにして、かつARオブジェクトもいい感じに継承できるっていうgem。

github.com

class HogeForm < ActiveType::Object
  attribute :name, :string
  attribute :email, :string

  validates :name, :email, presence: true
end

みたいなのがかけたり

class User < ApplicationRecord
  has_one :address, dependent: :destroy
end

みたいなARオブジェクトを

class FugaForm < ActiveType::Record[User]
  change_association :address, class_name: 'AddressForm'
  accepts_nested_attributes_for :address
end

みたいに完全に置き換えれたりする。

スライドに入れれなかったけどcreateとかupdateとかのコンテキストに応じて処理が違うのであれば、共通の処理はxxxForm::Baseに書いて、それを継承してxxxForm::Createとか、xxxForm::Updateみたいなこともできたりする。
(くっそ雑なので継承に関しては用法用量を正しく守ってお使いください)

class UserForm::Base < ActiveType::Record[User]
  # 関連付けとか共通な処理
end

class UserForm::Create < UserForm::Base
  # create用の処理
end

class UserForm::Update < UserForm::Base
  # update用の処理
end

ちなみにupdate等の処理に際してActiveTypeオブジェクトに変換する際には

user = User.find(params[:id])
@user_form = ActiveType.cast(user, UserForm)

といった感じでActiveType.castメソッドを用いると変換できる。

懇親会

懇親会で話があったのが、ARオブジェクトのバリデーションをFormオブジェクトに完全に移譲するとバッチ処理とか大丈夫なのか、と言う話があった。
これはたしかにけっこう悩みどころで、直接ARオブジェクトを操作することをいかに禁止するかというのはなかなか難しい部分だなと思う。
仮にバリデーション忘れてもpresence: true程度であれば、db上でnull: falseをきっちりかけてればこと足りるけど、複雑なバリデーションは対応難しいっていうのは間違いない。
ただ、バッチ処理に関してはActiveType.castの一手間がかかるけど、その1行の手間で責務のカオスさが減るならいいのかな、とは思ってます。 (エンジニアが一人だからOKな話で、ほかに強い人いたら即NGくらうかも)

この辺の各オブジェクトの責務と言う部分に関しては相談する相手等がいなくて真剣に分からないので、だれか教えていただきたいところです。

LTを終えて

だいぶ詰め込みすぎて、早口で喋らざるを得なくなってしまった。
喉のコンディションが悪かったのもあって、聴きづらかったであろうことは申し訳なかったなと。
あと、castの部分とかも話せてなかったので、だいぶ分かりにくくなってしまった気がする。
5分だからこそ伝える内容うまく絞らないとなー。

でも、やっぱり前で話すの楽しいよね。
やるまでは「なんでLT枠でエントリーしちゃったかなー」とか思うんだけども。
不思議なものである。

Rails初心者が個人的にもっと早く聞きたかったシリーズ1 type: :uuid

シリーズとか書いているけどシリーズ化するかは分かりません。

最近、改修という名の過去の自分と戦う羽目になるケースが非常に多いです。
気分はコロッセオディアボロです。
f:id:ys3128:20190707004047p:plain 1年経って見返してもよく分かるんですが、非常に無駄が多いんですね。
(無駄に気付けるってことは、それだけ成長したといえば聞こえはいいですが。)
ただ、無駄はよくない。リソースが少ない状況なので尚更。
そんなわけで今になって思う、もっと早く聞きたかったことをまとめておく。

何億番煎じか分からないけど、今回はuuidについて。

uuidとは

知らない人のために簡単に書いておくと
重複しない一意なid
のことです。

細かい原理とかはここで省きます。

いつ使うのか

Railsではデフォルトのprimary_keyがintegerのオートインクリメンタルになっている。
で、だいたいrails_wayに素直に従っていれば
/users/:id
とかのルーティングになっているので
/users/1
とかになっているわけですね。

これは簡単に別のユーザーのIDが推測できてしまうなどなど、色々な問題があります。
そこでuuidの出番です。

環境

rails 5.2.3
ruby 2.5.3
postgresql 9.6.4

導入方法

1.マイグレーションファイルを作成する
(ファイル名は分かりやすければなんでも良い)

rails g migration enable_uuid_extention

2.マイグレーションファイルの編集 & マイグレーション

# xxxxxxxxxxxxxxxxxx_enable_pgcrypto_extention.rb
class EnablePgcryptoExtention < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'pgcrypto'
  end
end
rails db:migrate

3.モデルを作成する

rails g model Hoge name:string

4.マイグレーションファイルを編集&マイグレーション

class CreateHoges < ActiveRecord::Migration[5.2]
  def change
    create_table :hoges, id: :uuid do |t| # id: :uuidを追記する
      t.string :name

      t.timestamps
    end
  end
end
rails db:migrate

5.レコードの作成

irb(main):001:0> Hoge.create(name: "sample")
   (0.2ms)  BEGIN
  Hoge Create (29.8ms)  INSERT INTO "hoges" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "sample"], ["created_at", "2019-07-06 04:42:08.809181"], ["updated_at", "2019-07-06 04:42:08.809181"]]
   (7.2ms)  COMMIT
=> #<Hoge id: "ff67302a-8319-42e6-b94e-be7b0241cde1", name: "sample", created_at: "2019-07-06 04:42:08", updated_at: "2019-07-06 04:42:08">

関連付け

class HogeRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :hoge_relationships do |t|
      t.references :hoge, foreign_key: true, type: :uuid # type: :uuidを指定する
      t.references :fuga, foreign_key: { to_table: :hoges }, type: :uuid

      t.timestamps
    end
  end
end

ちなみに今回の項目とは全く関係ないけど、
foreign_key: { to_table: :テーブル名 }
カラム名とは別のテーブルを指定できます。
同じモデル同士の関連付けでカラム名を変えたい場合などに使いましょう。

まとめ

サービス開発ではuuidほぼマストかなと思いますが、自分で調べてみないとこれ分からないですよね。
デフォルトでuuidがあると知らなかった頃は

 create_table :users, id:false do |t|
   t.string :id, null: false
  end

とか定義して、before_createでSecureRandomで作ったuuid値をセットするとかいう愚行をおかしてました。
(内輪向けのアプリでまだよかったけど)

パフォーマンスに関しても、通常のインクリメンタルと比べて低下は2割程度、というのを以前見かけました。 (ソースは探し直したけど見当たらず、、、。)
状況によると思いますが、それでidの重複を気にしなくて良くなるなら、全然OKなのかなという気がします。

Rails6になってActionTextが実装される今、あえてFroalaEditorを解説する

Rails6になって搭載されるActionText。 リッチテキストエディタが標準で使えるようになって万々歳。 そんな方々も多いのではないでしょうか。

が、しかし、世の中には
「Rails6にバージョンアップしたくてもできねえんだよ!」
とかっていう悲しい事情を抱えてる方も世の中にいるかもしれないし、
個人的に気に入ってるのもあってwysiwygエディタの
FroalaEditor
について導入方法とかを備忘録で残しておきます。

公式

www.froala.com

Apple, IBM, Amazonなどなど、いろんな企業で使われているようです。
↓のような多機能エディタが実装できます。

f:id:ys3128:20190705143049p:plain
froala_image

環境

ruby 2.5.3
rails 5.2.3
wysiwyg-rails(FroalaEditor) 3.0.1

導入方法

www.froala.com

公式が丁寧なので、ここ見ればよっぽど問題ないと思います。

1.Gemfileに追記とインストール

gem 'wysiwyg-rails'
bundle install

2.application.scssに追記

 @import "froala_editor.min";
 @import "froala_style.min";
 @import 'plugins/char_counter.min.css';
 @import 'plugins/code_view.min.css';
 @import 'plugins/colors.min.css';
 @import 'plugins/emoticons.min.css';
 @import 'plugins/file.min.css';
 @import 'plugins/fullscreen.min.css';
 @import 'plugins/image_manager.min.css';
 @import 'plugins/image.min.css';
 @import 'plugins/line_breaker.min.css';
 @import 'plugins/quick_insert.min.css';
 @import 'plugins/table.min.css';
 @import 'plugins/video.min.css';

3.application.jsに追記

//= require rails-ujs
//= require jquery
//= require activestorage
//= require turbolinks
--------ここから↓が追記箇所
//= require froala_editor.min.js
//= require languages/ja.js
//= require plugins/align.min.js
//= require plugins/char_counter.min.js
//= require plugins/code_beautifier.min.js
//= require plugins/code_view.min.js
//= require plugins/colors.min.js
//= require plugins/emoticons.min.js
//= require plugins/entities.min.js
//= require plugins/file.min.js
//= require plugins/font_family.min.js
//= require plugins/font_size.min.js
//= require plugins/fullscreen.min.js
//= require plugins/image.min.js
//= require plugins/image_manager.min.js
//= require plugins/inline_style.min.js
//= require plugins/line_breaker.min.js
//= require plugins/link.min.js
//= require plugins/lists.min.js
//= require plugins/paragraph_format.min.js
//= require plugins/paragraph_style.min.js
//= require plugins/quick_insert.min.js
//= require plugins/quote.min.js
//= require plugins/save.min.js
//= require plugins/table.min.js
//= require plugins/url.min.js
//= require plugins/video.min.js
----------ここから↑が追記箇所
//= require tree.

これで準備OK。

4.適当にサンプルモデルを作ってマイグレーション。今回はhtmlメールを作る機能、とでもイメージしています。

rails g scaffold MailContent subject:string content:text
rails db:migrate

5.app/views/mail_contents/_form.html.erbに追記

<%= form_with(model: mail_content, local: true) do |form| %>
  <% if mail_content.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(mail_content.errors.count, "error") %> prohibited this mail_content from being saved:</h2>

      <ul>
      <% mail_content.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :subject %>
    <%= form.text_field :subject %>
  </div>

  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
----------↓を追記
<script>
new FroalaEditor('#mail_content_content', {});
</script>

これで実装完了。

使い方

f:id:ys3128:20190705143049p:plain 機能は左から順に

  • 太字
  • イタリック(斜体)
  • アンダーライン
  • テキストその他
    • 横線
    • 添字(下部)
    • 添字(上部)
    • フォント変更(Arial, Georgia, Impact, Tahoma, Times New Roman, Verdana
    • フォントサイズ
    • 文字色
    • 背景色
    • プリセットスタイル
    • スタイルのリセット
  • 左寄せ
  • 中央寄せ
  • 番号付きリスト
  • 段落その他
    • 右寄せ
    • 均等寄せ
    • 番号付きリスト詳細
    • 箇条書きリスト詳細
    • 段落フォーマット(H1, H2, H3, H4, Code)
    • 段落スタイル(Gray:文字のグレー化、Borderd:上下線、Spaced:文字間隔を広げる、Uppercase:大文字化)
    • インデント減らす
    • インデント増やす
    • 引用スタイル
      • 引用スタイルを増やす
      • 引用スタイルを減らす
  • リンクの挿入
  • 画像の挿入
    • アップロード
    • URLから挿入
    • プリセット
  • 動画の挿入
    • URL
    • 埋め込みコード
    • アップロード
  • その他
    • テーブルの作成
    • 絵文字
    • ファイルアップロード
    • 水平線の挿入

となっています。

できることが大杉漣って感じです。

実際に触る

f:id:ys3128:20190705151933p:plain
froalaの操作
色の変更をして保存する。

f:id:ys3128:20190705152255p:plain
froalaでレコードを作成した後
はい、当然htmlタグがエスケープされていますね。 なので、エスケープをしないようにします。 今回はサンプルなので、とりあえず<%== %>でエスケープします。 (本番環境で一般ユーザーに使う方はsanitize等で厳密に許可するタグとかをきっちり設定したほうが良いと思います。よっぽどないと思いますが安易なhtml_safeとかは、ダメ、ゼッタイ)

# app/views/mail_contents/show.html.erb
<p>
  <strong>Content:</strong>
  <%= @mail_content.content %>
  ↓に変更
  <%== @mail_content.content %>
</p>

f:id:ys3128:20190705152717p:plain
エスケープせずに出力

無事スタイルが適用されました。

まとめ

というわけで、ActionTextに負けず劣らず、簡単にリッチなエディタが実装できました。 機会があれば、FroalaからのS3アップロードとかもまとめたいと思います。

db:migrate:status から ********** NO FILE ********** を消す

f:id:ys3128:20190525101428j:plain

何番煎じかわからんけども、備忘録。

開発中とかによくやっちゃうわけですよ。
migrateかけた後に「やっぱ違う」と思ってrollbackなどをせずにmigrationファイルを消す。
ってことを。

で、そうするとdb:migrate:statusを実行した際にこいつがあらわれます。

# bundle rake db:migrate:status
 up     20190523053718  ********** NO FILE **********

そうするとschema.rbにもversionの履歴にカウントされてしまうので、schema.rbさんが使い物にならなくなります。
というわけで、何かと鬱陶しいので消します。

①まず下記のようにファイルを作成します。

# (version)_tmp.rb
touch 20190523053718_tmp.rb

②①で作成したファイルに下記の内容を追加

class Tmp < ActiveRecord::Migration[5.2]
  def change
  end
end

railsのversionによってActiveRecord::Migration[x.x]の内容が違います。

③db:migrate:downを実行

bundle exec rake db:migrate:down VERSION=20190523053718

④作成したファイルを削除

rm 20190523053718_tmp.rb

これで
f:id:ys3128:20190525101428j:plain

マイグレーションファイル消す前にちゃんとstatus確認しましょう。