Compare commits
2 commits
286c683c93
...
652477bee0
| Author | SHA1 | Date | |
|---|---|---|---|
| 652477bee0 | |||
| 7fcb331806 |
12 changed files with 286 additions and 95 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -5,6 +5,16 @@ 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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttaWeb"
|
||||
version = "0.24.0"
|
||||
version = "0.25.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.25.0",
|
||||
"WuttJamaican[db]>=0.26.0",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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,87 +11,85 @@ 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):
|
||||
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)
|
||||
node = colander.SchemaNode(
|
||||
typ, widget=widgets.WuttaDateTimeWidget(self.request)
|
||||
)
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
# 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-"))
|
||||
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)
|
||||
node,
|
||||
datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
self.assertEqual(result, "2024-12-11T22:33:00+00:00")
|
||||
self.assertEqual(result, "2024-12-11T14:33:00-08:00")
|
||||
|
||||
# aware, local
|
||||
result = typ.serialize(
|
||||
node,
|
||||
datetime.datetime(
|
||||
2024,
|
||||
12,
|
||||
11,
|
||||
22,
|
||||
33,
|
||||
tzinfo=datetime.timezone(-datetime.timedelta(hours=5)),
|
||||
),
|
||||
datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
|
||||
)
|
||||
self.assertEqual(result, "2024-12-11T22:33:00-05:00")
|
||||
self.assertEqual(result, "2024-12-11T14:33:00-08: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)
|
||||
node = colander.SchemaNode(
|
||||
typ, widget=widgets.WuttaDateTimeWidget(self.request)
|
||||
)
|
||||
self.assertEqual(result, "2024-12-11 10:33 PM")
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
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
|
||||
# format #1
|
||||
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.assertEqual(
|
||||
result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
|
||||
)
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
# invalid
|
||||
self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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()
|
||||
dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=datetime.timezone.utc)
|
||||
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))
|
||||
# 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):
|
||||
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"])
|
||||
|
||||
# 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)
|
||||
# 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 13:44+0000")
|
||||
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