diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index c3567b5..4e415ca 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -746,17 +746,39 @@ class Form: kwargs = {} if self.model_instance: - # TODO: would it be smarter to test with hasattr() ? - # if hasattr(schema, 'dictify'): - if isinstance(self.model_instance, model.Base): + + # TODO: i keep finding problems with this, not sure + # what needs to happen. some forms will have a simple + # dict for model_instance, others will have a proper + # SQLAlchemy object. and in the latter case, it may + # not be "wutta-native" but from another DB. + + # so the problem is, how to detect whether we should + # use the model_instance as-is or if we should convert + # to a dict. some options include: + + # - check if instance has dictify() method + # i *think* this was tried and didn't work? but do not recall + + # - check if is instance of model.Base + # this is unreliable since model.Base is wutta-native + + # - check if form has a model_class + # has not been tried yet + + # - check if schema is from colanderalchemy + # this is what we are trying currently... + + if isinstance(schema, SQLAlchemySchemaNode): kwargs['appstruct'] = schema.dictify(self.model_instance) else: kwargs['appstruct'] = self.model_instance - form = deform.Form(schema, **kwargs) + # create the Deform instance # nb. must give a reference back to wutta form; this is # for sake of field schema nodes and widgets, e.g. to # access the main model instance + form = deform.Form(schema, **kwargs) form.wutta_form = self self.deform_form = form @@ -922,18 +944,13 @@ class Form: if field_type: attrs['type'] = field_type if messages: - if len(messages) == 1: - msg = messages[0] - if msg.startswith('`') and msg.endswith('`'): - attrs[':message'] = msg - else: - attrs['message'] = msg - # TODO - # else: - # # nb. must pass an array as JSON string - # attrs[':message'] = '[{}]'.format(', '.join([ - # "'{}'".format(msg.replace("'", r"\'")) - # for msg in messages])) + cls = 'is-size-7' + if field_type == 'is-danger': + cls += ' has-text-danger' + messages = [HTML.tag('p', c=[msg], class_=cls) + for msg in messages] + slot = HTML.tag('slot', name='messages', c=messages) + html = HTML.tag('div', c=[html, slot]) return HTML.tag('b-field', c=[html], **attrs) @@ -1076,7 +1093,7 @@ class Form: """ dform = self.get_deform() if field in dform: - error = dform[field].errormsg - if error: - return [error] + field = dform[field] + if field.error: + return field.error.messages() return [] diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index ee58a1a..6a7bc51 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -35,12 +35,14 @@ in the namespace: * :class:`deform:deform.widget.CheckedPasswordWidget` * :class:`deform:deform.widget.SelectWidget` * :class:`deform:deform.widget.CheckboxChoiceWidget` +* :class:`deform:deform.widget.MoneyInputWidget` """ import colander from deform.widget import (Widget, TextInputWidget, TextAreaWidget, PasswordWidget, CheckedPasswordWidget, - SelectWidget, CheckboxChoiceWidget) + SelectWidget, CheckboxChoiceWidget, + MoneyInputWidget) from webhelpers2.html import HTML from wuttaweb.db import Session diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index aa2e413..5d5984f 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -1047,7 +1047,7 @@ class Grid: filters = filters or {} if self.model_class: - for key in self.columns: + for key in self.get_model_columns(): if key in filters: continue prop = getattr(self.model_class, key, None) diff --git a/src/wuttaweb/templates/deform/moneyinput.pt b/src/wuttaweb/templates/deform/moneyinput.pt new file mode 100644 index 0000000..532a823 --- /dev/null +++ b/src/wuttaweb/templates/deform/moneyinput.pt @@ -0,0 +1,7 @@ + + + diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako index edcdc24..1ed408e 100644 --- a/src/wuttaweb/templates/grids/vue_template.mako +++ b/src/wuttaweb/templates/grids/vue_template.mako @@ -19,7 +19,7 @@ - + `` tag is also given a ``title`` + attribute with the original (full) text, so that appears on + mouse hover. + + To use this feature for your grid:: + + grid.set_renderer('my_notes_field', self.grid_render_notes) + + # you can also override maxlen + grid.set_renderer('my_notes_field', self.grid_render_notes, maxlen=50) + """ + if value is None: + return + + if len(value) < maxlen: + return value + + return HTML.tag('span', title=value, c=f"{value[:maxlen]}...") + ############################## # support methods ############################## @@ -1317,9 +1395,8 @@ class MasterView(View): Default logic for this method returns a "plain" query on the :attr:`model_class` if that is defined; otherwise ``None``. """ - model = self.app.model model_class = self.get_model_class() - if model_class and issubclass(model_class, model.Base): + if model_class: session = session or self.Session() return session.query(model_class) @@ -1344,33 +1421,6 @@ class MasterView(View): # for key in self.get_model_key(): # grid.set_link(key) - def grid_render_notes(self, record, key, value, maxlen=100): - """ - Custom grid renderer callable for "notes" fields. - - If the given text ``value`` is shorter than ``maxlen`` - characters, it is returned as-is. - - But if it is longer, then it is truncated and an ellispsis is - added. The resulting ```` tag is also given a ``title`` - attribute with the original (full) text, so that appears on - mouse hover. - - To use this feature for your grid:: - - grid.set_renderer('my_notes_field', self.grid_render_notes) - - # you can also override maxlen - grid.set_renderer('my_notes_field', self.grid_render_notes, maxlen=50) - """ - if value is None: - return - - if len(value) < maxlen: - return value - - return HTML.tag('span', title=value, c=f"{value[:maxlen]}...") - def get_instance(self, session=None): """ This should return the "current" model instance based on the diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py index 70cb51f..1fb02ec 100644 --- a/tests/forms/test_base.py +++ b/tests/forms/test_base.py @@ -461,15 +461,10 @@ class TestForm(TestCase): # nb. no error message self.assertNotIn('message', html) - # with single "static" error - dform['foo'].error = MagicMock(msg="something is wrong") - html = form.render_vue_field('foo') - self.assertIn(' message="something is wrong"', html) - - # with single "dynamic" error - dform['foo'].error = MagicMock(msg="`something is wrong`") - html = form.render_vue_field('foo') - self.assertIn(':message="`something is wrong`"', html) + # with error message + with patch.object(form, 'get_field_errors', return_value=['something is wrong']): + html = form.render_vue_field('foo') + self.assertIn('something is wrong', html) # add another field, but not to deform, so it should still # display but with no widget @@ -527,18 +522,21 @@ class TestForm(TestCase): def test_get_field_errors(self): schema = self.make_schema() + + # simple 'Required' validation failure form = self.make_form(schema=schema) - dform = form.get_deform() + self.request.method = 'POST' + self.request.POST = {'foo': 'one'} + self.assertFalse(form.validate()) + errors = form.get_field_errors('bar') + self.assertEqual(errors, ['Required']) - # no error - errors = form.get_field_errors('foo') - self.assertEqual(len(errors), 0) - - # simple error - dform['foo'].error = MagicMock(msg="something is wrong") - errors = form.get_field_errors('foo') - self.assertEqual(len(errors), 1) - self.assertEqual(errors[0], "something is wrong") + # no errors + form = self.make_form(schema=schema) + self.request.POST = {'foo': 'one', 'bar': 'two'} + self.assertTrue(form.validate()) + errors = form.get_field_errors('bar') + self.assertEqual(errors, []) def test_validate(self): schema = self.make_schema() diff --git a/tests/views/test_master.py b/tests/views/test_master.py index d000693..023449a 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -1,5 +1,6 @@ # -*- coding: utf-8; -*- +import decimal import functools from unittest import TestCase from unittest.mock import MagicMock, patch @@ -558,6 +559,44 @@ class TestMasterView(WebTestCase): view.configure_grid(grid) self.assertNotIn('uuid', grid.columns) + def test_grid_render_bool(self): + model = self.app.model + view = self.make_view() + user = model.User(username='barney', active=None) + + # null + value = view.grid_render_bool(user, 'active', None) + self.assertIsNone(value) + + # true + user.active = True + value = view.grid_render_bool(user, 'active', True) + self.assertEqual(value, "Yes") + + # false + user.active = False + value = view.grid_render_bool(user, 'active', False) + self.assertEqual(value, "No") + + def test_grid_render_currency(self): + model = self.app.model + view = self.make_view() + obj = {'amount': None} + + # null + value = view.grid_render_currency(obj, 'amount', None) + self.assertIsNone(value) + + # normal amount + obj['amount'] = decimal.Decimal('100.42') + value = view.grid_render_currency(obj, 'amount', '100.42') + self.assertEqual(value, "$100.42") + + # negative amount + obj['amount'] = decimal.Decimal('-100.42') + value = view.grid_render_currency(obj, 'amount', '-100.42') + self.assertEqual(value, "($100.42)") + def test_grid_render_notes(self): model = self.app.model view = self.make_view()