Compare commits
No commits in common. "652477bee02bd565ebe17e1a31f722de71b22792" and "286c683c9350a1f85c9ed6c3c0ecedaea7ce6803" have entirely different histories.
652477bee0
...
286c683c93
12 changed files with 95 additions and 286 deletions
10
CHANGELOG.md
10
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/)
|
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).
|
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)
|
## v0.24.0 (2025-12-15)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.25.0"
|
version = "0.24.0"
|
||||||
description = "Web App for Wutta Framework"
|
description = "Web App for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
@ -44,7 +44,7 @@ dependencies = [
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttJamaican[db]>=0.26.0",
|
"WuttJamaican[db]>=0.25.0",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import colander
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from wuttjamaican.conf import parse_list
|
from wuttjamaican.conf import parse_list
|
||||||
|
from wuttjamaican.util import localtime, make_utc
|
||||||
|
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
|
|
@ -52,18 +53,18 @@ class WuttaDateTime(colander.DateTime):
|
||||||
if not appstruct:
|
if not appstruct:
|
||||||
return colander.null
|
return colander.null
|
||||||
|
|
||||||
request = node.widget.request
|
if appstruct.tzinfo is None:
|
||||||
config = request.wutta_config
|
appstruct = localtime(appstruct)
|
||||||
app = config.get_app()
|
|
||||||
|
|
||||||
dt = app.localtime(appstruct)
|
|
||||||
if self.format:
|
if self.format:
|
||||||
return dt.strftime(self.format)
|
return appstruct.strftime(self.format)
|
||||||
return dt.isoformat()
|
|
||||||
|
|
||||||
def deserialize( # pylint: disable=inconsistent-return-statements
|
return appstruct.isoformat()
|
||||||
|
|
||||||
|
def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring
|
||||||
self, node, cstruct
|
self, node, cstruct
|
||||||
):
|
):
|
||||||
|
""" """
|
||||||
if not cstruct:
|
if not cstruct:
|
||||||
return colander.null
|
return colander.null
|
||||||
|
|
||||||
|
|
@ -72,16 +73,11 @@ class WuttaDateTime(colander.DateTime):
|
||||||
"%Y-%m-%dT%I:%M %p",
|
"%Y-%m-%dT%I:%M %p",
|
||||||
]
|
]
|
||||||
|
|
||||||
request = node.widget.request
|
|
||||||
config = request.wutta_config
|
|
||||||
app = config.get_app()
|
|
||||||
|
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
try:
|
try:
|
||||||
dt = datetime.datetime.strptime(cstruct, fmt)
|
dt = datetime.datetime.strptime(cstruct, fmt)
|
||||||
if not dt.tzinfo:
|
dt = dt.astimezone()
|
||||||
dt = app.localtime(dt, from_utc=False)
|
return make_utc(dt)
|
||||||
return app.make_utc(dt)
|
|
||||||
except Exception: # pylint: disable=broad-exception-caught
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,9 +252,7 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
|
||||||
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
readonly = kw.get("readonly", self.readonly)
|
readonly = kw.get("readonly", self.readonly)
|
||||||
if readonly:
|
if readonly and cstruct:
|
||||||
if not cstruct:
|
|
||||||
return ""
|
|
||||||
dt = datetime.datetime.fromisoformat(cstruct)
|
dt = datetime.datetime.fromisoformat(cstruct)
|
||||||
return self.app.render_datetime(dt)
|
return self.app.render_datetime(dt)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,6 @@
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Time Zone"
|
|
||||||
:message="timezoneFieldMessage"
|
|
||||||
:type="timezoneFieldType">
|
|
||||||
<b-input name="${app.appname}.timezone.default"
|
|
||||||
v-model="simpleSettings['${app.appname}.timezone.default']"
|
|
||||||
## TODO: ideally could use @change here but it does not work..?
|
|
||||||
##@change="timezoneCheck()"
|
|
||||||
@input="timezoneCheck(); settingsNeedSaved = true" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Menu Handler">
|
<b-field label="Menu Handler">
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="${app.appname}.web.menus.handler.spec"
|
name="${app.appname}.web.menus.handler.spec"
|
||||||
|
|
@ -280,74 +270,6 @@
|
||||||
|
|
||||||
ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
|
ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
|
||||||
|
|
||||||
ThisPageData.timezoneChecking = false
|
|
||||||
ThisPageData.timezoneInvalid = false
|
|
||||||
ThisPageData.timezoneError = false
|
|
||||||
|
|
||||||
ThisPage.computed.timezoneFieldMessage = function() {
|
|
||||||
if (this.timezoneChecking) {
|
|
||||||
return "Working, please wait..."
|
|
||||||
}
|
|
||||||
if (this.timezoneInvalid) {
|
|
||||||
return this.timezoneInvalid
|
|
||||||
}
|
|
||||||
if (this.timezoneError) {
|
|
||||||
return this.timezoneError
|
|
||||||
}
|
|
||||||
return "RESTART REQUIRED IF YOU CHANGE THIS. The system (default) timezone is: ${default_timezone}"
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.computed.timezoneFieldType = function() {
|
|
||||||
if (this.timezoneChecking) {
|
|
||||||
return 'is-warning'
|
|
||||||
}
|
|
||||||
if (this.timezoneInvalid || this.timezoneError) {
|
|
||||||
return 'is-danger'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.timezoneCheck = function() {
|
|
||||||
if (this.timezoneChecking) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timezoneError = false
|
|
||||||
|
|
||||||
if (!this.simpleSettings['${config.appname}.timezone.default']) {
|
|
||||||
this.timezoneInvalid = false
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.timezoneChecking = true
|
|
||||||
const url = '${url(f"{route_prefix}.check_timezone")}'
|
|
||||||
const params = {
|
|
||||||
tzname: this.simpleSettings['${config.appname}.timezone.default'],
|
|
||||||
}
|
|
||||||
this.wuttaGET(url, params, response => {
|
|
||||||
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.weblibs = ${json.dumps(weblibs or [])|n}
|
||||||
|
|
||||||
ThisPageData.editWebLibraryShowDialog = false
|
ThisPageData.editWebLibraryShowDialog = false
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,6 @@
|
||||||
<b-field horizontal label="Node Title">
|
<b-field horizontal label="Node Title">
|
||||||
<span>${app.get_node_title()}</span>
|
<span>${app.get_node_title()}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field horizontal label="Time Zone">
|
|
||||||
<span>${app.get_timezone_name()}</span>
|
|
||||||
</b-field>
|
|
||||||
<b-field horizontal label="Production Mode">
|
<b-field horizontal label="Production Mode">
|
||||||
<span>${"Yes" if config.production() else "No"}</span>
|
<span>${"Yes" if config.production() else "No"}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
Views for app settings
|
Views for app settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -32,7 +31,6 @@ import subprocess
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from wuttjamaican.db.model import Setting
|
from wuttjamaican.db.model import Setting
|
||||||
from wuttjamaican.util import get_timezone_by_name
|
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.util import get_libver, get_liburl
|
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}.node_title"},
|
||||||
{"name": f"{self.config.appname}.production", "type": bool},
|
{"name": f"{self.config.appname}.production", "type": bool},
|
||||||
{"name": "wuttaweb.themes.expose_picker", "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"},
|
{"name": f"{self.config.appname}.web.menus.handler.spec"},
|
||||||
# nb. this is deprecated; we define so it is auto-deleted
|
# nb. this is deprecated; we define so it is auto-deleted
|
||||||
# when we replace with newer setting
|
# when we replace with newer setting
|
||||||
|
|
@ -177,31 +174,12 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return simple_settings
|
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
|
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
|
||||||
self, **kwargs
|
self, **kwargs
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
|
|
||||||
# default system timezone
|
|
||||||
dt = datetime.datetime.now().astimezone()
|
|
||||||
context["default_timezone"] = dt.tzname()
|
|
||||||
|
|
||||||
# add registered menu handlers
|
# add registered menu handlers
|
||||||
web = self.app.get_web_handler()
|
web = self.app.get_web_handler()
|
||||||
handlers = web.get_menu_handler_specs()
|
handlers = web.get_menu_handler_specs()
|
||||||
|
|
@ -244,32 +222,6 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return context
|
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
|
class SettingView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,85 +11,87 @@ from pyramid import testing
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttjamaican.util import get_timezone_by_name
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
from wuttaweb.forms import schema as mod
|
from wuttaweb.forms import schema as mod
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaDateTime(WebTestCase):
|
class TestWuttaDateTime(TestCase):
|
||||||
|
|
||||||
def test_serialize(self):
|
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()
|
typ = mod.WuttaDateTime()
|
||||||
node = colander.SchemaNode(
|
node = colander.SchemaNode(typ)
|
||||||
typ, widget=widgets.WuttaDateTimeWidget(self.request)
|
|
||||||
)
|
|
||||||
|
|
||||||
# null
|
result = typ.serialize(node, colander.null)
|
||||||
self.assertIs(typ.serialize(node, colander.null), colander.null)
|
self.assertIs(result, colander.null)
|
||||||
self.assertIs(typ.serialize(node, None), colander.null)
|
|
||||||
self.assertIs(typ.serialize(node, ""), colander.null)
|
result = typ.serialize(node, None)
|
||||||
|
self.assertIs(result, colander.null)
|
||||||
|
|
||||||
|
result = typ.serialize(node, "")
|
||||||
|
self.assertIs(result, colander.null)
|
||||||
|
|
||||||
# naive, UTC
|
# naive, UTC
|
||||||
|
# TODO: must override local timezone for a complete test
|
||||||
result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
|
result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
|
||||||
self.assertEqual(result, "2024-12-11T14:33:00-08:00")
|
self.assertTrue(result.startswith("2024-12-"))
|
||||||
|
|
||||||
# aware, UTC
|
# aware, UTC
|
||||||
result = typ.serialize(
|
result = typ.serialize(
|
||||||
node,
|
node, datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc)
|
||||||
datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
|
|
||||||
)
|
)
|
||||||
self.assertEqual(result, "2024-12-11T14:33:00-08:00")
|
self.assertEqual(result, "2024-12-11T22:33:00+00:00")
|
||||||
|
|
||||||
# aware, local
|
# aware, local
|
||||||
result = typ.serialize(
|
result = typ.serialize(
|
||||||
node,
|
node,
|
||||||
datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
|
datetime.datetime(
|
||||||
|
2024,
|
||||||
|
12,
|
||||||
|
11,
|
||||||
|
22,
|
||||||
|
33,
|
||||||
|
tzinfo=datetime.timezone(-datetime.timedelta(hours=5)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(result, "2024-12-11T14:33:00-08:00")
|
self.assertEqual(result, "2024-12-11T22:33:00-05:00")
|
||||||
|
|
||||||
# custom format
|
# custom format
|
||||||
typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p")
|
typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p")
|
||||||
node = colander.SchemaNode(
|
node = colander.SchemaNode(typ)
|
||||||
typ, widget=widgets.WuttaDateTimeWidget(self.request)
|
|
||||||
)
|
|
||||||
result = typ.serialize(
|
result = typ.serialize(
|
||||||
node,
|
node, datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc)
|
||||||
datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
|
|
||||||
)
|
)
|
||||||
self.assertEqual(result, "2024-12-11 02:33 PM")
|
self.assertEqual(result, "2024-12-11 10:33 PM")
|
||||||
|
|
||||||
def test_deserialize(self):
|
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()
|
typ = mod.WuttaDateTime()
|
||||||
node = colander.SchemaNode(
|
node = colander.SchemaNode(typ)
|
||||||
typ, widget=widgets.WuttaDateTimeWidget(self.request)
|
|
||||||
)
|
|
||||||
|
|
||||||
# null
|
result = typ.deserialize(node, colander.null)
|
||||||
self.assertIs(typ.deserialize(node, colander.null), colander.null)
|
self.assertIs(result, colander.null)
|
||||||
self.assertIs(typ.deserialize(node, None), colander.null)
|
|
||||||
self.assertIs(typ.deserialize(node, ""), colander.null)
|
|
||||||
|
|
||||||
# format #1
|
result = typ.deserialize(node, None)
|
||||||
result = typ.deserialize(node, "2024-12-11T22:33:00")
|
self.assertIs(result, colander.null)
|
||||||
self.assertIsInstance(result, datetime.datetime)
|
|
||||||
self.assertEqual(
|
|
||||||
result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
|
|
||||||
)
|
|
||||||
|
|
||||||
# format #2
|
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")
|
result = typ.deserialize(node, "2024-12-11T10:33 PM")
|
||||||
self.assertIsInstance(result, datetime.datetime)
|
self.assertIsInstance(result, datetime.datetime)
|
||||||
self.assertEqual(
|
dt = datetime.datetime(2024, 12, 11, 22, 33)
|
||||||
result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
|
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)
|
||||||
|
|
||||||
# invalid
|
|
||||||
self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
|
self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import colander
|
||||||
import deform
|
import deform
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
from wuttjamaican.util import get_timezone_by_name
|
|
||||||
from wuttaweb import grids
|
from wuttaweb import grids
|
||||||
from wuttaweb.forms import widgets as mod
|
from wuttaweb.forms import widgets as mod
|
||||||
from wuttaweb.forms import schema
|
from wuttaweb.forms import schema
|
||||||
|
|
@ -146,36 +145,19 @@ class TestWuttaDateTimeWidget(WebTestCase):
|
||||||
def make_widget(self, **kwargs):
|
def make_widget(self, **kwargs):
|
||||||
return mod.WuttaDateTimeWidget(self.request, **kwargs)
|
return mod.WuttaDateTimeWidget(self.request, **kwargs)
|
||||||
|
|
||||||
def test_serialize_editable(self):
|
def test_serialize(self):
|
||||||
tzlocal = get_timezone_by_name("America/New_York")
|
node = colander.SchemaNode(WuttaDateTime())
|
||||||
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)
|
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"
|
# editable widget has normal picker html
|
||||||
dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=tzlocal)
|
result = widget.serialize(field, str(dt))
|
||||||
result = widget.serialize(field, dt.isoformat())
|
|
||||||
self.assertIn("<wutta-datepicker", result)
|
self.assertIn("<wutta-datepicker", result)
|
||||||
|
|
||||||
def test_serialize_readonly(self):
|
# readonly is rendered per app convention
|
||||||
tzlocal = get_timezone_by_name("America/New_York")
|
result = widget.serialize(field, str(dt), readonly=True)
|
||||||
with patch.object(self.app, "get_timezone", return_value=tzlocal):
|
self.assertEqual(result, "2024-12-12 13:49+0000")
|
||||||
widget = self.make_widget(readonly=True)
|
|
||||||
self.assertTrue(widget.readonly)
|
|
||||||
node = colander.SchemaNode(WuttaDateTime(), widget=widget)
|
|
||||||
field = self.make_field(node)
|
|
||||||
|
|
||||||
# null
|
|
||||||
self.assertEqual(widget.serialize(field, colander.null), "")
|
|
||||||
self.assertEqual(widget.serialize(field, None), "")
|
|
||||||
self.assertEqual(widget.serialize(field, ""), "")
|
|
||||||
|
|
||||||
# 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.assertEqual(result, "2024-12-12 13:49-0500")
|
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaMoneyInputWidget(WebTestCase):
|
class TestWuttaMoneyInputWidget(WebTestCase):
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ from paginate_sqlalchemy import SqlalchemyOrmPage
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttjamaican.util import get_timezone_by_name
|
|
||||||
from wuttaweb.grids import base as mod
|
from wuttaweb.grids import base as mod
|
||||||
from wuttaweb.grids.filters import (
|
from wuttaweb.grids.filters import (
|
||||||
GridFilter,
|
GridFilter,
|
||||||
|
|
@ -1655,21 +1654,16 @@ class TestGrid(WebTestCase):
|
||||||
self.assertEqual(result, "2025-01-13")
|
self.assertEqual(result, "2025-01-13")
|
||||||
|
|
||||||
def test_render_datetime(self):
|
def test_render_datetime(self):
|
||||||
tzlocal = get_timezone_by_name("America/Los_Angeles")
|
|
||||||
with patch.object(self.app, "get_timezone", return_value=tzlocal):
|
|
||||||
grid = self.make_grid(columns=["foo", "bar"])
|
grid = self.make_grid(columns=["foo", "bar"])
|
||||||
|
|
||||||
# null
|
|
||||||
obj = MagicMock(dt=None)
|
obj = MagicMock(dt=None)
|
||||||
result = grid.render_datetime(obj, "dt", None)
|
result = grid.render_datetime(obj, "dt", None)
|
||||||
self.assertEqual(result, "")
|
self.assertEqual(result, "")
|
||||||
|
|
||||||
# normal (naive utc)
|
dt = datetime.datetime(2024, 12, 12, 13, 44, tzinfo=datetime.timezone.utc)
|
||||||
dt = datetime.datetime(2024, 12, 12, 13, 44)
|
|
||||||
obj = MagicMock(dt=dt)
|
obj = MagicMock(dt=dt)
|
||||||
result = grid.render_datetime(obj, "dt", str(dt))
|
result = grid.render_datetime(obj, "dt", str(dt))
|
||||||
self.assertEqual(result, "2024-12-12 05:44-0800")
|
self.assertEqual(result, "2024-12-12 13:44+0000")
|
||||||
self.assertNotEqual(result, str(dt))
|
|
||||||
|
|
||||||
def test_render_vue_tag(self):
|
def test_render_vue_tag(self):
|
||||||
grid = self.make_grid(columns=["foo", "bar"])
|
grid = self.make_grid(columns=["foo", "bar"])
|
||||||
|
|
|
||||||
|
|
@ -1663,9 +1663,6 @@ class TestMasterView(WebTestCase):
|
||||||
def test_configure(self):
|
def test_configure(self):
|
||||||
self.pyramid_config.include("wuttaweb.views.common")
|
self.pyramid_config.include("wuttaweb.views.common")
|
||||||
self.pyramid_config.include("wuttaweb.views.auth")
|
self.pyramid_config.include("wuttaweb.views.auth")
|
||||||
self.pyramid_config.add_route(
|
|
||||||
"appinfo.check_timezone", "/appinfo/check-timezone"
|
|
||||||
)
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# mock settings
|
# mock settings
|
||||||
|
|
@ -1700,7 +1697,6 @@ class TestMasterView(WebTestCase):
|
||||||
def get_context(**kw):
|
def get_context(**kw):
|
||||||
kw = original_context(**kw)
|
kw = original_context(**kw)
|
||||||
kw["menu_handlers"] = []
|
kw["menu_handlers"] = []
|
||||||
kw["default_timezone"] = "UTC"
|
|
||||||
return kw
|
return kw
|
||||||
|
|
||||||
with patch.object(view, "configure_get_context", new=get_context):
|
with patch.object(view, "configure_get_context", new=get_context):
|
||||||
|
|
|
||||||
|
|
@ -46,26 +46,6 @@ class TestAppInfoView(WebTestCase):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
context = view.configure_get_context()
|
context = view.configure_get_context()
|
||||||
|
|
||||||
def test_configure_check_timezone(self):
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
# normal
|
|
||||||
with patch.object(self.request, "GET", new={"tzname": "America/Chicago"}):
|
|
||||||
result = view.configure_check_timezone()
|
|
||||||
self.assertFalse(result["invalid"])
|
|
||||||
|
|
||||||
# invalid
|
|
||||||
with patch.object(self.request, "GET", new={"tzname": "bad_name"}):
|
|
||||||
result = view.configure_check_timezone()
|
|
||||||
self.assertEqual(
|
|
||||||
result["invalid"], "'No time zone found with key bad_name'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# missing input
|
|
||||||
with patch.object(self.request, "GET", new={}):
|
|
||||||
result = view.configure_check_timezone()
|
|
||||||
self.assertEqual(result["invalid"], "Must provide 'tzname' parameter.")
|
|
||||||
|
|
||||||
|
|
||||||
class TestSettingView(WebTestCase):
|
class TestSettingView(WebTestCase):
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue