I'm trying to get a text field that my users can ente开发者_开发百科r in something that is parsable by the Chronic gem. Here is my model file:
require 'chronic'
class Event < ActiveRecord::Base
belongs_to :user
validates_presence_of :e_time
before_validation :parse_date
def parse_date
self.e_time = Chronic.parse(self.e_time_before_type_cast) if self.e_time_before_type_cast
end
end
I think that it is being called because if I misspell something in parse_date, it complains that it doesn't exist. I've also tried before_save :parse_date, but that doesn't work either.
How could I get this to work?
Thanks
This kind of situation looks like a good candidate for using virtual attributes in your Event
model to represent the natural language dates and times for the view's purpose while the real attribute is backed to the database. The general technique is described in this screencast
So you might have in your model:
class Event < ActiveRecord::Base
validates_presence_of :e_time
def chronic_e_time
self.e_time // Or whatever way you want to represent this
end
def chronic_e_time=(s)
self.e_time = Chronic.parse(s) if s
end
end
And in your view:
<% form_for @event do |f| %>
<% f.text_field :chronic_e_time %>
<% end %>
If the parse fails, then e_time
will remain nil
and your validation will stop the record being saved.
Building on what @bjg did, here's a working solution you can drop in config/initializers/active_record_extend.rb
module ActiveRecord
class Base
# Defines natural language getters/setters for date/time fields.
#
# chronic_attr :published_at
#
# ...will get you c_published_at & c_published_at=
def self.chronic_attr(*arguments)
arguments.each do |arg|
define_method "c_#{arg}=".to_sym do |dt|
self[arg] = Chronic::parse(dt)
end
define_method "c_#{arg}".to_sym do
if self[arg]
self[arg].to_s(:picker)
else
''
end
end
end
end
end
end
I know monkey-patching is passe these days but I think it is the most straight forward way to integrate Ruby, Rails and Chronic. I put this gist in my initializer:
# https://gist.github.com/eric1234/3739149
#
# Mass monkey-patching! Provides integration between Chronic, Ruby and
# Rails. So now these all work:
#
# Date.parse "next summer"
# DateTime.parse "in 3 hours"
# Time.parse "3 months ago saturday at 5:00 pm"
#
# In addition we override String#to_date, String#to_datetime, String#to_time.
# These methods are used by older version of ActiveRecord when parsing time.
# For newer versions of ActiveRecord, Date::_parse is overridden to also
# use Chronic. This means you can assign a simple string to a ActiveRecord
# attribute:
#
# my_obj.starts_at = "thursday last week"
#
# Also since the String method are redefined you can easily create dates
# from strings. For example if you want tomorrow at 2pm you can just do:
#
# 'tomorrow at 2pm'.to_time
#
# This is more readable than the following IMHO:
#
# 1.day.from_now.change hour: 14
module Chronic::Extensions
module String
def to_date
parsed = Chronic::Extensions.safe_parse self
return parsed.to_date if parsed
super
end
def to_datetime
parsed = Chronic::Extensions.safe_parse self
return parsed.to_datetime if parsed
super
end
def to_time
parsed = Chronic::Extensions.safe_parse self
return parsed.to_time if parsed
super
end
end
::String.prepend String
module DateTime
def parse datetime, *args
parsed = Chronic::Extensions.safe_parse datetime
return parsed.to_datetime if parsed
super
end
end
::DateTime.singleton_class.prepend DateTime
module Date
def _parse date, *args
parsed = Chronic::Extensions.safe_parse(date).try :to_datetime
if parsed
%i(year mon mday hour min sec sec_fraction offset).inject({}) do |result, fld|
value = case fld
when :offset then (parsed.offset * 86400).to_i
else parsed.public_send fld
end
result[fld] = value if value && value != 0
result
end
else
super
end
end
def parse date, *args
parsed = Chronic::Extensions.safe_parse date
return parsed.to_date if parsed
super
end
end
::Date.singleton_class.prepend Date
module Time
def parse time, now=self.now
parsed = Chronic::Extensions.safe_parse time, now: now
return parsed if parsed
super
end
def zone
super.tap do |cur|
Chronic.time_class = cur
end
end
def zone= timezone
super.tap do
Chronic.time_class = zone
end
end
end
::Time.singleton_class.prepend Time
def self.safe_parse value, options={}
without_recursion { Chronic.parse value, options }
end
# There are cases where Chronic actually uses the Ruby date/time libraries.
# This leads to infinate recursion as our monkey-patch will intercept the
# built-in libraries to hand off to Chronic which in turn hands back to the
# built-in libraries.
#
# To avoid this we have this function which acts as a guard to prevent the
# recursion. If we have already proxied off to Chronic we won't proxy again.
def self.without_recursion &blk
unless in_recursion
self.in_recursion = true
ret = blk.call
self.in_recursion = false
end
ret
end
mattr_accessor :in_recursion
end
精彩评论