Compare commits
7 commits
16ed125113
...
eda2326a97
Author | SHA1 | Date | |
---|---|---|---|
|
eda2326a97 | ||
|
bf8397ba23 | ||
|
fce1bf9de4 | ||
|
7fc5d84abd | ||
|
6e4f390f3f | ||
|
448dc9fc79 | ||
|
74e2a4f0e2 |
|
@ -33,6 +33,7 @@ intersphinx_mapping = {
|
|||
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', None),
|
||||
'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None),
|
||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
||||
'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None),
|
||||
|
|
|
@ -457,23 +457,62 @@ class Form:
|
|||
if self.schema:
|
||||
self.schema[key] = node
|
||||
|
||||
def set_widget(self, key, widget):
|
||||
def set_widget(self, key, widget, **kwargs):
|
||||
"""
|
||||
Set/override the widget for a field.
|
||||
|
||||
You can specify a widget instance or else a named "type" of
|
||||
widget, in which case that is passed along to
|
||||
:meth:`make_widget()`.
|
||||
|
||||
:param key: Name of field.
|
||||
|
||||
:param widget: Instance of
|
||||
:class:`deform:deform.widget.Widget`.
|
||||
:param widget: Either a :class:`deform:deform.widget.Widget`
|
||||
instance, or else a widget "type" name.
|
||||
|
||||
:param \**kwargs: Any remaining kwargs are passed along to
|
||||
:meth:`make_widget()` - if applicable.
|
||||
|
||||
Widget overrides are tracked via :attr:`widgets`.
|
||||
"""
|
||||
if not isinstance(widget, deform.widget.Widget):
|
||||
widget_obj = self.make_widget(widget, **kwargs)
|
||||
if not widget_obj:
|
||||
raise ValueError(f"widget type not supported: {widget}")
|
||||
widget = widget_obj
|
||||
|
||||
self.widgets[key] = widget
|
||||
|
||||
# update schema if necessary
|
||||
if self.schema and key in self.schema:
|
||||
self.schema[key].widget = widget
|
||||
|
||||
def make_widget(self, widget_type, **kwargs):
|
||||
"""
|
||||
Make and return a new field widget of the given type.
|
||||
|
||||
This has built-in support for the following types (although
|
||||
subclass can override as needed):
|
||||
|
||||
* ``'notes'`` => :class:`~wuttaweb.forms.widgets.NotesWidget`
|
||||
|
||||
See also :meth:`set_widget()` which may call this method
|
||||
automatically.
|
||||
|
||||
:param widget_type: Which of the above (or custom) widget
|
||||
type to create.
|
||||
|
||||
:param \**kwargs: Remaining kwargs are passed as-is to the
|
||||
widget factory.
|
||||
|
||||
:returns: New widget instance, or ``None`` if e.g. it could
|
||||
not determine how to create the widget.
|
||||
"""
|
||||
from wuttaweb.forms import widgets
|
||||
|
||||
if widget_type == 'notes':
|
||||
return widgets.NotesWidget(**kwargs)
|
||||
|
||||
def set_grid(self, key, grid):
|
||||
"""
|
||||
Establish a :term:`grid` to be displayed for a field. This
|
||||
|
@ -1111,6 +1150,32 @@ class Form:
|
|||
|
||||
return self.validated
|
||||
|
||||
def has_global_errors(self):
|
||||
"""
|
||||
Convenience function to check if the form has any "global"
|
||||
(not field-level) errors.
|
||||
|
||||
See also :meth:`get_global_errors()`.
|
||||
|
||||
:returns: ``True`` if global errors present, else ``False``.
|
||||
"""
|
||||
dform = self.get_deform()
|
||||
return bool(dform.error)
|
||||
|
||||
def get_global_errors(self):
|
||||
"""
|
||||
Returns a list of "global" (not field-level) error messages
|
||||
for the form.
|
||||
|
||||
See also :meth:`has_global_errors()`.
|
||||
|
||||
:returns: List of error messages (possibly empty).
|
||||
"""
|
||||
dform = self.get_deform()
|
||||
if dform.error is None:
|
||||
return []
|
||||
return dform.error.messages()
|
||||
|
||||
def get_field_errors(self, field):
|
||||
"""
|
||||
Return a list of error messages for the given field.
|
||||
|
|
|
@ -24,15 +24,40 @@
|
|||
Form schema types
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import uuid as _uuid
|
||||
|
||||
import colander
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.forms import widgets
|
||||
from wuttjamaican.db.model import Person
|
||||
|
||||
|
||||
class WuttaDateTime(colander.DateTime):
|
||||
"""
|
||||
Custom schema type for ``datetime`` fields.
|
||||
|
||||
This should be used automatically for
|
||||
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you
|
||||
register another default.
|
||||
|
||||
This schema type exists for sake of convenience, when working with
|
||||
the Buefy datepicker + timepicker widgets.
|
||||
"""
|
||||
|
||||
def deserialize(self, node, cstruct):
|
||||
""" """
|
||||
if not cstruct:
|
||||
return colander.null
|
||||
|
||||
try:
|
||||
return datetime.datetime.strptime(cstruct, '%Y-%m-%dT%I:%M %p')
|
||||
except:
|
||||
node.raise_invalid("Invalid date and/or time")
|
||||
|
||||
|
||||
class ObjectNode(colander.SchemaNode):
|
||||
"""
|
||||
Custom schema node class which adds methods for compatibility with
|
||||
|
@ -207,6 +232,11 @@ class ObjectRef(colander.SchemaType):
|
|||
|
||||
def serialize(self, node, appstruct):
|
||||
""" """
|
||||
# nb. normalize to empty option if no object ref, so that
|
||||
# works as expected
|
||||
if self.empty_option and not appstruct:
|
||||
return self.empty_option[0]
|
||||
|
||||
if appstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
|
@ -497,3 +527,7 @@ class FileDownload(colander.String):
|
|||
""" """
|
||||
kwargs.setdefault('url', self.url)
|
||||
return widgets.FileDownloadWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
# nb. colanderalchemy schema overrides
|
||||
sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}
|
||||
|
|
|
@ -102,7 +102,7 @@ class ObjectRefWidget(SelectWidget):
|
|||
# add url, only if rendering readonly
|
||||
readonly = kw.get('readonly', self.readonly)
|
||||
if readonly:
|
||||
if 'url' not in values and self.url and field.schema.model_instance:
|
||||
if 'url' not in values and self.url and hasattr(field.schema, 'model_instance'):
|
||||
values['url'] = self.url(field.schema.model_instance)
|
||||
|
||||
return values
|
||||
|
|
|
@ -26,7 +26,7 @@ Web Handler
|
|||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
|
||||
from wuttaweb import static
|
||||
from wuttaweb import static, forms, grids
|
||||
|
||||
|
||||
class WebHandler(GenericHandler):
|
||||
|
@ -122,3 +122,23 @@ class WebHandler(GenericHandler):
|
|||
default='wuttaweb.menus:MenuHandler')
|
||||
self.menu_handler = self.app.load_object(spec)(self.config)
|
||||
return self.menu_handler
|
||||
|
||||
def make_form(self, request, **kwargs):
|
||||
"""
|
||||
Make and return a new :class:`~wuttaweb.forms.base.Form`
|
||||
instance, per the given ``kwargs``.
|
||||
|
||||
This is the "base" factory which merely invokes the
|
||||
constructor.
|
||||
"""
|
||||
return forms.Form(request, **kwargs)
|
||||
|
||||
def make_grid(self, request, **kwargs):
|
||||
"""
|
||||
Make and return a new :class:`~wuttaweb.grids.base.Grid`
|
||||
instance, per the given ``kwargs``.
|
||||
|
||||
This is the "base" factory which merely invokes the
|
||||
constructor.
|
||||
"""
|
||||
return grids.Grid(request, **kwargs)
|
||||
|
|
|
@ -196,6 +196,11 @@
|
|||
width: 50%;
|
||||
}
|
||||
|
||||
.wutta-form-wrapper .field.is-horizontal .field-body .select,
|
||||
.wutta-form-wrapper .field.is-horizontal .field-body .select select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
|
|
6
src/wuttaweb/templates/deform/dateinput.pt
Normal file
6
src/wuttaweb/templates/deform/dateinput.pt
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
${field.start_mapping()}
|
||||
<wutta-datepicker name="date"
|
||||
value="${cstruct}" />
|
||||
${field.end_mapping()}
|
||||
</div>
|
10
src/wuttaweb/templates/deform/datetimeinput.pt
Normal file
10
src/wuttaweb/templates/deform/datetimeinput.pt
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div style="display: flex; gap: 0.5rem;">
|
||||
${field.start_mapping()}
|
||||
<wutta-datepicker name="date"
|
||||
style="flex-grow: 1;"
|
||||
value="${date}" />
|
||||
<wutta-timepicker name="time"
|
||||
style="flex-grow: 1;"
|
||||
value="${time}" />
|
||||
${field.end_mapping()}
|
||||
</div>
|
|
@ -4,6 +4,6 @@
|
|||
${str(field.schema.model_instance or '')}
|
||||
</a>
|
||||
<span tal:condition="not url">
|
||||
${str(field.schema.model_instance or '')}
|
||||
${str(getattr(field.schema, 'model_instance', None) or '')}
|
||||
</span>
|
||||
</tal:omit>
|
||||
|
|
|
@ -4,6 +4,14 @@
|
|||
${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
% if form.has_global_errors():
|
||||
% for msg in form.get_global_errors():
|
||||
<b-notification type="is-warning" :closable="false">
|
||||
${msg}
|
||||
</b-notification>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
<section>
|
||||
% for fieldname in form:
|
||||
${form.render_vue_field(fieldname)}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<%def name="make_wutta_components()">
|
||||
${self.make_wutta_request_mixin()}
|
||||
${self.make_wutta_button_component()}
|
||||
${self.make_wutta_datepicker_component()}
|
||||
${self.make_wutta_timepicker_component()}
|
||||
${self.make_wutta_filter_component()}
|
||||
${self.make_wutta_filter_value_component()}
|
||||
</%def>
|
||||
|
@ -149,6 +151,141 @@
|
|||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_wutta_datepicker_component()">
|
||||
<script type="text/x-template" id="wutta-datepicker-template">
|
||||
<b-datepicker :name="name"
|
||||
:editable="editable"
|
||||
icon-pack="fas"
|
||||
icon="calendar-alt"
|
||||
:date-formatter="formatDate"
|
||||
:value="buefyValue" />
|
||||
</script>
|
||||
<script>
|
||||
const WuttaDatepicker = {
|
||||
template: '#wutta-datepicker-template',
|
||||
props: {
|
||||
name: String,
|
||||
value: String,
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buefyValue: this.parseDate(this.value),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
formatDate(date) {
|
||||
if (date === null) {
|
||||
return null
|
||||
}
|
||||
// just need to convert to simple ISO date format here, seems
|
||||
// like there should be a more obvious way to do that?
|
||||
var year = date.getFullYear()
|
||||
var month = date.getMonth() + 1
|
||||
var day = date.getDate()
|
||||
month = month < 10 ? '0' + month : month
|
||||
day = day < 10 ? '0' + day : day
|
||||
return year + '-' + month + '-' + day
|
||||
},
|
||||
|
||||
parseDate(date) {
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
if (typeof(date) == 'string') {
|
||||
// nb. this assumes classic YYYY-MM-DD (ISO) format
|
||||
var parts = date.split('-')
|
||||
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
|
||||
}
|
||||
return date
|
||||
},
|
||||
},
|
||||
}
|
||||
Vue.component('wutta-datepicker', WuttaDatepicker)
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_wutta_timepicker_component()">
|
||||
<script type="text/x-template" id="wutta-timepicker-template">
|
||||
<b-timepicker :name="name"
|
||||
editable
|
||||
:value="buefyValue" />
|
||||
</script>
|
||||
<script>
|
||||
const WuttaTimepicker = {
|
||||
template: '#wutta-timepicker-template',
|
||||
props: {
|
||||
name: String,
|
||||
value: String,
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buefyValue: this.parseTime(this.value),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
formatTime(time) {
|
||||
if (time === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let h = time.getHours()
|
||||
let m = time.getMinutes()
|
||||
let s = time.getSeconds()
|
||||
|
||||
h = h < 10 ? '0' + h : h
|
||||
m = m < 10 ? '0' + m : m
|
||||
s = s < 10 ? '0' + s : s
|
||||
|
||||
return h + ':' + m + ':' + s
|
||||
},
|
||||
|
||||
parseTime(time) {
|
||||
if (time.getHours) {
|
||||
return time
|
||||
}
|
||||
|
||||
let found, hours, minutes
|
||||
|
||||
found = time.match(/^(\d\d):(\d\d):\d\d$/)
|
||||
if (found) {
|
||||
hours = parseInt(found[1])
|
||||
minutes = parseInt(found[2])
|
||||
return new Date(null, null, null, hours, minutes)
|
||||
}
|
||||
|
||||
found = time.match(/^\s*(\d\d?):(\d\d)\s*([AaPp][Mm])\s*$/)
|
||||
if (found) {
|
||||
hours = parseInt(found[1])
|
||||
minutes = parseInt(found[2])
|
||||
const ampm = found[3].toUpperCase()
|
||||
if (ampm == 'AM') {
|
||||
if (hours == 12) {
|
||||
hours = 0
|
||||
}
|
||||
} else { // PM
|
||||
if (hours < 12) {
|
||||
hours += 12
|
||||
}
|
||||
}
|
||||
return new Date(null, null, null, hours, minutes)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
Vue.component('wutta-timepicker', WuttaTimepicker)
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_wutta_filter_component()">
|
||||
<script type="text/x-template" id="wutta-filter-template">
|
||||
<div v-show="filter.visible"
|
||||
|
|
|
@ -30,7 +30,7 @@ from pyramid import httpexceptions
|
|||
from pyramid.renderers import render_to_response
|
||||
from pyramid.response import FileResponse
|
||||
|
||||
from wuttaweb import forms, grids
|
||||
from wuttaweb import grids
|
||||
|
||||
|
||||
class View:
|
||||
|
@ -75,7 +75,8 @@ class View:
|
|||
This is the "base" factory which merely invokes the
|
||||
constructor.
|
||||
"""
|
||||
return forms.Form(self.request, **kwargs)
|
||||
web = self.app.get_web_handler()
|
||||
return web.make_form(self.request, **kwargs)
|
||||
|
||||
def make_grid(self, **kwargs):
|
||||
"""
|
||||
|
@ -85,7 +86,8 @@ class View:
|
|||
This is the "base" factory which merely invokes the
|
||||
constructor.
|
||||
"""
|
||||
return grids.Grid(self.request, **kwargs)
|
||||
web = self.app.get_web_handler()
|
||||
return web.make_grid(self.request, **kwargs)
|
||||
|
||||
def make_grid_action(self, key, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -2000,7 +2000,7 @@ class MasterView(View):
|
|||
Default logic returns the value from ``str(instance)``;
|
||||
subclass may override if needed.
|
||||
"""
|
||||
return str(instance)
|
||||
return str(instance) or "(no title)"
|
||||
|
||||
def get_action_url(self, action, obj, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -34,7 +34,6 @@ from sqlalchemy import orm
|
|||
|
||||
from wuttjamaican.db.model import Upgrade
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms import widgets
|
||||
from wuttaweb.forms.schema import UserRef, WuttaEnum, FileDownload
|
||||
from wuttaweb.progress import get_progress_session
|
||||
|
||||
|
@ -147,7 +146,7 @@ class UpgradeView(MasterView):
|
|||
f.set_node('created_by', UserRef(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget('notes', widgets.NotesWidget())
|
||||
f.set_widget('notes', 'notes')
|
||||
|
||||
# status
|
||||
if self.creating:
|
||||
|
|
|
@ -142,6 +142,26 @@ class TestForm(TestCase):
|
|||
self.assertIs(form.widgets['foo'], new_widget)
|
||||
self.assertIs(schema['foo'].widget, new_widget)
|
||||
|
||||
# can also just specify widget pseudo-type (invalid)
|
||||
self.assertNotIn('bar', form.widgets)
|
||||
self.assertRaises(ValueError, form.set_widget, 'bar', 'ldjfadjfadj')
|
||||
|
||||
# can also just specify widget pseudo-type (valid)
|
||||
self.assertNotIn('bar', form.widgets)
|
||||
form.set_widget('bar', 'notes')
|
||||
self.assertIsInstance(form.widgets['bar'], widgets.NotesWidget)
|
||||
|
||||
def test_make_widget(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
|
||||
# notes
|
||||
widget = form.make_widget('notes')
|
||||
self.assertIsInstance(widget, widgets.NotesWidget)
|
||||
|
||||
# invalid
|
||||
widget = form.make_widget('fdajvdafjjf')
|
||||
self.assertIsNone(widget)
|
||||
|
||||
def test_set_grid(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertNotIn('foo', form.widgets)
|
||||
|
@ -544,6 +564,34 @@ class TestForm(TestCase):
|
|||
data = form.get_vue_model_data()
|
||||
self.assertEqual(list(data.values()), ['one', 'two', True])
|
||||
|
||||
def test_has_global_errors(self):
|
||||
|
||||
def fail(node, value):
|
||||
node.raise_invalid("things are bad!")
|
||||
|
||||
schema = self.make_schema()
|
||||
schema.validator = fail
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertFalse(form.has_global_errors())
|
||||
self.request.method = 'POST'
|
||||
self.request.POST = {'foo': 'one', 'bar': 'two'}
|
||||
self.assertFalse(form.validate())
|
||||
self.assertTrue(form.has_global_errors())
|
||||
|
||||
def test_get_global_errors(self):
|
||||
|
||||
def fail(node, value):
|
||||
node.raise_invalid("things are bad!")
|
||||
|
||||
schema = self.make_schema()
|
||||
schema.validator = fail
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertEqual(form.get_global_errors(), [])
|
||||
self.request.method = 'POST'
|
||||
self.request.POST = {'foo': 'one', 'bar': 'two'}
|
||||
self.assertFalse(form.validate())
|
||||
self.assertTrue(form.get_global_errors(), ["things are bad!"])
|
||||
|
||||
def test_get_field_errors(self):
|
||||
schema = self.make_schema()
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
import datetime
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
import colander
|
||||
|
@ -13,6 +15,22 @@ from wuttaweb.forms import widgets
|
|||
from tests.util import DataTestCase, WebTestCase
|
||||
|
||||
|
||||
class TestWutaDateTime(TestCase):
|
||||
|
||||
def test_deserialize(self):
|
||||
typ = mod.WuttaDateTime()
|
||||
node = colander.SchemaNode(typ)
|
||||
|
||||
result = typ.deserialize(node, colander.null)
|
||||
self.assertIs(result, colander.null)
|
||||
|
||||
result = typ.deserialize(node, '2024-12-11T10:33 PM')
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
|
||||
|
||||
self.assertRaises(colander.Invalid, typ.deserialize, node, 'bogus')
|
||||
|
||||
|
||||
class TestObjectNode(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -102,6 +120,11 @@ class TestObjectRef(DataTestCase):
|
|||
value = typ.serialize(node, person)
|
||||
self.assertEqual(value, person.uuid.hex)
|
||||
|
||||
# null w/ empty option
|
||||
typ = mod.ObjectRef(self.request, empty_option=('bad', 'BAD'))
|
||||
value = typ.serialize(node, colander.null)
|
||||
self.assertEqual(value, 'bad')
|
||||
|
||||
def test_deserialize(self):
|
||||
model = self.app.model
|
||||
node = colander.SchemaNode(colander.String())
|
||||
|
|
|
@ -61,6 +61,7 @@ class TestObjectRefWidget(WebTestCase):
|
|||
self.session.add(person)
|
||||
self.session.commit()
|
||||
|
||||
# standard
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
|
@ -68,6 +69,15 @@ class TestObjectRefWidget(WebTestCase):
|
|||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
|
||||
# readonly w/ empty option
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session,
|
||||
empty_option=('_empty_', '(empty)')))
|
||||
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, '_empty_', {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
|
||||
|
||||
class TestFileDownloadWidget(WebTestCase):
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from wuttaweb import handler as mod, static
|
||||
from wuttaweb.forms import Form
|
||||
from wuttaweb.grids import Grid
|
||||
from wuttaweb.menus import MenuHandler
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
@ -62,3 +64,13 @@ class TestWebHandler(WebTestCase):
|
|||
handler = self.make_handler()
|
||||
menus = handler.get_menu_handler()
|
||||
self.assertIsInstance(menus, MenuHandler)
|
||||
|
||||
def test_make_form(self):
|
||||
handler = self.make_handler()
|
||||
form = handler.make_form(self.request)
|
||||
self.assertIsInstance(form, Form)
|
||||
|
||||
def test_make_grid(self):
|
||||
handler = self.make_handler()
|
||||
grid = handler.make_grid(self.request)
|
||||
self.assertIsInstance(grid, Grid)
|
||||
|
|
Loading…
Reference in a new issue