fix: misc. improvements for display of grids, form errors
This commit is contained in:
parent
bf2ca4b475
commit
2503836ef5
|
@ -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 []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
7
src/wuttaweb/templates/deform/moneyinput.pt
Normal file
7
src/wuttaweb/templates/deform/moneyinput.pt
Normal file
|
@ -0,0 +1,7 @@
|
|||
<tal:omit tal:define="name name|field.name;
|
||||
oid oid|field.oid;
|
||||
vmodel vmodel|'modelData.'+oid;">
|
||||
<b-input name="${name}"
|
||||
v-model="${vmodel}"
|
||||
tal:attributes="attributes|field.widget.attributes|{};" />
|
||||
</tal:omit>
|
|
@ -19,7 +19,7 @@
|
|||
<b-button @click="copyDirectLink()"
|
||||
title="Copy grid link to clipboard"
|
||||
:is-small="smallFilters">
|
||||
<b-icon pack="fas" icon="share-alt" />
|
||||
<b-icon pack="fas" icon="share" />
|
||||
</b-button>
|
||||
|
||||
<b-button type="is-primary"
|
||||
|
|
|
@ -989,6 +989,84 @@ class MasterView(View):
|
|||
self.app.save_setting(session, setting['name'], setting['value'],
|
||||
force_create=True)
|
||||
|
||||
##############################
|
||||
# grid rendering methods
|
||||
##############################
|
||||
|
||||
def grid_render_bool(self, record, key, value):
|
||||
"""
|
||||
Custom grid value renderer for "boolean" fields.
|
||||
|
||||
This converts a bool value to "Yes" or "No" - unless the value
|
||||
is ``None`` in which case this renders empty string.
|
||||
To use this feature for your grid::
|
||||
|
||||
grid.set_renderer('my_bool_field', self.grid_render_bool)
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
|
||||
return "Yes" if value else "No"
|
||||
|
||||
def grid_render_currency(self, record, key, value, scale=2):
|
||||
"""
|
||||
Custom grid value renderer for "currency" fields.
|
||||
|
||||
This expects float or decimal values, and will round the
|
||||
decimal as appropriate, and add the currency symbol.
|
||||
|
||||
:param scale: Number of decimal digits to be displayed;
|
||||
default is 2 places.
|
||||
|
||||
To use this feature for your grid::
|
||||
|
||||
grid.set_renderer('my_currency_field', self.grid_render_currency)
|
||||
|
||||
# you can also override scale
|
||||
grid.set_renderer('my_currency_field', self.grid_render_currency, scale=4)
|
||||
"""
|
||||
|
||||
# nb. get new value since the one provided will just be a
|
||||
# (json-safe) *string* if the original type was Decimal
|
||||
value = record[key]
|
||||
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if value < 0:
|
||||
fmt = f"(${{:0,.{scale}f}})"
|
||||
return fmt.format(0 - value)
|
||||
|
||||
fmt = f"${{:0,.{scale}f}}"
|
||||
return fmt.format(value)
|
||||
|
||||
def grid_render_notes(self, record, key, value, maxlen=100):
|
||||
"""
|
||||
Custom grid value renderer 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 ``<span>`` 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 ``<span>`` 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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue