Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit 72385a7be65bb27b07cd2ee3bc9fe221d8e04f90 1 parent 692dbbf
@technoweenie technoweenie authored
View
4 activerecord/CHANGELOG
@@ -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]
View
42 activerecord/lib/active_record/attribute_methods.rb
@@ -8,6 +8,10 @@ def self.included(base)
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 @@ def define_attribute_methods
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 @@ def attribute_method_suffixes
@@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 @@ def define_read_method(symbol, attr_name, column)
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 @@ def define_question_method(attr_name)
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 @@ def respond_to?(method, include_priv = false)
end
super
end
-
private
View
12 activerecord/lib/active_record/base.rb
@@ -2470,8 +2470,12 @@ def assign_multiparameter_attributes(pairs)
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 @@ def execute_callstack_for_multiparameter_attributes(callstack)
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)
View
61 activerecord/test/cases/attribute_methods_test.rb
@@ -14,7 +14,6 @@ def teardown
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 @@ def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_m
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 @@ def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_els
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
View
52 activerecord/test/cases/base_test.rb
@@ -924,6 +924,58 @@ def test_multiparameter_attributes_on_time
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",
View
2  activesupport/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Add Time.zone_default accessor for setting the default time zone. Rails::Configuration.time_zone sets this. #10982 [Geoff Buesing]
+
* cache.fetch(key, :force => true) to force a cache miss. [Jeremy Kemper]
* Support retrieving TimeZones with a Duration. TimeZone[-28800] == TimeZone[-480.minutes]. [rick]
View
4 activesupport/lib/active_support/core_ext/time/zones.rb
@@ -9,8 +9,10 @@ def self.included(base) #:nodoc:
end
module ClassMethods
+ attr_accessor :zone_default
+
def zone
- Thread.current[:time_zone]
+ Thread.current[:time_zone] || zone_default
end
# Sets a global default time zone, separate from the system time zone in ENV['TZ'].
View
8 activesupport/lib/active_support/values/time_zone.rb
@@ -174,6 +174,14 @@ def <=>(zone)
def to_s
"(UTC#{formatted_offset}) #{name}"
end
+
+ # Method for creating new ActiveSupport::TimeWithZone instance in time zone of self. Example:
+ #
+ # Time.zone = "Hawaii" # => "Hawaii"
+ # Time.zone.new(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
+ def new(*args)
+ Time.utc_time(*args).change_time_zone(self)
+ end
begin # the following methods depend on the tzinfo gem
require_library_or_gem "tzinfo" unless Object.const_defined?(:TZInfo)
View
9 activesupport/test/core_ext/time_ext_test.rb
@@ -459,6 +459,15 @@ def test_minus_with_time_with_zone
assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), TimeZone['UTC'] )
end
+ def test_time_created_with_local_constructor_cannot_represent_times_during_hour_skipped_by_dst
+ with_env_tz 'US/Eastern' do
+ # On Apr 2 2006 at 2:00AM in US, clocks were moved forward to 3:00AM.
+ # Therefore, 2AM EST doesn't exist for this date; Time.local fails over to 3:00AM EDT
+ assert_equal Time.local(2006, 4, 2, 3), Time.local(2006, 4, 2, 2)
+ assert Time.local(2006, 4, 2, 2).dst?
+ end
+ end
+
protected
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
View
6 activesupport/test/time_zone_test.rb
@@ -128,5 +128,11 @@ def test_us_zones
assert TimeZone.us_zones.include?(TimeZone["Hawaii"])
assert !TimeZone.us_zones.include?(TimeZone["Kuala Lumpur"])
end
+
+ def test_new
+ time = TimeZone["Hawaii"].new(2007, 2, 5, 15, 30, 45)
+ assert_equal Time.utc(2007, 2, 5, 15, 30, 45), time.time
+ assert_equal TimeZone["Hawaii"], time.time_zone
+ end
end
View
2  railties/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Add config.time_zone for configuring the default Time.zone value. #10982 [Geoff Buesing]
+
* Reshuffle load order so that routes and observers are initialized after plugins and app initializers. Closes #10980 [rick]
* Git support for script/generate. #10690 [ssoroka]
View
16 railties/lib/initializer.rb
@@ -81,6 +81,7 @@ def process
initialize_dependency_mechanism
initialize_whiny_nils
initialize_temporary_session_directory
+ initialize_time_zone
initialize_framework_settings
add_support_load_paths
@@ -316,6 +317,16 @@ def initialize_temporary_session_directory
end
end
+ def initialize_time_zone
+ if configuration.time_zone
+ Time.zone_default = TimeZone[configuration.time_zone]
+ if configuration.frameworks.include?(:active_record)
+ ActiveRecord::Base.time_zone_aware_attributes = true
+ ActiveRecord::Base.default_timezone = :utc
+ end
+ end
+ end
+
# Initializes framework-specific settings for each of the loaded frameworks
# (Configuration#frameworks). The available settings map to the accessors
# on each of the corresponding Base classes.
@@ -456,6 +467,11 @@ def breakpoint_server(_ = nil)
end
alias_method :breakpoint_server=, :breakpoint_server
+ # Sets the default time_zone. Setting this will enable time_zone
+ # awareness for ActiveRecord models and set the ActiveRecord default
+ # timezone to :utc.
+ attr_accessor :time_zone
+
# Create a new Configuration instance, initialized with the default
# values.
def initialize
Please sign in to comment.
Something went wrong with that request. Please try again.