Compare commits
No commits in common. "de7fc4dcf08157d30e5063a3702bb80d72019cb5" and "205a1f7a652b36207426b4fca8d8019714b383d1" have entirely different histories.
de7fc4dcf0
...
205a1f7a65
9 changed files with 12 additions and 178 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -5,20 +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.29.0 (2026-03-04)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- allow widget factory override for `ObjectRef` schema type
|
|
||||||
- add way to declare related versions for history in MasterView
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- expose default grid pagesize in appinfo config
|
|
||||||
- fix timezone edge case for `WuttaDateWidget`
|
|
||||||
- sort roles by name when viewing user
|
|
||||||
- make pylint happy
|
|
||||||
|
|
||||||
## v0.28.2 (2026-02-25)
|
## v0.28.2 (2026-02-25)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.29.0"
|
version = "0.28.2"
|
||||||
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"}]
|
||||||
|
|
@ -61,7 +61,7 @@ dependencies = [
|
||||||
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
continuum = ["Wutta-Continuum>=0.3.3"]
|
continuum = ["Wutta-Continuum>=0.3.0"]
|
||||||
docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
|
docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
|
||||||
tests = ["pylint", "pytest", "pytest-cov", "tox", "WebTest"]
|
tests = ["pylint", "pytest", "pytest-cov", "tox", "WebTest"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,6 @@ class ObjectRef(colander.SchemaType):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.ObjectRefWidget`.
|
:class:`~wuttaweb.forms.widgets.ObjectRefWidget`.
|
||||||
"""
|
"""
|
||||||
factory = kwargs.pop("factory", widgets.ObjectRefWidget)
|
|
||||||
|
|
||||||
if "values" not in kwargs:
|
if "values" not in kwargs:
|
||||||
query = self.get_query()
|
query = self.get_query()
|
||||||
|
|
@ -470,7 +469,7 @@ class ObjectRef(colander.SchemaType):
|
||||||
if "url" not in kwargs:
|
if "url" not in kwargs:
|
||||||
kwargs["url"] = self.get_object_url
|
kwargs["url"] = self.get_object_url
|
||||||
|
|
||||||
return factory(self.request, **kwargs)
|
return widgets.ObjectRefWidget(self.request, **kwargs)
|
||||||
|
|
||||||
def get_object_url(self, obj):
|
def get_object_url(self, obj):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -234,9 +234,6 @@ class WuttaDateWidget(DateInputWidget):
|
||||||
""" """
|
""" """
|
||||||
readonly = kw.get("readonly", self.readonly)
|
readonly = kw.get("readonly", self.readonly)
|
||||||
if readonly and cstruct:
|
if readonly and cstruct:
|
||||||
try:
|
|
||||||
dt = datetime.date.fromisoformat(cstruct)
|
|
||||||
except ValueError:
|
|
||||||
dt = datetime.datetime.fromisoformat(cstruct)
|
dt = datetime.datetime.fromisoformat(cstruct)
|
||||||
return self.app.render_date(dt)
|
return self.app.render_date(dt)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,24 +158,6 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Grids</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem; width: 50%;">
|
|
||||||
|
|
||||||
<b-field label="Default Page Size"
|
|
||||||
:message="simpleSettings['wuttaweb.grids.default_pagesize'] ? '' : 'Current default page size is: ${grid_pagesize_default}'">
|
|
||||||
<b-select name="wuttaweb.grids.default_pagesize"
|
|
||||||
v-model="simpleSettings['wuttaweb.grids.default_pagesize']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
<option :value="null">(use default)</option>
|
|
||||||
<option v-for="option in gridPagesizeOptions"
|
|
||||||
:value="option">
|
|
||||||
{{ option }}
|
|
||||||
</option>
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Web Libraries</h3>
|
<h3 class="block is-size-3">Web Libraries</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
|
@ -366,8 +348,6 @@
|
||||||
|
|
||||||
ThisPageData.validators.push(ThisPage.methods.timezoneValidate)
|
ThisPageData.validators.push(ThisPage.methods.timezoneValidate)
|
||||||
|
|
||||||
ThisPageData.gridPagesizeOptions = ${json.dumps(grid_pagesize_options)|n}
|
|
||||||
|
|
||||||
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
|
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
|
||||||
|
|
||||||
ThisPageData.editWebLibraryShowDialog = false
|
ThisPageData.editWebLibraryShowDialog = false
|
||||||
|
|
|
||||||
|
|
@ -1248,11 +1248,8 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
Default query will locate SQLAlchemy-Continuum ``transaction``
|
Default query will locate SQLAlchemy-Continuum ``transaction``
|
||||||
records which are associated with versions of the given model
|
records which are associated with versions of the given model
|
||||||
instance. See also:
|
instance. See also
|
||||||
|
:func:`wutta-continuum:wutta_continuum.util.model_transaction_query()`.
|
||||||
* :meth:`get_version_joins()`
|
|
||||||
* :meth:`normalize_version_joins()`
|
|
||||||
* :func:`~wutta-continuum:wutta_continuum.util.model_transaction_query()`
|
|
||||||
|
|
||||||
:returns: :class:`~sqlalchemy:sqlalchemy.orm.Query` instance
|
:returns: :class:`~sqlalchemy:sqlalchemy.orm.Query` instance
|
||||||
"""
|
"""
|
||||||
|
|
@ -1263,67 +1260,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
model_class = self.get_model_class()
|
model_class = self.get_model_class()
|
||||||
txncls = continuum.transaction_class(model_class)
|
txncls = continuum.transaction_class(model_class)
|
||||||
query = model_transaction_query(instance, joins=self.normalize_version_joins())
|
query = model_transaction_query(instance)
|
||||||
return query.order_by(txncls.issued_at.desc())
|
return query.order_by(txncls.issued_at.desc())
|
||||||
|
|
||||||
def get_version_joins(self):
|
|
||||||
"""
|
|
||||||
Override this method to declare additional version tables
|
|
||||||
which should be joined when showing the overall revision
|
|
||||||
history for a given model instance.
|
|
||||||
|
|
||||||
Note that whatever this method returns, will be ran through
|
|
||||||
:meth:`normalize_version_joins()` before being passed along to
|
|
||||||
:func:`~wutta-continuum:wutta_continuum.util.model_transaction_query()`.
|
|
||||||
|
|
||||||
:returns: List of version joins info as described below.
|
|
||||||
|
|
||||||
In the simple scenario where an "extension" table is involved,
|
|
||||||
e.g. a ``UserExtension`` table::
|
|
||||||
|
|
||||||
def get_version_joins(self):
|
|
||||||
model = self.app.model
|
|
||||||
return super().get_version_joins() + [
|
|
||||||
model.UserExtension,
|
|
||||||
]
|
|
||||||
|
|
||||||
In the case where a secondary table is "related" to the main
|
|
||||||
model table, but not a standard extension (using the
|
|
||||||
``User.person`` relationship as example)::
|
|
||||||
|
|
||||||
def get_version_joins(self):
|
|
||||||
model = self.app.model
|
|
||||||
return super().get_version_joins() + [
|
|
||||||
(model.Person, "uuid", "person_uuid"),
|
|
||||||
]
|
|
||||||
|
|
||||||
See also :meth:`get_version_grid_data()`.
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def normalize_version_joins(self):
|
|
||||||
"""
|
|
||||||
This method calls :meth:`get_version_joins()` and normalizes
|
|
||||||
the result, which will then get passed along to
|
|
||||||
:func:`~wutta-continuum:wutta_continuum.util.model_transaction_query()`.
|
|
||||||
|
|
||||||
Subclass should (generally) not override this, but instead
|
|
||||||
override :meth:`get_version_joins()`.
|
|
||||||
|
|
||||||
Each element in the return value (list) will be a 3-tuple
|
|
||||||
conforming to what is needed for the query function.
|
|
||||||
|
|
||||||
See also :meth:`get_version_grid_data()`.
|
|
||||||
|
|
||||||
:returns: List of version joins info.
|
|
||||||
"""
|
|
||||||
joins = []
|
|
||||||
for join in self.get_version_joins():
|
|
||||||
if not isinstance(join, tuple):
|
|
||||||
join = (join, "uuid", "uuid")
|
|
||||||
joins.append(join)
|
|
||||||
return joins
|
|
||||||
|
|
||||||
def configure_version_grid(self, g):
|
def configure_version_grid(self, g):
|
||||||
"""
|
"""
|
||||||
Configure the grid for the :meth:`view_versions()` view.
|
Configure the grid for the :meth:`view_versions()` view.
|
||||||
|
|
@ -1387,9 +1326,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
model_class = self.get_model_class()
|
model_class = self.get_model_class()
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
txncls = continuum.transaction_class(model_class)
|
txncls = continuum.transaction_class(model_class)
|
||||||
transactions = model_transaction_query(
|
transactions = model_transaction_query(instance)
|
||||||
instance, joins=self.normalize_version_joins()
|
|
||||||
)
|
|
||||||
|
|
||||||
txnid = self.request.matchdict["txnid"]
|
txnid = self.request.matchdict["txnid"]
|
||||||
txn = transactions.filter(txncls.id == txnid).first()
|
txn = transactions.filter(txncls.id == txnid).first()
|
||||||
|
|
@ -1471,34 +1408,15 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
:returns: List of version records.
|
:returns: List of version records.
|
||||||
"""
|
"""
|
||||||
import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel
|
|
||||||
|
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
vercls = self.get_model_version_class()
|
vercls = self.get_model_version_class()
|
||||||
versions = []
|
return (
|
||||||
|
|
||||||
# first get all versions for the model instance proper
|
|
||||||
versions.extend(
|
|
||||||
session.query(vercls)
|
session.query(vercls)
|
||||||
.filter(vercls.transaction == transaction)
|
.filter(vercls.transaction == transaction)
|
||||||
.filter(vercls.uuid == instance.uuid)
|
.filter(vercls.uuid == instance.uuid)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
# then get all related versions, per declared joins
|
|
||||||
for child_class, foreign_attr, primary_attr in self.normalize_version_joins():
|
|
||||||
child_vercls = continuum.version_class(child_class)
|
|
||||||
versions.extend(
|
|
||||||
session.query(child_vercls)
|
|
||||||
.filter(child_vercls.transaction == transaction)
|
|
||||||
.filter(
|
|
||||||
getattr(child_vercls, foreign_attr)
|
|
||||||
== getattr(instance, primary_attr)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return versions
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# autocomplete methods
|
# autocomplete methods
|
||||||
##############################
|
##############################
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,6 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
{"name": f"{self.config.appname}.email.default.to"},
|
{"name": f"{self.config.appname}.email.default.to"},
|
||||||
{"name": f"{self.config.appname}.email.feedback.subject"},
|
{"name": f"{self.config.appname}.email.feedback.subject"},
|
||||||
{"name": f"{self.config.appname}.email.feedback.to"},
|
{"name": f"{self.config.appname}.email.feedback.to"},
|
||||||
# grids
|
|
||||||
{"name": "wuttaweb.grids.default_pagesize", "type": int},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def getval(key):
|
def getval(key):
|
||||||
|
|
@ -284,11 +282,6 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
handlers = [{"spec": spec} for spec in handlers]
|
handlers = [{"spec": spec} for spec in handlers]
|
||||||
context["menu_handlers"] = handlers
|
context["menu_handlers"] = handlers
|
||||||
|
|
||||||
# add pagesize options
|
|
||||||
g = self.make_grid()
|
|
||||||
context["grid_pagesize_options"] = g.get_pagesize_options()
|
|
||||||
context["grid_pagesize_default"] = g.get_pagesize()
|
|
||||||
|
|
||||||
# add `weblibs` to context, based on config values
|
# add `weblibs` to context, based on config values
|
||||||
weblibs = self.get_weblibs()
|
weblibs = self.get_weblibs()
|
||||||
for key in weblibs:
|
for key in weblibs:
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,6 @@ class TestWuttaDateWidget(WebTestCase):
|
||||||
return mod.WuttaDateWidget(self.request, **kwargs)
|
return mod.WuttaDateWidget(self.request, **kwargs)
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
self.config.setdefault("wuttatest.timezone.default", "America/Los_Angeles")
|
|
||||||
tzlocal = get_timezone_by_name("America/Los_Angeles")
|
|
||||||
|
|
||||||
node = colander.SchemaNode(colander.Date())
|
node = colander.SchemaNode(colander.Date())
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
|
|
||||||
|
|
@ -159,9 +156,7 @@ class TestWuttaDateWidget(WebTestCase):
|
||||||
|
|
||||||
# now try again with datetime
|
# now try again with datetime
|
||||||
widget = self.make_widget()
|
widget = self.make_widget()
|
||||||
# nb. local zone is Los_Angeles but this is presumed to be "naive UTC"
|
dt = datetime.datetime(2025, 1, 15, 8, 35)
|
||||||
# hence local date is 2025-01-14
|
|
||||||
dt = datetime.datetime(2025, 1, 15, 4, 35)
|
|
||||||
|
|
||||||
# editable widget has normal picker html
|
# editable widget has normal picker html
|
||||||
result = widget.serialize(field, str(dt))
|
result = widget.serialize(field, str(dt))
|
||||||
|
|
@ -169,7 +164,7 @@ class TestWuttaDateWidget(WebTestCase):
|
||||||
|
|
||||||
# readonly is rendered per app convention
|
# readonly is rendered per app convention
|
||||||
result = widget.serialize(field, str(dt), readonly=True)
|
result = widget.serialize(field, str(dt), readonly=True)
|
||||||
self.assertEqual(result, "2025-01-14")
|
self.assertEqual(result, "2025-01-15")
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaDateTimeWidget(WebTestCase):
|
class TestWuttaDateTimeWidget(WebTestCase):
|
||||||
|
|
|
||||||
|
|
@ -1785,8 +1785,6 @@ class TestMasterView(WebTestCase):
|
||||||
kw = original_context(**kw)
|
kw = original_context(**kw)
|
||||||
kw["menu_handlers"] = []
|
kw["menu_handlers"] = []
|
||||||
kw["default_timezone"] = "UTC"
|
kw["default_timezone"] = "UTC"
|
||||||
kw["grid_pagesize_options"] = [10, 20, 50]
|
|
||||||
kw["grid_pagesize_default"] = 20
|
|
||||||
return kw
|
return kw
|
||||||
|
|
||||||
with patch.object(view, "configure_get_context", new=get_context):
|
with patch.object(view, "configure_get_context", new=get_context):
|
||||||
|
|
@ -2214,26 +2212,6 @@ class TestVersionedMasterView(VersionWebTestCase):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
self.assertEqual(view.get_version_grid_columns(), ["issued_at", "user"])
|
self.assertEqual(view.get_version_grid_columns(), ["issued_at", "user"])
|
||||||
|
|
||||||
def test_get_version_joins(self):
|
|
||||||
view = self.make_view()
|
|
||||||
self.assertEqual(view.get_version_joins(), [])
|
|
||||||
|
|
||||||
def test_normalize_version_joins(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
joins = [(model.Person, "uuid", "person_uuid")]
|
|
||||||
with patch.object(view, "get_version_joins", return_value=joins):
|
|
||||||
normal = view.normalize_version_joins()
|
|
||||||
self.assertEqual(normal, joins)
|
|
||||||
self.assertEqual(normal, [(model.Person, "uuid", "person_uuid")])
|
|
||||||
|
|
||||||
joins = [model.Person]
|
|
||||||
with patch.object(view, "get_version_joins", return_value=joins):
|
|
||||||
normal = view.normalize_version_joins()
|
|
||||||
self.assertNotEqual(normal, joins)
|
|
||||||
self.assertEqual(normal, [(model.Person, "uuid", "uuid")])
|
|
||||||
|
|
||||||
def test_get_version_grid_data(self):
|
def test_get_version_grid_data(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
@ -2322,9 +2300,7 @@ class TestVersionedMasterView(VersionWebTestCase):
|
||||||
txncls = continuum.transaction_class(model.User)
|
txncls = continuum.transaction_class(model.User)
|
||||||
vercls = continuum.version_class(model.User)
|
vercls = continuum.version_class(model.User)
|
||||||
|
|
||||||
person = model.Person(full_name="Fred Flintstone")
|
user = model.User(username="fred")
|
||||||
self.session.add(person)
|
|
||||||
user = model.User(username="fred", person=person)
|
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
|
@ -2338,21 +2314,11 @@ class TestVersionedMasterView(VersionWebTestCase):
|
||||||
with patch.object(mod.MasterView, "model_class", new=model.User):
|
with patch.object(mod.MasterView, "model_class", new=model.User):
|
||||||
with patch.object(mod.MasterView, "Session", return_value=self.session):
|
with patch.object(mod.MasterView, "Session", return_value=self.session):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
# just one version if no joins are specified
|
|
||||||
versions = view.get_relevant_versions(txn, user)
|
versions = view.get_relevant_versions(txn, user)
|
||||||
self.assertEqual(len(versions), 1)
|
self.assertEqual(len(versions), 1)
|
||||||
version = versions[0]
|
version = versions[0]
|
||||||
self.assertIsInstance(version, vercls)
|
self.assertIsInstance(version, vercls)
|
||||||
|
|
||||||
# but two versions if we specify join
|
|
||||||
joins = [(model.Person, "uuid", "person_uuid")]
|
|
||||||
with patch.object(view, "get_version_joins", return_value=joins):
|
|
||||||
versions = view.get_relevant_versions(txn, user)
|
|
||||||
self.assertEqual(len(versions), 2)
|
|
||||||
types = sorted([v.__class__.__name__ for v in versions])
|
|
||||||
self.assertEqual(types, ["PersonVersion", "UserVersion"])
|
|
||||||
|
|
||||||
def test_view_version(self):
|
def test_view_version(self):
|
||||||
import sqlalchemy_continuum as continuum
|
import sqlalchemy_continuum as continuum
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue