mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Add Time Zone support to ActiveRecord, and config.time_zone property for specifying a default Time Zone. Closes #10982 [Geoff Buesing, rick]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8806 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
*SVN*
|
||||
|
||||
* Add timezone-aware attribute readers and writers. #10982 [Geoff Buesing]
|
||||
|
||||
* Instantiating time objects in multiparameter attributes uses Time.zone if available. #10982 [rick]
|
||||
|
||||
* Add note about how ActiveRecord::Observer classes are initialized in a Rails app. #10980 [fxn]
|
||||
|
||||
* MySQL: omit text/blob defaults from the schema instead of using an empty string. #10963 [mdeiters]
|
||||
|
||||
@@ -8,6 +8,10 @@ module ActiveRecord
|
||||
base.attribute_method_suffix(*DEFAULT_SUFFIXES)
|
||||
base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
|
||||
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
||||
base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
||||
base.time_zone_aware_attributes = false
|
||||
base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
||||
base.skip_time_zone_conversion_for_attributes = []
|
||||
end
|
||||
|
||||
# Declare and check for suffixed attribute methods.
|
||||
@@ -64,13 +68,19 @@ module ActiveRecord
|
||||
unless instance_method_already_implemented?(name)
|
||||
if self.serialized_attributes[name]
|
||||
define_read_method_for_serialized_attribute(name)
|
||||
elsif create_time_zone_conversion_attribute?(name, column)
|
||||
define_read_method_for_time_zone_conversion(name)
|
||||
else
|
||||
define_read_method(name.to_sym, name, column)
|
||||
end
|
||||
end
|
||||
|
||||
unless instance_method_already_implemented?("#{name}=")
|
||||
define_write_method(name.to_sym)
|
||||
if create_time_zone_conversion_attribute?(name, column)
|
||||
define_write_method_for_time_zone_conversion(name)
|
||||
else
|
||||
define_write_method(name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
unless instance_method_already_implemented?("#{name}?")
|
||||
@@ -121,6 +131,10 @@ module ActiveRecord
|
||||
@@attribute_method_suffixes ||= []
|
||||
end
|
||||
|
||||
def create_time_zone_conversion_attribute?(name, column)
|
||||
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
|
||||
end
|
||||
|
||||
# Define an attribute reader method. Cope with nil column.
|
||||
def define_read_method(symbol, attr_name, column)
|
||||
cast_code = column.type_cast_code('v') if column
|
||||
@@ -140,6 +154,18 @@ module ActiveRecord
|
||||
def define_read_method_for_serialized_attribute(attr_name)
|
||||
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
|
||||
end
|
||||
|
||||
def define_read_method_for_time_zone_conversion(attr_name)
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}(reload = false)
|
||||
cached = @attributes_cache['#{attr_name}']
|
||||
return cached if cached && !reload
|
||||
time = read_attribute('#{attr_name}')
|
||||
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_current_time_zone : time
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method attr_name, method_body
|
||||
end
|
||||
|
||||
# Define an attribute ? method.
|
||||
def define_question_method(attr_name)
|
||||
@@ -149,6 +175,19 @@ module ActiveRecord
|
||||
def define_write_method(attr_name)
|
||||
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
|
||||
end
|
||||
|
||||
def define_write_method_for_time_zone_conversion(attr_name)
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}=(time)
|
||||
if time
|
||||
time = time.to_time rescue time unless time.acts_like?(:time)
|
||||
time = time.in_current_time_zone if time.acts_like?(:time)
|
||||
end
|
||||
write_attribute(:#{attr_name}, time)
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
|
||||
end
|
||||
|
||||
# Evaluate the definition for an attribute related method
|
||||
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
|
||||
@@ -303,7 +342,6 @@ module ActiveRecord
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -2470,8 +2470,12 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
# Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
|
||||
def instantiate_time_object(*values)
|
||||
@@default_timezone == :utc ? Time.utc(*values) : Time.local(*values)
|
||||
def instantiate_time_object(name, values)
|
||||
if Time.zone && !self.class.skip_time_zone_conversion_for_attributes.include?(name.to_sym)
|
||||
Time.zone.new(*values)
|
||||
else
|
||||
@@default_timezone == :utc ? Time.utc(*values) : Time.local(*values)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_callstack_for_multiparameter_attributes(callstack)
|
||||
@@ -2483,12 +2487,12 @@ module ActiveRecord #:nodoc:
|
||||
else
|
||||
begin
|
||||
value = if Time == klass
|
||||
instantiate_time_object(*values)
|
||||
instantiate_time_object(name, values)
|
||||
elsif Date == klass
|
||||
begin
|
||||
Date.new(*values)
|
||||
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
|
||||
instantiate_time_object(*values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
||||
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
||||
end
|
||||
else
|
||||
klass.new(*values)
|
||||
|
||||
@@ -14,7 +14,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
||||
ActiveRecord::Base.attribute_method_suffix *@old_suffixes
|
||||
end
|
||||
|
||||
|
||||
def test_match_attribute_method_query_returns_match_data
|
||||
assert_not_nil md = @target.match_attribute_method?('title=')
|
||||
assert_equal 'title', md.pre_match
|
||||
@@ -97,7 +96,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
||||
def test_only_time_related_columns_are_meant_to_be_cached_by_default
|
||||
expected = %w(datetime timestamp time date).sort
|
||||
assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
|
||||
end
|
||||
end
|
||||
|
||||
def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
|
||||
default_attributes = Topic.cached_attributes
|
||||
@@ -138,9 +137,67 @@ end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_time_attributes_are_retrieved_in_current_time_zone
|
||||
in_time_zone "Pacific Time (US & Canada)" do
|
||||
utc_time = Time.utc(2008, 1, 1)
|
||||
record = @target.new
|
||||
record[:written_on] = utc_time
|
||||
assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time
|
||||
assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone
|
||||
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone
|
||||
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly
|
||||
end
|
||||
end
|
||||
|
||||
def test_setting_time_zone_aware_attribute_to_utc
|
||||
in_time_zone "Pacific Time (US & Canada)" do
|
||||
utc_time = Time.utc(2008, 1, 1)
|
||||
record = @target.new
|
||||
record.written_on = utc_time
|
||||
assert_equal utc_time, record.written_on
|
||||
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
|
||||
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
|
||||
end
|
||||
end
|
||||
|
||||
def test_setting_time_zone_aware_attribute_in_other_time_zone
|
||||
utc_time = Time.utc(2008, 1, 1)
|
||||
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
|
||||
in_time_zone "Pacific Time (US & Canada)" do
|
||||
record = @target.new
|
||||
record.written_on = cst_time
|
||||
assert_equal utc_time, record.written_on
|
||||
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
|
||||
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
|
||||
end
|
||||
end
|
||||
|
||||
def test_setting_time_zone_aware_attribute_in_current_time_zone
|
||||
utc_time = Time.utc(2008, 1, 1)
|
||||
in_time_zone "Pacific Time (US & Canada)" do
|
||||
record = @target.new
|
||||
record.written_on = utc_time.in_current_time_zone
|
||||
assert_equal utc_time, record.written_on
|
||||
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
|
||||
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def time_related_columns_on_topic
|
||||
Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
|
||||
end
|
||||
|
||||
def in_time_zone(zone)
|
||||
old_zone = Time.zone
|
||||
old_tz = ActiveRecord::Base.time_zone_aware_attributes
|
||||
|
||||
Time.zone = zone ? TimeZone[zone] : nil
|
||||
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
|
||||
yield
|
||||
ensure
|
||||
Time.zone = old_zone
|
||||
ActiveRecord::Base.time_zone_aware_attributes = old_tz
|
||||
end
|
||||
end
|
||||
|
||||
@@ -924,6 +924,58 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_with_utc
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
attributes = {
|
||||
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
|
||||
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
|
||||
}
|
||||
topic = Topic.find(1)
|
||||
topic.attributes = attributes
|
||||
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
|
||||
ensure
|
||||
ActiveRecord::Base.default_timezone = :local
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
|
||||
ActiveRecord::Base.time_zone_aware_attributes = true
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
Time.zone = TimeZone[-28800]
|
||||
attributes = {
|
||||
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
|
||||
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
|
||||
}
|
||||
topic = Topic.find(1)
|
||||
topic.attributes = attributes
|
||||
assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
|
||||
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
|
||||
assert_equal Time.zone, topic.written_on.time_zone
|
||||
ensure
|
||||
ActiveRecord::Base.time_zone_aware_attributes = false
|
||||
ActiveRecord::Base.default_timezone = :local
|
||||
Time.zone = nil
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
|
||||
ActiveRecord::Base.time_zone_aware_attributes = true
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
Time.zone = TimeZone[-28800]
|
||||
Topic.skip_time_zone_conversion_for_attributes = [:written_on]
|
||||
attributes = {
|
||||
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
|
||||
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
|
||||
}
|
||||
topic = Topic.find(1)
|
||||
topic.attributes = attributes
|
||||
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
|
||||
assert_equal false, topic.written_on.respond_to?(:time_zone)
|
||||
ensure
|
||||
ActiveRecord::Base.time_zone_aware_attributes = false
|
||||
ActiveRecord::Base.default_timezone = :local
|
||||
Time.zone = nil
|
||||
Topic.skip_time_zone_conversion_for_attributes = []
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_with_empty_seconds
|
||||
attributes = {
|
||||
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
|
||||
|
||||
Reference in New Issue
Block a user