diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbba860..470d4b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,16 +5,6 @@ All notable changes to wuttaweb will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## v0.25.0 (2025-12-17)
-
-### Feat
-
-- add "complete" (sic) timezone support
-
-### Fix
-
-- add local timezone awareness for datetime fields
-
## v0.24.0 (2025-12-15)
### Feat
diff --git a/pyproject.toml b/pyproject.toml
index 1cd984b..337c2a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.25.0"
+version = "0.24.0"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -44,7 +44,7 @@ dependencies = [
"pyramid_tm",
"waitress",
"WebHelpers2",
- "WuttJamaican[db]>=0.26.0",
+ "WuttJamaican[db]>=0.25.0",
"zope.sqlalchemy>=1.5",
]
diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py
index 37db3e9..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
@@ -52,18 +53,18 @@ class WuttaDateTime(colander.DateTime):
if not appstruct:
return colander.null
- request = node.widget.request
- config = request.wutta_config
- app = config.get_app()
+ if appstruct.tzinfo is None:
+ appstruct = localtime(appstruct)
- dt = app.localtime(appstruct)
if self.format:
- return dt.strftime(self.format)
- return dt.isoformat()
+ return appstruct.strftime(self.format)
- def deserialize( # pylint: disable=inconsistent-return-statements
+ return appstruct.isoformat()
+
+ def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring
self, node, cstruct
):
+ """ """
if not cstruct:
return colander.null
@@ -72,16 +73,11 @@ class WuttaDateTime(colander.DateTime):
"%Y-%m-%dT%I:%M %p",
]
- request = node.widget.request
- config = request.wutta_config
- app = config.get_app()
-
for fmt in formats:
try:
dt = datetime.datetime.strptime(cstruct, fmt)
- if not dt.tzinfo:
- dt = app.localtime(dt, from_utc=False)
- return app.make_utc(dt)
+ dt = dt.astimezone()
+ return make_utc(dt)
except Exception: # pylint: disable=broad-exception-caught
pass
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index 0ef6d87..8dc6241 100644
--- a/src/wuttaweb/forms/widgets.py
+++ b/src/wuttaweb/forms/widgets.py
@@ -252,9 +252,7 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
""" """
readonly = kw.get("readonly", self.readonly)
- if readonly:
- if not cstruct:
- return ""
+ if readonly and cstruct:
dt = datetime.datetime.fromisoformat(cstruct)
return self.app.render_datetime(dt)
diff --git a/src/wuttaweb/templates/appinfo/configure.mako b/src/wuttaweb/templates/appinfo/configure.mako
index fc0d886..556d76d 100644
--- a/src/wuttaweb/templates/appinfo/configure.mako
+++ b/src/wuttaweb/templates/appinfo/configure.mako
@@ -48,16 +48,6 @@
-
-
-
-
{
- this.timezoneInvalid = response.data.invalid
- this.timezoneChecking = false
- }, response => {
- this.timezoneError = response?.data?.error || "unknown error"
- this.timezoneChecking = false
- })
- }
- }
-
- ThisPage.methods.timezoneValidate = function() {
- if (this.timezoneChecking) {
- return "Still checking time zone, please try again in a moment."
- }
-
- if (this.timezoneError) {
- return "Error checking time zone! Please reload page and try again."
- }
-
- if (this.timezoneInvalid) {
- return "The time zone is invalid!"
- }
- }
-
- ThisPageData.validators.push(ThisPage.methods.timezoneValidate)
-
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
ThisPageData.editWebLibraryShowDialog = false
diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako
index 7f1abb5..ab49018 100644
--- a/src/wuttaweb/templates/appinfo/index.mako
+++ b/src/wuttaweb/templates/appinfo/index.mako
@@ -19,9 +19,6 @@
${app.get_node_title()}
-
- ${app.get_timezone_name()}
-
${"Yes" if config.production() else "No"}
diff --git a/src/wuttaweb/views/settings.py b/src/wuttaweb/views/settings.py
index d365da2..11cb0d0 100644
--- a/src/wuttaweb/views/settings.py
+++ b/src/wuttaweb/views/settings.py
@@ -24,7 +24,6 @@
Views for app settings
"""
-import datetime
import json
import os
import sys
@@ -32,7 +31,6 @@ import subprocess
from collections import OrderedDict
from wuttjamaican.db.model import Setting
-from wuttjamaican.util import get_timezone_by_name
from wuttaweb.views import MasterView
from wuttaweb.util import get_libver, get_liburl
@@ -136,7 +134,6 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
{"name": f"{self.config.appname}.node_title"},
{"name": f"{self.config.appname}.production", "type": bool},
{"name": "wuttaweb.themes.expose_picker", "type": bool},
- {"name": f"{self.config.appname}.timezone.default"},
{"name": f"{self.config.appname}.web.menus.handler.spec"},
# nb. this is deprecated; we define so it is auto-deleted
# when we replace with newer setting
@@ -177,31 +174,12 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
return simple_settings
- def configure_check_timezone(self):
- """
- AJAX view to validate a user-specified timezone name.
-
- Route name for this is: ``appinfo.check_timezone``
- """
- tzname = self.request.GET.get("tzname")
- if not tzname:
- return {"invalid": "Must provide 'tzname' parameter."}
- try:
- get_timezone_by_name(tzname)
- return {"invalid": False}
- except Exception as err: # pylint: disable=broad-exception-caught
- return {"invalid": str(err)}
-
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
self, **kwargs
):
""" """
context = super().configure_get_context(**kwargs)
- # default system timezone
- dt = datetime.datetime.now().astimezone()
- context["default_timezone"] = dt.tzname()
-
# add registered menu handlers
web = self.app.get_web_handler()
handlers = web.get_menu_handler_specs()
@@ -244,32 +222,6 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
return context
- @classmethod
- def defaults(cls, config): # pylint: disable=empty-docstring
- """ """
- cls._defaults(config)
- cls._appinfo_defaults(config)
-
- @classmethod
- def _appinfo_defaults(cls, config):
- route_prefix = cls.get_route_prefix()
- permission_prefix = cls.get_permission_prefix()
- url_prefix = cls.get_url_prefix()
-
- # check timezone
- config.add_route(
- f"{route_prefix}.check_timezone",
- f"{url_prefix}/check-timezone",
- request_method="GET",
- )
- config.add_view(
- cls,
- attr="configure_check_timezone",
- route_name=f"{route_prefix}.check_timezone",
- permission=f"{permission_prefix}.configure",
- renderer="json",
- )
-
class SettingView(MasterView): # pylint: disable=abstract-method
"""
diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py
index a01ce35..bfa54c1 100644
--- a/tests/forms/test_schema.py
+++ b/tests/forms/test_schema.py
@@ -11,86 +11,88 @@ from pyramid import testing
from sqlalchemy import orm
from wuttjamaican.conf import WuttaConfig
-from wuttjamaican.util import get_timezone_by_name
from wuttjamaican.testing import DataTestCase
from wuttaweb.forms import schema as mod
from wuttaweb.forms import widgets
from wuttaweb.testing import WebTestCase
-class TestWuttaDateTime(WebTestCase):
+class TestWuttaDateTime(TestCase):
def test_serialize(self):
- tzlocal = get_timezone_by_name("America/Los_Angeles")
- with patch.object(self.app, "get_timezone", return_value=tzlocal):
- typ = mod.WuttaDateTime()
- node = colander.SchemaNode(
- typ, widget=widgets.WuttaDateTimeWidget(self.request)
- )
+ typ = mod.WuttaDateTime()
+ node = colander.SchemaNode(typ)
- # null
- self.assertIs(typ.serialize(node, colander.null), colander.null)
- self.assertIs(typ.serialize(node, None), colander.null)
- self.assertIs(typ.serialize(node, ""), colander.null)
+ result = typ.serialize(node, colander.null)
+ self.assertIs(result, colander.null)
- # naive, UTC
- result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
- self.assertEqual(result, "2024-12-11T14:33:00-08:00")
+ result = typ.serialize(node, None)
+ self.assertIs(result, colander.null)
- # aware, UTC
- result = typ.serialize(
- node,
- datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
- )
- self.assertEqual(result, "2024-12-11T14:33:00-08:00")
+ result = typ.serialize(node, "")
+ self.assertIs(result, colander.null)
- # aware, local
- result = typ.serialize(
- node,
- datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
- )
- self.assertEqual(result, "2024-12-11T14:33:00-08:00")
+ # 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-"))
- # custom format
- typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p")
- node = colander.SchemaNode(
- typ, widget=widgets.WuttaDateTimeWidget(self.request)
- )
- result = typ.serialize(
- node,
- datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
- )
- self.assertEqual(result, "2024-12-11 02:33 PM")
+ # 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):
- tzlocal = get_timezone_by_name("America/Los_Angeles")
- with patch.object(self.app, "get_timezone", return_value=tzlocal):
- typ = mod.WuttaDateTime()
- node = colander.SchemaNode(
- typ, widget=widgets.WuttaDateTimeWidget(self.request)
- )
+ typ = mod.WuttaDateTime()
+ node = colander.SchemaNode(typ)
- # null
- self.assertIs(typ.deserialize(node, colander.null), colander.null)
- self.assertIs(typ.deserialize(node, None), colander.null)
- self.assertIs(typ.deserialize(node, ""), colander.null)
+ result = typ.deserialize(node, colander.null)
+ self.assertIs(result, colander.null)
- # format #1
- result = typ.deserialize(node, "2024-12-11T22:33:00")
- self.assertIsInstance(result, datetime.datetime)
- self.assertEqual(
- result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
- )
+ result = typ.deserialize(node, None)
+ self.assertIs(result, colander.null)
- # format #2
- result = typ.deserialize(node, "2024-12-11T10:33 PM")
- self.assertIsInstance(result, datetime.datetime)
- self.assertEqual(
- result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
- )
+ result = typ.deserialize(node, "")
+ self.assertIs(result, colander.null)
- # invalid
- self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
+ # TODO: must override local timezone for a complete test
+ result = typ.deserialize(node, "2024-12-11T10:33 PM")
+ self.assertIsInstance(result, datetime.datetime)
+ 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)
+ 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")
class TestObjectNode(DataTestCase):
diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py
index d3b17aa..22c4f00 100644
--- a/tests/forms/test_widgets.py
+++ b/tests/forms/test_widgets.py
@@ -8,7 +8,6 @@ import colander
import deform
from pyramid import testing
-from wuttjamaican.util import get_timezone_by_name
from wuttaweb import grids
from wuttaweb.forms import widgets as mod
from wuttaweb.forms import schema
@@ -146,36 +145,19 @@ class TestWuttaDateTimeWidget(WebTestCase):
def make_widget(self, **kwargs):
return mod.WuttaDateTimeWidget(self.request, **kwargs)
- def test_serialize_editable(self):
- tzlocal = get_timezone_by_name("America/New_York")
- with patch.object(self.app, "get_timezone", return_value=tzlocal):
- widget = self.make_widget()
- self.assertFalse(widget.readonly)
- node = colander.SchemaNode(WuttaDateTime(), widget=widget)
- field = self.make_field(node)
+ def test_serialize(self):
+ node = colander.SchemaNode(WuttaDateTime())
+ field = self.make_field(node)
+ widget = self.make_widget()
+ dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=datetime.timezone.utc)
- # nb. input data (from schema type) is always "local, zone-aware, isoformat"
- dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=tzlocal)
- result = widget.serialize(field, dt.isoformat())
- self.assertIn("