aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew White <andrew.white@unboxedconsulting.com>2016-04-23 14:46:50 +0100
committerAndrew White <andrew.white@unboxedconsulting.com>2016-04-23 15:03:50 +0100
commitc9c5788a527b70d7f983e2b4b47e3afd863d9f48 (patch)
tree3b06bbd1555d74180ea48ad5a1ec6bbd5284261b
parent1ffa1a852e79feee9d4793fb60992a920909c316 (diff)
downloadrails-c9c5788a527b70d7f983e2b4b47e3afd863d9f48.tar.gz
rails-c9c5788a527b70d7f983e2b4b47e3afd863d9f48.tar.bz2
rails-c9c5788a527b70d7f983e2b4b47e3afd863d9f48.zip
Add compatibility for Ruby 2.4 `to_time` changes
In Ruby 2.4 the `to_time` method for both `DateTime` and `Time` will preserve the timezone of the receiver when converting to an instance of `Time`. Since Rails 5.0 will support Ruby 2.2, 2.3 and later we need to introduce a compatibility layer so that apps that upgrade do not break. New apps will have a config initializer file that defaults to match the new Ruby 2.4 behavior going forward. For information about the changes to Ruby see: https://bugs.ruby-lang.org/issues/12189 https://bugs.ruby-lang.org/issues/12271 Fixes #24617.
-rw-r--r--activesupport/CHANGELOG.md20
-rw-r--r--activesupport/lib/active_support.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/date_time.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/compatibility.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/time/compatibility.rb5
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb8
-rw-r--r--activesupport/test/core_ext/date_and_time_compatibility_test.rb110
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb8
-rw-r--r--activesupport/test/time_zone_test_helpers.rb8
-rw-r--r--railties/CHANGELOG.md8
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb10
-rw-r--r--railties/test/generators/app_generator_test.rb28
16 files changed, 248 insertions, 7 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index f8423c3ef8..23ca8246e2 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,23 @@
+* Add `ActiveSupport.to_time_preserves_timezone` config option to control
+ how `to_time` handles timezones. In Ruby 2.4+ the behavior will change
+ from converting to the local system timezone to preserving the timezone
+ of the receiver. This config option defaults to false so that apps made
+ with earlier versions of Rails are not affected when upgrading, e.g:
+
+ >> ENV['TZ'] = 'US/Eastern'
+
+ >> "2016-04-23T10:23:12.000Z".to_time
+ => "2016-04-23T06:23:12.000-04:00"
+
+ >> ActiveSupport.to_time_preserves_timezone = true
+
+ >> "2016-04-23T10:23:12.000Z".to_time
+ => "2016-04-23T10:23:12.000Z"
+
+ Fixes #24617.
+
+ *Andrew White*
+
* `ActiveSupport::TimeZone.country_zones(country_code)` looks up the
country's time zones by its two-letter ISO3166 country code, e.g.
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 72777baecd..11569add37 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -26,6 +26,7 @@ require "active_support/dependencies/autoload"
require "active_support/version"
require "active_support/logger"
require "active_support/lazy_load_hooks"
+require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
extend ActiveSupport::Autoload
@@ -85,6 +86,14 @@ module ActiveSupport
def self.halt_callback_chains_on_return_false=(value)
Callbacks.halt_and_display_warning_on_return_false = value
end
+
+ def self.to_time_preserves_timezone
+ DateAndTime::Compatibility.preserve_timezone
+ end
+
+ def self.to_time_preserves_timezone=(value)
+ DateAndTime::Compatibility.preserve_timezone = value
+ end
end
autoload :I18n, "active_support/i18n"
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
new file mode 100644
index 0000000000..b8eb587390
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -0,0 +1,16 @@
+module DateAndTime
+ module Compatibility
+ # If true, +to_time+ preserves the the timezone offset.
+ #
+ # NOTE: With Ruby 2.4+ the default for +to_time+ changed from
+ # converting to the local system time to preserving the offset
+ # of the receiver. For backwards compatibility we're overriding
+ # this behavior but new apps will have an initializer that sets
+ # this to true because the new behavior is preferred.
+ mattr_accessor(:preserve_timezone, instance_writer: false) { false }
+
+ def to_time
+ preserve_timezone ? getlocal(utc_offset) : getlocal
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb
index 5450533935..86177488c0 100644
--- a/activesupport/lib/active_support/core_ext/date_time.rb
+++ b/activesupport/lib/active_support/core_ext/date_time.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/date_time/acts_like'
require 'active_support/core_ext/date_time/blank'
require 'active_support/core_ext/date_time/calculations'
+require 'active_support/core_ext/date_time/compatibility'
require 'active_support/core_ext/date_time/conversions'
diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
new file mode 100644
index 0000000000..63ac4c2f3a
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
@@ -0,0 +1,16 @@
+require 'active_support/core_ext/date_and_time/compatibility'
+
+class DateTime
+ prepend DateAndTime::Compatibility
+
+ # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your
+ # system's <tt>ENV['TZ']</tt> zone.
+ def getlocal(utc_offset = nil)
+ utc = getutc
+
+ Time.utc(
+ utc.year, utc.month, utc.day,
+ utc.hour, utc.min, utc.sec + utc.sec_fraction
+ ).getlocal(utc_offset)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 71612e09fa..946976c5e9 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -32,7 +32,7 @@ class String
parts.fetch(:offset, form == :utc ? 0 : nil)
)
- form == :utc ? time.utc : time.getlocal
+ form == :utc ? time.utc : time.to_time
end
# Converts a string to a Date value.
diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb
index 72c3234630..0bce632222 100644
--- a/activesupport/lib/active_support/core_ext/time.rb
+++ b/activesupport/lib/active_support/core_ext/time.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/time/acts_like'
require 'active_support/core_ext/time/calculations'
+require 'active_support/core_ext/time/compatibility'
require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/time/zones'
diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb
new file mode 100644
index 0000000000..945319461b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb
@@ -0,0 +1,5 @@
+require 'active_support/core_ext/date_and_time/compatibility'
+
+class Time
+ prepend DateAndTime::Compatibility
+end
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 79cc748cf5..a44041af82 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -1,6 +1,7 @@
require 'active_support/duration'
require 'active_support/values/time_zone'
require 'active_support/core_ext/object/acts_like'
+require 'active_support/core_ext/date_and_time/compatibility'
module ActiveSupport
# A Time-like class that can represent a time in any time zone. Necessary
@@ -44,7 +45,7 @@ module ActiveSupport
PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze }
PRECISIONS[0] = '%FT%T'.freeze
- include Comparable
+ include Comparable, DateAndTime::Compatibility
attr_reader :time_zone
def initialize(utc_time, time_zone, local_time = nil, period = nil)
@@ -401,11 +402,6 @@ module ActiveSupport
utc.to_r
end
- # Returns an instance of Time in the system timezone.
- def to_time
- utc.to_time
- end
-
# Returns an instance of DateTime with the timezone's UTC offset
#
# Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000
diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
new file mode 100644
index 0000000000..7cc2fae5be
--- /dev/null
+++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+require 'active_support/time'
+require 'time_zone_test_helpers'
+
+class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
+ def setup
+ @utc_time = Time.utc(2016, 4, 23, 14, 11, 12)
+ @utc_offset = 3600
+ @system_offset = -14400
+ @zone = ActiveSupport::TimeZone['London']
+ end
+
+ def test_time_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz 'US/Eastern' do
+ time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_time_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz 'US/Eastern' do
+ time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_datetime_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz 'US/Eastern' do
+ time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_datetime_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz 'US/Eastern' do
+ time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_twz_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz 'US/Eastern' do
+ time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_twz_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz 'US/Eastern' do
+ time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_string_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz 'US/Eastern' do
+ time = "2016-04-23T15:11:12+01:00".to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_string_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz 'US/Eastern' do
+ time = "2016-04-23T15:11:12+01:00".to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 16efeeadd5..d57e71df3b 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -40,6 +40,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
Time::DATE_FORMATS.delete(:custom)
end
+ def test_getlocal
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal
+ assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).getlocal
+ assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1,24)).getlocal
+ end
+ end
+
def test_to_date
assert_equal Date.new(2005, 2, 21), DateTime.new(2005, 2, 21, 14, 30, 0).to_date
end
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
index 9632b89d09..eb6f7d0f85 100644
--- a/activesupport/test/time_zone_test_helpers.rb
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -13,4 +13,12 @@ module TimeZoneTestHelpers
ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
+
+ def with_preserve_timezone(value)
+ old_preserve_tz = ActiveSupport.to_time_preserves_timezone
+ ActiveSupport.to_time_preserves_timezone = value
+ yield
+ ensure
+ ActiveSupport.to_time_preserves_timezone = old_preserve_tz
+ end
end
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 32bfdf272b..3a4a77724f 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Add `config/initializers/to_time_preserves_timezone.rb`, which tells
+ Active Support to preserve the receiver's timezone when calling `to_time`.
+ This matches the new behavior that will be part of Ruby 2.4.
+
+ Fixes #24617.
+
+ *Andrew White*
+
* Make `rails restart` command work with Puma by passing the restart command
which Puma can use to restart rails server.
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index f58e6ba653..4d5bb364b2 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -92,6 +92,7 @@ module Rails
cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb')
active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb')
+ to_time_preserves_timezone_config_exist = File.exist?('config/initializers/to_time_preserves_timezone.rb')
action_cable_config_exist = File.exist?('config/cable.yml')
ssl_options_exist = File.exist?('config/initializers/ssl_options.rb')
rack_cors_config_exist = File.exist?('config/initializers/cors.rb')
@@ -112,6 +113,10 @@ module Rails
remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
end
+ unless to_time_preserves_timezone_config_exist
+ remove_file 'config/initializers/to_time_preserves_timezone.rb'
+ end
+
unless action_cable_config_exist
template 'config/cable.yml'
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb
new file mode 100644
index 0000000000..8674be3227
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+# Preserve the timezone of the receiver when calling to `to_time`.
+# Ruby 2.4 will change the behavior of `to_time` to preserve the timezone
+# when converting to an instance of `Time` instead of the previous behavior
+# of converting to the local system timezone.
+#
+# Rails 5.0 introduced this config option so that apps made with earlier
+# versions of Rails are not affected when upgrading.
+ActiveSupport.to_time_preserves_timezone = true
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 2d9867fa9d..25a8635e7d 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -257,6 +257,34 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_rails_update_does_not_create_to_time_preserves_timezone
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.rm("#{app_root}/config/initializers/to_time_preserves_timezone.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/to_time_preserves_timezone.rb"
+ end
+ end
+
+ def test_rails_update_does_not_remove_to_time_preserves_timezone_if_already_present
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.touch("#{app_root}/config/initializers/to_time_preserves_timezone.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/to_time_preserves_timezone.rb"
+ end
+ end
+
def test_rails_update_does_not_create_ssl_options_by_default
app_root = File.join(destination_root, 'myapp')
run_generator [app_root]