From 40562c126e0c9c2a950c7dc8928458dba220a4a2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 9 Dec 2024 20:56:09 -0600 Subject: [PATCH] fix: add `GridWidget` and `form.set_grid()` for convenience omg how did i not do this sooner --- docs/glossary.rst | 9 ++++++-- src/wuttaweb/forms/base.py | 39 +++++++++++++++++++++++++---------- src/wuttaweb/forms/schema.py | 4 ++-- src/wuttaweb/forms/widgets.py | 38 ++++++++++++++++++++++++++++++++++ src/wuttaweb/grids/base.py | 2 +- tests/forms/test_base.py | 14 +++++++++++++ tests/forms/test_schema.py | 2 +- tests/forms/test_widgets.py | 26 +++++++++++++++++++++++ 8 files changed, 117 insertions(+), 17 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 0621e51..0626c00 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -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. diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index 9dd6760..8f5cafc 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -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, diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index f5f206b..6347b61 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -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) diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index 2521413..2d91eb9 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -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 diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 71b6a69..9bdd018 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -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 `. :param request: Reference to current :term:`request` object. diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py index 73e9d85..ef8e4b6 100644 --- a/tests/forms/test_base.py +++ b/tests/forms/test_base.py @@ -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, {}) diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py index 1c7680a..e7f9f36 100644 --- a/tests/forms/test_schema.py +++ b/tests/forms/test_schema.py @@ -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 diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index a51ce79..f479e5a 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -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('