Rails 4.1 で validates_timeliness を使う


Rails 4.1 で動く日付時刻用のバリデータを探していたところ、 adzap/validates_timeliness の fork である johncarney/validates_timeliness が Rails 4 に対応しているとのことなので試してみた。

jc-validates_timeliness | RubyGems.org | your community gem host
https://rubygems.org/gems/jc-validates_timeliness

johncarney/validates_timeliness
https://github.com/johncarney/validates_timeliness

### 環境

```shell-session
% bundle exec rake about
About your application's environment
Ruby version              2.1.3-p242 (x86_64-linux)
RubyGems version          2.2.2
Rack version              1.5
Rails version             4.1.6
JavaScript Runtime        Node.js (V8)
Active Record version     4.1.6
Action Pack version       4.1.6
Action View version       4.1.6
Action Mailer version     4.1.6
Active Support version    4.1.6
```

### 使ってみる

まずは適当なプロジェクトを作成。

```shell-session
% rails new blog --skip-bundle && cd blog && vim Gemfile
```

Gemfile に以下を追加

```ruby:Gemfile
gem 'jc-validates_timeliness'
```

追記したら `bundle install` を実行。

```shell-session
% bundle install
```

必要なファイルを生成。

```shell-session
% bundle exec rails g validates_timeliness:install
```

これで以下の設定ファイルが作成される。

- config/initializers/validates_timeliness.rb
- config/locales/validates_timeliness.en.yml

エラーメッセージを日本語で表示するために locale ファイルをコピー。

```shell-session
% cp config/locales/validates_timeliness.en.yml config/locales/validates_timeliness.ja.yml
```

コピーした validates_timeliness.ja.yml を以下のように変更した。

```yaml:validates_timeliness.ja.yml
ja:
  errors:
    messages:
      invalid_date: "は正しい形式で入力してください。"
      invalid_time: "は正しい形式で入力してください。"
      invalid_datetime: "は正しい形式で入力してください。"
      is_at: "は %{restriction} である必要があります。"
      before: "は %{restriction} より前を指定してください。"
      on_or_before: "は %{restriction} 以前を指定してください。"
      after: "は %{restriction} より後を指定してください。"
      on_or_after: "は %{restriction} 以降を指定してください。"
  validates_timeliness:
    error_value_formats:
      date: '%Y-%m-%d'
      time: '%H:%M'
      datetime: '%Y-%m-%d %H:%M'
```

error_value_formats はバリデーションエラー時に画面上に表示される日付のフォーマットを指定する。
ここでは秒は不要だろ、ということで `%S` を除去した。

せっかく日本語リソースを作成したのでついでに config/application.rb にも以下を追記。

```ruby:config/application.rb
config.active_record.default_timezone = :local
config.time_zone = 'Tokyo' 
config.i18n.default_locale = :ja
```

これだけだと日本語リソースがなくてエラーになるので以下も落としてきておく。

```shell-session
% wget -O config/locales/ja.yml https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml
```

適当な scaffold を作成してマイグレーション実行。

```shell-session
% bundle exec rails g scaffold post title:string body:text published_at:datetime closed_at:datetime
% bundle exec rake db:create db:migrate
```

model クラスに以下のような感じでバリデーションを追加。

```ruby:app/models/post.rb
class Post < ActiveRecord::Base
  validates :published_at, timeliness: { on_or_after: :now,    format: '%Y/%m/%d %H:%M' }, allow_blank: false
  validates :closed_at,    timeliness: { after: :published_at, format: '%Y/%m/%d %H:%M' }, allow_blank: false
end
```
lib/validates_timeliness/extensions.rb がまだ 4.1 に完全に対応していないのか、このままだと datetime_select を使うために `config.enable_date_time_select_extension!` や ` config.enable_multiparameter_extension!` を指定した場合にエラーになる。

仕方ないので、View を編集して text_field に変える。
入力はなんか適当な datepicker ライブラリを使うので取り敢えずはこれで良しとする。

```erb:app/views/posts/_form.html.erb
<div class="field">
  <%= f.label :published_at %><br>
  <%= f.text_field :published_at %>
</div>
<div class="field">
  <%= f.label :closed_at %><br>
  <%= f.text_field :closed_at %>
</div>
```

上記の設定で

- 日付のフォーマットは秒を除いた `%Y/%m/%d %H:%M` (ex. 2014/11/08 10:30)
- published_at は現在日時以降
- closed_at は published_at より後

という条件のバリデーションが設定される。

なんか意外と正式に 4 に対応しているものが見つけられなかったんだけど、定番ってないのだろうか。

### 参考

[Ruby on Rails] 日付・時刻のバリデーションなら「ValidatesTimeliness」 | CodeNote.net
http://codenote.net/ruby/rails/1797.html

validation - In Rails, how can I validate two date attributes on a comparison basis with validates_timeliness gem? - Stack Overflow
http://stackoverflow.com/questions/13651966/in-rails-how-can-i-validate-two-date-attributes-on-a-comparison-basis-with-vali

validation - Testing that rails model validates date with RSpec - Stack Overflow
http://stackoverflow.com/questions/13138208/testing-that-rails-model-validates-date-with-rspec