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()