diff --git a/src/wuttaweb/app.py b/src/wuttaweb/app.py
index 845b41f..88318b4 100644
--- a/src/wuttaweb/app.py
+++ b/src/wuttaweb/app.py
@@ -61,7 +61,7 @@ class WebAppProvider(AppProvider):
return self.web_handler
-def make_wutta_config(settings):
+def make_wutta_config(settings, config_maker=None, **kwargs):
"""
Make a WuttaConfig object from the given settings.
@@ -93,8 +93,9 @@ def make_wutta_config(settings):
"section of config to the path of your "
"config file. Lame, but necessary.")
- # make config per usual, add to settings
- wutta_config = make_config(path)
+ # make config, add to settings
+ config_maker = config_maker or make_config
+ wutta_config = config_maker(path, **kwargs)
settings['wutta_config'] = wutta_config
# configure database sessions
diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py
index c3567b5..ceaeeb7 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)
@@ -978,7 +995,16 @@ class Form:
model_data = {}
def assign(field):
- model_data[field.oid] = make_json_safe(field.cstruct)
+ value = field.cstruct
+
+ # TODO: we need a proper true/false on the Vue side,
+ # but deform/colander want 'true' and 'false' ..so
+ # for now we explicitly translate here, ugh. also
+ # note this does not yet allow for null values.. :(
+ if isinstance(field.typ, colander.Boolean):
+ value = True if field.typ.true_val else False
+
+ model_data[field.oid] = make_json_safe(value)
for key in self.fields:
@@ -1076,7 +1102,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/schema.py b/src/wuttaweb/forms/schema.py
index a3a464b..4402fde 100644
--- a/src/wuttaweb/forms/schema.py
+++ b/src/wuttaweb/forms/schema.py
@@ -258,7 +258,12 @@ class PersonRef(ObjectRef):
This is a subclass of :class:`ObjectRef`.
"""
- model_class = Person
+
+ @property
+ def model_class(self):
+ """ """
+ model = self.app.model
+ return model.Person
def sort_query(self, query):
""" """
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index ee58a1a..837b6f1 100644
--- a/src/wuttaweb/forms/widgets.py
+++ b/src/wuttaweb/forms/widgets.py
@@ -33,14 +33,17 @@ in the namespace:
* :class:`deform:deform.widget.TextAreaWidget`
* :class:`deform:deform.widget.PasswordWidget`
* :class:`deform:deform.widget.CheckedPasswordWidget`
+* :class:`deform:deform.widget.CheckboxWidget`
* :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)
+ CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
+ MoneyInputWidget)
from webhelpers2.html import HTML
from wuttaweb.db import Session
@@ -220,7 +223,7 @@ class UserRefsWidget(WuttaCheckboxChoiceWidget):
users = []
if cstruct:
for uuid in cstruct:
- user = self.session.query(model.User).get(uuid)
+ user = self.session.get(model.User, uuid)
if user:
users.append(dict([(key, getattr(user, key))
for key in columns + ['uuid']]))
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index aa2e413..0f2c812 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -1047,6 +1047,12 @@ class Grid:
filters = filters or {}
if self.model_class:
+ # TODO: i tried using self.get_model_columns() here but in
+ # many cases that will be too aggressive. however it is
+ # often the case that the *grid* columns are a subset of
+ # the unerlying *table* columns. so until a better way
+ # is found, we choose "too few" instead of "too many"
+ # filters here. surely must improve it at some point.
for key in self.columns:
if key in filters:
continue
diff --git a/src/wuttaweb/templates/deform/checkbox.pt b/src/wuttaweb/templates/deform/checkbox.pt
index 92c9f62..c4536e8 100644
--- a/src/wuttaweb/templates/deform/checkbox.pt
+++ b/src/wuttaweb/templates/deform/checkbox.pt
@@ -6,6 +6,6 @@
v-model="${vmodel}"
native-value="true"
tal:attributes="attributes|field.widget.attributes|{};">
- {{ ${vmodel} }}
+ {{ ${vmodel} ? "Yes" : "No" }}
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/deform/readonly/checkbox.pt b/src/wuttaweb/templates/deform/readonly/checkbox.pt
new file mode 100644
index 0000000..4988213
--- /dev/null
+++ b/src/wuttaweb/templates/deform/readonly/checkbox.pt
@@ -0,0 +1,4 @@
+
+ Yes
+ No
+
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/src/wuttaweb/views/people.py b/src/wuttaweb/views/people.py
index a19df57..78cf931 100644
--- a/src/wuttaweb/views/people.py
+++ b/src/wuttaweb/views/people.py
@@ -123,6 +123,12 @@ class PersonView(MasterView):
@classmethod
def defaults(cls, config):
""" """
+
+ # nb. Person may come from custom model
+ wutta_config = config.registry.settings['wutta_config']
+ app = wutta_config.get_app()
+ cls.model_class = app.model.Person
+
cls._defaults(config)
cls._people_defaults(config)
diff --git a/src/wuttaweb/views/settings.py b/src/wuttaweb/views/settings.py
index a20e1f6..aa28416 100644
--- a/src/wuttaweb/views/settings.py
+++ b/src/wuttaweb/views/settings.py
@@ -51,6 +51,7 @@ class AppInfoView(MasterView):
model_name = 'AppInfo'
model_title_plural = "App Info"
route_prefix = 'appinfo'
+ filterable = False
sort_on_backend = False
sort_defaults = 'name'
paginated = False
diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py
index 91ed2e0..5e28a26 100644
--- a/src/wuttaweb/views/users.py
+++ b/src/wuttaweb/views/users.py
@@ -207,6 +207,17 @@ class UserView(MasterView):
role = session.get(model.Role, uuid)
user.roles.remove(role)
+ @classmethod
+ def defaults(cls, config):
+ """ """
+
+ # nb. User may come from custom model
+ wutta_config = config.registry.settings['wutta_config']
+ app = wutta_config.get_app()
+ cls.model_class = app.model.User
+
+ cls._defaults(config)
+
def defaults(config, **kwargs):
base = globals()
diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py
index 70cb51f..73e9d85 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
@@ -525,20 +520,33 @@ class TestForm(TestCase):
data = form.get_vue_model_data()
self.assertEqual(len(data), 2)
+ # confirm bool values make it thru as-is
+ schema.add(colander.SchemaNode(colander.Bool(), name='baz'))
+ form = self.make_form(schema=schema, model_instance={
+ 'foo': 'one',
+ 'bar': 'two',
+ 'baz': True,
+ })
+ data = form.get_vue_model_data()
+ self.assertEqual(list(data.values()), ['one', 'two', True])
+
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()