diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index f19e8c3..dda5290 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -31,6 +31,7 @@ import colander import sqlalchemy as sa from wuttjamaican.conf import parse_list +from wuttjamaican.util import localtime, make_utc from wuttaweb.db import Session from wuttaweb.forms import widgets @@ -48,6 +49,18 @@ class WuttaDateTime(colander.DateTime): the Buefy datepicker + timepicker widgets. """ + def serialize(self, node, appstruct): + if not appstruct: + return colander.null + + if appstruct.tzinfo is None: + appstruct = localtime(appstruct) + + if self.format: + return appstruct.strftime(self.format) + + return appstruct.isoformat() + def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring self, node, cstruct ): @@ -62,7 +75,9 @@ class WuttaDateTime(colander.DateTime): for fmt in formats: try: - return datetime.datetime.strptime(cstruct, fmt) + dt = datetime.datetime.strptime(cstruct, fmt) + dt = dt.astimezone() + return make_utc(dt) except Exception: # pylint: disable=broad-exception-caught pass diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py index a4189b5..bfa54c1 100644 --- a/tests/forms/test_schema.py +++ b/tests/forms/test_schema.py @@ -19,6 +19,52 @@ from wuttaweb.testing import WebTestCase class TestWuttaDateTime(TestCase): + def test_serialize(self): + typ = mod.WuttaDateTime() + node = colander.SchemaNode(typ) + + result = typ.serialize(node, colander.null) + self.assertIs(result, colander.null) + + result = typ.serialize(node, None) + self.assertIs(result, colander.null) + + result = typ.serialize(node, "") + self.assertIs(result, colander.null) + + # naive, UTC + # TODO: must override local timezone for a complete test + result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33)) + self.assertTrue(result.startswith("2024-12-")) + + # aware, UTC + result = typ.serialize( + node, datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc) + ) + self.assertEqual(result, "2024-12-11T22:33:00+00:00") + + # aware, local + result = typ.serialize( + node, + datetime.datetime( + 2024, + 12, + 11, + 22, + 33, + tzinfo=datetime.timezone(-datetime.timedelta(hours=5)), + ), + ) + self.assertEqual(result, "2024-12-11T22:33:00-05:00") + + # custom format + typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p") + node = colander.SchemaNode(typ) + result = typ.serialize( + node, datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc) + ) + self.assertEqual(result, "2024-12-11 10:33 PM") + def test_deserialize(self): typ = mod.WuttaDateTime() node = colander.SchemaNode(typ) @@ -26,14 +72,24 @@ class TestWuttaDateTime(TestCase): result = typ.deserialize(node, colander.null) self.assertIs(result, colander.null) + result = typ.deserialize(node, None) + self.assertIs(result, colander.null) + + result = typ.deserialize(node, "") + self.assertIs(result, colander.null) + + # TODO: must override local timezone for a complete test result = typ.deserialize(node, "2024-12-11T10:33 PM") self.assertIsInstance(result, datetime.datetime) - self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33)) + dt = datetime.datetime(2024, 12, 11, 22, 33) + self.assertLess(abs((result - dt).total_seconds()), 60 * 60 * 24) self.assertIsNone(result.tzinfo) + # TODO: must override local timezone for a complete test result = typ.deserialize(node, "2024-12-11T22:33:00") self.assertIsInstance(result, datetime.datetime) - self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33)) + dt = datetime.datetime(2024, 12, 11, 22, 33) + self.assertLess(abs((result - dt).total_seconds()), 60 * 60 * 24) self.assertIsNone(result.tzinfo) self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")