RSpec+Capybaraによるチェックボックスの存在確認とCookieの有効期限日を取得する方法

Ruby on Rails Tutorial*19.6 Exercisesを終えた後、"Remember me"というチェックボックスSign inページに追加しようと思いました。

この本で作成するマイクロブログSample App」は、ログイン後、Cookieに保存する"remember_token"の有効期限日を20年後*2に設定しています*3。[Remember me]チェックボックスをチェックせずに[Sign in]ボタンを押下した場合、このクッキーの有効期限日をセッション終了時になるよう改修してみました。

Sample Appは、RSpec+CapybaraによるTDDで開発されていますので、私もそれに従います。

[追記: 8/29 23:19]
第3版の第8章で[Remember me]チェックボックスを追加するそうです。本記事は、第3版が出版される以前に書いたもので、関係はありません。
*4

続きを読む

Solrのコアに写真の撮影日をTika経由で登録する

Apache Solrのコアに、Exifで記録された撮影日をTika経由で登録してみました。Solrのバージョンは、4.9.0です。

solrconfig.xml

uprefixの「ignored_」が、ここで設定しなかった撮影情報の接頭語になります。

<requestHandler name="/update/extract"
                startup="lazy"
                class="solr.extraction.ExtractingRequestHandler" >
  <lst name="defaults">
    <str name="lowernames">true</str>
    <str name="uprefix">ignored_</str>
    <str name="captureAttr">true</str>
    <str name="fmap.date_time_original">date_time_original</str>
  </lst>
  <lst name="date.formats">
    <str>yyyy:MM:dd HH:mm:ss</str>
  </lst>
</requestHandler>

最後の「date.formats」は、Tikaが撮影情報を読み取ると「2014:07:16 21:19:47」という形式でSolrに返します。しかし、この日時形式は標準ではサポートされていない為、日時の書式を追加しました。追加しなかった場合、写真をPOSTすると以下のエラーがSolrから返ってきます。

<lst name="responseHeader">
  <int name="status">400</int>
  <int name="QTime">1455</int>
</lst>
<lst name="error">
  <str name="msg">Invalid Date String:'2014:07:16 21:19:47'</str>
  <int name="code">400</int>
</lst>

schema.xml

コアのスキーマに、撮影日(DateTimeOriginal)のフィールドを追加しました。ダイナミックフィールドの「ignored_*」は、スキーマに定義されていない撮影情報に関する設定です。

<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false"/>
<field name="date_time_original" type="tdate" indexed="true" stored="true"/>
<dynamicField name="ignored_*" type="string" indexed="false" stored="false" multiValued="true"/>
<uniqueKey>id</uniqueKey>

写真のアップロード

curlコマンドで写真をアップロードしてテストしました。

$ curl 'http://127.0.0.1:8983/solr/exif/update/extract?literal.id=1&commit=true' -F "myFile=@DSCN0005.jpg"
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader"><int name="status">0</int><int name="QTime">1297</int></lst>
</response>

結果

Solrの管理画面で、撮影日がdate_time_originalフィールドに追加されているのを確認しました。


余談

日付型のフィールドに撮影日を登録する方法で悩んだので記事にしました。日付型にしたかったのは、撮影日でファセット検索したかったからです。

テーブルのヘッダと左列を固定するプラグイン

fixedTblHdrLftColというjQueryプラグインを作りました。 これは、テーブルのヘッダ(THEAD要素)と左列(固定するTD要素数を指定)を固定します。

仕様

  • jQuery v2.1.1+
  • ヘッダはTHEAD要素内、内容はTBODY要素内。

設定

https://raw.github.com/nkmrshn/fixedTblHdrLftCol/master/samples/sample1.png

Option 1 Option 2 Option 3 既定値 データ型 備考
scroll {} height null String スクロール領域の高さ
width null String スクロール領域の幅
headRow {} className 'fTHLC-head-row' String 固定したTH要素のクラス名
enabled true Boolean スクロールイベントの有効・無効
overflow 'auto' String CSSのoverflowプロパティ
leftCol {} className 'fTHLC-left-col' String 固定したTD要素のクラス名
enabled true Boolean スクロールイベントの有効・無効
overflow 'auto' String CSSのoverflowプロパティ
fixedSpan 1 Number 固定する左列の数
syncWith null String 同期するテーブルのjQueryセレクタ
wrapper {} outer {} idName null String 外側DIV要素のID名
className 'fTHLC-outer-wrapper' String 外側DIV要素のクラス名
inner {} idName null String 内側DIV要素のID名
className 'fTHLC-inner-wrapper' String 内側DIV要素のクラス名
corner {} append true Boolean 左上の角を追加
deepClone false Boolean ディープクローンする
outer {} idName null String 外側DIV要素のID名
className 'fTHLC-outer-corner' String 外側DIV要素のクラス名
inner {} idName null String 内側DIV要素のID名
className 'fTHLC-inner-corner' String 内側DIV要素のクラス名
左上の角

corner.appendedオプションをfalseに設定して、テーブルをスクロールさせると、左上の角がこのようになります。

https://raw.github.com/nkmrshn/fixedTblHdrLftCol/master/samples/sample2.png

このオプションがtrueの場合、プラグインは自動的に左上の角をクローンして、テーブルの最上に追加します。もし、このプラグインでテーブルを設定する前に、この部分にイベントハンドラをバインドしている場合は、corner.deepCloneオプションにtrueを設定してください。

スクロールイベントの同期

スクロールイベントを同期したいテーブルが2つ以上ある場合、scroll.syncWithオプションにjQueryセレクタを設定してください。

ウェブブラウザ

動作を確認したブラウザの一覧です。

未対応

COLSPAN属性やROWSPAN属性には対応していません。

サンプル

HTML5/CSS3で書いています。jQuery CDNYUI CSS Reset CDNを使っています。

  1. Basic
  2. Multi-fixed 1
  3. Multi-fixed 2
  4. Syncronize scroll events

ライセンス

MIT licenceでリリースしました。

余談

GitHubで公開し、jQuery Plugin Registryに登録したのは、20日以上も前の事なのですが、やっと記事を追加できる時間ができたので、READMEの日本語版を書いてみました。

Ruby on Rails Tutorialのsample_appをさくらVPSにインストールした手順

Ruby on Rails Tutorialで作る「sample_app」をさくらVPSにインストールしました。これは、その備忘録です。

構築した環境は、CentOS 6.4+Apache Httpd 2.2.15-28+MySQL 5.1.69をベースに、rvm 1.20.13+Ruby 1.9.3+Passenger 4.0.5+Rails 3.2.13です。

複数のRailsアプリが、同一バージョンのRubyで稼働するものとして、Passengerは「global」にインストールし、アプリ毎にgemsetを用意するという方針にしました。

謝辞

インストールにあたって、以下のサイトを参考にさせていただき、誠にありがとうございました。

前提

Apache HttpdMySQLは、yum installでインストールし、設定済みです。

この順番で実行したかと思います。申し訳ありませんが、試行錯誤した結果なので、記載漏れなどがあるかもしれません。

rvmとRubyのインストール

$ sudo bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer )
$ sudo usermod -a -G rvm root
$ sudo gpasswd -a [USER_NAME] rvm
$ sudo gpasswd -a apache rvm
$ source /etc/profile.d/rvm.sh
$ su
# rvm get head
# exit
(この後、一旦、再起動させたと思います。)
$ sudo reboot
$ rvm install 1.9.3
$ gem install bundler --no-ri --no-rdoc

Passengerのインストール

$ vi .gemrc
gem: --no-rdoc --no-ri
$ rvm gemset use global
$ gem install passenger
$ passenger-install-apache2-module
$ sudo vi /etc/httpd/conf.d/passenger.conf
LoadModule passenger_module /usr/local/rvm/gems/ruby-1.9.3-p429@global/gems/passenger-4.0.5/libout/apache2/mod_passenger.so
PassengerRoot /usr/local/rvm/gems/ruby-1.9.3-p429@global/gems/passenger-4.0.5
PassengerDefaultRuby /usr/local/rvm/wrappers/ruby-1.9.3-p429@global/ruby

sample_appのインストール

sample_appで必要なgemは、「sample」というgemsetにインストールしています。

$ git clone git@github.com:[USER_NAME]/sample_app.git
$ cd sample_app
$ vi Gemfile
gem 'execjs'
gem 'therubyracer'

group :production do
  gem 'mysql2'
end
$ rvm gemset create sample
$ rvm gemset use sample --default
$ bundle install
$ vi config/setup_load_paths.rb
if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
  begin
    gems_path = ENV['MY_RUBY_HOME'].split(/@/)[0].sub(/rubies/, 'gems')
    ENV['GEM_PATH'] = "#{gems_path}:#{gems_path}@global"
    require 'rvm'
    RVM.use_from_path! File.dirname(File.dirname(__FILE__))
  rescue LoadError
    # RVM is unavailable at this point.
    raise "RVM ruby lib is currently unavailable."
  end
end
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'
$ vi .ruby-version
1.9.3-p429
$ vi .ruby-gemset
sample
$ mkdir tmp
$ sudo chown apache:apache tmp
$ touch log/production.log
$ sudo chmod 0666 log/production.log
$ rake db:create RAILS_ENV=production
$ rake db:migrate RAILS_ENV=production
$ sudo rake assets:precompile RAILS_ENV=production

httpd.confの設定

VirtualHostの設定を既存のhttpd.confに追加しました。

一般公開するわけではありませんので、Digest認証をかけています。

また「/home/[USER_NAME]」ディレクトリは、予め「sudo chmod o+r /home/[USER_NAME]」して、apacheユーザが読めるようにしています。

$ cd /var/www
$ sudo ln -s /home/[USER_NAME]/sample_app sample_app
$ sudo vi /etc/httpd/conf/httpd.conf
<VirtualHost *:80>
    ServerName [FQDN]
    RackEnv production
    DocumentRoot /var/www/sample_app/public
    ErrorLog /var/log/httpd/sample_app.error_log
    CustomLog /var/log/httpd/sample_app.access_log combined
    <Directory /var/www/sample_app/public>
        AuthType Digest
        AuthName "Auth Zone"
        AuthUserFile /etc/httpd/conf/.htdigest
        Require valid-user
        Options -Indexes -MultiViews
        AllowOverride all
    </Directory>
</VirtualHost>

[追記:7/1 19:08]
Apachehttpd.confで「KeepAlive On」に変更。

[追記: 7/2 0:19]
さらに、「Header always unset X-Powered-By」を追加。

httpdの再起動

$ service httpd configtest
Syntax OK
$ sudo service httpd restart

余談

インストール中につまずいた箇所。

  1. gemsetを「global」から「sample」に変更する方法がよくわからず丸2日悩む。→「config/setup_load_paths.rb」を作成して対処。
  2. assetsのprecompleに「execjs」などがGemfileに必要だったこと。

GitHubにpushしたら、自動的にテストして、問題なければ本番環境にデプロイする仕組みを入れたい。Jenkins CIにチャレンジしてみようかな...

[追記: 8/20 13:42]
その後、Jenkins CIを入れてみたり、nginx+unicornに変えてみるなど、さくらVPSの1Gプラン(月980円)でかなり遊ばせてもらってます。

exif_thumbnailを作った理由と実験

libthumbを作ったのは、『サムネイルの一覧を表示する際、Exifの全タグを読み込む必要はないのでは?』という単純な思いつきからでした。

アプリがネイティブであろうがウェブであろうが、サムネイルの一覧はできるだけ早く表示し、後でユーザがサムネイルをクリックしたら、その画像ファイルのExifのタグを改めて読み込めばいい。

また、libthumbのRuby拡張ライブラリ、exif_thumbnailを作ったのは、Ruby on RailsSinatraなどでlibthumbを使いたかったからです。

そこで、exifrexif_thumbnailをRubyBenchmarkモジュールで比較してみました。

前提

ソース

第一引数で指定したExifファイルからサムネイルを取り出し、それを第二引数で指定したファイル名で保存するプログラムです。test.rbとして保存し、実行しました。

require 'benchmark'
require 'exifr'
require 'exif_thumbnail'

def use_exifr
  File.binwrite(ARGV[1], EXIFR::JPEG.new(ARGV[0]).thumbnail);
end

def use_exif_thumbnail
  File.binwrite(ARGV[1], EXIFThumbnail.read(ARGV[0]).data)
end

n = 1000

Benchmark.bm(17) do |x|
  x.report("exifr : ") { for i in 1..n; use_exifr; end }
  x.report("exif_thumbnail : ") { for i in 1..n; use_exif_thumbnail; end }
end

結果

$ ruby test.rb /Users/nkmrshn/Pictures/FOOD.JPG /Users/nkmrshn/Desktop/Thumbnail.jpg
                        user     system      total        real
exifr :             1.040000   0.080000   1.120000 (  1.116702)
exif_thumbnail :    0.030000   0.080000   0.110000 (  0.163770)

考察

systemは変わらないのに、userを見るとexifrの方が遅いです。これは、Exifのタグをexifrは全て読み込んでいるのが原因かは、このテストではわかりません。しかし、サムネイルだけ*1を取得したい場合は、exif_thumbnailの方が早いことがわかりました。

余談

これは、愚考だ。そもそも、サムネイルが必要なら画像ファイルをサーバなどに保存する時点で、サムネイルを抽出・保存しておけばいいだけの話ではないか。

*1:サムネイル(data)以外にサムネイルサイズ(length)と回転方向(orientation)も取得しています。

libthumbのRuby拡張ライブラリ

Exifファイルのサムネイルを取得するライブラリlibthumb)をRubyで使いたいと思い、拡張ライブラリを作ってみました。

gemファイル

exif_thumbnail-0.0.1.gem

実行例

これは、ruby 1.9.3で実行した例です。

$ irb
> require 'exif_thumbnail'
=> true 
> foo = EXIFThumbnail.read("/Users/nkmrshn/Pictures/FOOD.JPG")
=> # 
> foo.length
=> 12107 
> foo.orientation
=> 6 
> File.binwrite("/Users/nkmrshn/Desktop/Thumbnail.jpg", foo.data)
=> 12107 
>

余談

Rubyの拡張ライブラリやgemを作るのはこれが初めてで、いろいろ調べてやっと動くものができました。

疑問点も多々あり、勉強不足です。例えば、unsigned char *をRubyに返すには、なんでchar *にキャストして、rb_str_newで変換すればよいのか、またこの方法で良いのかも含め、よくわかっていません。

Exifファイルのサムネイルを取得するライブラリ

指定したExifファイルから、サムネイルを取得するライブラリを作ってみました。サムネイルのサイズと回転方向も取得します。

余談

先日作った、libdtorgと連携させて使うと、ディレクトリにあるExifファイルのサムネイルを取得する場面などで使えます。アプリを作りたいなぁ...

Exifの勉強がしたくてlibdtorgやlibthumbを作り始めましたが、『C言語 ポインタ完全制覇』(技術評論社, 2012, 前橋和弥著)を読みながらライブラリを作っていたので、ポインタについて曖昧に覚えていたことなどが整理できて良かったです。

ニコンのMakernoteにあるサムネイルを取得できるようにしようかなぁ...