3
0
Fork 0

fix: add GridWidget and form.set_grid() for convenience

omg how did i not do this sooner
This commit is contained in:
Lance Edgar 2024-12-09 20:56:09 -06:00
parent f68fe26ada
commit 40562c126e
8 changed files with 117 additions and 17 deletions

View file

@ -6,5 +6,10 @@ Glossary
.. glossary::
:sorted:
view
TODO
grid
This refers to a "table of data, with features" essentially.
Sometimes it may be displayed as a simple table with no features,
or sometimes it has sortable columns, search filters and other
tools.
See also the :class:`~wuttaweb.grids.base.Grid` base class.

View file

@ -474,6 +474,34 @@ class Form:
if self.schema and key in self.schema:
self.schema[key].widget = widget
def set_grid(self, key, grid):
"""
Establish a :term:`grid` to be displayed for a field. This
uses a :class:`~wuttaweb.forms.widgets.GridWidget` to wrap the
rendered grid.
:param key: Name of field.
:param widget: :class:`~wuttaweb.grids.base.Grid` instance,
pre-configured and (usually) with data.
"""
from wuttaweb.forms.widgets import GridWidget
widget = GridWidget(self.request, grid)
self.set_widget(key, widget)
self.add_grid_vue_context(grid)
def add_grid_vue_context(self, grid):
""" """
if not grid.key:
raise ValueError("grid must have a key!")
if grid.key in self.grid_vue_context:
log.warning("grid data with key '%s' already registered, "
"but will be replaced", grid.key)
self.grid_vue_context[grid.key] = grid.get_vue_context()
def set_validator(self, key, validator):
"""
Set/override the validator for a field, or the form.
@ -848,17 +876,6 @@ class Form:
output = render(template, context)
return HTML.literal(output)
def add_grid_vue_context(self, grid):
""" """
if not grid.key:
raise ValueError("grid must have a key!")
if grid.key in self.grid_vue_context:
log.warning("grid data with key '%s' already registered, "
"but will be replaced", grid.key)
self.grid_vue_context[grid.key] = grid.get_vue_context()
def render_vue_field(
self,
fieldname,

View file

@ -214,7 +214,7 @@ class ObjectRef(colander.SchemaType):
node.model_instance = appstruct
# serialize to uuid
return appstruct.uuid
return appstruct.uuid.hex
def deserialize(self, node, cstruct):
""" """
@ -296,7 +296,7 @@ class ObjectRef(colander.SchemaType):
if 'values' not in kwargs:
query = self.get_query()
objects = query.all()
values = [(obj.uuid, str(obj))
values = [(obj.uuid.hex, str(obj))
for obj in objects]
if self.empty_option:
values.insert(0, self.empty_option)

View file

@ -210,6 +210,44 @@ class FileDownloadWidget(Widget):
return humanize.naturalsize(size)
class GridWidget(Widget):
"""
Widget for fields whose data is represented by a :term:`grid`.
This is a subclass of :class:`deform:deform.widget.Widget` but
does not use any Deform templates.
This widget only supports "readonly" mode, is not editable. It is
merely a convenience around the grid itself, which does the heavy
lifting.
Instead of creating this widget directly you probably should call
:meth:`~wuttaweb.forms.base.Form.set_grid()` on your form.
:param request: Current :term:`request` object.
:param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to
display the field data.
"""
def __init__(self, request, grid, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.grid = grid
def serialize(self, field, cstruct, **kw):
"""
This widget simply calls
:meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
the ``grid`` to serialize.
"""
readonly = kw.get('readonly', self.readonly)
if not readonly:
raise NotImplementedError("edit not allowed for this widget")
return self.grid.render_table_element()
class RoleRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for use with User

View file

@ -56,7 +56,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
class Grid:
"""
Base class for all grids.
Base class for all :term:`grids <grid>`.
:param request: Reference to current :term:`request` object.

View file

@ -142,6 +142,20 @@ class TestForm(TestCase):
self.assertIs(form.widgets['foo'], new_widget)
self.assertIs(schema['foo'].widget, new_widget)
def test_set_grid(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertNotIn('foo', form.widgets)
self.assertNotIn('foogrid', form.grid_vue_context)
grid = Grid(self.request, key='foogrid',
columns=['a', 'b'],
data=[{'a': 1, 'b': 2}, {'a': 3, 'b': 4}])
form.set_grid('foo', grid)
self.assertIn('foo', form.widgets)
self.assertIsInstance(form.widgets['foo'], widgets.GridWidget)
self.assertIn('foogrid', form.grid_vue_context)
def test_set_validator(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.validators, {})

View file

@ -100,7 +100,7 @@ class TestObjectRef(DataTestCase):
self.assertIsNotNone(person.uuid)
typ = mod.ObjectRef(self.request)
value = typ.serialize(node, person)
self.assertEqual(value, person.uuid)
self.assertEqual(value, person.uuid.hex)
def test_deserialize(self):
model = self.app.model

View file

@ -6,6 +6,7 @@ import colander
import deform
from pyramid import testing
from wuttaweb import grids
from wuttaweb.forms import widgets as mod
from wuttaweb.forms.schema import FileDownload, PersonRef, RoleRefs, UserRefs, Permissions
from tests.util import WebTestCase
@ -117,6 +118,31 @@ class TestFileDownloadWidget(WebTestCase):
self.assertEqual(html2, html)
class TestGridWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
grid = grids.Grid(self.request,
columns=['foo', 'bar'],
data=[{'foo': 1, 'bar': 2}, {'foo': 3, 'bar': 4}])
node = colander.SchemaNode(colander.String())
widget = mod.GridWidget(self.request, grid)
field = self.make_field(node)
# readonly works okay
html = widget.serialize(field, None, readonly=True)
self.assertIn('<b-table ', html)
# but otherwise, error
self.assertRaises(NotImplementedError, widget.serialize, field, None)
class TestRoleRefsWidget(WebTestCase):
def make_field(self, node, **kwargs):