feat: add "complete" (sic) timezone support
at least for now, this is enough to let admin define the global default timezone for app, and override system local timezone. eventually should support per-user timezone..some day..
This commit is contained in:
parent
286c683c93
commit
7fcb331806
10 changed files with 274 additions and 93 deletions
|
|
@ -31,7 +31,6 @@ 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
|
||||
|
|
@ -53,18 +52,18 @@ class WuttaDateTime(colander.DateTime):
|
|||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
if appstruct.tzinfo is None:
|
||||
appstruct = localtime(appstruct)
|
||||
request = node.widget.request
|
||||
config = request.wutta_config
|
||||
app = config.get_app()
|
||||
|
||||
dt = app.localtime(appstruct)
|
||||
if self.format:
|
||||
return appstruct.strftime(self.format)
|
||||
return dt.strftime(self.format)
|
||||
return dt.isoformat()
|
||||
|
||||
return appstruct.isoformat()
|
||||
|
||||
def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring
|
||||
def deserialize( # pylint: disable=inconsistent-return-statements
|
||||
self, node, cstruct
|
||||
):
|
||||
""" """
|
||||
if not cstruct:
|
||||
return colander.null
|
||||
|
||||
|
|
@ -73,11 +72,16 @@ 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)
|
||||
dt = dt.astimezone()
|
||||
return make_utc(dt)
|
||||
if not dt.tzinfo:
|
||||
dt = app.localtime(dt, from_utc=False)
|
||||
return app.make_utc(dt)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,9 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
|
|||
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly and cstruct:
|
||||
if readonly:
|
||||
if not cstruct:
|
||||
return ""
|
||||
dt = datetime.datetime.fromisoformat(cstruct)
|
||||
return self.app.render_datetime(dt)
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@
|
|||
</b-checkbox>
|
||||
</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">
|
||||
<input type="hidden"
|
||||
name="${app.appname}.web.menus.handler.spec"
|
||||
|
|
@ -270,6 +280,74 @@
|
|||
|
||||
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.editWebLibraryShowDialog = false
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
<b-field horizontal label="Node Title">
|
||||
<span>${app.get_node_title()}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Time Zone">
|
||||
<span>${app.get_timezone_name()}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Production Mode">
|
||||
<span>${"Yes" if config.production() else "No"}</span>
|
||||
</b-field>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
Views for app settings
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -31,6 +32,7 @@ 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
|
||||
|
||||
|
|
@ -134,6 +136,7 @@ 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
|
||||
|
|
@ -174,12 +177,31 @@ 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()
|
||||
|
|
@ -222,6 +244,32 @@ 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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -11,88 +11,86 @@ 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(TestCase):
|
||||
class TestWuttaDateTime(WebTestCase):
|
||||
|
||||
def test_serialize(self):
|
||||
typ = mod.WuttaDateTime()
|
||||
node = colander.SchemaNode(typ)
|
||||
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)
|
||||
)
|
||||
|
||||
result = typ.serialize(node, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
# 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, None)
|
||||
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, "")
|
||||
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")
|
||||
|
||||
# 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, local
|
||||
result = typ.serialize(
|
||||
node,
|
||||
datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
|
||||
)
|
||||
self.assertEqual(result, "2024-12-11T14:33:00-08:00")
|
||||
|
||||
# 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")
|
||||
# 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")
|
||||
|
||||
def test_deserialize(self):
|
||||
typ = mod.WuttaDateTime()
|
||||
node = colander.SchemaNode(typ)
|
||||
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)
|
||||
)
|
||||
|
||||
result = typ.deserialize(node, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
# 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, None)
|
||||
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, "")
|
||||
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)
|
||||
)
|
||||
|
||||
# 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")
|
||||
# invalid
|
||||
self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
|
||||
|
||||
|
||||
class TestObjectNode(DataTestCase):
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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
|
||||
|
|
@ -145,19 +146,36 @@ class TestWuttaDateTimeWidget(WebTestCase):
|
|||
def make_widget(self, **kwargs):
|
||||
return mod.WuttaDateTimeWidget(self.request, **kwargs)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# editable widget has normal picker html
|
||||
result = widget.serialize(field, str(dt))
|
||||
self.assertIn("<wutta-datepicker", result)
|
||||
# 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("<wutta-datepicker", result)
|
||||
|
||||
# readonly is rendered per app convention
|
||||
result = widget.serialize(field, str(dt), readonly=True)
|
||||
self.assertEqual(result, "2024-12-12 13:49+0000")
|
||||
def test_serialize_readonly(self):
|
||||
tzlocal = get_timezone_by_name("America/New_York")
|
||||
with patch.object(self.app, "get_timezone", return_value=tzlocal):
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from paginate_sqlalchemy import SqlalchemyOrmPage
|
|||
from pyramid import testing
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.util import get_timezone_by_name
|
||||
from wuttaweb.grids import base as mod
|
||||
from wuttaweb.grids.filters import (
|
||||
GridFilter,
|
||||
|
|
@ -1654,16 +1655,21 @@ class TestGrid(WebTestCase):
|
|||
self.assertEqual(result, "2025-01-13")
|
||||
|
||||
def test_render_datetime(self):
|
||||
grid = self.make_grid(columns=["foo", "bar"])
|
||||
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"])
|
||||
|
||||
obj = MagicMock(dt=None)
|
||||
result = grid.render_datetime(obj, "dt", None)
|
||||
self.assertEqual(result, "")
|
||||
# null
|
||||
obj = MagicMock(dt=None)
|
||||
result = grid.render_datetime(obj, "dt", None)
|
||||
self.assertEqual(result, "")
|
||||
|
||||
dt = datetime.datetime(2024, 12, 12, 13, 44, tzinfo=datetime.timezone.utc)
|
||||
obj = MagicMock(dt=dt)
|
||||
result = grid.render_datetime(obj, "dt", str(dt))
|
||||
self.assertEqual(result, "2024-12-12 13:44+0000")
|
||||
# normal (naive utc)
|
||||
dt = datetime.datetime(2024, 12, 12, 13, 44)
|
||||
obj = MagicMock(dt=dt)
|
||||
result = grid.render_datetime(obj, "dt", str(dt))
|
||||
self.assertEqual(result, "2024-12-12 05:44-0800")
|
||||
self.assertNotEqual(result, str(dt))
|
||||
|
||||
def test_render_vue_tag(self):
|
||||
grid = self.make_grid(columns=["foo", "bar"])
|
||||
|
|
|
|||
|
|
@ -1663,6 +1663,9 @@ class TestMasterView(WebTestCase):
|
|||
def test_configure(self):
|
||||
self.pyramid_config.include("wuttaweb.views.common")
|
||||
self.pyramid_config.include("wuttaweb.views.auth")
|
||||
self.pyramid_config.add_route(
|
||||
"appinfo.check_timezone", "/appinfo/check-timezone"
|
||||
)
|
||||
model = self.app.model
|
||||
|
||||
# mock settings
|
||||
|
|
@ -1697,6 +1700,7 @@ class TestMasterView(WebTestCase):
|
|||
def get_context(**kw):
|
||||
kw = original_context(**kw)
|
||||
kw["menu_handlers"] = []
|
||||
kw["default_timezone"] = "UTC"
|
||||
return kw
|
||||
|
||||
with patch.object(view, "configure_get_context", new=get_context):
|
||||
|
|
|
|||
|
|
@ -46,6 +46,26 @@ class TestAppInfoView(WebTestCase):
|
|||
view = self.make_view()
|
||||
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):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue