いつか役に立つかもしれないRailsの技3選
k-shogo
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。
こんにちは、クリスマスの予定を聞かれてもnil
を返すk−shogoです。今回はいつか役に立つかもしれないちょっとニッチなRailsの技を紹介します。
has_manyを拡張する
Railsのhas_many
は自動的にリレーションを構築してくれて便利ですね。実はこのhas_many
のリレーションは拡張することが出来るのです。
さっそくサンプルを作成します。今回はUser
,Group
,UserGrouping
の3つのモデルが存在し、User
とGroup
はUserGrouping
を介して多対多の関係を持つことにします。さらにUserGrouping
には役割を示すrole
カラムも持たせます。マイグレーションで示すと以下のようになります。
class CreateUserGroupings < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.timestamps null: false
end
create_table :groups do |t|
t.string :name
t.timestamps null: false
end
create_table :user_groupings do |t|
t.references :user, index: true, foreign_key: true
t.references :group, index: true, foreign_key: true
t.string :role, null: false, default: 'viewer'
t.timestamps null: false
t.index [:user_id, :group_id], unique: true
end
end
end
続いてモデルの実装です。User
とGroup
はhas_many :user_groupings
を持ち、互いにthrough
オプションでuser_groupings
を介して多対多の関連を記述します。UserGrouping
のロールは3種類定義してあり、初期値設定にはFooBarWidget/default_value_forを用いています。
class User < ActiveRecord::Base
has_many :user_groupings
has_many :groups, through: :user_groupings
end
class Group < ActiveRecord::Base
has_many :user_groupings
has_many :users, through: :user_groupings
end
class UserGrouping < ActiveRecord::Base
enum role: {manager: 'manager', submitter: 'submitter', viewer: 'viewer'}
default_value_for :role, :viewer
belongs_to :user
belongs_to :group
validates :user, presence: true
validates :group, presence: true, uniqueness: {scope: :user}
end
これで多対多の関連であるUser#groups
やGroup#users
を使うことが出来るようになりました。ここでGroup
からみてmanager
ロールを持つユーザーを取得したい場合はどうするでしょうか。スコープブロックを用いて以下のように新たな関連を定義することも出来ます。
class Group < ActiveRecord::Base
# ...snip...
has_many :manager_groupings, -> {manager}, class_name: :UserGrouping
has_many :managers, through: :manager_groupings, class_name: :User, source: :user
end
しかしこの方法では柔軟な拡張が出来ず、仕様追加のたびに関連の定義を増やさなければなりません。このような場合にhas_many
の拡張は有効な手段です。has_many
の拡張はブロックでメソッドを追加するだけです。今回はUser
のgroups
関連にロールで絞り込みが出来るメソッドを追加しています。絞り込みはUserGrouping
に追加したスコープをmerge
することで実現しています。
class UserGrouping < ActiveRecord::Base
# ...snip...
scope :role_is, -> (*roles) { where(role: roles) }
end
class User < ActiveRecord::Base
has_many :user_groupings
has_many :groups, through: :user_groupings do
def role_is *roles
merge(UserGrouping.role_is(*roles))
end
end
end
これでgroups
関連の拡張が出来たので、User.first.groups.role_is(:manager, :submitter)
のような絞り込みが可能になりました。しかしロールでの絞り込みはUser
とGroup
のどちらからの関連でも必要になり、ブロックの記述が重複してしまいます。そんな場合にはモジュールにしてextending
オプションを指定しましょう。モジュールでの拡張は複数指定指定することも可能です。
module UserGroupingExtension
def role_is *roles
merge(UserGrouping.role_is(*roles))
end
end
class User < ActiveRecord::Base
has_many :user_groupings
has_many :groups, -> {extending UserGroupingExtension}, through: :user_groupings
end
class Group < ActiveRecord::Base
has_many :user_groupings
has_many :users, -> {extending UserGroupingExtension}, through: :user_groupings
end
has_many
の拡張をうまく使うと、散らかりがちな関連の定義をすっきりとまとめるとが出来ます。年末のモデル大掃除の時に思い出してみて下さい。
データURIスキームでファイルをアップロードする
Railsでファイルアップロードを実装したいとき、様々なGemが存在していて簡単に実現することができます。今回はcarrierwaveuploader/carrierwaveを使ってUser
モデルにアバター画像を設定できるようにしてみます。Getting Startedに習ってAvatarUploader
を作成し、User
モデルにmount_uploader
するだけです。
class User < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
end
ここでアップロード時にユーザーがクロッピングできるようにしたいという要望が来たとしましょう。単純なリサイズの場合carrierwaveには resize_to_fit
などのリサイズ用メソッドが用意されています。しかし任意の座標でのクロッピングは自分で作らなければなりません。RailsCastsでpaperclipとJcropを用いたクロッピングのサンプル(#182 Cropping Images)を発見しましたが、この手法は切り抜き開始座標と縦横サイズを表すパラメータ( :crop_x, :crop_y, :crop_w, :crop_h
)を扱わなければならず面倒ですね。そもそもサーバーサイドに画像を加工する為に必要なImageMagickを準備するのが面倒な場合もあります。
そんなときにはクライアントで加工してデータURIスキームでサーバーに送信する手法を検討してみてはいかがでしょうか。データURIスキームはwebページにデータを埋め込む際に用いられますが、逆にサーバーに送信することも可能です。クライアントでのクロッピングにはこれも沢山の選択肢がありますが、今回はcropitを用いました。データURIスキームでの出力をサポートしていればクライアントのライブラリは何でも構いません。
実装に入りましょう。まずはデータURIスキームを処理するためのモジュールを用意します。data_uri_to_file
メソッドはデータURIスキーム、つまり文字列をRailsにおいてアップロードされたファイルを扱うActionDispatch::Http::UploadedFile
に変換します。メソッドは、データURIスキームを正規表現で分割、Tempfile
への書き出し、ActionDispatch::Http::UploadedFile
の生成の3つのパートで構成されています。
module DataUriParseable
extend ActiveSupport::Concern
module ClassMethods
def data_uri_to_file data_uri
data = data_uri.try do |uri|
uri.match(%r{\Adata:(?<type>.*?);(?<encoder>.*?),(?<data>.*)\z}) do |md|
{
type: md[:type],
encoder: md[:encoder],
data: Base64.decode64(md[:data]),
extension: md[:type].split('/')[1]
}
end
end
return nil unless data
temp_file = Tempfile.new('uploaded-data_uri').tap do |file|
file.binmode
file << data[:data]
file.rewind
end
ActionDispatch::Http::UploadedFile.new(
filename: "data_uri.#{data[:extension]}",
type: data[:type],
tempfile: temp_file
)
end
end
end
続いては作成したモジュールを使用する側のUser
クラスです。モジュールをinclude
してdata_uri_to_file
メソッドを使えるようにしておきます。そしてデータURIスキームをフォームから受け取るためにattr_accessor
でavatar_data_uri
を用意しました。データURIスキームからActionDispatch::Http::UploadedFile
への変換については今回はbefore_validation
コールバックで行っていますが、これは要件次第でしょう。
class User < ActiveRecord::Base
include DataUriParseable
mount_uploader :avatar, AvatarUploader
attr_accessor :avatar_data_uri
before_validation :set_avatar_from_data_uri, if: -> { self.avatar_data_uri.present? }
def set_avatar_from_data_uri
self.avatar = self.class.data_uri_to_file(avatar_data_uri)
end
end
フォームも準備します。サンプルではslim
記法で、かつsimple_formも使っていますが、重要なのはhidden_field
でデータURIスキームを格納するavatar_data_uri
を準備することです。CSSは画像のプレビュー要素の為に用意しました。
= simple_form_for @user, html: {id: :user_form} do |f|
= f.error_notification
.form-inputs#image-cropper
.cropit-image-preview
input.cropit-image-zoom-input type="range"
= f.label :avatar
= f.file_field :avatar, class: 'cropit-image-input'
= f.hidden_field :avatar_data_uri
.form-actions
= f.button :submit
.cropit-image-preview
width: 300px
height: 300px
background-color: #f0f0f0
最後ににちょっとしたCoffeeScriptを追加します。最初はcropitを有効化するためのもので、2行目以降はサブミット時に画面に表示されている画像をデータURIスキームでavatar_data_uri
に書き込むためのコードです。
$('#image-cropper').cropit()
$('#user_form').on 'submit', ->
imageData = $('#image-cropper').cropit('export')
$('#user_avatar_data_uri').val(imageData)
$('#user_avatar').replaceWith($('#user_avatar').clone())
これでクライアントで画像のクロッピングを行い、サーバー側はクロッピング済み画像をデータURIスキームで受け取ることが出来ました。クライアントはデータURIスキームで書き出せさえすれば良いので、cropit以外のライブラリに変更してもサーバー側の実装は変更する必要はありません。Canvas
とJavaScriptist
を用いてクライアントで画像にフィルターをかけてから加工済み画像をサーバーに送る、なんて使い方も出来ますね。
絵文字の対応
絵文字の対応していますか?モバイルからの入力を受け付ける場合、簡単に絵文字が入力できるので対応する場面は増えていますね。データベースにMySQLを使用している場合、文字コード指定をutf8mb4
として4バイト文字を扱えるようにしておかなければなりません(MySQL 5.5.3 以降)。
データベースに突っ込むだけなら文字コード指定だけで良いのですが、DB以外に検索用途でElasticsearchやRedisにもデータを格納する場合や、独自の絵文字に対応したいなんて時には絵文字を特定フォーマットの文字列に変換して格納する方法も検討してみてはいかがでしょうか。今回は絵文字を画像として扱うためのgithub/gemoji、gemojiを扱いやすくするgmac/gemoji-parser、そして入力のノーマライズを行うdimko/normalizrを用いてサンプルを作成します。
まず、gemojiの絵文字表示用の画像をアセットのパスに追加します。config/initializers/assets.rb
にgemojiの設定を追記します。
Rails.application.config.assets.paths << Emoji.images_path
Rails.application.config.assets.precompile << "emoji/**/*.png"
続いて、入力時に使用する絵文字用のノーマライザを用意します。config/initializers/normalizers.rb
を用意し、以下の記述を追加します。EmojiParser.tokenize
によって絵文字が:smile:
のようなコロンで囲まれた文字列に変換されます。例えば熱帯魚の絵文字🐠は:tropical_fish:
に変換されます。
Normalizr.configure do
add :emoji do |value|
String === value ? EmojiParser.tokenize(value) : value
end
end
ユーザーモデルは絵文字用のノーマライザを用いるカラム(ここではname
)を指定するだけです。
これでフォームから絵文字を入力された場合、絵文字はコロンで囲まれた文字列に変換された後にデータベースに格納されます。
class User < ActiveRecord::Base
normalize :name, with: :emoji
end
用意されている絵文字だけでは面白くないので、独自の絵文字も追加してみましょう。app/assets/images
以下にemoji
ディレクトリを用意してファイルを配置します。今回はruby.svg
というファイルを配置しました。そしてconfig/initializers/emoji.rb
を作成し、独自の絵文字を登録します。ここではruby
として登録を行いました。
Emoji.create('ruby') do |char|
char.image_filename = 'emoji/ruby.svg'
end
最後に表示用のヘルパーを作成します。app/helpers/emoji_helper.rb
を用意し、emojify
メソッドを定義します。EmojiParser.parse_tokens
はコロンで囲まれた文字列が登録されている絵文字の場合にgemojiのEmoji::Character
へと変換してくれます。独自に登録した絵文字か否かでパスの調整を行い、image_tag
ヘルパーに渡しています。
module EmojiHelper
def emojify content
EmojiParser.parse_tokens(content.to_s) do |emoji|
path = [(emoji.custom? ? nil : 'emoji'), emoji.image_filename].compact.join('/')
image_tag(path, class: 'emoji', alt: emoji.name)
end.html_safe if content.present?
end
end
あとは絵文字用のCSSを用意し、表示時に絵文字用のヘルパーを使うだけです。
img.emoji
vertical-align: middle
width: 20px
height: 20px
p
strong Name:
= emojify @user.name
これでI love :ruby:
と入力してみると自分で追加した絵文字も表示されます。
今回のサンプルでは絵文字を変換して格納し、表示時は画像で置き換える手法をとりました。この手法ではPCでも絵文字を入力しやすく出来るメリットもあります。本格的にするならJavaScriptなどでサジェストしてあげると良いですね。自分のサービス独自の絵文字を用意するのも楽しいです。