Skip to content

Commit

Permalink
Time columns should support time zone aware attributes
Browse files Browse the repository at this point in the history
The types that are affected by `time_zone_aware_attributes` (which is on
by default) have been made configurable, in case this is a breaking
change for existing applications.
  • Loading branch information
sgrif committed Jan 15, 2015
1 parent d8e7104 commit 5cd3bbb
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 8 deletions.
14 changes: 14 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,17 @@
* `time` columns can now affected by `time_zone_aware_attributes`. If you have
set `config.time_zone` to a value other than `'UTC'`, they will be treated
as in that time zone by default in Rails 5.0. If this is not the desired
behavior, you can set

ActiveRecord::Base.time_zone_aware_types = [:datetime]

A deprecation warning will be emitted if you have a `:time` column, and have
not explicitly opted out.

Fixes #3145

*Sean Griffin*

* `nil` as a value for a binary column in a query no longer logs as
"<NULL binary data>", and instead logs as just "nil".

Expand Down
Expand Up @@ -13,7 +13,7 @@ def type_cast_from_user(value)
value.map { |v| type_cast_from_user(v) }
elsif value.respond_to?(:in_time_zone)
begin
value.in_time_zone || super
user_input_in_time_zone(value) || super
rescue ArgumentError
nil
end
Expand All @@ -39,6 +39,9 @@ def convert_time_to_time_zone(value)

class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
self.skip_time_zone_conversion_for_attributes = []

class_attribute :time_zone_aware_types, instance_writer: false
self.time_zone_aware_types = [:datetime, :not_explicitly_configured]
end

module ClassMethods
Expand All @@ -59,9 +62,31 @@ def inherited(subclass)
end

def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
(:datetime == cast_type.type)
enabled_for_column = time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym)
result = enabled_for_column &&
time_zone_aware_types.include?(cast_type.type)

if enabled_for_column &&
!result &&
cast_type.type == :time &&
time_zone_aware_types.include?(:not_explicitly_configured)
ActiveSupport::Deprecation.warn(<<-MESSAGE)
Time columns will become time zone aware in Rails 5.1. This

This comment has been minimized.

Copy link
@sobrinho

sobrinho Jan 16, 2015

Contributor

@sgrif the changelog says 5.0 and the message says 5.1.

This comment has been minimized.

Copy link
@sgrif

sgrif via email Jan 16, 2015

Author Contributor
sill cause `String`s to be parsed as if they were in `Time.zone`,
and `Time`s to be converted to `Time.zone`.
To keep the old behavior, you must add the following to your initializer:
config.active_record.time_zone_aware_types = [:datetime]
To silence this deprecation warning, add the following:
config.active_record.time_zone_aware_types << :time
MESSAGE
end

result
end
end
end
Expand Down
Expand Up @@ -18,7 +18,7 @@ class Array < Type::Value # :nodoc:
end

attr_reader :subtype, :delimiter
delegate :type, to: :subtype
delegate :type, :user_input_in_time_zone, to: :subtype

def initialize(subtype, delimiter = ',')
@subtype = subtype
Expand Down
13 changes: 13 additions & 0 deletions activerecord/lib/active_record/type/time.rb
Expand Up @@ -7,6 +7,19 @@ def type
:time
end

def user_input_in_time_zone(value)
return unless value.present?

case value
when ::String
value = "2000-01-01 #{value}"
when ::Time
value = value.change(year: 2000, day: 1, month: 1)
end

super(value)
end

private

def cast_value(value)
Expand Down
4 changes: 4 additions & 0 deletions activerecord/lib/active_record/type/time_value.rb
Expand Up @@ -9,6 +9,10 @@ def type_cast_for_schema(value)
"'#{value.to_s(:db)}'"
end

def user_input_in_time_zone(value)
value.in_time_zone
end

private

def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
Expand Down
51 changes: 50 additions & 1 deletion activerecord/test/cases/attribute_methods_test.rb
Expand Up @@ -657,7 +657,7 @@ def test_setting_time_zone_aware_attribute_interprets_time_zone_unaware_string_i
end
end

def test_setting_time_zone_aware_attribute_in_current_time_zone
def test_setting_time_zone_aware_datetime_in_current_time_zone
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
Expand All @@ -676,6 +676,47 @@ def test_yaml_dumping_record_with_time_zone_aware_attribute
end
end

def test_setting_time_zone_aware_time_in_current_time_zone
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "10:00:00"
expected_time = Time.zone.parse("2000-01-01 #{time_string}")

record.bonus_time = time_string
assert_equal expected_time, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone

record.bonus_time = ''
assert_nil record.bonus_time
end
end

def test_setting_time_zone_aware_time_with_dst
in_time_zone "Pacific Time (US & Canada)" do
current_time = Time.zone.local(2014, 06, 15, 10)
record = @target.new(bonus_time: current_time)
time_before_save = record.bonus_time

record.save
record.reload

assert_equal time_before_save, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
end
end

def test_removing_time_zone_aware_types
with_time_zone_aware_types(:datetime) do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: "10:00:00")
expected_time = Time.utc(2000, 01, 01, 10)

assert_equal expected_time, record.bonus_time
assert record.bonus_time.utc?
end
end
end

def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
Expand Down Expand Up @@ -908,6 +949,14 @@ def new_topic_like_ar_class(&block)
klass
end

def with_time_zone_aware_types(*types)
old_types = ActiveRecord::Base.time_zone_aware_types
ActiveRecord::Base.time_zone_aware_types = types
yield
ensure
ActiveRecord::Base.time_zone_aware_types = old_types
end

def cached_columns
Topic.columns.map(&:name)
end
Expand Down
3 changes: 3 additions & 0 deletions activerecord/test/cases/helper.rb
Expand Up @@ -30,6 +30,9 @@
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')

# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends.
ActiveRecord::Base.time_zone_aware_types << :time

def current_adapter?(*types)
types.any? do |type|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/cases/multiparameter_attributes_test.rb
Expand Up @@ -250,8 +250,8 @@ def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attr
}
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
assert topic.bonus_time.utc?
assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
assert_not topic.bonus_time.utc?
end
end
end
Expand Down

0 comments on commit 5cd3bbb

Please sign in to comment.