From 9e0e36d536cb7b78576dc339bc94672faf0e7a2e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 08:40:56 -0600 Subject: [PATCH] fix: add `WuttaDateWidget` and associated logic --- src/wuttaweb/forms/base.py | 16 ++++++++------ src/wuttaweb/forms/widgets.py | 40 ++++++++++++++++++++++++++++++++++- tests/forms/test_base.py | 14 ++++++++++++ tests/forms/test_widgets.py | 40 +++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index c9567bc..edd1e2d 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -546,10 +546,13 @@ class Form: This is generally only possible if :attr:`model_class` is set to a valid SQLAlchemy mapped class. - As of writing this only looks for - :class:`sqlalchemy:sqlalchemy.types.DateTime` fields and if - any are found, they are configured to use - :class:`~wuttaweb.forms.widgets.WuttaDateTimeWidget()`. + This only checks for a couple of data types, with mapping as + follows: + + * :class:`sqlalchemy:sqlalchemy.types.Date` -> + :class:`~wuttaweb.forms.widgets.WuttaDateWidget` + * :class:`sqlalchemy:sqlalchemy.types.DateTime` -> + :class:`~wuttaweb.forms.widgets.WuttaDateTimeWidget` """ from wuttaweb.forms import widgets @@ -565,8 +568,9 @@ class Form: prop = getattr(attr, 'prop', None) if prop and isinstance(prop, orm.ColumnProperty): column = prop.columns[0] - if isinstance(column.type, sa.DateTime): - # self.set_renderer(key, self.render_datetime) + if isinstance(column.type, sa.Date): + self.set_widget(key, widgets.WuttaDateWidget(self.request)) + elif isinstance(column.type, sa.DateTime): self.set_widget(key, widgets.WuttaDateTimeWidget(self.request)) def set_grid(self, key, grid): diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index 1d3035c..f87e8b1 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -36,6 +36,7 @@ in the namespace: * :class:`deform:deform.widget.CheckboxWidget` * :class:`deform:deform.widget.SelectWidget` * :class:`deform:deform.widget.CheckboxChoiceWidget` +* :class:`deform:deform.widget.DateInputWidget` * :class:`deform:deform.widget.DateTimeInputWidget` * :class:`deform:deform.widget.MoneyInputWidget` """ @@ -49,7 +50,7 @@ import humanize from deform.widget import (Widget, TextInputWidget, TextAreaWidget, PasswordWidget, CheckedPasswordWidget, CheckboxWidget, SelectWidget, CheckboxChoiceWidget, - DateTimeInputWidget, MoneyInputWidget) + DateInputWidget, DateTimeInputWidget, MoneyInputWidget) from webhelpers2.html import HTML from wuttjamaican.conf import parse_list @@ -153,6 +154,43 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): self.app = self.config.get_app() +class WuttaDateWidget(DateInputWidget): + """ + Custom widget for :class:`python:datetime.date` fields. + + The main purpose of this widget is to leverage + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()` + for the readonly display. + + It is automatically used for SQLAlchemy mapped classes where the + field maps to a :class:`sqlalchemy:sqlalchemy.types.Date` column. + For other (non-mapped) date fields, or mapped datetime fields for + which a date widget is preferred, use + :meth:`~wuttaweb.forms.base.Form.set_widget()`. + + This is a subclass of + :class:`deform:deform.widget.DateInputWidget` and uses these + Deform templates: + + * ``dateinput`` + """ + + def __init__(self, request, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = request + self.config = self.request.wutta_config + self.app = self.config.get_app() + + def serialize(self, field, cstruct, **kw): + """ """ + readonly = kw.get('readonly', self.readonly) + if readonly and cstruct: + dt = datetime.datetime.fromisoformat(cstruct) + return self.app.render_date(dt) + + return super().serialize(field, cstruct, **kw) + + class WuttaDateTimeWidget(DateTimeInputWidget): """ Custom widget for :class:`python:datetime.datetime` fields. diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py index 914c9f7..bc229eb 100644 --- a/tests/forms/test_base.py +++ b/tests/forms/test_base.py @@ -3,6 +3,8 @@ from unittest import TestCase from unittest.mock import MagicMock, patch +import sqlalchemy as sa + import colander import deform from pyramid import testing @@ -188,6 +190,18 @@ class TestForm(TestCase): self.assertIn('created', form.widgets) self.assertIsInstance(form.widgets['created'], MyWidget) + # mock up a table with all relevant column types + class Whatever(model.Base): + __tablename__ = 'whatever' + id = sa.Column(sa.Integer(), primary_key=True) + date = sa.Column(sa.Date()) + date_time = sa.Column(sa.DateTime()) + + # widget set for all known types + form = self.make_form(model_class=Whatever) + self.assertIsInstance(form.widgets['date'], widgets.WuttaDateWidget) + self.assertIsInstance(form.widgets['date_time'], widgets.WuttaDateTimeWidget) + def test_set_grid(self): form = self.make_form(fields=['foo', 'bar']) self.assertNotIn('foo', form.widgets) diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index 4874c25..e571b88 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -87,6 +87,46 @@ class TestObjectRefWidget(WebTestCase): self.assertNotIn('url', values) +class TestWuttaDateWidget(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 make_widget(self, **kwargs): + return mod.WuttaDateWidget(self.request, **kwargs) + + def test_serialize(self): + node = colander.SchemaNode(colander.Date()) + field = self.make_field(node) + + # first try normal date + widget = self.make_widget() + dt = datetime.date(2025, 1, 15) + + # editable widget has normal picker html + result = widget.serialize(field, str(dt)) + self.assertIn('