Add a config for preserving timezone information

when calling `to_time` on TimeWithZone object

Co-authored-by: jhawthorn <jhawthorn@github.com>
This commit is contained in:
Jason Kim 2024-06-10 15:13:50 -07:00
parent f5355a21ae
commit 9dcf17ec4c
10 changed files with 121 additions and 24 deletions

@ -8,4 +8,8 @@
*Richard Böhme*, *Jean Boussier*
* Add a new configuration value `:zone` for `ActiveSupport.to_time_preserves_timezone` and rename the previous `true` value to `:offset`. The new default value is `:zone`.
*Jason Kim*, *John Hawthorn*
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activesupport/CHANGELOG.md) for previous changes.

@ -115,9 +115,15 @@ def self.to_time_preserves_timezone
end
def self.to_time_preserves_timezone=(value)
unless value
if !value
ActiveSupport.deprecator.warn(
"Support for the pre-Ruby 2.4 behavior of to_time has been deprecated and will be removed in Rails 8.0."
"`to_time` will always preserve the receiver timezone rather than system local time in Rails 8.0." \
"To opt in to the new behavior, set `config.active_support.to_time_preserves_timezone = :zone`."
)
elsif value != :zone
ActiveSupport.deprecator.warn(
"`to_time` will always preserve the full timezone rather than offset of the receiver in Rails 8.0. " \
"To opt in to the new behavior, set `config.active_support.to_time_preserves_timezone = :zone`."
)
end

@ -26,8 +26,8 @@ def self.preserve_timezone # :nodoc:
# Only warn once, the first time the value is used (which should
# be the first time #to_time is called).
ActiveSupport.deprecator.warn(
"to_time will always preserve the timezone offset of the receiver in Rails 8.0. " \
"To opt in to the new behavior, set `ActiveSupport.to_time_preserves_timezone = true`."
"`to_time` will always preserve the receiver timezone rather than system local time in Rails 8.0." \
"To opt in to the new behavior, set `config.active_support.to_time_preserves_timezone = :zone`."
)
@@preserve_timezone = false

@ -96,6 +96,10 @@ class Railtie < Rails::Railtie # :nodoc:
config.eager_load_namespaces << TZInfo
end
initializer "active_support.to_time_preserves_timezone" do |app|
ActiveSupport.to_time_preserves_timezone = app.config.active_support.to_time_preserves_timezone
end
# Sets the default week start
# If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
initializer "active_support.initialize_beginning_of_week" do |app|

@ -479,11 +479,13 @@ def to_datetime
@to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
end
# Returns an instance of +Time+, either with the same UTC offset
# as +self+ or in the local system timezone depending on the setting
# of +ActiveSupport.to_time_preserves_timezone+.
# Returns an instance of +Time+, either with the same timezone as +self+,
# with the same UTC offset as +self+ or in the local system timezone
# depending on the setting of +ActiveSupport.to_time_preserves_timezone+.
def to_time
if preserve_timezone
if preserve_timezone == :zone
@to_time_with_timezone ||= getlocal(time_zone)
elsif preserve_timezone
@to_time_with_instance_offset ||= getlocal(utc_offset)
else
@to_time_with_system_offset ||= getlocal

@ -280,8 +280,8 @@ def test_to_time_preserves_timezone_is_deprecated
ActiveSupport.to_time_preserves_timezone
end
assert_not_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = true
assert_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = :offset
end
assert_deprecated(ActiveSupport.deprecator) do
@ -306,4 +306,36 @@ def test_to_time_preserves_timezone_is_deprecated
ActiveSupport.to_time_preserves_timezone = current_preserve_tz
end
end
def test_to_time_preserves_timezone_supports_new_values
current_preserve_tz = ActiveSupport.to_time_preserves_timezone
assert_not_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone
end
assert_not_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = :zone
end
assert_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = :offset
end
assert_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = true
end
assert_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = "offset"
end
assert_deprecated(ActiveSupport.deprecator) do
ActiveSupport.to_time_preserves_timezone = :foo
end
ensure
ActiveSupport.deprecator.silence do
ActiveSupport.to_time_preserves_timezone = current_preserve_tz
end
end
end

@ -517,7 +517,34 @@ def test_time_at
assert_equal time, Time.at(time)
end
def test_to_time_with_preserve_timezone
def test_to_time_with_preserve_timezone_using_zone
with_preserve_timezone(:zone) do
time = @twz.to_time
local_time = with_env_tz("US/Eastern") { Time.local(1999, 12, 31, 19) }
assert_equal Time, time.class
assert_equal time.object_id, @twz.to_time.object_id
assert_equal local_time, time
assert_equal local_time.utc_offset, time.utc_offset
assert_equal @time_zone, time.zone
end
end
def test_to_time_with_preserve_timezone_using_offset
with_preserve_timezone(:offset) do
with_env_tz "US/Eastern" do
time = @twz.to_time
assert_equal Time, time.class
assert_equal time.object_id, @twz.to_time.object_id
assert_equal Time.local(1999, 12, 31, 19), time
assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
assert_nil time.zone
end
end
end
def test_to_time_with_preserve_timezone_using_true
with_preserve_timezone(true) do
with_env_tz "US/Eastern" do
time = @twz.to_time
@ -526,6 +553,7 @@ def test_to_time_with_preserve_timezone
assert_equal time.object_id, @twz.to_time.object_id
assert_equal Time.local(1999, 12, 31, 19), time
assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
assert_nil time.zone
end
end
end
@ -539,6 +567,7 @@ def test_to_time_without_preserve_timezone
assert_equal time.object_id, @twz.to_time.object_id
assert_equal Time.local(1999, 12, 31, 19), time
assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
assert_equal Time.local(1999, 12, 31, 19).zone, time.zone
end
end
end
@ -552,6 +581,7 @@ def test_to_time_without_preserve_timezone_configured
assert_equal time.object_id, @twz.to_time.object_id
assert_equal Time.local(1999, 12, 31, 19), time
assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
assert_equal Time.local(1999, 12, 31, 19).zone, time.zone
assert_equal false, ActiveSupport.to_time_preserves_timezone
end

@ -58,6 +58,10 @@ NOTE: If you need to apply configuration directly to a class, use a [lazy load h
Below are the default values associated with each target version. In cases of conflicting values, newer versions take precedence over older versions.
#### Default Values for Target Version 8.0
- [`config.active_support.to_time_preserves_timezone`](#config-active-support-to-time-preserves-timezone): `:zone`
#### Default Values for Target Version 7.2
- [`config.active_job.enqueue_after_transaction_commit`](#config-active-job-enqueue-after-transaction-commit): `:default`
@ -154,10 +158,10 @@ Below are the default values associated with each target version. In cases of co
#### Default Values for Target Version 5.0
- [`ActiveSupport.to_time_preserves_timezone`](#activesupport-to-time-preserves-timezone): `true`
- [`config.action_controller.forgery_protection_origin_check`](#config-action-controller-forgery-protection-origin-check): `true`
- [`config.action_controller.per_form_csrf_tokens`](#config-action-controller-per-form-csrf-tokens): `true`
- [`config.active_record.belongs_to_required_by_default`](#config-active-record-belongs-to-required-by-default): `true`
- [`config.active_support.to_time_preserves_timezone`](#config-active-support-to-time-preserves-timezone): `:offset`
- [`config.ssl_options`](#config-ssl-options): `{ hsts: { subdomains: true } }`
### Rails General Configuration
@ -2694,6 +2698,18 @@ The default value depends on the `config.load_defaults` target version:
| (original) | `false` |
| 7.0 | `true` |
#### `config.active_support.to_time_preserves_timezone`
Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. If set to `:zone`, `to_time` methods will use the timezone of their receivers. If set to `:offset`, `to_time` methods will use the UTC offset. If `false`, `to_time` methods will convert to the local system UTC offset instead.
The default value depends on the `config.load_defaults` target version:
| Starting with version | The default value is |
| --------------------- | -------------------- |
| (original) | `false` |
| 5.0 | `:offset` |
| 8.0 | `:zone` |
#### `ActiveSupport::Logger.silencer`
Is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@ -2702,17 +2718,6 @@ Is set to `false` to disable the ability to silence logging in a block. The defa
Specifies the logger to use within cache store operations.
#### `ActiveSupport.to_time_preserves_timezone`
Specifies whether `to_time` methods preserve the UTC offset of their receivers. If `false`, `to_time` methods will convert to the local system UTC offset instead.
The default value depends on the `config.load_defaults` target version:
| Starting with version | The default value is |
| --------------------- | -------------------- |
| (original) | `false` |
| 5.0 | `true` |
#### `ActiveSupport.utc_to_local_returns_utc_offset_times`
Configures `ActiveSupport::TimeZone.utc_to_local` to return a time with a UTC

@ -114,7 +114,9 @@ def load_defaults(target_version)
action_controller.forgery_protection_origin_check = true
end
ActiveSupport.to_time_preserves_timezone = true
if respond_to?(:active_support)
active_support.to_time_preserves_timezone = :offset
end
if respond_to?(:active_record)
active_record.belongs_to_required_by_default = true
@ -337,6 +339,10 @@ def load_defaults(target_version)
end
when "8.0"
load_defaults "7.2"
if respond_to?(:active_support)
active_support.to_time_preserves_timezone = :zone
end
else
raise "Unknown version #{target_version.to_s.inspect}"
end

@ -8,3 +8,11 @@
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
###
# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone.
# If set to `:zone`, `to_time` methods will use the timezone of their receivers.
# If set to `:offset`, `to_time` methods will use the UTC offset.
# If `false`, `to_time` methods will convert to the local system UTC offset instead.
#++
# Rails.application.config.active_support.to_time_preserves_timezone = :zone