Compare commits
No commits in common. "028f8e767d5d9925bec8fa91feb39d2f47e556f1" and "4307b5a9eb4c04749d4b474ba965f2c017de1eab" have entirely different histories.
028f8e767d
...
4307b5a9eb
9 changed files with 30 additions and 388 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.28.0 (2026-02-17)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add support for association proxy fields in model forms
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- fix first_name, last_name handling for User form
|
|
||||||
- add support for rendering association proxy values in grid
|
|
||||||
- add "centered" flag for grid columns
|
|
||||||
- prevent delete also, for users with prevent edit flag
|
|
||||||
- render enabled flag as bool for Email Settings grid
|
|
||||||
|
|
||||||
## v0.27.6 (2026-02-13)
|
## v0.27.6 (2026-02-13)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
|
"alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
|
||||||
"colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None),
|
"colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None),
|
||||||
"colanderalchemy": ("https://colanderalchemy.readthedocs.io/en/latest/", None),
|
|
||||||
"deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None),
|
"deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None),
|
||||||
"fanstatic": ("https://www.fanstatic.org/en/latest/", None),
|
"fanstatic": ("https://www.fanstatic.org/en/latest/", None),
|
||||||
"pyramid": ("https://docs.pylonsproject.org/projects/pyramid/en/latest/", None),
|
"pyramid": ("https://docs.pylonsproject.org/projects/pyramid/en/latest/", None),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.28.0"
|
version = "0.27.6"
|
||||||
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"}]
|
||||||
|
|
@ -45,7 +45,7 @@ dependencies = [
|
||||||
"SQLAlchemy-Utils",
|
"SQLAlchemy-Utils",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttJamaican[db]>=0.28.7",
|
"WuttJamaican[db]>=0.28.1",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
|
|
||||||
# nb. this must be pinned for now, until pyramid can remove
|
# nb. this must be pinned for now, until pyramid can remove
|
||||||
|
|
|
||||||
|
|
@ -827,7 +827,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
includes.append(key)
|
includes.append(key)
|
||||||
|
|
||||||
# make initial schema with ColanderAlchemy magic
|
# make initial schema with ColanderAlchemy magic
|
||||||
schema = WuttaSchemaNode(self.model_class, includes=includes)
|
schema = SQLAlchemySchemaNode(self.model_class, includes=includes)
|
||||||
|
|
||||||
# fill in the blanks if anything got missed
|
# fill in the blanks if anything got missed
|
||||||
for key in fields:
|
for key in fields:
|
||||||
|
|
@ -1144,7 +1144,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
# render static text if field not in deform/schema
|
# render static text if field not in deform/schema
|
||||||
# TODO: need to abstract this somehow
|
# TODO: need to abstract this somehow
|
||||||
if self.model_instance:
|
if self.model_instance:
|
||||||
value = self.app.get_value(self.model_instance, fieldname)
|
value = self.model_instance[fieldname]
|
||||||
html = str(value) if value is not None else ""
|
html = str(value) if value is not None else ""
|
||||||
else:
|
else:
|
||||||
html = ""
|
html = ""
|
||||||
|
|
@ -1407,160 +1407,3 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
if field.error:
|
if field.error:
|
||||||
return field.error.messages()
|
return field.error.messages()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class WuttaSchemaNode(SQLAlchemySchemaNode): # pylint: disable=abstract-method
|
|
||||||
"""
|
|
||||||
Custom schema node type based on ColanderAlchemy, but adding
|
|
||||||
support for association proxy fields.
|
|
||||||
|
|
||||||
This class is used under the hood but you will not normally
|
|
||||||
interact with it directly.
|
|
||||||
|
|
||||||
It's a subclass of
|
|
||||||
:class:`colanderalchemy:colanderalchemy.SQLAlchemySchemaNode`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def add_nodes(self, includes, excludes, overrides):
|
|
||||||
super().add_nodes(includes, excludes, overrides)
|
|
||||||
|
|
||||||
for name in includes or []:
|
|
||||||
prop = self.inspector.attrs.get(name, name)
|
|
||||||
if isinstance(prop, str):
|
|
||||||
|
|
||||||
# add node for association proxy field
|
|
||||||
if column := get_association_proxy_column(self.inspector, name):
|
|
||||||
name_overrides_copy = overrides.get(name, {}).copy()
|
|
||||||
node = self.get_schema_from_column(column, name_overrides_copy)
|
|
||||||
if node is not None:
|
|
||||||
self.add(node)
|
|
||||||
|
|
||||||
def dictify(self, obj): # pylint: disable=empty-docstring
|
|
||||||
""" """
|
|
||||||
dct = super().dictify(obj)
|
|
||||||
|
|
||||||
# loop thru all fields to add the association proxies
|
|
||||||
for node in self:
|
|
||||||
name = node.name
|
|
||||||
if name in dct:
|
|
||||||
continue # value already set
|
|
||||||
|
|
||||||
# we only care about association proxies here
|
|
||||||
if not get_association_proxy_column(self.inspector, name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
dct[name] = getattr(obj, name)
|
|
||||||
|
|
||||||
# special handling when value is None
|
|
||||||
if dct[name] is None:
|
|
||||||
|
|
||||||
# nb. colander/deform know how to behave when using
|
|
||||||
# their dedicated colander.null value, but ``None``
|
|
||||||
# seems to cause issues for string fields, so swap
|
|
||||||
# that out here if applicable
|
|
||||||
if isinstance(node.typ, colander.String):
|
|
||||||
dct[name] = colander.null
|
|
||||||
|
|
||||||
return dct
|
|
||||||
|
|
||||||
def objectify(self, dict_, context=None): # pylint: disable=empty-docstring
|
|
||||||
""" """
|
|
||||||
context = super().objectify(dict_, context=context)
|
|
||||||
|
|
||||||
for attr in dict_:
|
|
||||||
if self.inspector.has_property(attr):
|
|
||||||
continue # upstream logic handles these
|
|
||||||
|
|
||||||
# try to process association proxy field
|
|
||||||
if get_association_proxy_column(self.inspector, attr):
|
|
||||||
value = dict_[attr]
|
|
||||||
if value is colander.null:
|
|
||||||
# `colander.null` is never an appropriate
|
|
||||||
# value to be placed on an SQLAlchemy object
|
|
||||||
# so we translate it into `None`.
|
|
||||||
value = None
|
|
||||||
setattr(context, attr, value)
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def get_association_proxy(mapper, field):
|
|
||||||
"""
|
|
||||||
Return the association proxy "descriptor" corresponding to the
|
|
||||||
given field name, if it exists.
|
|
||||||
|
|
||||||
:param mapper: SQLAlchemy mapper for the main class.
|
|
||||||
|
|
||||||
:param field: Field name on the main class, which may (or may not)
|
|
||||||
be proxied via association.
|
|
||||||
|
|
||||||
:returns:
|
|
||||||
:class:`~sqlalchemy:sqlalchemy.ext.associationproxy.AssociationProxy`
|
|
||||||
instance, or ``None``.
|
|
||||||
|
|
||||||
Using the ``User.first_name`` (which proxies to
|
|
||||||
``User.person.first_name``) example, this would return the proxy
|
|
||||||
descriptor for ``User.first_name``.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
desc = getattr(mapper.all_orm_descriptors, field)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if desc.extension_type.name == "ASSOCIATION_PROXY":
|
|
||||||
return desc
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_association_proxy_target(mapper, field):
|
|
||||||
"""
|
|
||||||
Return the relationship property involved in the association proxy
|
|
||||||
for the given field, if applicable.
|
|
||||||
|
|
||||||
:param mapper: SQLAlchemy mapper for the main class.
|
|
||||||
|
|
||||||
:param field: Field name on the main class, which may (or may not)
|
|
||||||
be proxied via association.
|
|
||||||
|
|
||||||
:returns: :class:`~sqlalchemy:sqlalchemy.orm.RelationshipProperty`
|
|
||||||
instance, or ``None``.
|
|
||||||
|
|
||||||
Using the ``User.first_name`` (which proxies to
|
|
||||||
``User.person.first_name``) example, this would return the
|
|
||||||
``User.person`` relationship property.
|
|
||||||
"""
|
|
||||||
if proxy := get_association_proxy(mapper, field):
|
|
||||||
proxy_target = mapper.get_property(proxy.target_collection)
|
|
||||||
if (
|
|
||||||
isinstance(proxy_target, orm.RelationshipProperty)
|
|
||||||
and not proxy_target.uselist
|
|
||||||
):
|
|
||||||
return proxy_target
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_association_proxy_column(mapper, field):
|
|
||||||
"""
|
|
||||||
Return the target column property involved in the association
|
|
||||||
proxy for the given field, if applicable.
|
|
||||||
|
|
||||||
:param mapper: SQLAlchemy mapper for the main class.
|
|
||||||
|
|
||||||
:param field: Field name on the main class, which may (or may not)
|
|
||||||
be proxied via association.
|
|
||||||
|
|
||||||
:returns: :class:`~sqlalchemy:sqlalchemy.orm.ColumnProperty`
|
|
||||||
instance, or ``None``.
|
|
||||||
|
|
||||||
Using the ``User.first_name`` (which proxies to
|
|
||||||
``User.person.first_name``) example, this would return the
|
|
||||||
``Person.first_name`` column property.
|
|
||||||
"""
|
|
||||||
if proxy_target := get_association_proxy_target(mapper, field):
|
|
||||||
if proxy_target.mapper.has_property(field):
|
|
||||||
prop = proxy_target.mapper.get_property(field)
|
|
||||||
if isinstance(prop, orm.ColumnProperty) and isinstance(
|
|
||||||
prop.columns[0], sa.Column
|
|
||||||
):
|
|
||||||
return prop
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -2415,15 +2415,6 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
except TypeError:
|
except TypeError:
|
||||||
dct = dict(obj.__dict__)
|
dct = dict(obj.__dict__)
|
||||||
dct.pop("_sa_instance_state", None)
|
dct.pop("_sa_instance_state", None)
|
||||||
|
|
||||||
# nb. inject association proxy(-like) fields if applicable
|
|
||||||
for field in self.columns:
|
|
||||||
if field not in dct:
|
|
||||||
try:
|
|
||||||
dct[field] = getattr(obj, field)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return dct
|
return dct
|
||||||
|
|
||||||
def get_vue_context(self):
|
def get_vue_context(self):
|
||||||
|
|
|
||||||
|
|
@ -198,13 +198,30 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
user = super().objectify(form)
|
user = super().objectify(form)
|
||||||
|
|
||||||
# maybe update person name
|
# maybe update person name
|
||||||
if "first_name" in form and "last_name" in form:
|
if "first_name" in form or "last_name" in form:
|
||||||
first_name = data.get("first_name")
|
first_name = data.get("first_name")
|
||||||
last_name = data.get("last_name")
|
last_name = data.get("last_name")
|
||||||
if first_name or last_name:
|
if self.creating and (first_name or last_name):
|
||||||
user.person.full_name = self.app.make_full_name(first_name, last_name)
|
user.person = auth.make_person(
|
||||||
else:
|
first_name=first_name, last_name=last_name
|
||||||
user.person = None
|
)
|
||||||
|
elif self.editing:
|
||||||
|
if first_name or last_name:
|
||||||
|
if user.person:
|
||||||
|
person = user.person
|
||||||
|
if "first_name" in form:
|
||||||
|
person.first_name = first_name
|
||||||
|
if "last_name" in form:
|
||||||
|
person.last_name = last_name
|
||||||
|
person.full_name = self.app.make_full_name(
|
||||||
|
person.first_name, person.last_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user.person = auth.make_person(
|
||||||
|
first_name=first_name, last_name=last_name
|
||||||
|
)
|
||||||
|
elif user.person:
|
||||||
|
user.person = None
|
||||||
|
|
||||||
# maybe set user password
|
# maybe set user password
|
||||||
if "set_password" in form and data.get("set_password"):
|
if "set_password" in form and data.get("set_password"):
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,11 @@ import os
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
|
||||||
from sqlalchemy.ext.associationproxy import AssociationProxy
|
|
||||||
|
|
||||||
import deform
|
|
||||||
import colander
|
import colander
|
||||||
from colanderalchemy import SQLAlchemySchemaNode
|
import deform
|
||||||
from pyramid.renderers import get_renderer
|
from pyramid.renderers import get_renderer
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
|
||||||
|
|
||||||
from wuttaweb.forms import base as mod, widgets
|
from wuttaweb.forms import base as mod, widgets
|
||||||
from wuttaweb.grids import Grid
|
from wuttaweb.grids import Grid
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
@ -739,174 +734,3 @@ class TestForm(WebTestCase):
|
||||||
self.assertNotIn("foo", dform)
|
self.assertNotIn("foo", dform)
|
||||||
self.assertIn("bar", schema)
|
self.assertIn("bar", schema)
|
||||||
self.assertIn("bar", dform)
|
self.assertIn("bar", dform)
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaSchemaNode(WebTestCase):
|
|
||||||
|
|
||||||
def test_add_nodes(self):
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
# ColanderAlchemy does not add nodes for association proxy fields
|
|
||||||
schema = SQLAlchemySchemaNode(
|
|
||||||
model.User, includes=["username", "first_name", "last_name"]
|
|
||||||
)
|
|
||||||
self.assertIn("username", schema)
|
|
||||||
self.assertNotIn("first_name", schema)
|
|
||||||
self.assertNotIn("last_name", schema)
|
|
||||||
|
|
||||||
# but our custom class handles them okay
|
|
||||||
schema = mod.WuttaSchemaNode(
|
|
||||||
model.User, includes=["username", "first_name", "last_name"]
|
|
||||||
)
|
|
||||||
self.assertIn("username", schema)
|
|
||||||
self.assertIn("first_name", schema)
|
|
||||||
self.assertIn("last_name", schema)
|
|
||||||
|
|
||||||
def test_dictify(self):
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
person = model.Person(first_name="Fred", last_name="Flintstone")
|
|
||||||
user = model.User(username="freddie", person=person)
|
|
||||||
user.foo_field = "bar"
|
|
||||||
|
|
||||||
foo_field = colander.SchemaNode(colander.String())
|
|
||||||
|
|
||||||
# ColanderAlchemy does not include association proxy fields
|
|
||||||
schema = SQLAlchemySchemaNode(
|
|
||||||
model.User, includes=["username", "first_name", "last_name", foo_field]
|
|
||||||
)
|
|
||||||
dct = schema.dictify(user)
|
|
||||||
self.assertIn("username", dct)
|
|
||||||
self.assertEqual(dct["username"], "freddie")
|
|
||||||
self.assertNotIn("first_name", dct)
|
|
||||||
self.assertNotIn("last_name", dct)
|
|
||||||
self.assertNotIn("foo_field", dct)
|
|
||||||
|
|
||||||
# but our custom class handles them okay
|
|
||||||
schema = mod.WuttaSchemaNode(
|
|
||||||
model.User, includes=["username", "first_name", "last_name", foo_field]
|
|
||||||
)
|
|
||||||
dct = schema.dictify(user)
|
|
||||||
self.assertIn("username", dct)
|
|
||||||
self.assertEqual(dct["username"], "freddie")
|
|
||||||
self.assertIn("first_name", dct)
|
|
||||||
self.assertEqual(dct["first_name"], "Fred")
|
|
||||||
self.assertIn("last_name", dct)
|
|
||||||
self.assertEqual(dct["last_name"], "Flintstone")
|
|
||||||
# nb. foo_field is not assn proxy, so not auto-dictified
|
|
||||||
self.assertNotIn("foo_field", dct)
|
|
||||||
|
|
||||||
# also note special handling for null values on string proxy fields
|
|
||||||
user.last_name = None
|
|
||||||
dct = schema.dictify(user)
|
|
||||||
self.assertIn("first_name", dct)
|
|
||||||
self.assertIs(dct["first_name"], "Fred")
|
|
||||||
self.assertIn("last_name", dct)
|
|
||||||
self.assertIsNotNone(dct["last_name"])
|
|
||||||
self.assertIs(dct["last_name"], colander.null)
|
|
||||||
|
|
||||||
def test_objectify(self):
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
# ColanderAlchemy does not set association proxy fields
|
|
||||||
schema = SQLAlchemySchemaNode(
|
|
||||||
model.User,
|
|
||||||
includes=["username", "first_name", "last_name"],
|
|
||||||
)
|
|
||||||
user = schema.objectify(
|
|
||||||
{
|
|
||||||
"username": "freddie",
|
|
||||||
"first_name": "Fred",
|
|
||||||
"last_name": "Flintstone",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertIsInstance(user, model.User)
|
|
||||||
self.assertEqual(user.username, "freddie")
|
|
||||||
self.assertIsNone(user.person)
|
|
||||||
self.assertIsNone(user.first_name)
|
|
||||||
self.assertIsNone(user.last_name)
|
|
||||||
|
|
||||||
# but our custom class handles them okay
|
|
||||||
schema = mod.WuttaSchemaNode(
|
|
||||||
model.User,
|
|
||||||
includes=["username", "first_name", "last_name"],
|
|
||||||
)
|
|
||||||
user = schema.objectify(
|
|
||||||
{
|
|
||||||
"username": "freddie",
|
|
||||||
"first_name": "Fred",
|
|
||||||
"last_name": "Flintstone",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertIsInstance(user, model.User)
|
|
||||||
self.assertEqual(user.username, "freddie")
|
|
||||||
self.assertIsInstance(user.person, model.Person)
|
|
||||||
self.assertEqual(user.person.first_name, "Fred")
|
|
||||||
self.assertEqual(user.person.last_name, "Flintstone")
|
|
||||||
self.assertEqual(user.first_name, "Fred")
|
|
||||||
self.assertEqual(user.last_name, "Flintstone")
|
|
||||||
|
|
||||||
# also note special handling for null values on string proxy fields
|
|
||||||
user = schema.objectify(
|
|
||||||
{
|
|
||||||
"username": "freddie",
|
|
||||||
"first_name": "Fred",
|
|
||||||
"last_name": colander.null,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertIsInstance(user, model.User)
|
|
||||||
self.assertEqual(user.username, "freddie")
|
|
||||||
self.assertIsInstance(user.person, model.Person)
|
|
||||||
self.assertEqual(user.person.first_name, "Fred")
|
|
||||||
self.assertIsNone(user.person.last_name)
|
|
||||||
self.assertEqual(user.first_name, "Fred")
|
|
||||||
self.assertIsNone(user.last_name)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAssociationProxy(DataTestCase):
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
model = self.app.model
|
|
||||||
mapper = sa.inspect(model.User)
|
|
||||||
|
|
||||||
# typical
|
|
||||||
proxy = mod.get_association_proxy(mapper, "first_name")
|
|
||||||
self.assertIsInstance(proxy, AssociationProxy)
|
|
||||||
self.assertEqual(proxy.target_collection, "person")
|
|
||||||
self.assertEqual(proxy.value_attr, "first_name")
|
|
||||||
|
|
||||||
# no such proxy
|
|
||||||
proxy = mod.get_association_proxy(mapper, "blarg")
|
|
||||||
self.assertIsNone(proxy)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAssociationProxyTarget(DataTestCase):
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
model = self.app.model
|
|
||||||
mapper = sa.inspect(model.User)
|
|
||||||
|
|
||||||
# typical
|
|
||||||
target = mod.get_association_proxy_target(mapper, "first_name")
|
|
||||||
self.assertIsInstance(target, orm.RelationshipProperty)
|
|
||||||
self.assertIs(target, mapper.get_property("person"))
|
|
||||||
|
|
||||||
# no such proxy
|
|
||||||
target = mod.get_association_proxy_target(mapper, "blarg")
|
|
||||||
self.assertIsNone(target)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAssociationProxyColumn(DataTestCase):
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
model = self.app.model
|
|
||||||
mapper = sa.inspect(model.User)
|
|
||||||
|
|
||||||
# typical
|
|
||||||
column = mod.get_association_proxy_column(mapper, "first_name")
|
|
||||||
self.assertIsInstance(column, orm.ColumnProperty)
|
|
||||||
self.assertIs(column, sa.inspect(model.Person).get_property("first_name"))
|
|
||||||
|
|
||||||
# no such proxy
|
|
||||||
column = mod.get_association_proxy_column(mapper, "blarg")
|
|
||||||
self.assertIsNone(column)
|
|
||||||
|
|
|
||||||
|
|
@ -1923,24 +1923,10 @@ class TestGrid(WebTestCase):
|
||||||
def __init__(self, **kw):
|
def __init__(self, **kw):
|
||||||
self.__dict__.update(kw)
|
self.__dict__.update(kw)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
if name == "something":
|
|
||||||
return "else"
|
|
||||||
raise AttributeError(f"attr not found: {name}")
|
|
||||||
|
|
||||||
mock = MockSetting(**setting)
|
mock = MockSetting(**setting)
|
||||||
dct = grid.object_to_dict(mock)
|
dct = grid.object_to_dict(mock)
|
||||||
self.assertIsInstance(dct, dict)
|
self.assertIsInstance(dct, dict)
|
||||||
self.assertEqual(dct, setting)
|
self.assertEqual(dct, setting)
|
||||||
self.assertNotIn("something", dct)
|
|
||||||
|
|
||||||
# test (mock) association proxy behavior
|
|
||||||
grid.columns.append("something")
|
|
||||||
grid.columns.append("another")
|
|
||||||
dct = grid.object_to_dict(mock)
|
|
||||||
self.assertIn("something", dct)
|
|
||||||
self.assertEqual(dct["something"], "else")
|
|
||||||
self.assertNotIn("another", dct)
|
|
||||||
|
|
||||||
def test_get_vue_context(self):
|
def test_get_vue_context(self):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,14 +202,12 @@ class TestUserView(WebTestCase):
|
||||||
|
|
||||||
# form can update user password
|
# form can update user password
|
||||||
self.assertTrue(auth.check_user_password(barney, "testpass"))
|
self.assertTrue(auth.check_user_password(barney, "testpass"))
|
||||||
self.assertFalse(auth.check_user_password(barney, "testpass2"))
|
|
||||||
form = view.make_model_form(model_instance=barney)
|
form = view.make_model_form(model_instance=barney)
|
||||||
form.validated = {"username": "barney", "set_password": "testpass2"}
|
form.validated = {"username": "barney", "set_password": "testpass2"}
|
||||||
with patch.object(view, "Session", return_value=self.session):
|
with patch.object(view, "Session", return_value=self.session):
|
||||||
user = view.objectify(form)
|
user = view.objectify(form)
|
||||||
self.assertIs(user, barney)
|
self.assertIs(user, barney)
|
||||||
self.assertTrue(auth.check_user_password(barney, "testpass2"))
|
self.assertTrue(auth.check_user_password(barney, "testpass2"))
|
||||||
self.assertFalse(auth.check_user_password(barney, "testpass"))
|
|
||||||
|
|
||||||
# form can update user roles
|
# form can update user roles
|
||||||
form = view.make_model_form(model_instance=barney)
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
|
@ -250,12 +248,10 @@ class TestUserView(WebTestCase):
|
||||||
# nb. re-attach the person
|
# nb. re-attach the person
|
||||||
barney.person = self.session.query(model.Person).one()
|
barney.person = self.session.query(model.Person).one()
|
||||||
|
|
||||||
# nb. first/last name was blanked out in last edit
|
|
||||||
self.assertEqual(barney.person.first_name, "")
|
|
||||||
self.assertEqual(barney.person.last_name, "")
|
|
||||||
self.assertEqual(barney.person.full_name, "Barney Rubble")
|
|
||||||
|
|
||||||
# person name is updated
|
# person name is updated
|
||||||
|
self.assertEqual(barney.person.first_name, "Barney")
|
||||||
|
self.assertEqual(barney.person.last_name, "Rubble")
|
||||||
|
self.assertEqual(barney.person.full_name, "Barney Rubble")
|
||||||
form = view.make_model_form(model_instance=barney)
|
form = view.make_model_form(model_instance=barney)
|
||||||
form.validated = {
|
form.validated = {
|
||||||
"username": "barney",
|
"username": "barney",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue