diff options
-rw-r--r-- | activerecord/CHANGELOG | 4 | ||||
-rw-r--r-- | activerecord/lib/active_record/attribute_methods.rb | 42 | ||||
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 12 | ||||
-rwxr-xr-x | activerecord/test/cases/attribute_methods_test.rb | 61 | ||||
-rwxr-xr-x | activerecord/test/cases/base_test.rb | 52 | ||||
-rw-r--r-- | activesupport/CHANGELOG | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/time/zones.rb | 4 | ||||
-rw-r--r-- | activesupport/lib/active_support/values/time_zone.rb | 8 | ||||
-rw-r--r-- | activesupport/test/core_ext/time_ext_test.rb | 9 | ||||
-rw-r--r-- | activesupport/test/time_zone_test.rb | 6 | ||||
-rw-r--r-- | railties/CHANGELOG | 2 | ||||
-rw-r--r-- | railties/lib/initializer.rb | 16 |
12 files changed, 209 insertions, 9 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index b2e2a117fb..1fd8cbd722 100644 --- a/activerecord/CHANGELOG +++ b/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] diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 657c575866..f976e70a06 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -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 diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f1759aa68f..d1729b6ff4 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -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) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d2bfb02846..427c14edbd 100755 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -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 diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 04ed751eb5..bd076888e1 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -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", diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index a39be78ca5..c5cc50f652 100644 --- a/activesupport/CHANGELOG +++ b/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] diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 7b9106b4ac..33c7800705 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -9,8 +9,10 @@ module ActiveSupport #: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']. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index d1dba65f86..fed64579e4 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -174,6 +174,14 @@ class TimeZone 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) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 37fd6f027b..40e8c5ecfd 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -459,6 +459,15 @@ class TimeExtCalculationsTest < Test::Unit::TestCase 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 diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index c32218cee2..cd3972a41c 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -128,5 +128,11 @@ class TimeZoneTest < Test::Unit::TestCase 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 diff --git a/railties/CHANGELOG b/railties/CHANGELOG index e4a0a98d37..1e1f328837 100644 --- a/railties/CHANGELOG +++ b/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] diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 4e94b850d8..d839415e49 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -81,6 +81,7 @@ module Rails initialize_dependency_mechanism initialize_whiny_nils initialize_temporary_session_directory + initialize_time_zone initialize_framework_settings add_support_load_paths @@ -316,6 +317,16 @@ module Rails 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 @@ module Rails 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 |