feat: refactor forms/grids/views/templates per wuttaweb compat

this starts to get things more aligned between wuttaweb and tailbone.
the use case in mind so far is for a wuttaweb view to be included in a
tailbone app.

form and grid classes now have some new methods to match wuttaweb, so
templates call the shared method names where possible.

templates can no longer assume they have tailbone-native master view,
form, grid etc. so must inspect context more closely in some cases.
This commit is contained in:
Lance Edgar 2024-08-15 14:34:20 -05:00
parent b53479f8e4
commit a6ce5eb21d
39 changed files with 1037 additions and 300 deletions

View file

@ -12,9 +12,6 @@ class TestCase(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
# TODO: this probably shouldn't (need to) be here
self.config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group')
self.config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission')
def tearDown(self):
testing.tearDown()

0
tests/forms/__init__.py Normal file
View file

153
tests/forms/test_core.py Normal file
View file

@ -0,0 +1,153 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
import deform
from pyramid import testing
from tailbone.forms import core as mod
from tests.util import WebTestCase
class TestForm(WebTestCase):
def setUp(self):
self.setup_web()
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
def make_form(self, **kwargs):
kwargs.setdefault('request', self.request)
return mod.Form(**kwargs)
def test_basic(self):
form = self.make_form()
self.assertIsInstance(form, mod.Form)
def test_vue_tagname(self):
# default
form = self.make_form()
self.assertEqual(form.vue_tagname, 'tailbone-form')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.vue_tagname, 'something-else')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.vue_tagname, 'legacy-name')
def test_vue_component(self):
# default
form = self.make_form()
self.assertEqual(form.vue_component, 'TailboneForm')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.vue_component, 'SomethingElse')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.vue_component, 'LegacyName')
def test_component(self):
# default
form = self.make_form()
self.assertEqual(form.component, 'tailbone-form')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.component, 'something-else')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.component, 'legacy-name')
def test_component_studly(self):
# default
form = self.make_form()
self.assertEqual(form.component_studly, 'TailboneForm')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.component_studly, 'SomethingElse')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.component_studly, 'LegacyName')
def test_button_label_submit(self):
form = self.make_form()
# default
self.assertEqual(form.button_label_submit, "Submit")
# can set submit_label
with patch.object(form, 'submit_label', new="Submit Label", create=True):
self.assertEqual(form.button_label_submit, "Submit Label")
# can set save_label
with patch.object(form, 'save_label', new="Save Label"):
self.assertEqual(form.button_label_submit, "Save Label")
# can set button_label_submit
form.button_label_submit = "New Label"
self.assertEqual(form.button_label_submit, "New Label")
def test_get_deform(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
dform = form.get_deform()
self.assertIsInstance(dform, deform.Form)
def test_render_vue_tag(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_tag()
self.assertIn('<tailbone-form', html)
def test_render_vue_template(self):
self.pyramid_config.include('tailbone.views.common')
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_template(session=self.session)
self.assertIn('<form ', html)
def test_get_vue_field_value(self):
model = self.app.model
form = self.make_form(model_class=model.Setting)
# TODO: yikes what a hack (?)
dform = form.get_deform()
dform.set_appstruct({'name': 'foo', 'value': 'bar'})
# null for missing field
value = form.get_vue_field_value('doesnotexist')
self.assertIsNone(value)
# normal value is returned
value = form.get_vue_field_value('name')
self.assertEqual(value, 'foo')
# but not if we remove field from deform
# TODO: what is the use case here again?
dform.children.remove(dform['name'])
value = form.get_vue_field_value('name')
self.assertIsNone(value)
def test_render_vue_field(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_field('name', session=self.session)
self.assertIn('<b-field ', html)

0
tests/grids/__init__.py Normal file
View file

139
tests/grids/test_core.py Normal file
View file

@ -0,0 +1,139 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from tailbone.grids import core as mod
from tests.util import WebTestCase
class TestGrid(WebTestCase):
def setUp(self):
self.setup_web()
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
def make_grid(self, key, data=[], **kwargs):
kwargs.setdefault('request', self.request)
return mod.Grid(key, data=data, **kwargs)
def test_basic(self):
grid = self.make_grid('foo')
self.assertIsInstance(grid, mod.Grid)
def test_vue_tagname(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.vue_tagname, 'tailbone-grid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.vue_tagname, 'something-else')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.vue_tagname, 'legacy-name')
def test_vue_component(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.vue_component, 'TailboneGrid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.vue_component, 'SomethingElse')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.vue_component, 'LegacyName')
def test_component(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.component, 'tailbone-grid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.component, 'something-else')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.component, 'legacy-name')
def test_component_studly(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.component_studly, 'TailboneGrid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.component_studly, 'SomethingElse')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.component_studly, 'LegacyName')
def test_actions(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.actions, [])
# main actions
grid = self.make_grid('foo', main_actions=['foo'])
self.assertEqual(grid.actions, ['foo'])
# more actions
grid = self.make_grid('foo', main_actions=['foo'], more_actions=['bar'])
self.assertEqual(grid.actions, ['foo', 'bar'])
def test_render_vue_tag(self):
model = self.app.model
# standard
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_tag()
self.assertIn('<tailbone-grid', html)
self.assertNotIn('@deleteActionClicked', html)
# with delete hook
master = MagicMock(deletable=True, delete_confirm='simple')
master.has_perm.return_value = True
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_tag(master=master)
self.assertIn('<tailbone-grid', html)
self.assertIn('@deleteActionClicked', html)
def test_render_vue_template(self):
# self.pyramid_config.include('tailbone.views.common')
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_template(session=self.session)
self.assertIn('<b-table', html)
def test_get_vue_columns(self):
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
columns = grid.get_vue_columns()
self.assertEqual(len(columns), 2)
self.assertEqual(columns[0]['field'], 'name')
self.assertEqual(columns[1]['field'], 'value')
def test_get_vue_data(self):
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
data = grid.get_vue_data()
self.assertEqual(data, [])
# calling again returns same data
data2 = grid.get_vue_data()
self.assertIs(data2, data)

View file

@ -3,14 +3,14 @@
import os
from unittest import TestCase
from sqlalchemy import create_engine
from pyramid.config import Configurator
from wuttjamaican.testing import FileConfigTestCase
from rattail.config import RattailConfig
from rattail.exceptions import ConfigurationError
from rattail.db import Session as RattailSession
from tailbone import app
from tailbone.db import Session as TailboneSession
from rattail.config import RattailConfig
from tailbone import app as mod
from tests.util import DataTestCase
class TestRattailConfig(TestCase):
@ -18,15 +18,34 @@ class TestRattailConfig(TestCase):
config_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), 'data', 'tailbone.conf'))
def tearDown(self):
# may or may not be necessary depending on test
TailboneSession.remove()
def test_settings_arg_must_include_config_path_by_default(self):
# error raised if path not provided
self.assertRaises(ConfigurationError, app.make_rattail_config, {})
self.assertRaises(ConfigurationError, mod.make_rattail_config, {})
# get a config object if path provided
result = app.make_rattail_config({'rattail.config': self.config_path})
result = mod.make_rattail_config({'rattail.config': self.config_path})
# nb. cannot test isinstance(RattailConfig) b/c now uses wrapper!
self.assertIsNotNone(result)
self.assertTrue(hasattr(result, 'get'))
class TestMakePyramidConfig(DataTestCase):
def make_config(self):
myconf = self.write_file('web.conf', """
[rattail.db]
default.url = sqlite://
""")
self.settings = {
'rattail.config': myconf,
'mako.directories': 'tailbone:templates',
}
return mod.make_rattail_config(self.settings)
def test_basic(self):
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
# sanity check
pyramid_config = mod.make_pyramid_config(self.settings)
self.assertIsInstance(pyramid_config, Configurator)

3
tests/test_auth.py Normal file
View file

@ -0,0 +1,3 @@
# -*- coding: utf-8; -*-
from tailbone import auth as mod

12
tests/test_config.py Normal file
View file

@ -0,0 +1,12 @@
# -*- coding: utf-8; -*-
from tailbone import config as mod
from tests.util import DataTestCase
class TestConfigExtension(DataTestCase):
def test_basic(self):
# sanity / coverage check
ext = mod.ConfigExtension()
ext.configure(self.config)

58
tests/test_subscribers.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from pyramid import testing
from tailbone import subscribers as mod
from tests.util import DataTestCase
class TestNewRequest(DataTestCase):
def setUp(self):
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
})
def tearDown(self):
self.teardown_db()
testing.tearDown()
def make_request(self, **kwargs):
return testing.DummyRequest(**kwargs)
def make_event(self):
return MagicMock(request=self.request)
def test_continuum_remote_addr(self):
event = self.make_event()
# nothing happens
mod.new_request(event, session=self.session)
self.assertFalse(hasattr(self.session, 'continuum_remote_addr'))
# unless request has client_addr
self.request.client_addr = '127.0.0.1'
mod.new_request(event, session=self.session)
self.assertEqual(self.session.continuum_remote_addr, '127.0.0.1')
def test_register_component(self):
event = self.make_event()
# function added
self.assertFalse(hasattr(self.request, 'register_component'))
mod.new_request(event, session=self.session)
self.assertTrue(callable(self.request.register_component))
# call function
self.request.register_component('tailbone-datepicker', 'TailboneDatepicker')
self.assertEqual(self.request._tailbone_registered_components,
{'tailbone-datepicker': 'TailboneDatepicker'})
# duplicate registration ignored
self.request.register_component('tailbone-datepicker', 'TailboneDatepicker')
self.assertEqual(self.request._tailbone_registered_components,
{'tailbone-datepicker': 'TailboneDatepicker'})

75
tests/util.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from pyramid import testing
from tailbone import subscribers
from wuttaweb.menus import MenuHandler
# from wuttaweb.subscribers import new_request_set_user
from rattail.testing import DataTestCase
class WebTestCase(DataTestCase):
"""
Base class for test suites requiring a full (typical) web app.
"""
def setUp(self):
self.setup_web()
def setup_web(self):
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'rattail_config': self.config,
'mako.directories': ['tailbone:templates'],
# 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
})
# init web
# self.pyramid_config.include('pyramid_deform')
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_directive('add_wutta_permission_group',
'wuttaweb.auth.add_permission_group')
self.pyramid_config.add_directive('add_wutta_permission',
'wuttaweb.auth.add_permission')
self.pyramid_config.add_directive('add_tailbone_permission_group',
'wuttaweb.auth.add_permission_group')
self.pyramid_config.add_directive('add_tailbone_permission',
'wuttaweb.auth.add_permission')
self.pyramid_config.add_directive('add_tailbone_index_page',
'tailbone.app.add_index_page')
self.pyramid_config.add_directive('add_tailbone_model_view',
'tailbone.app.add_model_view')
self.pyramid_config.add_subscriber('tailbone.subscribers.before_render',
'pyramid.events.BeforeRender')
self.pyramid_config.include('tailbone.static')
# setup new request w/ anonymous user
event = MagicMock(request=self.request)
subscribers.new_request(event, session=self.session)
# def user_getter(request, **kwargs): pass
# new_request_set_user(event, db_session=self.session,
# user_getter=user_getter)
def tearDown(self):
self.teardown_web()
def teardown_web(self):
testing.tearDown()
self.teardown_db()
def make_request(self, **kwargs):
kwargs.setdefault('rattail_config', self.config)
# kwargs.setdefault('wutta_config', self.config)
return testing.DummyRequest(**kwargs)
class NullMenuHandler(MenuHandler):
"""
Dummy menu handler for testing.
"""
def make_menus(self, request, **kwargs):
return []

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from tailbone.views import master as mod
from tests.util import WebTestCase
class TestMasterView(WebTestCase):
def make_view(self):
return mod.MasterView(self.request)
def test_make_form_kwargs(self):
self.pyramid_config.add_route('settings.view', '/settings/{name}')
model = self.app.model
setting = model.Setting(name='foo', value='bar')
self.session.add(setting)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = self.make_view()
# sanity / coverage check
kw = view.make_form_kwargs(model_instance=setting)
self.assertIsNotNone(kw['action_url'])

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch, MagicMock
from tailbone.views import principal as mod
from tests.util import WebTestCase
class TestPrincipalMasterView(WebTestCase):
def make_view(self):
return mod.PrincipalMasterView(self.request)
def test_find_by_perm(self):
model = self.app.model
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
self.pyramid_config.include('tailbone.views.common')
self.pyramid_config.include('tailbone.views.auth')
self.pyramid_config.add_route('roles', '/roles/')
with patch.multiple(mod.PrincipalMasterView, create=True,
model_class=model.Role,
get_help_url=MagicMock(return_value=None),
get_help_markdown=MagicMock(return_value=None),
can_edit_help=MagicMock(return_value=False)):
# sanity / coverage check
view = self.make_view()
response = view.find_by_perm()
self.assertEqual(response.status_code, 200)

80
tests/views/test_roles.py Normal file
View file

@ -0,0 +1,80 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from tailbone.views import roles as mod
from tests.util import WebTestCase
class TestRoleView(WebTestCase):
def make_view(self):
return mod.RoleView(self.request)
def test_includeme(self):
self.pyramid_config.include('tailbone.views.roles')
def get_permissions(self):
return {
'widgets': {
'label': "Widgets",
'perms': {
'widgets.list': {
'label': "List widgets",
},
'widgets.polish': {
'label': "Polish the widgets",
},
'widgets.view': {
'label': "View widget",
},
},
},
}
def test_get_available_permissions(self):
model = self.app.model
auth = self.app.get_auth_handler()
blokes = model.Role(name="Blokes")
auth.grant_permission(blokes, 'widgets.list')
self.session.add(blokes)
barney = model.User(username='barney')
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
all_perms = self.get_permissions()
self.request.registry.settings['wutta_permissions'] = all_perms
def has_perm(perm):
if perm == 'widgets.list':
return True
return False
with patch.object(self.request, 'has_perm', new=has_perm, create=True):
# sanity check; current request has 1 perm
self.assertTrue(self.request.has_perm('widgets.list'))
self.assertFalse(self.request.has_perm('widgets.polish'))
self.assertFalse(self.request.has_perm('widgets.view'))
# when editing, user sees only the 1 perm
with patch.object(view, 'editing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']), ['widgets.list'])
# but when viewing, same user sees all perms
with patch.object(view, 'viewing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])
# also, when admin user is editing, sees all perms
self.request.is_admin = True
with patch.object(view, 'editing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])

33
tests/views/test_users.py Normal file
View file

@ -0,0 +1,33 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch, MagicMock
from tailbone.views import users as mod
from tailbone.views.principal import PermissionsRenderer
from tests.util import WebTestCase
class TestUserView(WebTestCase):
def make_view(self):
return mod.UserView(self.request)
def test_includeme(self):
self.pyramid_config.include('tailbone.views.users')
def test_configure_form(self):
self.pyramid_config.include('tailbone.views.users')
model = self.app.model
barney = model.User(username='barney')
self.session.add(barney)
self.session.commit()
view = self.make_view()
# must use mock configure when making form
def configure(form): pass
form = view.make_form(instance=barney, configure=configure)
with patch.object(view, 'viewing', new=True):
self.assertNotIn('permissions', form.renderers)
view.configure_form(form)
self.assertIsInstance(form.renderers['permissions'], PermissionsRenderer)