Skip to content

Commit 5e4a34d

Browse files
authored
Store timezone-aware datetimes in empty fields (#1943)
* Store timezone-aware datetimes in empty fields * changelog
1 parent cdd2f72 commit 5e4a34d

4 files changed

Lines changed: 42 additions & 28 deletions

File tree

news/+tz.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When deserializing datetimes into an empty field, store them as timezone-aware datetimes. @davisagli

src/plone/restapi/deserializer/dxfields.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,18 @@ def __call__(self, value):
123123
# Otherwise let's check what is currently stored.
124124
dm = queryMultiAdapter((self.context, self.field), IDataManager)
125125
current = dm.get()
126-
if current is not None and current.tzinfo is not None:
127-
# Timezone-aware. Convert to the same timezone.
128-
tz = timezone(current.tzinfo.zone)
129-
value = tz.normalize(dt.astimezone(tz))
126+
if current is not None:
127+
# There's an existing value. Let's match it.
128+
if current.tzinfo is not None:
129+
# Timezone-aware. Convert to the same timezone.
130+
tz = timezone(current.tzinfo.zone)
131+
value = tz.normalize(dt.astimezone(tz))
132+
else:
133+
# Timezone-naive. Convert to UTC and remove the tzinfo.
134+
value = utc.normalize(dt.astimezone(utc)).replace(tzinfo=None)
130135
else:
131-
# Timezone-naive. Convert to UTC and remove the tzinfo.
132-
value = utc.normalize(dt.astimezone(utc)).replace(tzinfo=None)
136+
# No current value. Store as timezone-aware.
137+
value = dt
133138

134139
self.field.validate(value)
135140
return value

src/plone/restapi/tests/dxtypes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ class IDXTestDocumentSchema(model.Schema):
259259
test_constraint_field = schema.TextLine(
260260
required=False, constraint=lambda x: "00" in x
261261
)
262-
test_datetime_min_field = schema.Datetime(required=False, min=datetime(2000, 1, 1))
262+
test_datetime_min_field = schema.Datetime(
263+
required=False, min=timezone("UTC").localize(datetime(2000, 1, 1))
264+
)
263265
test_time_min_field = schema.Time(required=False, min=time(1))
264266
test_timedelta_min_field = schema.Timedelta(required=False, min=timedelta(100))
265267
test_list_value_type_field = schema.List(required=False, value_type=schema.Int())

src/plone/restapi/tests/test_dxfield_deserializer.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,48 +114,54 @@ def test_date_deserialization_returns_date(self):
114114
self.assertTrue(isinstance(value, date))
115115
self.assertEqual(date(2015, 12, 20), value)
116116

117-
def test_datetime_deserialization_returns_datetime(self):
118-
value = self.deserialize("test_datetime_field", "2015-12-20T10:39:54.361Z")
119-
self.assertTrue(isinstance(value, datetime), "Not a <datetime>")
120-
self.assertEqual(datetime(2015, 12, 20, 10, 39, 54, 361000), value)
121-
122-
def test_datetime_deserialization_handles_timezone(self):
117+
def test_datetime_deserialization_defaults_to_timezone_from_request(self):
118+
self.portal.doc1.test_datetime_field = None
123119
value = self.deserialize("test_datetime_field", "2015-12-20T10:39:54.361+01")
124-
self.assertEqual(datetime(2015, 12, 20, 9, 39, 54, 361000), value)
125-
126-
def test_datetime_deserialization_with_tznaive_stored(self):
127-
self.portal.doc1.test_datetime_field = datetime.now()
128-
value = self.deserialize("test_datetime_field", "2015-12-20T10:39:54.361")
129-
self.assertEqual(datetime(2015, 12, 20, 10, 39, 54, 361000), value)
130-
131-
def test_datetime_with_tz_deserialization_keeps_timezone(self):
132-
value = self.deserialize("test_datetime_tz_field", "2015-12-20T10:39:54.361+01")
133120
self.assertEqual(
134121
timezone("Europe/Zurich").localize(
135122
datetime(2015, 12, 20, 10, 39, 54, 361000)
136123
),
137124
value,
138125
)
139126

140-
def test_datetime_with_tz_deserialization_converts_timezone(self):
141-
value = self.deserialize("test_datetime_tz_field", "2015-12-20T10:39:54.361-04")
127+
def test_datetime_deserialization_defaults_to_utc(self):
128+
self.portal.doc1.test_datetime_field = None
129+
value = self.deserialize("test_datetime_field", "2015-12-20T10:39:54.361")
130+
self.assertEqual(
131+
datetime(2015, 12, 20, 10, 39, 54, 361000, timezone("UTC")), value
132+
)
133+
134+
def test_datetime_deserialization_converts_to_existing_timezone(self):
135+
self.portal.doc1.test_datetime_field = timezone("Europe/Zurich").localize(
136+
datetime.now()
137+
)
138+
value = self.deserialize("test_datetime_tz_field", "2015-12-20T10:39:54.361+01")
142139
self.assertEqual(
143140
timezone("Europe/Zurich").localize(
144-
datetime(2015, 12, 20, 15, 39, 54, 361000)
141+
datetime(2015, 12, 20, 10, 39, 54, 361000)
145142
),
146143
value,
147144
)
148145

149-
def test_datetime_with_tz_deserialization_adds_timezone(self):
150-
value = self.deserialize("test_datetime_tz_field", "2015-12-20T10:39:54.361")
146+
def test_datetime_deserialization_adds_existing_timezone(self):
147+
self.portal.doc1.test_datetime_field = timezone("Europe/Zurich").localize(
148+
datetime.now()
149+
)
150+
value = self.deserialize("test_datetime_tz_field", "2015-12-20T9:39:54.361")
151151
self.assertEqual(
152152
timezone("Europe/Zurich").localize(
153-
datetime(2015, 12, 20, 11, 39, 54, 361000)
153+
datetime(2015, 12, 20, 10, 39, 54, 361000)
154154
),
155155
value,
156156
)
157157

158+
def test_datetime_deserialization_converts_to_stored_tznaive(self):
159+
self.portal.doc1.test_datetime_field = datetime.now()
160+
value = self.deserialize("test_datetime_field", "2015-12-20T10:39:54.361Z")
161+
self.assertEqual(datetime(2015, 12, 20, 10, 39, 54, 361000), value)
162+
158163
def test_datetime_with_tz_deserialization_handles_dst(self):
164+
self.portal.doc1.test_datetime_field = None
159165
value = self.deserialize("test_datetime_tz_field", "2015-05-20T10:39:54.361+02")
160166
self.assertEqual(
161167
timezone("Europe/Zurich").localize(

0 commit comments

Comments
 (0)