bower に table のカラムの表示/非表示を自由に選べるライブラリを追加してみた
皆さん。こんにちは。MUGENUP のosada
です。
電車を降りようとしたら、スマホを線路に落としました!
駅員さんにお願いして取っていただいたところ、
なんと画面が割れることもなく無事でした!ラッキー!
そんな運が良い osada がお送りする今回は、bower
のお話です。
js のパッケージ管理ツール Bower。
皆さん使っていらっしゃいますか?
弊社では、bower
パッケージを自動でgem
にしてくれるRailsAssets
というサービスを使って、
Rails
上でbower
を使っています。
今回はそんなbower
のパッケージを作ってたみたので、レポートします。
要点
- js ライブラリのデモページは、
github pages
が良かった github
のソースを、<script>
として読み込むrawgit
- RailsAssets に bower を直ちに反映させたければ、
Add Componet
表示するカラムを自分で決められるTable 用 jQuery プラグイン
弊社はクリエイティブの幅広い部分をカバーする会社ですので、
多くの案件と複数の職種が入り乱れて、ゴールを目指します。
すると、同じ案件の情報でも、見たいものが異なるのは自然なことです。
さらにいうと、同じ職種の人でも、見たいものが異なる
ということも、
よくあります。
そこで作ったのが、table_as_u_like
です。
- Table as u like
- 機能
- テーブルの
th
を読み込みトグルボタンにする - トグルによって、カラムの表示/非表示が切り替えられる
- session によって、個人ごとに状態を維持する
- テーブルの
coffeescript
で 45行の簡単なプラグインですが、便利に使っています。
今回はこれをbower
に登録しました。
bower に登録する前に
Rails用に開発したので、ライブラリとして公開するのに必要なものが2つありました。
Rails
の代わりにcoffeescript
をコンパイルする環境- デモ用のページ です。
gulp で coffee を コンパイル
coffeescript のコンパイルだと、 Gruntが思いつきますが、 今回は gulp.js を使ってみました。
設定ファイルが書きやすい、速い、など、Grunt
を改良したツールということでしたが、
1つの coffee
をコンパイルする程度だと、あまり違いが感じられませんでした。
今度は大規模なものに使ってみたいところです。
- インストール
npm install -g bower npm install -g gulp npm install --save-dev coffee-script npm install --save-dev gulp-coffee
- 設定ファイル
gulp.task 'coffee', () -> gulp.src files.coffee .pipe coffee() .pipe gulp.dest('lib')
設定ファイル、というより、シェルスクリプト、といった感じでした。
勿論watch
もあるので、安心です。
github pages でデモ用のページを作る
動的な要素が不要な場合、github pages
はとても便利です。
github
の settings
から
github-pages
を有効にすると、
gh-pages
というブランチが作られます。
以降はそのブランチへpush
することで、反映されるという簡単構造です。
(多少遅延があります)
jekyll
が使えるということでしたので、
大きなページを作る場合は、そちらの方が良いですね。
今回は1ページだったので、
ローカルでサーバを起動して、
開発して、push
という手順でした。
こんなときはpython
が大活躍。
python -m http.server
小さなものなら、これで十分ですよね。
github に置いたまま <script>
で読み込む rawgit
さて、そんな便利なgithub-pages
ですが、唯一困ったのか、
ライブラリの読み込みです。
jquery は cdn を使って読み込めます。
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
ですが、github
においてあるファイルは、そのままでは読み込めません。
例えば、こう書くと
<script src="https://raw.githubusercontent.com/osdakira/table_as_u_like/master/lib/jquery.table_as_u_like.js"></script>"
こう怒られます。
Refused to execute script from 'https://raw.githubusercontent.com/osdakira/table_as_u_like/master/lib/jquery.table_as_u_like.js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
github がこれを禁止していて、理由は、遅いから、 *1 ということのようです。
この制限を迂回するサービスがrawgit.com
です。
ヘッダを付与することで、github
へのダイレクトなアクセスを行い、
かつ、キャッシュすることでスピードを上げているようです。
デモの用途であれば*2、使用できるようです。
そうしてできたデモが Table as u like です。 index.html一枚で、js, css は全て外部から読み込んでいます。
bower に登録する
bower の登録については、既に多くの記事があるので(皆さん、ありがとう!)、概要のみ記載します。
bower
をインストールbower init
で対話形式でbower.json
を作成bower register
で登録
npm install -g bower bower init bower register table_as_u_like git@github.com:osdakira/table_as_u_like.git`
とても簡単でした。
RailsAssets に Add Componet する
ところが、bower
には登録されていても、RailsAssets
には出てきませんでした。
RailsAssets
への登録は非同期で行われているようで、遅延があるようです。
この場合、AddComponetを使うことで、
直ちに反映させることができました。
bower
が自動的にgem
になる。なんて便利なんでしょう!
まとめ
以上がjsライブラリ
をbower
に登録してみたレポートです。
- js ライブラリのデモページは、
github pages
が良かった github
に置いたまま、<script>
として読み込むrawgit
- RailsAssets に bower を直ちに反映させたければ、
Add Componet
そうそう、最後にもう一つ。 線路にものを落とした時は、 何番線の何号車の位置に落としたかを伝えましょう。 びっくりしますし、焦ってしまいますが、 それを伝えることができたら、話はスムーズです。 (私は上りと下りを間違えて、ウロウロしてしまいました)
では、良い geek ライフを!
参考
- Bowerまとめ(概要・導入・コマンド一覧) - Qiita
- bowerにパッケージを登録する - Qiita
- Link and execute external JavaScript file hosted on GitHub - Stack Overflow
*1:Heads up: nosniff header support coming to Chrome and Firefox
*2:アクセスが少なく、負荷をかけないもの
【Rails】Scaffoldされたコントローラを読んでみる
こんにちは、MUGENUPの倉成です。 僕はRailsを使い始めてあと少しで丸2年になるのですが、初めの頃は覚えることがたくさんあって、とても大変だった記憶があります。
今回はそんな中でも、Railsで初めてWebプログラミングをする人向けに、コントローラのアクションであるnew
,create
,show
について解説してみたいと思います。
この記事ではソースコードは説明のため、実際にscaffoldされたコードを抜粋、もしくは省略されているコードを挿入して取り上げます。 なお、使用するコードは
rails g scaffold user name:string email:string postal_code:string address:string
で生成されたコードを元にしており、モデル・ビュー・コントローラはこれを使うことにします。
それではnew
から見て行きましょう。
def new
: 新規登録フォームを作成
def new # (1) Userオブジェクトを生成 @user = User.new # (2) フォームを生成(Rails4では省略されている) render :new end
このアクションでは(1)
で@user
を作成し、(2)
では(1)
で生成したオブジェクト(@user
)とテンプレート(new.html.erb
)を使ってhtmlを生成しています。
図にすると以下の様なイメージです。
今回は@user
の全ての属性の初期状態がnilなので、空のinputフォームが生成されています。
さて、次はcreate
アクションを見て行きましょう。
def create
: 登録 or 差戻し
scaffoldされたcontrollerは複雑で悩むポイントはたくさんあると思うのですが、特に難しいのが行数の多いcreate
だと思います。
今回は簡単のためformat.html
やformat.json
そしてnotice
は一旦脇に置いておいて
def create # (1) フォームから渡されたパラメータを使用し、Userオブジェクトを生成 @user = User.new(user_params) # (2) DBへ保存(を試みる) if @user.save # (3) 新規登録されたユーザーページヘリダイレクト redirect_to @user else # (3') @userを使って、もう一度フォームを表示 render :new end end
のコードを追いましょう。
(1) @userを生成
「@user
ならnew
するときに作ったよ」と思うかもしれません。
しかし、ブラウザからリクエストとして送られてくるのは、フォームに入力されたテキストデータのみのため、改めて@user
を生成する必要があります。
同じコントローラでも「一回ごとのリクエストでインスタンス変数が共有化されない」というのはWebアプリが初めてだと取っ付きづらく感じる原因かもしれないですね。
create
内で、フォームからの情報を使って、@user
を作ります。
(2) DBへ保存
(1)
で生成した@user
の値を検証し、DBへの保存が成功した場合には、(3)
でredirect_to
、失敗した時には(3')
でrender
をします。
saveが失敗するケースとしては「ユーザー名が空白です」「郵便番号は7桁で入力して下さい」のように、バリデーションに引っかかることが多いかと思います。
(3)登録に成功: 登録内容を表示する
ユーザーの登録に成功したら、そのユーザーの登録内容を表示します。
ここでは登録に成功した@user
のページを表示するため
redirect_to @user # 以下と同じ意味 # redirect_to user_path(@user) # redirect_to /users/#{@user.id}
としています。
しかしnew
にあったrender
と違い、redirect_to
はテンプレートを使用してHTMLを生成するわけではありません。
redirect_to
はブラウザに「localhost:3000/users/1
にアクセスしなおしてね」と教えるだけです(urlはドメインがlocalhost:3000
、@user.id=1
の場合)。
ブラウザはサーバーからのredirect
の指示に従ってlocalhost:3000/users/1
にアクセスすることで、詳細ページが表示されることになります。
render
じゃないのはなぜ?
さて、ここでredirect_to
を使用していて、render :show
としないのはなぜでしょう?
上の図でも真ん中の2本の矢印は不要に思えますし、create
内で@user
が既に存在しているので、再度DBから読み直すのは無駄な気がします。
実は、saveに成功時にrender :show
としてしまうと、ページをリロードするたび、新規ユーザー登録処理が発生してしまうという問題があります。
Railsの視点から説明すると、ブラウザをリロードした時には、最後にrender
を呼び出したアクション、今回の場合はcreate
アクションが呼ばれてしまうことになります。*1。
下はcreate
アクションでrender :show
をした後、ブラウザをリロードさせた図です。多重登録を防ぐため「フォーム再送信の確認」ダイアログが表示されているのが分かるかと思います。
今回はscaffoldのコードに習いshow
にリダイレクトしましたが、もちろん一覧ページであるindex
に飛んでもいいですし、ホームのページ(localhost:3000/
)に飛ぶことも多いと思います。
DBの更新が発生した後には、ページを表示するだけのアクションにリダイレクトするということを覚えておきましょう。
また、このpost → redirect → getの一連の流れはその頭文字を取ってPRGパターンと呼ばれているようです。この記事の説明でしっくりこないなぁという方は、ぜひPRGパターンで検索してみてください。
(3')登録に失敗: もう一度入力フォームを表示する
入力内容に不備があった場合、もう一度フォームを表示して、再度情報を入力してもらうため、フォームのテンプレートであるnew.html.erb
をrender
します。create
アクションでrender :new
をすると、保存に失敗した@user
のインスタンス変数をそのまま使えるため、不正な項目のみを再度入力してもらうことが出来ます。
redirect_to
じゃないのはなぜ?
一方、再度フォーム画面を表示さるためにredirect_to "users/new"
を使ってしまうとフォームは表示させることが出来るのですが、ユーザーがこれまでに入力した内容が全て消え、再度全ての項目を入力してもらわなければなりません。
new
の処理を思い出すと、@user
の生成時に@user = User.new
としているので、白紙のフォームが表示されてしまうのは納得できるかと思います。
ユーザー登録時に「一箇所ミスがあるくらいで、全ての内容を再入力させるなー!」という経験は一度くらいあるはずですが、このようなシステムを作らないためにはredirect_to
ではなくrender
を使う必要があるのです。
def show
: 登録情報を表示
ユーザーの情報を表示するのがshow
の役割です。
create
からredirect
してくる場合でも、create
で使用した@user
は使用できないため、改めてDBからデータを取得し、@user
を生成する必要があります。
def show # Rails4ではbefore_actionで呼ばれる # (1) params[:id]のユーザーを取得 @user = User.find(params[:id]) # (2)Viewを生成(Rails4では省略されている) render :show end
@user
を作って、View(show.html.erb
)のhtmlを生成するということで、フォームなのかテキストなのかという違いがあれど、図はnew
と似たものになります。
最後に
さて、今回は僕がRails初心者だった頃に「render
とredirect_to
は何が違うの?」とか「ここの@user
って、こっちのアクションで使えないの?」とか、よく混乱していた部分を中心にRailsのコントローラを解説してみました。
この記事を読んで少しでも理解が深まれば嬉しいです。
この記事はDavid Griffiths著『Head First Rails』の3章を参考にさせていただきました。 この本はRails2系が対象と、やや古くRails3/4では、写経したコードが動かないことが多々あるのですが、Railsの仕組みを知る上ではかなり良書だと思います。 リファレンス的には使用できず、回り道に感じるHead First Railsですが、Railsで初めてWebプログラミングをするという方はこの本から始めてみるとよいかもしれません。
enctype='multipart/form-data'ってなんだ?
こんにちは、MUGENUPアルバイトの倉成です。
今回は僕が前々から気になっていた、フォームからファイルを送信するときのおまじないenctype="multipart/form-data"
について調べてみたので、得られた知識をまとめて見ようと思います。
また、マルチパートの情報を検索していると、HTMLのフォームだけではなく、メールのマルチパートの情報に当たることも多くありました。 調べてみると、HTMLの仕様と電子メールの仕様が似ているのは、どうやら歴史的な経緯があるようなので、後半ではインターネット成長の歴史についても少しだけ触れてみようと思います。
multipart/form-data
: ファイルを送るおまじない
それでは、フォームでファイルをアップロードするシチュエーションを考えましょう。
ファイルアップロードをする場合input要素は<input type="file" />
を使い、その親のform
要素には以下のようにenctype="multipart/form-data"
と書く必要があります。
<form action="URI" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> </form>
さて、このおまじないenctype="multipart/form-data"
はなぜ必要なのでしょうか?
Firebugを使ってリクエストを見る
今回はenctype
の有無によりHTTPリクエストのパラメータがどのように変化するかFirefoxのプラグインであるFirebugを使って調べてみようと思います。
なお、今回使用するhtmlのソースコードは
<html> <!-- enctype指定あり --> <form action="http://localhost:4567" method="post" enctype="multipart/form-data"> <input type="file" name="datafile" /> <input type="submit" name="submit_btn" /> </form> <!-- enctype指定なし --> <form action="http://localhost:4567" method="post"> <input type="file" name="datafile" /> <input type="submit" name="submit_btn" /> </form> </html>
で、送信するファイルは
hello world! hogehoge fugafuga
とします(添付ファイル名はsample.txtです)。
さて、それぞれを試したところFirebugは以下の画像のようになりました。
enctype指定あり
enctype指定なし
enctype指定ありでは、Content-Disposition
やContent-Type
など添付ファイルに関する情報とともに、ファイルの本文の情報が存在することが分かります。今回はテキストですが、画像や音声などバイナリの情報でも、同様にここにデータが格納されます。
また、--------
で始まる行があることも特徴的です。
enctype指定なしでは、filedata
にファイル名の情報のみが与えられていて、添付ファイルの情報は見当たりません。
どうも、inputエリアにファイル名のみを指定した
と同様な印象を受けます。
つまり、enctype="multipart/form-data"
を指定しない場合、添付ファイルの情報を送信できていないので、サーバー側では添付ファイルを扱えないという事になりそうです。
なお、-----------------------------146617270317...
のような区切り文字をboundary
と呼び、この区切りを使って複数のパートに分割することからマルチパートと呼ばれているようです。boundary
を使うと入れ子構造も再現できるようです、興味のある方は以下のリンクが詳しくまとまっているので御覧ください。
フォームよるファイルアップロードの仕様 - Jakarta Commons FileUploadの利用手順
メールの場合は
boundary
を使ってファイルの情報を送信するというのがmultipart/form-data
の仕組みでした。
しかし、冒頭で述べたようにメールにも同様のマルチパートという仕組みがあるようです。
では、これまで見てきたフォームのマルチパートはメールのマルチパートとどのような関係があるのでしょうか?
ここでは「Webを支える技術」の引用をさせていただきます。
HTTPの最初のバージョン0.9にはヘッダがありませんでした。HTTPの仕様策定が進められるに従って、HTTPで転送する本文のメタデータを表現するために電子メールのメッセージ仕様(RFC822)のヘッダ形式を借りてくる形で追加されました。
Webを支える技術 P.126
つまり本記事では、formつまりHTTPのマルチパートを先に紹介していたのですが時系列で言うとこれは逆で、HTTPの仕様策定時に電子メールの仕様を参考にしたというのが歴史的な流れのようです。マルチパートに限らず、HTTPの仕様が電子メールの仕様に似ているのはこのような経緯があるためなのですね。
なぜ電子メールの仕様にマルチパートが加わったのか(MIMEが策定されたのか)については
で詳しく解説されています。
ざっくりというとMIME策定前の仕様ではメールのヘッダ・本文がASCII限定で、日本語など非ASCIIの言語が正しく認識できなかったり、添付ファイルがうまく扱えなかったという問題があったようです。
multipart/alternative
:textとhtmlを同時送信
「マルチパート メール」などで検索してヒットする記事にはmultipart/alternative
に関する記事も多いようです。
multipart/alternative
はマルチパートの構造を利用して一つのメールにtext
版とhtml
版の2つの内容を含めて送信し、メーラーの設定次第でtext
かhtml
を選択して表示できるものです。
-----------------------------1948084979928559891542425288 はじめに こんにちはMUGENUPの倉成です。 -----------------------------1948084979928559891542425288 <h1>はじめに</h1> <b>こんにちは</b>MUGENUPの倉成です。 -----------------------------1948084979928559891542425288--
こんなメールを送ると、メーラーの設定次第でプレーンテキストでもリッチテキストでも表示できるようになるわけですね。
さいごに
enctype
を見るたびに「これ、なんだろうなぁー」と思っていたことから始まり、調べて行ったらインターネット成長の歴史的な経緯まで知ることができて、楽しい夏休みの自由研究となりました。
RFCの仕様も機会があればもう少し読んでみようと思います。
HTTPの仕様やRFC策定の歴史については山本陽平さんの「Webを支える技術」を参考にさせていただきました。 URI設計やステータスコード、JSON、HTTPメソッドの使い分けなど基礎的な情報が網羅されているので、特にWebプログラミングが初めてのエンジニアさんにおすすめの一冊です。
意外と簡単。HTML5のデスクトップ通知を実装してみる
こんにちは、MUGENUPの倉成です。 最近はWebアプリでもデスクトップ通知が出来るものが増えていますよね。 今日はそんなデスクトップ通知の実装を取り上げてみようと思います。
便利なライブラリ
デスクトップ通知はブラウザによって実装が異なり、各ブラウザの対応は手間がかかるので、今回はクロスブラウザ対応を簡単にできるHTML5-Desktop-Notificationsを使います。 他のデスクトップ通知のライブラリにはnotifyもあり、こちらもHTML5-Desktop-Notificationsと同じくらいのStarが付いているようです。
使い方
さて、ここからはHTML5-Desktop-Notificationsの使い方をREADMEにそって
- ブラウザ対応状況の確認
- ユーザーに通知の許可を求める
- 通知を発行
の3段階で説明していきたいと思います。
なお、本記事はこのコミット時のコードを対象にしており、今後の開発により変更が発生する場合があります。
また、HTML5-Desktop-NotificationsのサンプルコードはAngular.jsベースとなっており、馴染みのない方も多いと思うので、gistにサンプルコードを書いてみました、こちらも合わせて見ていただければと思います。
https://gist.github.com/kuranari-tm/e8d8b6411b90da10910e
Step1(ブラウザが通知に対応しているかチェック)
まずは以下のコードでブラウザがデスクトップ通知に対応しているか確認しましょう。 コードは
notify.isSupported // ブラウザが対応していればtrue, そうでなければfalse
です。なおnotify
はHTML5-Desktop-Notificationsで定義されているグローバル変数です。
Step2(ユーザーに通知を求める)
デスクトップ通知は、ユーザーから通知の許可をもらわなければ通知が発行できません。
次は、notify.permissionLevel()
でドメインに対する通知の許可状況をチェックします。
許可・拒否の状態は
notify.PERMISSION_DEFAULT // 通知が許可されていない notify.PERMISSION_GRANTED // 通知が許可されている notify.PERMISSION_DENIED // 通知が拒否されている
の3状態で、PERMISSION_DEFAULT
となっている場合は
notify.requestPermission()
で、ユーザーから通知の許可をもらいましょう。
DEFAULT
とDENIED
の違いですが、notify.requestPermission()
を実行時にDEFAULT
では上のような「デスクトップ通知の表示を許可しますか?」のメッセージが表示されますが、DENIED
ではこのメッセージは表示されず、ユーザーがブラウザの設定を変更するまでは通知機能を使用することが出来ません。
その場合、例えばChromeでは以下の箇所からドメインに対する通知の許可をユーザーに行ってもらわなければなりません。
通知の設定はドメイン単位で保存されるため、初回一度だけ許可をもらえば、その後は通知を自由に発行することが出来ます*1
Step3(通知を発行)
さて、Step2までで設定が終了したので、notify.createNotification
で通知を発行します。
notify.createNotification(String title [, Object options])
この関数を実行すると、MacのChrome(version.36)では以下のような表示がされます。
optionsに渡せるパラメータは
body, icon, tag, timeout
の4つです。
第一引数のtitle
、そしてオプションのbody
,icon
は上の通知画像を見れば、大体どこに対応するか分かるかと思います。ただし、icon
が必須パラメータになっていることには注意しましょう。
tag
はユニークな値を設定することで、複数のタブでページを開いていた場合に、開いているタブと同じ数の通知が発行される
ことを防ぐことが出来ます。
timeout
はREADMEには通知が閉じるまでの時間
を設定できると書いてありますが、README通りに設定を行っても求める挙動が実現できません。
一定時間経過後に自動で閉じる設定をするには
notify.config({autoClose: 1000}); // 1000[ミリ秒後]に通知を閉じる
とする必要があります。issueに上がっているようにみえるのですが、ちょっとハマりどころです。
なお、通知をクリックした時に何らかの処理をしたいといった処理は本家では実装されていませんが、Fork先では実装されているものもあるようなので、必要があればこちらを参考に機能を追加しても良いかもしれません。
終わりに
gistのサンプルコードでもjsは30行程度で、とても簡単にデスクトップ通知(しかもクロスブラウザ対応まで)を実装することが出来ました。
Webサービスにデスクトップ通知があることで使い勝手が格段に良くなるケースも少なく無いと思うので、意外と簡単に実装できてしまうデスクトップ通知機能、ぜひ一度使ってみてください!
おまけ:ローカルで通知が表示されない場合
セキリティ設定の影響でChromeなど幾つかのブラウザではサーバーを通さないローカルのjsが動作しないことがあります。 その場合は以下のサイトを参考に
$ python -m SimpleHTTPServer 8080
コマンド1つで今すぐWebサーバを起動させるためのワンライナー(Ruby or Python) - 元RX-7乗りの適当な日々
とし、ブラウザからlocalhost:8080
にアクセスすると、簡単にサーバー経由で動作を確認することが出来ます。
*1:逆に、一度でもDENIEDの状態にされるとブラウザの設定から許可状態にしてもらわなければならないので、やや面倒なことになります。
【Rails】after_createが発動するタイミングはいつでしょう?
MUGENUPの倉成です。
今回はRailsのCallbackであるafter_create
とafter_commit
の処理順番を改めて確認し、処理順番を誤解していた事によって僕が遭遇した問題ついて記事を書こうと思います。
シチュエーション
ブログ記事の新規投稿があった時、購読者に対してメールを送信する。
ブログの投稿時に通知のメールを送信するような場合、記事の投稿に必要な最低限の処理のみを行い、購読者へのメール送信などリアルタイムな処理が必要でないものは非同期で処理することで、レスポンス速度を向上させることが出来ます。
非同期処理を行うためのライブラリとしてはresqueやsidekiq、delayed_jobなど幾つかのものがありますが、弊社ではこの用途にresqueを使っているので、この記事では特にresqueを取り上げて説明します*1。
実装
さて、上の要件の実装として、僕は以下のようなコードを書きました。
# app/models/article.rb class Article < ActiveRecord::Base # 記事が作成されたらメール送信タスクをqueueに積む after_create do Resque.enqueue(SendMail, id) end end
# app/workers/send_mail.rb class SendMail @queue = :send_mail def self.perform(article_id) # 投稿された記事をDBから読み込む article = Article.find(article_id) # ブログの購読者にメールを送信 # .... # .... end end
self.perform(article_id)
の第一引数article_id
はResque.enqueue(SendMail, id)
の第二引数id
と対応しており、
新規記事のIDが渡されることになります。
さて、実際にアプリケーションを立ち上げて実行してみるとself.perform
内部で
article = Article.find(article_id)で稀にNoRecordFoundが発生する
という現象が起きていました。
after_create
という名のとおり記事は既にcreate
されていて、レコードは存在しているはずです。その証拠に既にidは与えられていますよね。
なぜでしょう。
after_createはDBへのコミット直前に実行される
不思議に思い、出力されたログを見てみると、ResqueからのSELECT
はBEGIN ~ COMMIT
の内部で実行されていました。
DBはINSERT
を実行しても、COMMIT
されるまでは、レコードは永続化されません。
つまりRailsがDBにCOMMIT
する前に、外部プロセスであるResque
のプロセスがSELECT
をしていたため、NoRecordFound
が発生していまうという状態になっていました。
BEGIN INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..') /* COMMIT前にResqueからのSELECTが発行される */ SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1 COMMIT
これは、after_create
がDBへのコミット直前
に実行されることが原因です*2。
after_commitを使う
原因がわかりました。
after_create
の代わりに、コミット直後
に実行されるafter_commit on: :create
を使いましょう。
これでCOMMIT
のあとにResqueの処理が実行されるため、NoRecordFound
が発生することもなくなりました。
BEGIN INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..') COMMIT /* COMMIT後にResqueからのSELECTが発行される */ SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1
BEGIN ~ COMMIT
の外側でSELECT
が実行されていることが、ログでも確認できます。
NoRecordFoundの原因は
稀にNoRecordFoundが発生する状態だったのは「COMMIT
の前に外部プロセスであるResqueのSELECT
が発生するケースがあったため」というのが結論です。なお常に発生する
では無いのは、Resqueが新規ジョブをポーリングする間隔により、Resqueの処理がCOMMIT
後に行われることもあったためです。
今回のようにRailsのcallback
をトリガーにして外部プロセスからDBにアクセスする場合はafter_commit
を使うようにしましょう。
なお,同様の注意点は http://apidock.com/rails/ActiveRecord/Callbacks/after_createにも書いてありました*3。
おわりに
今回は「after_create
はDBにCOMMIT
された後に呼び出される」という間違った認識によって遭遇した問題について記事を書かせていただきました。
ResqueなどRails外のプロセスを使う場合にこの記事を思い出していただけたら幸いです。
*1:今回は説明のため使用しませんがメール送信を非同期で行うならresque_mailerが便利です。
*2:コールバックの順番はこちらの記事にとても詳しくまとまっています http://techracho.bpsinc.jp/baba/2013_08_23/12670
初めてのVimプラグイン開発とMUGENUPエンジニアのエディタ事情
初めまして!MUGENUPアルバイトの倉成です。こちらで書く初めての記事として、今回は僕が初めて作ったVimプラグインとMUGENUPエンジニアのエディタ事情について紹介しようと思います。
僕自身はMUGENUPでアルバイトを始めてから、Emacs, SublimeText, RubyMineといろいろなエディタに手を出していましたが、現在はVimに落ち着いています。
もちろん、それぞれのエディタには一長一短があり「他で出来たことがこっちでは出来ない」というようなことはよくありますよね。
後置記法を支援するプラグイン
そんな中でも、RubyMineを使っていた時に便利だと思った「bodyが1行のif, unless, while, until文を後置記法に変換する*1」ことがVimではできず、軽く調べてもプラグインが見つからなかったので、自分で作ってみることにしました*2。
Github: https://github.com/kuranari-tm/backend_if
内部の処理は比較的簡単で、カーソル行が一行化・複数行化できるかを判定し、もし変換可能であれば正規表現等を使って置換を行っています。
このプラグインはif cond then
のようにthen
が入っているとうまく動かなかったり、コードを整形するのにnormal! 2k3==
と強引なことをやっていたりと、不十分な部分は多いのですが、普段の開発で使えるプラグインを開発できたのが何よりの成果でした。便利なプラグインを使うのももちろんよいですが、自給自足で必要な物を作る楽しみは何物にも代えがたいですよね。
if文の一行化・複数行化はよく行うと思うので、if文を後置化するのにddkPJ==jdd
などしている方、ぜひこのbackend_ifやsplitjoinを使ってみてください!
MUGENUPエンジニアのエディタ事情
さてさて、エンジニア同士の会話で「エディタ何使っていますか?」というのは鉄板ネタだと思うので、MUGENUPエンジニアのエディタ事情について少し紹介します。
現在MUGENUPでは8人のエンジニアで開発をしているのですが、使用エディタは
と圧倒的にSublime Textが人気です。
確かに、新人さんやIDEしか使ったことがない人がメンバーに加わる時には「とりあえずSublime使ってみよう!」ってなりますもんね。 学習コストが比較的低い割に、高機能で拡張性もあり、とてもよいエディタだと思います。
ということでSublime勢の勢力に押され、社内でVimを使っているのは少数派となってしまっているのですが、サーバー設定の場面などではVim(Vi)が使えたほうが何かと便利だったりするので、これからも少しずつ良さを広めていけたらなと思います。
おわりに
今回は僕が初めて作ったプラグインの紹介、そしてMUGENUPエンジニア陣のエディタ事情について書かせていただきました。
if文を一行化・複数行化する機能は初めてのプラグイン開発にちょうどいい難易度に感じたのでEmacsやSublime textなど他のエディタを使っている方も同様なプラグインを作ってみてはいかがでしょうか。
GithubへのPull Requestもお待ちしています!
Yammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた
皆さん、こんにちは。MUGENUP の osada です。
今回のテーマは、
の3点です。
読者ターゲットは、
画像は S3 に置きたいけど、管理はしたい
人や、
<input type="file"> を使わずに画像をPOSTしたい
人です。
注: 一部画像にモザイクを掛けてあります。copyrightを守ります
Yammer から GitHub に
開発部では、社内コミニケーションツールとして、Yammer
を使っています。
Yammer は社内専用の Facebook のようなSNSです。
そこに LGTM な画像置き場を作っているのですが、Yammer の URL をそのまま GitHub に投稿しても表示できません。
Yammer は会員制でクローズドなので GitHub はアクセスすることができないからです。
そこで今までは、
- yammer から ローカルPC にダウンロード
- ローカルPC から GitHub にアップロード
という2手順を踏んで、LGTM に使っていました。
しかしこの手順は意外と面倒くさい!
結局、LGTM.in
と拡張機能 LGTMでめでたさを伝えるChrome拡張をつくった - Thinking-megane を使ってしまい、
このままでは Yammer は、ただの画像置き場になってしまう!
ということで、今回、Chrome 拡張を作ることにしました。
Chrome拡張の動作
動作は大きく3つに分かれます
Yammer API
で、投稿を取得(JSON
)し、サムネイル画像をレンダリングする- 選択したサムネイル画像を、
BLOB形式
でダウンロードする - 画像を
GitHub
を経由してS3にアップロードする
鍵となるのは、下記の2点です。
- 画像を BLOB として扱うこと
- GitHub は画像を3段階でアップロードすること
では、1つずつ解説していきます。
Chrome 拡張の構造
と、その前に、Chrome 拡張の構造を解説します。
この Chrome 拡張は、下記の3つから成り立っています。
- chrome拡張を定義する、
manifest.json
- chrome拡張側の、アイコンをクリックしてポップアップする、
popup.html
とyammer.js
。 - 表示中のページで読み込まれる
content.js
chrome 拡張と、表示中のタブ間は、完全に独立しているため、 yammer.js と content.js は、メッセージで通信することになります。
Yammer API
で、投稿を取得(JSON
)し、サムネイル画像をレンダリングする
chrome 拡張側、アイコンをクリックすると、popup.html を開き、yammer.js を読み込んで投稿を取得します。
function loadAndAppendGroupImages($body, groupID){ var url = "https://www.yammer.com/api/v1/messages/in_group/"+ groupID + ".json"; $.getJSON(url) .done(function(data, status, xhr){ var images = []; data.messages.forEach(function(message){ if(message.attachments){ message.attachments.forEach(function(attachment){ if(image = createImage(attachment)){ images.push(image); } }); } }); var image_tags = []; images.forEach(function(image){ image_tags.push(toHTML(image)); }); $body.replaceWith(image_tags); }) .fail(function(xhr, status){ $body.replaceWith("<div style='width: 200px'>読み取れません。ログインしていないか、ネットワークが違います</div>"); }); }
この json は、messages の array が返ってきます。
この中で画像の情報は attachments
に含まれています。
ここから、サムネイルURLとダウンロードURLを取り出して、popup.html にレンダリングします。
なお、createImage
と toHTML
は、取り出しと、HTML整形です。
function createImage(attachment){ return { thumbnail_url: attachment.thumbnail_url, download_url: attachment.download_url }; } function toHTML(image){ return "<a href='" + image.download_url + "'>" + "<img src='" + image.thumbnail_url + "'>" + "</a>" + "<br>"; }
{ "threaded_extended": {}, "messages": [ { "id": , ...... "attachments": [ { "id": xxxxx, "url": "https://www.yammer.com/api/v1/uploaded_files/xxxxx", "web_url": "https://www.yammer.com/mugenup.com/uploaded_files/xxxxx", "type": "image", "name": "xxxx.gif", "original_name": "xxxx.gif", "full_name": "xxxx", "description": "", "content_type": "image/gif", "small_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_39x50_icon.png", "large_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_79x102_icon.png", "download_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/download", "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail", "preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif", "large_preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/large_preview/xxxx.gif", "size": 1014474, ...... "height": 278, "width": 500, "scaled_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/scaled/{{width}}x{{height}}", "image": { "url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif", "size": 1014474, "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail" }, } ], },......
選択したサムネイル画像を、BLOB形式
でダウンロードする
popup.html
に表示された画像一覧から、画像をクリックしたとき、
download_url
を使って、画像をダウンロードします。
このダウンロード動作は拡張側ではなく、ページ側で行うため、download_url
を tab
側に メッセージとして送信します。
// 画像がクリックされたとき、ダウンロードURLを、コンテンツスクリプトに委譲 $(document).on("click", "a", function(e){ $this = $(this); $this.replaceWith("<img src='./imgs/loading.gif'>"); var download_url = $this.attr("href"); chrome.tabs.getSelected(null, function(tab) { chrome.tabs.sendRequest( tab.id, { download_url: download_url }, function(response){ window.close(); }); }); });
コンテンツスクリプト側では、addListener
を使い、
メッセージをキャッチします。
chrome.extension.onRequest.addListener( function(request, sender, sendResponse) { var download_url = request.download_url; xhr = new XMLHttpRequest(); xhr.open('get', download_url, true); xhr.responseType = 'blob'; xhr.onload = function(){ if(this.status == 200){ var blob = this.response; var ContentDisposition = xhr.getResponseHeader("Content-Disposition"); blob.filename = ContentDisposition.match(/filename="([^"]+)"/)[1]; postGithub(blob); } }; xhr.send(); } );
ここで大事なのは、画像としてダウンロードすることです。
よって、xhr.responseType = 'blob';
を指定しています。
blob
というのは、バイナリ・ラージ・オブジェクトのことであり、コレを使うことで、response
をバイナリとして扱うことができます。
(ここだけ、jQuery
ではなく、XMLHttpRequest
を使っているのは、
jQuery で responseType
を指定する方法が不明だったためです)
画像をGitHub
を経由してS3にアップロードする
GitHub の画像のアップロードは、大変優れた方法です。 GitHub側では、レコードを管理し、大きなバイナリデータはS3に直接アップロードさせます。
(ご存じの方は3ウェイ・ハンドシェイク を 思い浮かべていただければ、わかりやすいと思います。)
GitHub へのアップロードは下記の動作で実行されます。
1つずつ解説します
1. GitHub に post する
name
, size
, content_type
を GitHub に post します。
function postGithub(blob){ var formData = new FormData(); formData.append("name", blob.filename); formData.append("size", blob.size); formData.append("content_type", blob.type); var url = "https://github.com/upload/policies/assets"; $.ajax({ url: url, type: "post", headers: {"X-CSRF-Token": csrf}, data: formData, processData: false, contentType: false }).done(function(data){ postS3(data, blob); }); }
ここでは、ファイル自体は post していないことに注目してください
2. GitHub は、S3 へのアップロードに必要な情報を返す
すると、GitHub は、S3へのアップロードに必要な情報を返してきます。
(一部の箇所は、xxxxx
で隠しています)
{ "upload_url": "https://s3.amazonaws.com/github-cloud", "header": {}, "asset": { "id": xxxxx, "name": "xxxxx.jpg", "size": 59192, "content_type": "image/jpeg", "href": "https://cloud.githubusercontent.com/assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg", "original_name": "xxxxx.jpg" }, "form": { "key": "assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg", "AWSAccessKeyId": "xxxxx", "acl": "public-read", "policy": "xxxxx", "signature": "xxxxx", "Content-Type": "image/jpeg", "Cache-Control": "max-age=31557600", "x-amz-meta-Surrogate-Control": "max-age=31557600", "x-amz-meta-Surrogate-Key": "user-xxxxx" }, "asset_upload_url": "/upload/assets/xxxxx" }
3. S3 にアップロードする
返された json の、form
に含まれるデータと、
実際の画像データのBLOB
データを formData
化して、
S3にアップロードします。
function postS3(data, blob){ var formData = new FormData(); for ( var key in data.form ) { formData.append(key, data.form[key]); } formData.append("file", blob); var asset_upload_url = data.asset_upload_url; $.ajax({ url: data.upload_url, type: "post", headers: {"X-CSRF-Token": csrf}, data: formData, processData: false, contentType: false }).done(function(data){ putGitHub(data, asset_upload_url); }); }
4. GitHub に put して、データを更新する(?)
実は、この put がなくても、url にアクセスすれば、画像は見られます。 よって、この put が何のデータを更新しているのか不明なのですが、 何かのデータを更新しているに違いありません(!?)。
(個人的には、S3へのアップロードが完了したフラグを更新しているのではないかと思っています)
function putGithub(data, asset_upload_url){ $.ajax({ url: asset_upload_url, type: "put", headers: {"X-CSRF-Token": csrf}, processData: false, contentType: false }).done(function(data, status, xhr){ writeLGTM(data); }); }
そして、最後に、アップロードされた画像データのURLを textarea に貼り付けて終了です。
function writeLGTM(data){ var lgtm = "![LGTM](" + data.href + ")"; var oldMessage = $("textarea[name='comment[body]']").val(); if(oldMessage != ""){ lgtm = oldMessage + "\n" + lgtm; } $("textarea[name='comment[body]']").val(lgtm); sendResponse({}); }
画像をアップロード、というと、一回のPOSTで全部処理してしまいそうですが、 RESTful を遵守することで、3アクションにはなっていますが、シンプルな構成になっています。 さすが GitHub といったところでしょうか。
まとめ
さて今回はYammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた
というテーマでした。
動作は概要図を再掲します。
今回わかったことは、下記の3点です。
- Chrome 拡張 は表示中のページに介入できる
- ajax で画像を扱うときは、
xhr.responseType = 'blob';
を使うと便利 - GitHub の画像アップロードは、3アクションの
RESTful
で美しい
まず、Chrome 拡張 は表示中のページに介入できる
というのは、本当に怖いことです。
yammer
や、github
へのログイン認証についての、説明が無いことに気づかれたかもしれません。
Chrome拡張はブラウザのセッションを使うことができるので、ログインさえしていれば、拡張側では処理が必要ないのです。
自分がログインしているサービスに対して、バックグラウンドで、Chrome拡張が通信することができてしまいます。
例えば、Facebook
にログインしている Chrome に、勝手に投稿させる
という javascript は簡単に書くことができそうです。野良拡張には本当に気をつけましょう。
気をつけ方としては、manifest.json
の "permissions":
を確認することです。
ここに記載があるサイトに関して、Chrome拡張は権限を持つので、オカシイな?と思ったら、使うのを止めましょう(そもそも使わないですよね)。
(一番安心なのは、ソースコードを全部読んでしまうことです。unzip で解凍することができます)
unzip hoge.crx
次に、responseType ですが、普段ajax
はjQuery
を使うことが多いため、今回初めて知りました。xhr.responseType
を使うと、返り値を制御するとができますが、jQuery ではどうやると良いのでしょうか?
最後に、GitHub の画像の扱いはスマートでした。DBとストレージが異なる、というのは、昨今当たり前なので、大変参考になりました。皆さんもこのように実装されてはいかがでしょうか?(もうやってる?)
ということで、Chrome拡張を作ってみた、というお話でした。
拡張は、html+css+js なので、WEBエンジニアならとても簡単に作ることができます。是非、作ってみることをおすすめします。きっと新しい発見があると思います。