2
0
Fork 0

feat: add initial views for upgrades

CRUD only so far, still need execute features
This commit is contained in:
Lance Edgar 2024-08-24 11:29:52 -05:00
parent 1804e74d13
commit 6650ee698e
14 changed files with 656 additions and 117 deletions

View file

@ -32,4 +32,5 @@
views.people
views.roles
views.settings
views.upgrades
views.users

View file

@ -0,0 +1,6 @@
``wuttaweb.views.upgrades``
===========================
.. automodule:: wuttaweb.views.upgrades
:members:

View file

@ -92,6 +92,53 @@ class ObjectNode(colander.SchemaNode):
raise NotImplementedError(f"you must define {class_name}.objectify()")
class WuttaEnum(colander.Enum):
"""
Custom schema type for enum fields.
This is a subclass of :class:`colander.Enum`, but adds a
default widget (``SelectWidget``) with enum choices.
:param request: Current :term:`request` object.
"""
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
def widget_maker(self, **kwargs):
""" """
if 'values' not in kwargs:
kwargs['values'] = [(getattr(e, self.attr), getattr(e, self.attr))
for e in self.enum_cls]
return widgets.SelectWidget(**kwargs)
class WuttaSet(colander.Set):
"""
Custom schema type for :class:`python:set` fields.
This is a subclass of :class:`colander.Set`, but adds
Wutta-related params to the constructor.
:param request: Current :term:`request` object.
:param session: Optional :term:`db session` to use instead of
:class:`wuttaweb.db.Session`.
"""
def __init__(self, request, session=None):
super().__init__()
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
self.session = session or Session()
class ObjectRef(colander.SchemaType):
"""
Custom schema type for a model class reference field.
@ -199,7 +246,7 @@ class ObjectRef(colander.SchemaType):
# fetch object from DB
model = self.app.model
obj = self.session.query(self.model_class).get(value)
obj = self.session.get(self.model_class, value)
# raise error if not found
if not obj:
@ -247,14 +294,28 @@ class ObjectRef(colander.SchemaType):
kwargs['values'] = values
if 'url' not in kwargs:
kwargs['url'] = lambda person: self.request.route_url('people.view', uuid=person.uuid)
kwargs['url'] = self.get_object_url
return widgets.ObjectRefWidget(self.request, **kwargs)
def get_object_url(self, obj):
"""
Returns the "view" URL for the given object, if applicable.
This is used when rendering the field readonly. If this
method returns a URL then the field text will be wrapped with
a hyperlink, otherwise it will be shown as-is.
Default logic always returns ``None``; subclass should
override as needed.
"""
class PersonRef(ObjectRef):
"""
Custom schema type for a ``Person`` reference field.
Custom schema type for a
:class:`~wuttjamaican:wuttjamaican.db.model.base.Person` reference
field.
This is a subclass of :class:`ObjectRef`.
"""
@ -269,26 +330,33 @@ class PersonRef(ObjectRef):
""" """
return query.order_by(self.model_class.full_name)
def get_object_url(self, person):
""" """
return self.request.route_url('people.view', uuid=person.uuid)
class WuttaSet(colander.Set):
class UserRef(ObjectRef):
"""
Custom schema type for :class:`python:set` fields.
Custom schema type for a
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` reference
field.
This is a subclass of :class:`colander.Set`, but adds
Wutta-related params to the constructor.
:param request: Current :term:`request` object.
:param session: Optional :term:`db session` to use instead of
:class:`wuttaweb.db.Session`.
This is a subclass of :class:`ObjectRef`.
"""
def __init__(self, request, session=None):
super().__init__()
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
self.session = session or Session()
@property
def model_class(self):
""" """
model = self.app.model
return model.User
def sort_query(self, query):
""" """
return query.order_by(self.model_class.username)
def get_object_url(self, user):
""" """
return self.request.route_url('users.view', uuid=user.uuid)
class RoleRefs(WuttaSet):

View file

@ -1078,6 +1078,7 @@ class Grid:
:returns: A :class:`~wuttaweb.grids.filters.GridFilter`
instance.
"""
key = kwargs.pop('key', None)
# model_property is required
model_property = None
@ -1102,7 +1103,7 @@ class Grid:
# make filter
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
return factory(self.request, key or model_property.key, **kwargs)
def set_filter(self, key, filterinfo=None, **kwargs):
"""
@ -1132,6 +1133,7 @@ class Grid:
# filtr = filterinfo
raise NotImplementedError
else:
kwargs['key'] = key
kwargs.setdefault('label', self.get_label(key))
filtr = self.make_filter(filterinfo or key, **kwargs)

View file

@ -168,6 +168,11 @@ class MenuHandler(GenericHandler):
'route': 'settings',
'perm': 'settings.list',
},
{
'title': "Upgrades",
'route': 'upgrades',
'perm': 'upgrades.list',
},
],
}

View file

@ -83,6 +83,32 @@ class FieldList(list):
field, newfield)
self.append(newfield)
def set_sequence(self, fields):
"""
Sort the list such that it matches the same sequence as the
given fields list.
This does not add or remove any elements, it just
(potentially) rearranges the internal list elements.
Therefore you do not need to explicitly declare *all* fields;
just the ones you care about.
The resulting field list will have the requested fields in
order, at the *beginning* of the list. Any unrequested fields
will remain in the same order as they were previously, but
will be placed *after* the requested fields.
:param fields: List of fields in the desired order.
"""
unimportant = len(self) + 1
def getkey(field):
if field in fields:
return fields.index(field)
return unimportant
self.sort(key=getkey)
def get_form_data(request):
"""

View file

@ -154,6 +154,11 @@ class CommonView(View):
'settings.view',
'settings.edit',
'settings.delete',
'upgrades.list',
'upgrades.create',
'upgrades.view',
'upgrades.edit',
'upgrades.delete',
'users.list',
'users.create',
'users.view',

View file

@ -35,6 +35,7 @@ That will in turn include the following modules:
* :mod:`wuttaweb.views.people`
* :mod:`wuttaweb.views.roles`
* :mod:`wuttaweb.views.users`
* :mod:`wuttaweb.views.upgrades`
"""
@ -47,6 +48,7 @@ def defaults(config, **kwargs):
config.include(mod('wuttaweb.views.people'))
config.include(mod('wuttaweb.views.roles'))
config.include(mod('wuttaweb.views.users'))
config.include(mod('wuttaweb.views.upgrades'))
def includeme(config):

View file

@ -1040,6 +1040,56 @@ class MasterView(View):
fmt = f"${{:0,.{scale}f}}"
return fmt.format(value)
def grid_render_datetime(self, record, key, value, fmt=None):
"""
Custom grid value renderer for
:class:`~python:datetime.datetime` fields.
:param fmt: Optional format string to use instead of the
default: ``'%Y-%m-%d %I:%M:%S %p'``
To use this feature for your grid::
grid.set_renderer('my_datetime_field', self.grid_render_datetime)
# you can also override format
grid.set_renderer('my_datetime_field', self.grid_render_datetime,
fmt='%Y-%m-%d %H:%M:%S')
"""
# nb. get new value since the one provided will just be a
# (json-safe) *string* if the original type was datetime
value = record[key]
if value is None:
return
return value.strftime(fmt or '%Y-%m-%d %I:%M:%S %p')
def grid_render_enum(self, record, key, value, enum=None):
"""
Custom grid value renderer for "enum" fields.
:param enum: Enum class for the field. This should be an
instance of :class:`~python:enum.Enum`.
To use this feature for your grid::
from enum import Enum
class MyEnum(Enum):
ONE = 1
TWO = 2
THREE = 3
grid.set_renderer('my_enum_field', self.grid_render_enum, enum=MyEnum)
"""
if enum:
original = record[key]
if original:
return original.name
return value
def grid_render_notes(self, record, key, value, maxlen=100):
"""
Custom grid value renderer for "notes" fields.

View file

@ -0,0 +1,187 @@
# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Upgrade Views
"""
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
class UpgradeView(MasterView):
"""
Master view for upgrades.
Default route prefix is ``upgrades``.
Notable URLs provided by this class:
* ``/upgrades/``
* ``/upgrades/new``
* ``/upgrades/XXX``
* ``/upgrades/XXX/edit``
* ``/upgrades/XXX/delete``
"""
model_class = Upgrade
grid_columns = [
'created',
'description',
'status',
'executed',
'executed_by',
]
sort_defaults = ('created', 'desc')
def configure_grid(self, g):
""" """
super().configure_grid(g)
model = self.app.model
enum = self.app.enum
# description
g.set_link('description')
# created
g.set_renderer('created', self.grid_render_datetime)
# created_by
g.set_link('created_by')
Creator = orm.aliased(model.User)
g.set_joiner('created_by', lambda q: q.join(Creator,
Creator.uuid == model.Upgrade.created_by_uuid))
g.set_filter('created_by', Creator.username,
label="Created By Username")
# status
g.set_renderer('status', self.grid_render_enum, enum=enum.UpgradeStatus)
# executed_by
g.set_link('executed_by')
Executor = orm.aliased(model.User)
g.set_joiner('executed_by', lambda q: q.outerjoin(Executor,
Executor.uuid == model.Upgrade.executed_by_uuid))
g.set_filter('executed_by', Executor.username,
label="Executed By Username")
def grid_row_class(self, upgrade, data, i):
""" """
enum = self.app.enum
if upgrade.status == enum.UpgradeStatus.EXECUTING:
return 'has-background-warning'
if upgrade.status == enum.UpgradeStatus.FAILURE:
return 'has-background-warning'
def configure_form(self, f):
""" """
super().configure_form(f)
enum = self.app.enum
upgrade = f.model_instance
# never show these
f.remove('created_by_uuid',
'executing',
'executed_by_uuid')
# sequence sanity
f.fields.set_sequence([
'description',
'notes',
'status',
'created',
'created_by',
'executed',
'executed_by',
])
# created
if self.creating or self.editing:
f.remove('created')
# created_by
if self.creating or self.editing:
f.remove('created_by')
else:
f.set_node('created_by', UserRef(self.request))
# notes
f.set_widget('notes', widgets.NotesWidget())
# status
if self.creating:
f.remove('status')
else:
f.set_node('status', WuttaEnum(self.request, enum.UpgradeStatus))
# exit_code
if self.creating or not upgrade.executed:
f.remove('exit_code')
# executed
if self.creating or self.editing or not upgrade.executed:
f.remove('executed')
# executed_by
if self.creating or self.editing or not upgrade.executed:
f.remove('executed_by')
else:
f.set_node('executed_by', UserRef(self.request))
def objectify(self, form):
""" """
upgrade = super().objectify(form)
enum = self.app.enum
# set user, status when creating
if self.creating:
upgrade.created_by = self.request.user
upgrade.status = enum.UpgradeStatus.PENDING
return upgrade
@classmethod
def defaults(cls, config):
""" """
# nb. Upgrade may come from custom model
wutta_config = config.registry.settings['wutta_config']
app = wutta_config.get_app()
cls.model_class = app.model.Upgrade
cls._defaults(config)
def defaults(config, **kwargs):
base = globals()
UpgradeView = kwargs.get('UpgradeView', base['UpgradeView'])
UpgradeView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -10,7 +10,7 @@ from sqlalchemy import orm
from wuttjamaican.conf import WuttaConfig
from wuttaweb.forms import schema as mod
from wuttaweb.forms import widgets
from tests.util import DataTestCase
from tests.util import DataTestCase, WebTestCase
class TestObjectNode(DataTestCase):
@ -47,6 +47,15 @@ class TestObjectNode(DataTestCase):
self.assertIs(value, person)
class TestWuttaEnum(WebTestCase):
def test_widget_maker(self):
enum = self.app.enum
typ = mod.WuttaEnum(self.request, enum.UpgradeStatus)
widget = typ.widget_maker()
self.assertIsInstance(widget, widgets.SelectWidget)
class TestObjectRef(DataTestCase):
def setUp(self):
@ -140,10 +149,17 @@ class TestObjectRef(DataTestCase):
self.session.commit()
self.assertIsNotNone(person.uuid)
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
# can specify as uuid
typ = mod.ObjectRef(self.request, session=self.session)
value = typ.objectify(person.uuid)
self.assertIs(value, person)
# or can specify object proper
typ = mod.ObjectRef(self.request, session=self.session)
value = typ.objectify(person)
self.assertIs(value, person)
# error if not found
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
typ = mod.ObjectRef(self.request, session=self.session)
@ -186,11 +202,7 @@ class TestObjectRef(DataTestCase):
self.assertEqual(widget.values[1][1], "Betty Boop")
class TestPersonRef(DataTestCase):
def setUp(self):
self.setup_db()
self.request = testing.DummyRequest(wutta_config=self.config)
class TestPersonRef(WebTestCase):
def test_sort_query(self):
typ = mod.PersonRef(self.request, session=self.session)
@ -200,6 +212,43 @@ class TestPersonRef(DataTestCase):
self.assertIsInstance(sorted_query, orm.Query)
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('people.view', '/people/{uuid}')
model = self.app.model
typ = mod.PersonRef(self.request, session=self.session)
person = model.Person(full_name="Barney Rubble")
self.session.add(person)
self.session.commit()
url = typ.get_object_url(person)
self.assertIsNotNone(url)
self.assertIn(f'/people/{person.uuid}', url)
class TestUserRef(WebTestCase):
def test_sort_query(self):
typ = mod.UserRef(self.request, session=self.session)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
sorted_query = typ.sort_query(query)
self.assertIsInstance(sorted_query, orm.Query)
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('users.view', '/users/{uuid}')
model = self.app.model
typ = mod.UserRef(self.request, session=self.session)
user = model.User(username='barney')
self.session.add(user)
self.session.commit()
url = typ.get_object_url(user)
self.assertIsNotNone(url)
self.assertIn(f'/users/{user.uuid}', url)
class TestUserRefs(DataTestCase):

View file

@ -9,13 +9,13 @@ from fanstatic import Library, Resource
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
from wuttaweb import util
from wuttaweb import util as mod
class TestFieldList(TestCase):
def test_insert_before(self):
fields = util.FieldList(['f1', 'f2'])
fields = mod.FieldList(['f1', 'f2'])
self.assertEqual(fields, ['f1', 'f2'])
# typical
@ -29,7 +29,7 @@ class TestFieldList(TestCase):
self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2', 'ZZZ'])
def test_insert_after(self):
fields = util.FieldList(['f1', 'f2'])
fields = mod.FieldList(['f1', 'f2'])
self.assertEqual(fields, ['f1', 'f2'])
# typical
@ -42,6 +42,14 @@ class TestFieldList(TestCase):
fields.insert_after('f3', 'ZZZ')
self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2', 'ZZZ'])
def test_set_sequence(self):
fields = mod.FieldList(['f5', 'f1', 'f3', 'f4', 'f2'])
# setting sequence will only "sort" for explicit fields.
# other fields remain in original order, but at the end.
fields.set_sequence(['f1', 'f2', 'f3'])
self.assertEqual(fields, ['f1', 'f2', 'f3', 'f5', 'f4'])
class TestGetLibVer(TestCase):
@ -51,153 +59,153 @@ class TestGetLibVer(TestCase):
self.request.wutta_config = self.config
def test_buefy_default(self):
version = util.get_libver(self.request, 'buefy')
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, 'latest')
def test_buefy_custom_old(self):
self.config.setdefault('wuttaweb.buefy_version', '0.9.29')
version = util.get_libver(self.request, 'buefy')
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, '0.9.29')
def test_buefy_custom_old_tailbone(self):
self.config.setdefault('tailbone.libver.buefy', '0.9.28')
version = util.get_libver(self.request, 'buefy', prefix='tailbone')
version = mod.get_libver(self.request, 'buefy', prefix='tailbone')
self.assertEqual(version, '0.9.28')
def test_buefy_custom_new(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = util.get_libver(self.request, 'buefy')
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, '0.9.29')
def test_buefy_configured_only(self):
version = util.get_libver(self.request, 'buefy', configured_only=True)
version = mod.get_libver(self.request, 'buefy', configured_only=True)
self.assertIsNone(version)
def test_buefy_default_only(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = util.get_libver(self.request, 'buefy', default_only=True)
version = mod.get_libver(self.request, 'buefy', default_only=True)
self.assertEqual(version, 'latest')
def test_buefy_css_default(self):
version = util.get_libver(self.request, 'buefy.css')
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, 'latest')
def test_buefy_css_custom_old(self):
# nb. this uses same setting as buefy (js)
self.config.setdefault('wuttaweb.buefy_version', '0.9.29')
version = util.get_libver(self.request, 'buefy.css')
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, '0.9.29')
def test_buefy_css_custom_new(self):
# nb. this uses same setting as buefy (js)
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = util.get_libver(self.request, 'buefy.css')
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, '0.9.29')
def test_buefy_css_configured_only(self):
version = util.get_libver(self.request, 'buefy.css', configured_only=True)
version = mod.get_libver(self.request, 'buefy.css', configured_only=True)
self.assertIsNone(version)
def test_buefy_css_default_only(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = util.get_libver(self.request, 'buefy.css', default_only=True)
version = mod.get_libver(self.request, 'buefy.css', default_only=True)
self.assertEqual(version, 'latest')
def test_vue_default(self):
version = util.get_libver(self.request, 'vue')
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '2.6.14')
def test_vue_custom_old(self):
self.config.setdefault('wuttaweb.vue_version', '3.4.31')
version = util.get_libver(self.request, 'vue')
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '3.4.31')
def test_vue_custom_new(self):
self.config.setdefault('wuttaweb.libver.vue', '3.4.31')
version = util.get_libver(self.request, 'vue')
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '3.4.31')
def test_vue_configured_only(self):
version = util.get_libver(self.request, 'vue', configured_only=True)
version = mod.get_libver(self.request, 'vue', configured_only=True)
self.assertIsNone(version)
def test_vue_default_only(self):
self.config.setdefault('wuttaweb.libver.vue', '3.4.31')
version = util.get_libver(self.request, 'vue', default_only=True)
version = mod.get_libver(self.request, 'vue', default_only=True)
self.assertEqual(version, '2.6.14')
def test_vue_resource_default(self):
version = util.get_libver(self.request, 'vue_resource')
version = mod.get_libver(self.request, 'vue_resource')
self.assertEqual(version, 'latest')
def test_vue_resource_custom(self):
self.config.setdefault('wuttaweb.libver.vue_resource', '1.5.3')
version = util.get_libver(self.request, 'vue_resource')
version = mod.get_libver(self.request, 'vue_resource')
self.assertEqual(version, '1.5.3')
def test_fontawesome_default(self):
version = util.get_libver(self.request, 'fontawesome')
version = mod.get_libver(self.request, 'fontawesome')
self.assertEqual(version, '5.3.1')
def test_fontawesome_custom(self):
self.config.setdefault('wuttaweb.libver.fontawesome', '5.6.3')
version = util.get_libver(self.request, 'fontawesome')
version = mod.get_libver(self.request, 'fontawesome')
self.assertEqual(version, '5.6.3')
def test_bb_vue_default(self):
version = util.get_libver(self.request, 'bb_vue')
version = mod.get_libver(self.request, 'bb_vue')
self.assertEqual(version, '3.4.31')
def test_bb_vue_custom(self):
self.config.setdefault('wuttaweb.libver.bb_vue', '3.4.30')
version = util.get_libver(self.request, 'bb_vue')
version = mod.get_libver(self.request, 'bb_vue')
self.assertEqual(version, '3.4.30')
def test_bb_oruga_default(self):
version = util.get_libver(self.request, 'bb_oruga')
version = mod.get_libver(self.request, 'bb_oruga')
self.assertEqual(version, '0.8.12')
def test_bb_oruga_custom(self):
self.config.setdefault('wuttaweb.libver.bb_oruga', '0.8.11')
version = util.get_libver(self.request, 'bb_oruga')
version = mod.get_libver(self.request, 'bb_oruga')
self.assertEqual(version, '0.8.11')
def test_bb_oruga_bulma_default(self):
version = util.get_libver(self.request, 'bb_oruga_bulma')
version = mod.get_libver(self.request, 'bb_oruga_bulma')
self.assertEqual(version, '0.3.0')
version = util.get_libver(self.request, 'bb_oruga_bulma_css')
version = mod.get_libver(self.request, 'bb_oruga_bulma_css')
self.assertEqual(version, '0.3.0')
def test_bb_oruga_bulma_custom(self):
self.config.setdefault('wuttaweb.libver.bb_oruga_bulma', '0.2.11')
version = util.get_libver(self.request, 'bb_oruga_bulma')
version = mod.get_libver(self.request, 'bb_oruga_bulma')
self.assertEqual(version, '0.2.11')
def test_bb_fontawesome_svg_core_default(self):
version = util.get_libver(self.request, 'bb_fontawesome_svg_core')
version = mod.get_libver(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(version, '6.5.2')
def test_bb_fontawesome_svg_core_custom(self):
self.config.setdefault('wuttaweb.libver.bb_fontawesome_svg_core', '6.5.1')
version = util.get_libver(self.request, 'bb_fontawesome_svg_core')
version = mod.get_libver(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(version, '6.5.1')
def test_bb_free_solid_svg_icons_default(self):
version = util.get_libver(self.request, 'bb_free_solid_svg_icons')
version = mod.get_libver(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(version, '6.5.2')
def test_bb_free_solid_svg_icons_custom(self):
self.config.setdefault('wuttaweb.libver.bb_free_solid_svg_icons', '6.5.1')
version = util.get_libver(self.request, 'bb_free_solid_svg_icons')
version = mod.get_libver(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(version, '6.5.1')
def test_bb_vue_fontawesome_default(self):
version = util.get_libver(self.request, 'bb_vue_fontawesome')
version = mod.get_libver(self.request, 'bb_vue_fontawesome')
self.assertEqual(version, '3.0.6')
def test_bb_vue_fontawesome_custom(self):
self.config.setdefault('wuttaweb.libver.bb_vue_fontawesome', '3.0.8')
version = util.get_libver(self.request, 'bb_vue_fontawesome')
version = mod.get_libver(self.request, 'bb_vue_fontawesome')
self.assertEqual(version, '3.0.8')
@ -238,191 +246,191 @@ class TestGetLibUrl(TestCase):
self.request.script_name = '/wutta'
def test_buefy_default(self):
url = util.get_liburl(self.request, 'buefy')
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, 'https://unpkg.com/buefy@latest/dist/buefy.min.js')
def test_buefy_custom(self):
self.config.setdefault('wuttaweb.liburl.buefy', '/lib/buefy.js')
url = util.get_liburl(self.request, 'buefy')
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, '/lib/buefy.js')
def test_buefy_custom_tailbone(self):
self.config.setdefault('tailbone.liburl.buefy', '/tailbone/buefy.js')
url = util.get_liburl(self.request, 'buefy', prefix='tailbone')
url = mod.get_liburl(self.request, 'buefy', prefix='tailbone')
self.assertEqual(url, '/tailbone/buefy.js')
def test_buefy_default_only(self):
self.config.setdefault('wuttaweb.liburl.buefy', '/lib/buefy.js')
url = util.get_liburl(self.request, 'buefy', default_only=True)
url = mod.get_liburl(self.request, 'buefy', default_only=True)
self.assertEqual(url, 'https://unpkg.com/buefy@latest/dist/buefy.min.js')
def test_buefy_configured_only(self):
url = util.get_liburl(self.request, 'buefy', configured_only=True)
url = mod.get_liburl(self.request, 'buefy', configured_only=True)
self.assertIsNone(url)
def test_buefy_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'buefy')
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, '/wutta/fanstatic/buefy.js')
def test_buefy_fanstatic_tailbone(self):
self.setup_fanstatic(register=False)
self.config.setdefault('tailbone.static_libcache.module', 'tests.test_util')
url = util.get_liburl(self.request, 'buefy', prefix='tailbone')
url = mod.get_liburl(self.request, 'buefy', prefix='tailbone')
self.assertEqual(url, '/wutta/fanstatic/buefy.js')
def test_buefy_css_default(self):
url = util.get_liburl(self.request, 'buefy.css')
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, 'https://unpkg.com/buefy@latest/dist/buefy.min.css')
def test_buefy_css_custom(self):
self.config.setdefault('wuttaweb.liburl.buefy.css', '/lib/buefy.css')
url = util.get_liburl(self.request, 'buefy.css')
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, '/lib/buefy.css')
def test_buefy_css_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'buefy.css')
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, '/wutta/fanstatic/buefy.css')
def test_vue_default(self):
url = util.get_liburl(self.request, 'vue')
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, 'https://unpkg.com/vue@2.6.14/dist/vue.min.js')
def test_vue_custom(self):
self.config.setdefault('wuttaweb.liburl.vue', '/lib/vue.js')
url = util.get_liburl(self.request, 'vue')
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, '/lib/vue.js')
def test_vue_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'vue')
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, '/wutta/fanstatic/vue.js')
def test_vue_resource_default(self):
url = util.get_liburl(self.request, 'vue_resource')
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/vue-resource@latest')
def test_vue_resource_custom(self):
self.config.setdefault('wuttaweb.liburl.vue_resource', '/lib/vue-resource.js')
url = util.get_liburl(self.request, 'vue_resource')
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, '/lib/vue-resource.js')
def test_vue_resource_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'vue_resource')
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, '/wutta/fanstatic/vue_resource.js')
def test_fontawesome_default(self):
url = util.get_liburl(self.request, 'fontawesome')
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, 'https://use.fontawesome.com/releases/v5.3.1/js/all.js')
def test_fontawesome_custom(self):
self.config.setdefault('wuttaweb.liburl.fontawesome', '/lib/fontawesome.js')
url = util.get_liburl(self.request, 'fontawesome')
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, '/lib/fontawesome.js')
def test_fontawesome_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'fontawesome')
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, '/wutta/fanstatic/fontawesome.js')
def test_bb_vue_default(self):
url = util.get_liburl(self.request, 'bb_vue')
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, 'https://unpkg.com/vue@3.4.31/dist/vue.esm-browser.prod.js')
def test_bb_vue_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_vue', '/lib/vue.js')
url = util.get_liburl(self.request, 'bb_vue')
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, '/lib/vue.js')
def test_bb_vue_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_vue')
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, '/wutta/fanstatic/bb_vue.js')
def test_bb_oruga_default(self):
url = util.get_liburl(self.request, 'bb_oruga')
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/oruga-next@0.8.12/dist/oruga.mjs')
def test_bb_oruga_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga', '/lib/oruga.js')
url = util.get_liburl(self.request, 'bb_oruga')
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, '/lib/oruga.js')
def test_bb_oruga_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_oruga')
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga.js')
def test_bb_oruga_bulma_default(self):
url = util.get_liburl(self.request, 'bb_oruga_bulma')
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/theme-bulma@0.3.0/dist/bulma.mjs')
def test_bb_oruga_bulma_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga_bulma', '/lib/oruga_bulma.js')
url = util.get_liburl(self.request, 'bb_oruga_bulma')
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, '/lib/oruga_bulma.js')
def test_bb_oruga_bulma_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_oruga_bulma')
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga_bulma.js')
def test_bb_oruga_bulma_css_default(self):
url = util.get_liburl(self.request, 'bb_oruga_bulma_css')
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/theme-bulma@0.3.0/dist/bulma.css')
def test_bb_oruga_bulma_css_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga_bulma_css', '/lib/oruga-bulma.css')
url = util.get_liburl(self.request, 'bb_oruga_bulma_css')
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, '/lib/oruga-bulma.css')
def test_bb_oruga_bulma_css_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_oruga_bulma_css')
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga_bulma.css')
def test_bb_fontawesome_svg_core_default(self):
url = util.get_liburl(self.request, 'bb_fontawesome_svg_core')
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@6.5.2/+esm')
def test_bb_fontawesome_svg_core_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_fontawesome_svg_core', '/lib/fontawesome-svg-core.js')
url = util.get_liburl(self.request, 'bb_fontawesome_svg_core')
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, '/lib/fontawesome-svg-core.js')
def test_bb_fontawesome_svg_core_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_fontawesome_svg_core')
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, '/wutta/fanstatic/bb_fontawesome_svg_core.js')
def test_bb_free_solid_svg_icons_default(self):
url = util.get_liburl(self.request, 'bb_free_solid_svg_icons')
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@6.5.2/+esm')
def test_bb_free_solid_svg_icons_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_free_solid_svg_icons', '/lib/free-solid-svg-icons.js')
url = util.get_liburl(self.request, 'bb_free_solid_svg_icons')
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, '/lib/free-solid-svg-icons.js')
def test_bb_free_solid_svg_icons_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_free_solid_svg_icons')
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, '/wutta/fanstatic/bb_free_solid_svg_icons.js')
def test_bb_vue_fontawesome_default(self):
url = util.get_liburl(self.request, 'bb_vue_fontawesome')
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@3.0.6/+esm')
def test_bb_vue_fontawesome_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_vue_fontawesome', '/lib/vue-fontawesome.js')
url = util.get_liburl(self.request, 'bb_vue_fontawesome')
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, '/lib/vue-fontawesome.js')
def test_bb_vue_fontawesome_fanstatic(self):
self.setup_fanstatic()
url = util.get_liburl(self.request, 'bb_vue_fontawesome')
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, '/wutta/fanstatic/bb_vue_fontawesome.js')
@ -439,17 +447,17 @@ class TestGetFormData(TestCase):
def test_default(self):
request = self.make_request()
data = util.get_form_data(request)
data = mod.get_form_data(request)
self.assertEqual(data, {'foo1': 'bar'})
def test_is_xhr(self):
request = self.make_request(POST=None, is_xhr=True)
data = util.get_form_data(request)
data = mod.get_form_data(request)
self.assertEqual(data, {'foo2': 'baz'})
def test_content_type(self):
request = self.make_request(POST=None, content_type='application/json')
data = util.get_form_data(request)
data = mod.get_form_data(request)
self.assertEqual(data, {'foo2': 'baz'})
@ -460,16 +468,16 @@ class TestGetModelFields(TestCase):
self.app = self.config.get_app()
def test_empty_model_class(self):
fields = util.get_model_fields(self.config)
fields = mod.get_model_fields(self.config)
self.assertIsNone(fields)
def test_unknown_model_class(self):
fields = util.get_model_fields(self.config, TestCase)
fields = mod.get_model_fields(self.config, TestCase)
self.assertIsNone(fields)
def test_basic(self):
model = self.app.model
fields = util.get_model_fields(self.config, model.Setting)
fields = mod.get_model_fields(self.config, model.Setting)
self.assertEqual(fields, ['name', 'value'])
@ -484,9 +492,9 @@ class TestGetCsrfToken(TestCase):
# same token returned for same request
# TODO: dummy request is always returning same token!
# so this isn't really testing anything.. :(
first = util.get_csrf_token(self.request)
first = mod.get_csrf_token(self.request)
self.assertIsNotNone(first)
second = util.get_csrf_token(self.request)
second = mod.get_csrf_token(self.request)
self.assertEqual(first, second)
# TODO: ideally would make a new request here and confirm it
@ -497,7 +505,7 @@ class TestGetCsrfToken(TestCase):
# nb. dummy request always returns same token, so must
# trick it into thinking it doesn't have one yet
with patch.object(self.request.session, 'get_csrf_token', return_value=None):
token = util.get_csrf_token(self.request)
token = mod.get_csrf_token(self.request)
self.assertIsNotNone(token)
@ -508,10 +516,10 @@ class TestRenderCsrfToken(TestCase):
self.request = testing.DummyRequest(wutta_config=self.config)
def test_basics(self):
html = util.render_csrf_token(self.request)
html = mod.render_csrf_token(self.request)
self.assertIn('type="hidden"', html)
self.assertIn('name="_csrf"', html)
token = util.get_csrf_token(self.request)
token = mod.get_csrf_token(self.request)
self.assertIn(f'value="{token}"', html)
@ -522,17 +530,17 @@ class TestMakeJsonSafe(TestCase):
self.app = self.config.get_app()
def test_null(self):
value = util.make_json_safe(colander.null)
value = mod.make_json_safe(colander.null)
self.assertIsNone(value)
value = util.make_json_safe(None)
value = mod.make_json_safe(None)
self.assertIsNone(value)
def test_invalid(self):
model = self.app.model
person = model.Person(full_name="Betty Boop")
self.assertRaises(TypeError, json.dumps, person)
value = util.make_json_safe(person, key='person')
value = mod.make_json_safe(person, key='person')
self.assertEqual(value, "Betty Boop")
def test_dict(self):
@ -545,7 +553,7 @@ class TestMakeJsonSafe(TestCase):
}
self.assertRaises(TypeError, json.dumps, data)
value = util.make_json_safe(data)
value = mod.make_json_safe(data)
self.assertEqual(value, {
'foo': 'bar',
'person': "Betty Boop",

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8; -*-
import datetime
import decimal
import functools
from unittest import TestCase
@ -579,7 +580,6 @@ class TestMasterView(WebTestCase):
self.assertEqual(value, "No")
def test_grid_render_currency(self):
model = self.app.model
view = self.make_view()
obj = {'amount': None}
@ -597,6 +597,33 @@ class TestMasterView(WebTestCase):
value = view.grid_render_currency(obj, 'amount', '-100.42')
self.assertEqual(value, "($100.42)")
def test_grid_render_datetime(self):
view = self.make_view()
obj = {'dt': None}
# null
value = view.grid_render_datetime(obj, 'dt', None)
self.assertIsNone(value)
# normal
obj['dt'] = datetime.datetime(2024, 8, 24, 11)
value = view.grid_render_datetime(obj, 'dt', '2024-08-24T11:00:00')
self.assertEqual(value, '2024-08-24 11:00:00 AM')
def test_grid_render_enum(self):
enum = self.app.enum
view = self.make_view()
obj = {'status': None}
# null
value = view.grid_render_enum(obj, 'status', None, enum=enum.UpgradeStatus)
self.assertIsNone(value)
# normal
obj['status'] = enum.UpgradeStatus.SUCCESS
value = view.grid_render_enum(obj, 'status', 'SUCCESS', enum=enum.UpgradeStatus)
self.assertEqual(value, 'SUCCESS')
def test_grid_render_notes(self):
model = self.app.model
view = self.make_view()

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8; -*-
import datetime
from unittest.mock import patch, MagicMock
from wuttaweb.views import upgrades as mod
from tests.util import WebTestCase
class TestUpgradeView(WebTestCase):
def make_view(self):
return mod.UpgradeView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.upgrades')
def test_configure_grid(self):
model = self.app.model
view = self.make_view()
# sanity / coverage check
grid = view.make_grid(model_class=model.Upgrade)
view.configure_grid(grid)
def test_grid_row_class(self):
model = self.app.model
enum = self.app.enum
upgrade = model.Upgrade(description="test", status=enum.UpgradeStatus.PENDING)
data = dict(upgrade)
view = self.make_view()
self.assertIsNone(view.grid_row_class(upgrade, data, 1))
upgrade.status = enum.UpgradeStatus.EXECUTING
self.assertEqual(view.grid_row_class(upgrade, data, 1), 'has-background-warning')
upgrade.status = enum.UpgradeStatus.SUCCESS
self.assertIsNone(view.grid_row_class(upgrade, data, 1))
upgrade.status = enum.UpgradeStatus.FAILURE
self.assertEqual(view.grid_row_class(upgrade, data, 1), 'has-background-warning')
def test_configure_form(self):
model = self.app.model
enum = self.app.enum
user = model.User(username='barney')
self.session.add(user)
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
self.session.add(upgrade)
self.session.commit()
view = self.make_view()
# some fields exist when viewing
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('created', form)
view.configure_form(form)
self.assertIn('created', form)
# but then are removed when creating
with patch.object(view, 'creating', new=True):
form = view.make_form(model_class=model.Upgrade)
self.assertIn('created', form)
view.configure_form(form)
self.assertNotIn('created', form)
# test executed field when viewing
with patch.object(view, 'viewing', new=True):
# executed is *not* shown by default
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('executed', form)
view.configure_form(form)
self.assertNotIn('executed', form)
# but it *is* shown if upgrade is executed
upgrade.executed = datetime.datetime.now()
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('executed', form)
view.configure_form(form)
self.assertIn('executed', form)
def test_objectify(self):
model = self.app.model
enum = self.app.enum
user = model.User(username='barney')
self.session.add(user)
self.session.commit()
view = self.make_view()
# user and status are auto-set when creating
self.request.user = user
self.request.method = 'POST'
self.request.POST = {'description': "new one"}
with patch.object(view, 'creating', new=True):
form = view.make_model_form()
self.assertTrue(form.validate())
upgrade = view.objectify(form)
self.assertEqual(upgrade.description, "new one")
self.assertIs(upgrade.created_by, user)
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)