3
0
Fork 0

feat: add basic support for "view" part of CRUD

still no SQLAlchemy yet, view must be explicit about data/model.  but
should support simple dict records, which will be needed in a few
places anyway
This commit is contained in:
Lance Edgar 2024-08-07 19:47:24 -05:00
parent 754e0989e4
commit 4c467f5267
14 changed files with 745 additions and 82 deletions

View file

@ -59,8 +59,8 @@ class TestForm(TestCase):
def tearDown(self):
testing.tearDown()
def make_form(self, request=None, **kwargs):
return base.Form(request or self.request, **kwargs)
def make_form(self, **kwargs):
return base.Form(self.request, **kwargs)
def make_schema(self):
schema = colander.Schema(children=[
@ -124,19 +124,33 @@ class TestForm(TestCase):
self.assertIs(form.schema, schema)
self.assertIs(form.get_schema(), schema)
# auto-generating schema not yet supported
# schema is auto-generated if fields provided
form = self.make_form(fields=['foo', 'bar'])
schema = form.get_schema()
self.assertEqual(len(schema.children), 2)
self.assertEqual(schema['foo'].name, 'foo')
# but auto-generating without fields is not supported
form = self.make_form()
self.assertIsNone(form.schema)
self.assertRaises(NotImplementedError, form.get_schema)
def test_get_deform(self):
schema = self.make_schema()
# basic
form = self.make_form(schema=schema)
self.assertFalse(hasattr(form, 'deform_form'))
dform = form.get_deform()
self.assertIsInstance(dform, deform.Form)
self.assertIs(form.deform_form, dform)
# with model instance / cstruct
myobj = {'foo': 'one', 'bar': 'two'}
form = self.make_form(schema=schema, model_instance=myobj)
dform = form.get_deform()
self.assertEqual(dform.cstruct, myobj)
def test_get_label(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.get_label('foo'), "Foo")
@ -193,6 +207,13 @@ class TestForm(TestCase):
# nb. no error message
self.assertNotIn('message', html)
# readonly
html = form.render_vue_field('foo', readonly=True)
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
self.assertNotIn('<b-input name="foo"', html)
# 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')

View file

@ -75,3 +75,76 @@ class TestGrid(TestCase):
first = columns[0]
self.assertEqual(first['field'], 'foo')
self.assertEqual(first['label'], 'Foo')
def test_get_vue_data(self):
# null by default
grid = self.make_grid()
data = grid.get_vue_data()
self.assertIsNone(data)
# is usually a list
mydata = [
{'foo': 'bar'},
]
grid = self.make_grid(data=mydata)
data = grid.get_vue_data()
self.assertIs(data, mydata)
self.assertEqual(data, [{'foo': 'bar'}])
# if grid has actions, that list may be supplemented
grid.actions.append(base.GridAction(self.request, 'view', url='/blarg'))
data = grid.get_vue_data()
self.assertIsNot(data, mydata)
self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])
class TestGridAction(TestCase):
def setUp(self):
self.config = WuttaConfig()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
def make_action(self, key, **kwargs):
return base.GridAction(self.request, key, **kwargs)
def test_render_icon(self):
# icon is derived from key by default
action = self.make_action('blarg')
html = action.render_icon()
self.assertIn('<i class="fas fa-blarg">', html)
# oruga not yet supported
self.request.use_oruga = True
self.assertRaises(NotImplementedError, action.render_icon)
def test_render_label(self):
# label is derived from key by default
action = self.make_action('blarg')
label = action.render_label()
self.assertEqual(label, "Blarg")
# otherwise use what caller provides
action = self.make_action('foo', label="Bar")
label = action.render_label()
self.assertEqual(label, "Bar")
def test_get_url(self):
obj = {'foo': 'bar'}
# null by default
action = self.make_action('blarg')
url = action.get_url(obj)
self.assertIsNone(url)
# or can be "static"
action = self.make_action('blarg', url='/foo')
url = action.get_url(obj)
self.assertEqual(url, '/foo')
# or can be "dynamic"
action = self.make_action('blarg', url=lambda o, i: '/yeehaw')
url = action.get_url(obj)
self.assertEqual(url, '/yeehaw')

View file

@ -19,8 +19,9 @@ class TestMasterView(WebTestCase):
def test_defaults(self):
master.MasterView.model_name = 'Widget'
# TODO: should inspect pyramid routes after this, to be certain
master.MasterView.defaults(self.pyramid_config)
with patch.object(master.MasterView, 'viewable', new=False):
# TODO: should inspect pyramid routes after this, to be certain
master.MasterView.defaults(self.pyramid_config)
del master.MasterView.model_name
##############################
@ -122,6 +123,16 @@ class TestMasterView(WebTestCase):
self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
del master.MasterView.model_class
def test_get_model_key(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_model_key)
# subclass may specify model key
master.MasterView.model_key = 'uuid'
self.assertEqual(master.MasterView.get_model_key(), ('uuid',))
del master.MasterView.model_key
def test_get_route_prefix(self):
# error by default (since no model class)
@ -179,6 +190,25 @@ class TestMasterView(WebTestCase):
self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
del master.MasterView.model_class
def test_get_instance_url_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_instance_url_prefix)
# typical example with url_prefix and simple key
master.MasterView.url_prefix = '/widgets'
master.MasterView.model_key = 'uuid'
self.assertEqual(master.MasterView.get_instance_url_prefix(), '/widgets/{uuid}')
del master.MasterView.url_prefix
del master.MasterView.model_key
# typical example with composite key
master.MasterView.url_prefix = '/widgets'
master.MasterView.model_key = ('foo', 'bar')
self.assertEqual(master.MasterView.get_instance_url_prefix(), '/widgets/{foo}|{bar}')
del master.MasterView.url_prefix
del master.MasterView.model_key
def test_get_template_prefix(self):
# error by default (since no model class)
@ -281,12 +311,6 @@ class TestMasterView(WebTestCase):
# support methods
##############################
def test_get_index_title(self):
master.MasterView.model_title_plural = "Wutta Widgets"
view = master.MasterView(self.request)
self.assertEqual(view.get_index_title(), "Wutta Widgets")
del master.MasterView.model_title_plural
def test_render_to_response(self):
def widgets(request): return {}
@ -317,24 +341,51 @@ class TestMasterView(WebTestCase):
self.assertRaises(IOError, view.render_to_response, 'nonexistent', {})
del master.MasterView.model_name
def test_get_index_title(self):
master.MasterView.model_title_plural = "Wutta Widgets"
view = master.MasterView(self.request)
self.assertEqual(view.get_index_title(), "Wutta Widgets")
del master.MasterView.model_title_plural
def test_get_instance(self):
view = master.MasterView(self.request)
self.assertRaises(NotImplementedError, view.get_instance)
##############################
# view methods
##############################
def test_index(self):
# basic sanity check using /appinfo
master.MasterView.model_name = 'AppInfo'
master.MasterView.route_prefix = 'appinfo'
master.MasterView.template_prefix = '/appinfo'
master.MasterView.grid_columns = ['foo', 'bar']
# sanity/coverage check using /settings/
master.MasterView.model_name = 'Setting'
master.MasterView.model_key = 'name'
master.MasterView.grid_columns = ['name', 'value']
view = master.MasterView(self.request)
response = view.index()
# then again with data, to include view action url
data = [{'name': 'foo', 'value': 'bar'}]
with patch.object(view, 'index_get_grid_data', return_value=data):
response = view.index()
del master.MasterView.model_name
del master.MasterView.route_prefix
del master.MasterView.template_prefix
del master.MasterView.model_key
del master.MasterView.grid_columns
def test_view(self):
# sanity/coverage check using /settings/XXX
master.MasterView.model_name = 'Setting'
master.MasterView.grid_columns = ['name', 'value']
master.MasterView.form_fields = ['name', 'value']
view = master.MasterView(self.request)
setting = {'name': 'foo.bar', 'value': 'baz'}
self.request.matchdict = {'name': 'foo.bar'}
with patch.object(view, 'get_instance', return_value=setting):
response = view.view()
del master.MasterView.model_name
del master.MasterView.grid_columns
del master.MasterView.form_fields
def test_configure(self):
model = self.app.model

View file

@ -2,6 +2,8 @@
from tests.views.utils import WebTestCase
from pyramid.httpexceptions import HTTPNotFound
from wuttaweb.views import settings
@ -43,3 +45,23 @@ class TestSettingView(WebTestCase):
self.session.commit()
data = view.index_get_grid_data(session=self.session)
self.assertEqual(len(data), 1)
def test_get_instance(self):
view = self.make_view()
self.request.matchdict = {'name': 'foo'}
# setting not found
setting = view.get_instance(session=self.session)
self.assertIsInstance(setting, HTTPNotFound)
# setting is returned
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
setting = view.get_instance(session=self.session)
self.assertEqual(setting, {'name': 'foo', 'value': 'bar'})
def test_get_instance_title(self):
setting = {'name': 'foo', 'value': 'bar'}
view = self.make_view()
title = view.get_instance_title(setting)
self.assertEqual(title, 'foo')