3
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

@ -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)