Compare commits
No commits in common. "0a08918657845b36dbbe9bf0a67ace5ecc2c1ee9" and "601dec777713c3aa4ceeffddc0fde61ac27a8438" have entirely different histories.
0a08918657
...
601dec7777
38 changed files with 397 additions and 584 deletions
38
.pylintrc
38
.pylintrc
|
@ -2,7 +2,37 @@
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
disable=fixme,
|
disable=fixme,
|
||||||
|
abstract-method,
|
||||||
[SIMILARITIES]
|
arguments-differ,
|
||||||
# nb. cuts out some noise for duplicate-code
|
arguments-renamed,
|
||||||
min-similarity-lines=5
|
assignment-from-no-return,
|
||||||
|
attribute-defined-outside-init,
|
||||||
|
consider-using-dict-comprehension,
|
||||||
|
consider-using-dict-items,
|
||||||
|
consider-using-generator,
|
||||||
|
consider-using-get,
|
||||||
|
consider-using-set-comprehension,
|
||||||
|
duplicate-code,
|
||||||
|
isinstance-second-argument-not-valid-type,
|
||||||
|
keyword-arg-before-vararg,
|
||||||
|
missing-function-docstring,
|
||||||
|
missing-module-docstring,
|
||||||
|
no-else-raise,
|
||||||
|
no-member,
|
||||||
|
not-callable,
|
||||||
|
protected-access,
|
||||||
|
redefined-outer-name,
|
||||||
|
simplifiable-if-expression,
|
||||||
|
singleton-comparison,
|
||||||
|
super-init-not-called,
|
||||||
|
too-few-public-methods,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-lines,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-nested-blocks,
|
||||||
|
too-many-positional-arguments,
|
||||||
|
too-many-public-methods,
|
||||||
|
too-many-statements,
|
||||||
|
ungrouped-imports,
|
||||||
|
unidiomatic-typecheck,
|
||||||
|
unnecessary-comprehension,
|
||||||
|
|
|
@ -11,9 +11,6 @@ project.
|
||||||
|
|
||||||
.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
|
.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen
|
|
||||||
:target: https://github.com/pylint-dev/pylint
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
:target: https://github.com/psf/black
|
:target: https://github.com/psf/black
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
"""
|
|
||||||
Package Version
|
|
||||||
"""
|
|
||||||
|
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
|
|
@ -65,13 +65,13 @@ class WebAppProvider(AppProvider):
|
||||||
|
|
||||||
:returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
|
:returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
|
||||||
"""
|
"""
|
||||||
if "web" not in self.app.handlers:
|
if "web_handler" not in self.__dict__:
|
||||||
spec = self.config.get(
|
spec = self.config.get(
|
||||||
f"{self.appname}.web.handler_spec",
|
f"{self.appname}.web.handler_spec",
|
||||||
default="wuttaweb.handler:WebHandler",
|
default="wuttaweb.handler:WebHandler",
|
||||||
)
|
)
|
||||||
self.app.handlers["web"] = self.app.load_object(spec)(self.config)
|
self.web_handler = self.app.load_object(spec)(self.config)
|
||||||
return self.app.handlers["web"]
|
return self.web_handler
|
||||||
|
|
||||||
|
|
||||||
def make_wutta_config(settings, config_maker=None, **kwargs):
|
def make_wutta_config(settings, config_maker=None, **kwargs):
|
||||||
|
@ -239,14 +239,14 @@ def make_wsgi_app(main_app=None, config=None):
|
||||||
|
|
||||||
# determine the app factory
|
# determine the app factory
|
||||||
if isinstance(main_app, str):
|
if isinstance(main_app, str):
|
||||||
factory = app.load_object(main_app)
|
make_wsgi_app = app.load_object(main_app)
|
||||||
elif callable(main_app):
|
elif callable(main_app):
|
||||||
factory = main_app
|
make_wsgi_app = main_app
|
||||||
else:
|
else:
|
||||||
raise ValueError("main_app must be spec or callable")
|
raise ValueError("main_app must be spec or callable")
|
||||||
|
|
||||||
# construct a pyramid app "per usual"
|
# construct a pyramid app "per usual"
|
||||||
return factory({}, **settings)
|
return make_wsgi_app({}, **settings)
|
||||||
|
|
||||||
|
|
||||||
def make_asgi_app(main_app=None, config=None):
|
def make_asgi_app(main_app=None, config=None):
|
||||||
|
|
|
@ -105,8 +105,7 @@ class WuttaSecurityPolicy:
|
||||||
self.identity_cache = RequestLocalCache(self.load_identity)
|
self.identity_cache = RequestLocalCache(self.load_identity)
|
||||||
self.db_session = db_session or Session()
|
self.db_session = db_session or Session()
|
||||||
|
|
||||||
def load_identity(self, request): # pylint: disable=empty-docstring
|
def load_identity(self, request):
|
||||||
""" """
|
|
||||||
config = request.registry.settings["wutta_config"]
|
config = request.registry.settings["wutta_config"]
|
||||||
app = config.get_app()
|
app = config.get_app()
|
||||||
model = app.model
|
model = app.model
|
||||||
|
@ -123,29 +122,22 @@ class WuttaSecurityPolicy:
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def identity(self, request): # pylint: disable=empty-docstring
|
def identity(self, request):
|
||||||
""" """
|
|
||||||
return self.identity_cache.get_or_create(request)
|
return self.identity_cache.get_or_create(request)
|
||||||
|
|
||||||
def authenticated_userid(self, request): # pylint: disable=empty-docstring
|
def authenticated_userid(self, request):
|
||||||
""" """
|
|
||||||
user = self.identity(request)
|
user = self.identity(request)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return user.uuid
|
return user.uuid
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
|
def remember(self, request, userid, **kw):
|
||||||
""" """
|
|
||||||
return self.session_helper.remember(request, userid, **kw)
|
return self.session_helper.remember(request, userid, **kw)
|
||||||
|
|
||||||
def forget(self, request, **kw): # pylint: disable=empty-docstring
|
def forget(self, request, **kw):
|
||||||
""" """
|
|
||||||
return self.session_helper.forget(request, **kw)
|
return self.session_helper.forget(request, **kw)
|
||||||
|
|
||||||
def permits( # pylint: disable=unused-argument,empty-docstring
|
def permits(self, request, context, permission): # pylint: disable=unused-argument
|
||||||
self, request, context, permission
|
|
||||||
):
|
|
||||||
""" """
|
|
||||||
|
|
||||||
# nb. root user can do anything
|
# nb. root user can do anything
|
||||||
if getattr(request, "is_root", False):
|
if getattr(request, "is_root", False):
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
from wuttjamaican.email import EmailSetting
|
from wuttjamaican.email import EmailSetting
|
||||||
|
|
||||||
|
|
||||||
class feedback(EmailSetting): # pylint: disable=invalid-name,too-few-public-methods
|
class feedback(EmailSetting): # pylint: disable=invalid-name
|
||||||
"""
|
"""
|
||||||
Sent when user submits feedback via the web app.
|
Sent when user submits feedback via the web app.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"""
|
"""
|
||||||
Base form classes
|
Base form classes
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -37,19 +36,13 @@ from colanderalchemy import SQLAlchemySchemaNode
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
from webhelpers2.html import HTML
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
from wuttaweb.util import (
|
from wuttaweb.util import FieldList, get_form_data, get_model_fields, make_json_safe
|
||||||
FieldList,
|
|
||||||
get_form_data,
|
|
||||||
get_model_fields,
|
|
||||||
make_json_safe,
|
|
||||||
render_vue_finalize,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Form: # pylint: disable=too-many-instance-attributes,too-many-public-methods
|
class Form: # pylint: disable=too-many-instance-attributes
|
||||||
"""
|
"""
|
||||||
Base class for all forms.
|
Base class for all forms.
|
||||||
|
|
||||||
|
@ -270,12 +263,11 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
|
|
||||||
If the :meth:`validate()` method was called, and it succeeded,
|
If the :meth:`validate()` method was called, and it succeeded,
|
||||||
this will be set to the validated data dict.
|
this will be set to the validated data dict.
|
||||||
|
|
||||||
|
Note that in all other cases, this attribute may not exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
deform_form = None
|
def __init__(
|
||||||
validated = None
|
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
fields=None,
|
fields=None,
|
||||||
|
@ -338,7 +330,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
self.model_class = model_class
|
self.model_class = model_class
|
||||||
self.model_instance = model_instance
|
self.model_instance = model_instance
|
||||||
if self.model_instance and not self.model_class:
|
if self.model_instance and not self.model_class:
|
||||||
if not isinstance(self.model_instance, dict):
|
if type(self.model_instance) is not dict:
|
||||||
self.model_class = type(self.model_instance)
|
self.model_class = type(self.model_instance)
|
||||||
|
|
||||||
self.set_fields(fields or self.get_fields())
|
self.set_fields(fields or self.get_fields())
|
||||||
|
@ -881,7 +873,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
Return the :class:`deform:deform.Form` instance for the form,
|
Return the :class:`deform:deform.Form` instance for the form,
|
||||||
generating it automatically if necessary.
|
generating it automatically if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.deform_form:
|
if not hasattr(self, "deform_form"):
|
||||||
schema = self.get_schema()
|
schema = self.get_schema()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
@ -990,7 +982,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
output = render(template, context)
|
output = render(template, context)
|
||||||
return HTML.literal(output)
|
return HTML.literal(output)
|
||||||
|
|
||||||
def render_vue_field( # pylint: disable=unused-argument,too-many-locals
|
def render_vue_field( # pylint: disable=unused-argument
|
||||||
self,
|
self,
|
||||||
fieldname,
|
fieldname,
|
||||||
readonly=None,
|
readonly=None,
|
||||||
|
@ -1101,7 +1093,12 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
The actual output may depend on various form attributes, in
|
The actual output may depend on various form attributes, in
|
||||||
particular :attr:`vue_tagname`.
|
particular :attr:`vue_tagname`.
|
||||||
"""
|
"""
|
||||||
return render_vue_finalize(self.vue_tagname, self.vue_component)
|
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
|
||||||
|
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
|
||||||
|
return HTML.tag(
|
||||||
|
"script",
|
||||||
|
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
|
||||||
|
)
|
||||||
|
|
||||||
def get_vue_model_data(self):
|
def get_vue_model_data(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1127,7 +1124,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
# for now we explicitly translate here, ugh. also
|
# for now we explicitly translate here, ugh. also
|
||||||
# note this does not yet allow for null values.. :(
|
# note this does not yet allow for null values.. :(
|
||||||
if isinstance(field.typ, colander.Boolean):
|
if isinstance(field.typ, colander.Boolean):
|
||||||
value = value == field.typ.true_val
|
value = True if value == field.typ.true_val else False
|
||||||
|
|
||||||
model_data[field.oid] = make_json_safe(value)
|
model_data[field.oid] = make_json_safe(value)
|
||||||
|
|
||||||
|
@ -1176,9 +1173,9 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
:attr:`validated` attribute.
|
:attr:`validated` attribute.
|
||||||
|
|
||||||
However if the data is not valid, ``False`` is returned, and
|
However if the data is not valid, ``False`` is returned, and
|
||||||
the :attr:`validated` attribute will be ``None``. In that
|
there will be no :attr:`validated` attribute. In that case
|
||||||
case you should inspect the form errors to learn/display what
|
you should inspect the form errors to learn/display what went
|
||||||
went wrong for the user's sake. See also
|
wrong for the user's sake. See also
|
||||||
:meth:`get_field_errors()`.
|
:meth:`get_field_errors()`.
|
||||||
|
|
||||||
This uses :meth:`deform:deform.Field.validate()` under the
|
This uses :meth:`deform:deform.Field.validate()` under the
|
||||||
|
@ -1194,7 +1191,8 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
|
|
||||||
:returns: Data dict, or ``False``.
|
:returns: Data dict, or ``False``.
|
||||||
"""
|
"""
|
||||||
self.validated = None
|
if hasattr(self, "validated"):
|
||||||
|
del self.validated
|
||||||
|
|
||||||
if self.request.method != "POST":
|
if self.request.method != "POST":
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -69,7 +69,7 @@ class WuttaDateTime(colander.DateTime):
|
||||||
node.raise_invalid("Invalid date and/or time")
|
node.raise_invalid("Invalid date and/or time")
|
||||||
|
|
||||||
|
|
||||||
class ObjectNode(colander.SchemaNode): # pylint: disable=abstract-method
|
class ObjectNode(colander.SchemaNode):
|
||||||
"""
|
"""
|
||||||
Custom schema node class which adds methods for compatibility with
|
Custom schema node class which adds methods for compatibility with
|
||||||
ColanderAlchemy. This is a direct subclass of
|
ColanderAlchemy. This is a direct subclass of
|
||||||
|
@ -183,7 +183,7 @@ class WuttaDictEnum(colander.String):
|
||||||
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
|
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
if "values" not in kwargs:
|
if "values" not in kwargs:
|
||||||
kwargs["values"] = list(self.enum_dct.items())
|
kwargs["values"] = [(k, v) for k, v in self.enum_dct.items()]
|
||||||
|
|
||||||
return widgets.SelectWidget(**kwargs)
|
return widgets.SelectWidget(**kwargs)
|
||||||
|
|
||||||
|
@ -288,8 +288,13 @@ class ObjectRef(colander.SchemaType):
|
||||||
|
|
||||||
default_empty_option = ("", "(none)")
|
default_empty_option = ("", "(none)")
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(
|
||||||
empty_option = kwargs.pop("empty_option", None)
|
self,
|
||||||
|
request,
|
||||||
|
empty_option=None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
# nb. allow session injection for tests
|
# nb. allow session injection for tests
|
||||||
self.session = kwargs.pop("session", Session())
|
self.session = kwargs.pop("session", Session())
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -376,9 +381,7 @@ class ObjectRef(colander.SchemaType):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
|
if isinstance(value, self.model_class):
|
||||||
value, self.model_class
|
|
||||||
):
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# fetch object from DB
|
# fetch object from DB
|
||||||
|
@ -472,9 +475,8 @@ class PersonRef(ObjectRef):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.full_name)
|
return query.order_by(self.model_class.full_name)
|
||||||
|
|
||||||
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
def get_object_url(self, person): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
person = obj
|
|
||||||
return self.request.route_url("people.view", uuid=person.uuid)
|
return self.request.route_url("people.view", uuid=person.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@ -497,9 +499,8 @@ class RoleRef(ObjectRef):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.name)
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
def get_object_url(self, role): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
role = obj
|
|
||||||
return self.request.route_url("roles.view", uuid=role.uuid)
|
return self.request.route_url("roles.view", uuid=role.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@ -522,9 +523,8 @@ class UserRef(ObjectRef):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.username)
|
return query.order_by(self.model_class.username)
|
||||||
|
|
||||||
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
def get_object_url(self, user): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
user = obj
|
|
||||||
return self.request.route_url("users.view", uuid=user.uuid)
|
return self.request.route_url("users.view", uuid=user.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@ -557,7 +557,7 @@ class RoleRefs(WuttaSet):
|
||||||
auth.get_role_authenticated(session),
|
auth.get_role_authenticated(session),
|
||||||
auth.get_role_anonymous(session),
|
auth.get_role_anonymous(session),
|
||||||
}
|
}
|
||||||
avoid = {role.uuid for role in avoid}
|
avoid = set([role.uuid for role in avoid])
|
||||||
|
|
||||||
# also avoid admin unless current user is root
|
# also avoid admin unless current user is root
|
||||||
if not self.request.is_root:
|
if not self.request.is_root:
|
||||||
|
|
|
@ -103,8 +103,7 @@ class ObjectRefWidget(SelectWidget):
|
||||||
|
|
||||||
readonly_template = "readonly/objectref"
|
readonly_template = "readonly/objectref"
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, url=None, *args, **kwargs):
|
||||||
url = kwargs.pop("url", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -300,7 +299,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class FileDownloadWidget(Widget): # pylint: disable=abstract-method
|
class FileDownloadWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
||||||
fields.
|
fields.
|
||||||
|
@ -358,7 +357,7 @@ class FileDownloadWidget(Widget): # pylint: disable=abstract-method
|
||||||
return humanize.naturalsize(size)
|
return humanize.naturalsize(size)
|
||||||
|
|
||||||
|
|
||||||
class GridWidget(Widget): # pylint: disable=abstract-method
|
class GridWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget for fields whose data is represented by a :term:`grid`.
|
Widget for fields whose data is represented by a :term:`grid`.
|
||||||
|
|
||||||
|
@ -407,7 +406,6 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
readonly_template = "readonly/rolerefs"
|
readonly_template = "readonly/rolerefs"
|
||||||
session = None
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
@ -464,7 +462,6 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
|
||||||
template = "permissions"
|
template = "permissions"
|
||||||
readonly_template = "readonly/permissions"
|
readonly_template = "readonly/permissions"
|
||||||
permissions = None
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
@ -515,7 +512,7 @@ class EmailRecipientsWidget(TextAreaWidget):
|
||||||
return ", ".join(values)
|
return ", ".join(values)
|
||||||
|
|
||||||
|
|
||||||
class BatchIdWidget(Widget): # pylint: disable=abstract-method
|
class BatchIdWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget for use with the
|
Widget for use with the
|
||||||
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
|
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"""
|
"""
|
||||||
Base grid classes
|
Base grid classes
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
@ -39,12 +38,7 @@ from pyramid.renderers import render
|
||||||
from webhelpers2.html import HTML
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
from wuttjamaican.db.util import UUID
|
from wuttjamaican.db.util import UUID
|
||||||
from wuttaweb.util import (
|
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
|
||||||
FieldList,
|
|
||||||
get_model_fields,
|
|
||||||
make_json_safe,
|
|
||||||
render_vue_finalize,
|
|
||||||
)
|
|
||||||
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
|
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +53,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-methods
|
class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
"""
|
"""
|
||||||
Base class for all :term:`grids <grid>`.
|
Base class for all :term:`grids <grid>`.
|
||||||
|
|
||||||
|
@ -269,7 +263,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
``active_sorters`` defines the "current/effective" sorters.
|
``active_sorters`` defines the "current/effective" sorters.
|
||||||
|
|
||||||
This attribute is set by :meth:`load_settings()`; until that is
|
This attribute is set by :meth:`load_settings()`; until that is
|
||||||
called its value will be ``None``.
|
called it will not exist.
|
||||||
|
|
||||||
This is conceptually a "subset" of :attr:`sorters` although a
|
This is conceptually a "subset" of :attr:`sorters` although a
|
||||||
different format is used here::
|
different format is used here::
|
||||||
|
@ -377,11 +371,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
See also :meth:`add_tool()` and :meth:`set_tools()`.
|
See also :meth:`add_tool()` and :meth:`set_tools()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
active_sorters = None
|
def __init__(
|
||||||
joined = None
|
|
||||||
pager = None
|
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
vue_tagname="wutta-grid",
|
vue_tagname="wutta-grid",
|
||||||
|
@ -698,7 +688,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
"percent": self.render_percent,
|
"percent": self.render_percent,
|
||||||
}
|
}
|
||||||
|
|
||||||
if renderer in builtins: # pylint: disable=consider-using-get
|
if renderer in builtins:
|
||||||
renderer = builtins[renderer]
|
renderer = builtins[renderer]
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
@ -1096,8 +1086,8 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
# TODO: this should be improved; is needed in tailbone for
|
# TODO: this should be improved; is needed in tailbone for
|
||||||
# multi-column sorting with sqlalchemy queries
|
# multi-column sorting with sqlalchemy queries
|
||||||
if model_property:
|
if model_property:
|
||||||
sorter._class = model_class # pylint: disable=protected-access
|
sorter._class = model_class
|
||||||
sorter._column = model_property # pylint: disable=protected-access
|
sorter._column = model_property
|
||||||
|
|
||||||
return sorter
|
return sorter
|
||||||
|
|
||||||
|
@ -1382,10 +1372,10 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
if filterinfo and callable(filterinfo):
|
if filterinfo and callable(filterinfo):
|
||||||
# filtr = filterinfo
|
# filtr = filterinfo
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
else:
|
||||||
kwargs["key"] = key
|
kwargs["key"] = key
|
||||||
kwargs.setdefault("label", self.get_label(key))
|
kwargs.setdefault("label", self.get_label(key))
|
||||||
filtr = self.make_filter(filterinfo or key, **kwargs)
|
filtr = self.make_filter(filterinfo or key, **kwargs)
|
||||||
|
|
||||||
self.filters[key] = filtr
|
self.filters[key] = filtr
|
||||||
|
|
||||||
|
@ -1483,9 +1473,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
# configuration methods
|
# configuration methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def load_settings( # pylint: disable=too-many-branches,too-many-statements
|
def load_settings(self, persist=True): # pylint: disable=too-many-branches
|
||||||
self, persist=True
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Load all effective settings for the grid.
|
Load all effective settings for the grid.
|
||||||
|
|
||||||
|
@ -1638,7 +1626,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_setting( # pylint: disable=empty-docstring,too-many-arguments,too-many-positional-arguments
|
def get_setting( # pylint: disable=empty-docstring
|
||||||
self, settings, key, src="session", default=None, normalize=lambda v: v
|
self, settings, key, src="session", default=None, normalize=lambda v: v
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
|
@ -2217,7 +2205,12 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
The actual output may depend on various grid attributes, in
|
The actual output may depend on various grid attributes, in
|
||||||
particular :attr:`vue_tagname`.
|
particular :attr:`vue_tagname`.
|
||||||
"""
|
"""
|
||||||
return render_vue_finalize(self.vue_tagname, self.vue_component)
|
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
|
||||||
|
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
|
||||||
|
return HTML.tag(
|
||||||
|
"script",
|
||||||
|
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
|
||||||
|
)
|
||||||
|
|
||||||
def get_vue_columns(self):
|
def get_vue_columns(self):
|
||||||
"""
|
"""
|
||||||
|
@ -2297,11 +2290,12 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
:returns: The first sorter in format ``[sortkey, sortdir]``,
|
:returns: The first sorter in format ``[sortkey, sortdir]``,
|
||||||
or ``None``.
|
or ``None``.
|
||||||
"""
|
"""
|
||||||
if self.active_sorters:
|
if hasattr(self, "active_sorters"):
|
||||||
sorter = self.active_sorters[0]
|
if self.active_sorters:
|
||||||
return [sorter["key"], sorter["dir"]]
|
sorter = self.active_sorters[0]
|
||||||
|
return [sorter["key"], sorter["dir"]]
|
||||||
|
|
||||||
if self.sort_defaults:
|
elif self.sort_defaults:
|
||||||
sorter = self.sort_defaults[0]
|
sorter = self.sort_defaults[0]
|
||||||
return [sorter.sortkey, sorter.sortdir]
|
return [sorter.sortkey, sorter.sortdir]
|
||||||
|
|
||||||
|
@ -2392,9 +2386,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
record = make_json_safe(record, warn=False)
|
record = make_json_safe(record, warn=False)
|
||||||
|
|
||||||
# customize value rendering where applicable
|
# customize value rendering where applicable
|
||||||
for key, renderer in self.renderers.items():
|
for key in self.renderers:
|
||||||
value = record.get(key, None)
|
value = record.get(key, None)
|
||||||
record[key] = renderer(original_record, key, value)
|
record[key] = self.renderers[key](original_record, key, value)
|
||||||
|
|
||||||
# add action urls to each record
|
# add action urls to each record
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
|
@ -2543,7 +2537,7 @@ class GridAction: # pylint: disable=too-many-instance-attributes
|
||||||
Optional HTML class attribute for the action's ``<a>`` tag.
|
Optional HTML class attribute for the action's ``<a>`` tag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -59,10 +59,9 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
:param request: Current :term:`request` object.
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
:param nullable: Boolean indicating whether the filter should
|
:param model_property: Property of a model class, representing the
|
||||||
include ``is_null`` and ``is_not_null`` verbs. If not
|
column by which to filter. For instance,
|
||||||
specified, the column will be inspected (if possible) and use
|
``model.Person.full_name``.
|
||||||
its nullable flag.
|
|
||||||
|
|
||||||
:param \\**kwargs: Any additional kwargs will be set as attributes
|
:param \\**kwargs: Any additional kwargs will be set as attributes
|
||||||
on the filter instance.
|
on the filter instance.
|
||||||
|
@ -170,14 +169,13 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
"is_not_null",
|
"is_not_null",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
key,
|
key,
|
||||||
label=None,
|
label=None,
|
||||||
verbs=None,
|
verbs=None,
|
||||||
choices=None,
|
choices=None,
|
||||||
nullable=None,
|
|
||||||
default_active=False,
|
default_active=False,
|
||||||
default_verb=None,
|
default_verb=None,
|
||||||
default_value=None,
|
default_value=None,
|
||||||
|
@ -198,14 +196,10 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
self.verbs = verbs
|
self.verbs = verbs
|
||||||
if default_verb:
|
if default_verb:
|
||||||
self.default_verb = default_verb
|
self.default_verb = default_verb
|
||||||
self.verb = None # active verb is set later
|
|
||||||
|
|
||||||
# choices
|
# choices
|
||||||
self.set_choices(choices or {})
|
self.set_choices(choices or {})
|
||||||
|
|
||||||
# nullable
|
|
||||||
self.nullable = nullable
|
|
||||||
|
|
||||||
# value
|
# value
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.value = self.default_value
|
self.value = self.default_value
|
||||||
|
@ -254,7 +248,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
Returns a dict of all defined verb labels.
|
Returns a dict of all defined verb labels.
|
||||||
"""
|
"""
|
||||||
# TODO: should traverse hierarchy
|
# TODO: should traverse hierarchy
|
||||||
labels = {verb: verb for verb in self.get_verbs()}
|
labels = dict([(verb, verb) for verb in self.get_verbs()])
|
||||||
labels.update(self.default_verb_labels)
|
labels.update(self.default_verb_labels)
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
|
@ -384,7 +378,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
raise VerbNotSupported(verb)
|
raise VerbNotSupported(verb)
|
||||||
|
|
||||||
# invoke filter method
|
# invoke filter method
|
||||||
return func(data, value) # pylint: disable=not-callable
|
return func(data, value)
|
||||||
|
|
||||||
def filter_is_any(self, data, value): # pylint: disable=unused-argument
|
def filter_is_any(self, data, value): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
|
@ -404,12 +398,18 @@ class AlchemyFilter(GridFilter):
|
||||||
:param model_property: Property of a model class, representing the
|
:param model_property: Property of a model class, representing the
|
||||||
column by which to filter. For instance,
|
column by which to filter. For instance,
|
||||||
``model.Person.full_name``.
|
``model.Person.full_name``.
|
||||||
|
|
||||||
|
:param nullable: Boolean indicating whether the filter should
|
||||||
|
include ``is_null`` and ``is_not_null`` verbs. If not
|
||||||
|
specified, the column will be inspected and use its nullable
|
||||||
|
flag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.model_property = kwargs.pop("model_property")
|
nullable = kwargs.pop("nullable", None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.nullable = nullable
|
||||||
if self.nullable is None:
|
if self.nullable is None:
|
||||||
columns = self.model_property.prop.columns
|
columns = self.model_property.prop.columns
|
||||||
if len(columns) == 1:
|
if len(columns) == 1:
|
||||||
|
@ -446,7 +446,7 @@ class AlchemyFilter(GridFilter):
|
||||||
# probably does not expect that, so explicitly include them.
|
# probably does not expect that, so explicitly include them.
|
||||||
return query.filter(
|
return query.filter(
|
||||||
sa.or_(
|
sa.or_(
|
||||||
self.model_property == None, # pylint: disable=singleton-comparison
|
self.model_property == None,
|
||||||
self.model_property != value,
|
self.model_property != value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -491,18 +491,14 @@ class AlchemyFilter(GridFilter):
|
||||||
"""
|
"""
|
||||||
Filter data with an ``IS NULL`` query. The value is ignored.
|
Filter data with an ``IS NULL`` query. The value is ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(self.model_property == None)
|
||||||
self.model_property == None # pylint: disable=singleton-comparison
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_is_not_null(self, query, value): # pylint: disable=unused-argument
|
def filter_is_not_null(self, query, value): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
Filter data with an ``IS NOT NULL`` query. The value is
|
Filter data with an ``IS NOT NULL`` query. The value is
|
||||||
ignored.
|
ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(self.model_property != None)
|
||||||
self.model_property != None # pylint: disable=singleton-comparison
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StringAlchemyFilter(AlchemyFilter):
|
class StringAlchemyFilter(AlchemyFilter):
|
||||||
|
@ -554,12 +550,7 @@ class StringAlchemyFilter(AlchemyFilter):
|
||||||
|
|
||||||
# sql probably excludes null values from results, but user
|
# sql probably excludes null values from results, but user
|
||||||
# probably does not expect that, so explicitly include them.
|
# probably does not expect that, so explicitly include them.
|
||||||
return query.filter(
|
return query.filter(sa.or_(self.model_property == None, sa.and_(*criteria)))
|
||||||
sa.or_(
|
|
||||||
self.model_property == None, # pylint: disable=singleton-comparison
|
|
||||||
sa.and_(*criteria),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NumericAlchemyFilter(AlchemyFilter):
|
class NumericAlchemyFilter(AlchemyFilter):
|
||||||
|
@ -637,18 +628,14 @@ class BooleanAlchemyFilter(AlchemyFilter):
|
||||||
Filter data with an "is true" condition. The value is
|
Filter data with an "is true" condition. The value is
|
||||||
ignored.
|
ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(self.model_property == True)
|
||||||
self.model_property == True # pylint: disable=singleton-comparison
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_is_false(self, query, value): # pylint: disable=unused-argument
|
def filter_is_false(self, query, value): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
Filter data with an "is false" condition. The value is
|
Filter data with an "is false" condition. The value is
|
||||||
ignored.
|
ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(self.model_property == False)
|
||||||
self.model_property == False # pylint: disable=singleton-comparison
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_is_false_null(self, query, value): # pylint: disable=unused-argument
|
def filter_is_false_null(self, query, value): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
|
@ -656,10 +643,7 @@ class BooleanAlchemyFilter(AlchemyFilter):
|
||||||
ignored.
|
ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(
|
||||||
sa.or_(
|
sa.or_(self.model_property == False, self.model_property == None)
|
||||||
self.model_property == False, # pylint: disable=singleton-comparison
|
|
||||||
self.model_property == None, # pylint: disable=singleton-comparison
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -229,7 +229,7 @@ class MenuHandler(GenericHandler):
|
||||||
# that somewhat to produce our final menus
|
# that somewhat to produce our final menus
|
||||||
self._mark_allowed(request, raw_menus)
|
self._mark_allowed(request, raw_menus)
|
||||||
final_menus = []
|
final_menus = []
|
||||||
for topitem in raw_menus: # pylint: disable=too-many-nested-blocks
|
for topitem in raw_menus:
|
||||||
|
|
||||||
if topitem["allowed"]:
|
if topitem["allowed"]:
|
||||||
|
|
||||||
|
@ -323,7 +323,7 @@ class MenuHandler(GenericHandler):
|
||||||
Traverse the menu set, and mark each item as "allowed" (or
|
Traverse the menu set, and mark each item as "allowed" (or
|
||||||
not) based on current user permissions.
|
not) based on current user permissions.
|
||||||
"""
|
"""
|
||||||
for topitem in menus: # pylint: disable=too-many-nested-blocks
|
for topitem in menus:
|
||||||
|
|
||||||
if topitem.get("type", "menu") == "link":
|
if topitem.get("type", "menu") == "link":
|
||||||
topitem["allowed"] = True
|
topitem["allowed"] = True
|
||||||
|
|
|
@ -91,7 +91,7 @@ class SessionProgress(ProgressBase): # pylint: disable=too-many-instance-attrib
|
||||||
:attr:`success_url`.
|
:attr:`success_url`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,super-init-not-called
|
def __init__(
|
||||||
self, request, key, success_msg=None, success_url=None, error_url=None
|
self, request, key, success_msg=None, success_url=None, error_url=None
|
||||||
):
|
):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -70,5 +70,5 @@ testing = Resource(img, "testing.png", renderer=True)
|
||||||
|
|
||||||
|
|
||||||
# TODO: should consider deprecating this?
|
# TODO: should consider deprecating this?
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
config.add_static_view("wuttaweb", "wuttaweb:static")
|
config.add_static_view("wuttaweb", "wuttaweb:static")
|
||||||
|
|
|
@ -159,20 +159,20 @@ def new_request(event):
|
||||||
Register a Vue 3 component, so the base template knows to
|
Register a Vue 3 component, so the base template knows to
|
||||||
declare it for use within the app (page).
|
declare it for use within the app (page).
|
||||||
"""
|
"""
|
||||||
if not hasattr(request, "wuttaweb_registered_components"):
|
if not hasattr(request, "_wuttaweb_registered_components"):
|
||||||
request.wuttaweb_registered_components = OrderedDict()
|
request._wuttaweb_registered_components = OrderedDict()
|
||||||
|
|
||||||
if tagname in request.wuttaweb_registered_components:
|
if tagname in request._wuttaweb_registered_components:
|
||||||
log.warning(
|
log.warning(
|
||||||
"component with tagname '%s' already registered "
|
"component with tagname '%s' already registered "
|
||||||
"with class '%s' but we are replacing that "
|
"with class '%s' but we are replacing that "
|
||||||
"with class '%s'",
|
"with class '%s'",
|
||||||
tagname,
|
tagname,
|
||||||
request.wuttaweb_registered_components[tagname],
|
request._wuttaweb_registered_components[tagname],
|
||||||
classname,
|
classname,
|
||||||
)
|
)
|
||||||
|
|
||||||
request.wuttaweb_registered_components[tagname] = classname
|
request._wuttaweb_registered_components[tagname] = classname
|
||||||
|
|
||||||
request.register_component = register_component
|
request.register_component = register_component
|
||||||
|
|
||||||
|
@ -411,7 +411,7 @@ def before_render(event):
|
||||||
context["available_themes"] = get_available_themes(config)
|
context["available_themes"] = get_available_themes(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
config.add_subscriber(new_request, "pyramid.events.NewRequest")
|
config.add_subscriber(new_request, "pyramid.events.NewRequest")
|
||||||
config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
|
config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
|
||||||
config.add_subscriber(before_render, "pyramid.events.BeforeRender")
|
config.add_subscriber(before_render, "pyramid.events.BeforeRender")
|
||||||
|
|
|
@ -58,8 +58,8 @@
|
||||||
const app = createApp()
|
const app = createApp()
|
||||||
app.component('vue-fontawesome', FontAwesomeIcon)
|
app.component('vue-fontawesome', FontAwesomeIcon)
|
||||||
|
|
||||||
% if hasattr(request, 'wuttaweb_registered_components'):
|
% if hasattr(request, '_wuttaweb_registered_components'):
|
||||||
% for tagname, classname in request.wuttaweb_registered_components.items():
|
% for tagname, classname in request._wuttaweb_registered_components.items():
|
||||||
app.component('${tagname}', ${classname})
|
app.component('${tagname}', ${classname})
|
||||||
% endfor
|
% endfor
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -39,14 +39,10 @@ class WebTestCase(DataTestCase):
|
||||||
Base class for test suites requiring a full (typical) web app.
|
Base class for test suites requiring a full (typical) web app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self): # pylint: disable=empty-docstring
|
def setUp(self):
|
||||||
""" """
|
|
||||||
self.setup_web()
|
self.setup_web()
|
||||||
|
|
||||||
def setup_web(self):
|
def setup_web(self):
|
||||||
"""
|
|
||||||
Perform setup for the testing web app.
|
|
||||||
"""
|
|
||||||
self.setup_db()
|
self.setup_db()
|
||||||
self.request = self.make_request()
|
self.request = self.make_request()
|
||||||
self.pyramid_config = testing.setUp(
|
self.pyramid_config = testing.setUp(
|
||||||
|
@ -91,14 +87,8 @@ class WebTestCase(DataTestCase):
|
||||||
self.teardown_web()
|
self.teardown_web()
|
||||||
|
|
||||||
def teardown_web(self):
|
def teardown_web(self):
|
||||||
"""
|
|
||||||
Perform teardown for the testing web app.
|
|
||||||
"""
|
|
||||||
testing.tearDown()
|
testing.tearDown()
|
||||||
self.teardown_db()
|
self.teardown_db()
|
||||||
|
|
||||||
def make_request(self):
|
def make_request(self):
|
||||||
"""
|
|
||||||
Make and return a new dummy request object.
|
|
||||||
"""
|
|
||||||
return testing.DummyRequest()
|
return testing.DummyRequest()
|
||||||
|
|
|
@ -618,71 +618,6 @@ def make_json_safe(value, key=None, warn=True):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def render_vue_finalize(vue_tagname, vue_component):
|
|
||||||
"""
|
|
||||||
Render the Vue "finalize" script for a form or grid component.
|
|
||||||
|
|
||||||
This is a convenience for shared logic; it returns e.g.:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
<script>
|
|
||||||
WuttaGrid.data = function() { return WuttaGridData }
|
|
||||||
Vue.component('wutta-grid', WuttaGrid)
|
|
||||||
</script>
|
|
||||||
"""
|
|
||||||
set_data = f"{vue_component}.data = function() {{ return {vue_component}Data }}"
|
|
||||||
make_component = f"Vue.component('{vue_tagname}', {vue_component})"
|
|
||||||
return HTML.tag(
|
|
||||||
"script",
|
|
||||||
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_users_grid(request, **kwargs):
|
|
||||||
"""
|
|
||||||
Make and return a users (sub)grid.
|
|
||||||
|
|
||||||
This grid is shown for the Users field when viewing a Person or
|
|
||||||
Role, for instance. It is called by the following methods:
|
|
||||||
|
|
||||||
* :meth:`wuttaweb.views.people.PersonView.make_users_grid()`
|
|
||||||
* :meth:`wuttaweb.views.roles.RoleView.make_users_grid()`
|
|
||||||
|
|
||||||
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
|
||||||
instance.
|
|
||||||
"""
|
|
||||||
config = request.wutta_config
|
|
||||||
app = config.get_app()
|
|
||||||
model = app.model
|
|
||||||
web = app.get_web_handler()
|
|
||||||
|
|
||||||
if "key" not in kwargs:
|
|
||||||
route_prefix = kwargs.pop("route_prefix")
|
|
||||||
kwargs["key"] = f"{route_prefix}.view.users"
|
|
||||||
|
|
||||||
kwargs.setdefault("model_class", model.User)
|
|
||||||
grid = web.make_grid(request, **kwargs)
|
|
||||||
|
|
||||||
if request.has_perm("users.view"):
|
|
||||||
|
|
||||||
def view_url(user, i): # pylint: disable=unused-argument
|
|
||||||
return request.route_url("users.view", uuid=user.uuid)
|
|
||||||
|
|
||||||
grid.add_action("view", icon="eye", url=view_url)
|
|
||||||
grid.set_link("person")
|
|
||||||
grid.set_link("username")
|
|
||||||
|
|
||||||
if request.has_perm("users.edit"):
|
|
||||||
|
|
||||||
def edit_url(user, i): # pylint: disable=unused-argument
|
|
||||||
return request.route_url("users.edit", uuid=user.uuid)
|
|
||||||
|
|
||||||
grid.add_action("edit", url=edit_url)
|
|
||||||
|
|
||||||
return grid
|
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# theme functions
|
# theme functions
|
||||||
##############################
|
##############################
|
||||||
|
@ -822,14 +757,14 @@ def set_app_theme(request, theme, session=None):
|
||||||
|
|
||||||
# there's only one global template lookup; can get to it via any renderer
|
# there's only one global template lookup; can get to it via any renderer
|
||||||
# but should *not* use /base.mako since that one is about to get volatile
|
# but should *not* use /base.mako since that one is about to get volatile
|
||||||
renderer = get_renderer("/page.mako")
|
renderer = get_renderer("/menu.mako")
|
||||||
lookup = renderer.lookup
|
lookup = renderer.lookup
|
||||||
|
|
||||||
# overwrite first entry in lookup's directory list
|
# overwrite first entry in lookup's directory list
|
||||||
lookup.directories[0] = theme_path
|
lookup.directories[0] = theme_path
|
||||||
|
|
||||||
# clear template cache for lookup object, so it will reload each (as needed)
|
# clear template cache for lookup object, so it will reload each (as needed)
|
||||||
lookup._collection.clear() # pylint: disable=protected-access
|
lookup._collection.clear()
|
||||||
|
|
||||||
# persist current theme in db settings
|
# persist current theme in db settings
|
||||||
with app.short_session(session=session) as s:
|
with app.short_session(session=session) as s:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -34,5 +34,5 @@ from .base import View
|
||||||
from .master import MasterView
|
from .master import MasterView
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
config.include("wuttaweb.views.essential")
|
config.include("wuttaweb.views.essential")
|
||||||
|
|
|
@ -95,8 +95,7 @@ class AuthView(View):
|
||||||
# 'referrer': referrer,
|
# 'referrer': referrer,
|
||||||
}
|
}
|
||||||
|
|
||||||
def login_make_schema(self): # pylint: disable=empty-docstring
|
def login_make_schema(self):
|
||||||
""" """
|
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
# nb. we must explicitly declare the widgets in order to also
|
# nb. we must explicitly declare the widgets in order to also
|
||||||
|
@ -221,19 +220,13 @@ class AuthView(View):
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def change_password_validate_current_password( # pylint: disable=empty-docstring
|
def change_password_validate_current_password(self, node, value):
|
||||||
self, node, value
|
|
||||||
):
|
|
||||||
""" """
|
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if not auth.check_user_password(user, value):
|
if not auth.check_user_password(user, value):
|
||||||
node.raise_invalid("Current password is incorrect.")
|
node.raise_invalid("Current password is incorrect.")
|
||||||
|
|
||||||
def change_password_validate_new_password( # pylint: disable=empty-docstring
|
def change_password_validate_new_password(self, node, value):
|
||||||
self, node, value
|
|
||||||
):
|
|
||||||
""" """
|
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if auth.check_user_password(user, value):
|
if auth.check_user_password(user, value):
|
||||||
|
@ -284,8 +277,7 @@ class AuthView(View):
|
||||||
return self.redirect(url)
|
return self.redirect(url)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config): # pylint: disable=empty-docstring
|
def defaults(cls, config):
|
||||||
""" """
|
|
||||||
cls._auth_defaults(config)
|
cls._auth_defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -319,14 +311,12 @@ class AuthView(View):
|
||||||
config.add_view(cls, attr="stop_root", route_name="stop_root")
|
config.add_view(cls, attr="stop_root", route_name="stop_root")
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
AuthView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
AuthView = kwargs.get("AuthView", base["AuthView"]) # pylint: disable=invalid-name
|
||||||
"AuthView", base["AuthView"]
|
|
||||||
)
|
|
||||||
AuthView.defaults(config)
|
AuthView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -51,8 +51,6 @@ class BatchMasterView(MasterView):
|
||||||
from :meth:`get_batch_handler()`.
|
from :meth:`get_batch_handler()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
executable = True
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"id": "Batch ID",
|
"id": "Batch ID",
|
||||||
"status_code": "Status",
|
"status_code": "Status",
|
||||||
|
@ -123,9 +121,8 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
return super().render_to_response(template, context)
|
return super().render_to_response(template, context)
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -156,17 +153,15 @@ class BatchMasterView(MasterView):
|
||||||
return f"{batch_id:08d}"
|
return f"{batch_id:08d}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_instance_title(self, instance): # pylint: disable=empty-docstring
|
def get_instance_title(self, batch): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
batch = instance
|
|
||||||
if batch.description:
|
if batch.description:
|
||||||
return f"{batch.id_str} {batch.description}"
|
return f"{batch.id_str} {batch.description}"
|
||||||
return batch.id_str
|
return batch.id_str
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=too-many-branches,empty-docstring
|
def configure_form(self, f): # pylint: disable=too-many-branches,empty-docstring
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(form)
|
super().configure_form(f)
|
||||||
f = form
|
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
|
|
||||||
# id
|
# id
|
||||||
|
@ -240,11 +235,13 @@ class BatchMasterView(MasterView):
|
||||||
batch = schema.objectify(form.validated, context=form.model_instance)
|
batch = schema.objectify(form.validated, context=form.model_instance)
|
||||||
|
|
||||||
# then we collect attributes from the new batch
|
# then we collect attributes from the new batch
|
||||||
kw = {
|
kw = dict(
|
||||||
key: getattr(batch, key)
|
[
|
||||||
for key in form.validated
|
(key, getattr(batch, key))
|
||||||
if hasattr(batch, key)
|
for key in form.validated
|
||||||
}
|
if hasattr(batch, key)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# and set attribute for user creating the batch
|
# and set attribute for user creating the batch
|
||||||
kw["created_by"] = self.request.user
|
kw["created_by"] = self.request.user
|
||||||
|
@ -258,7 +255,7 @@ class BatchMasterView(MasterView):
|
||||||
# when not creating, normal logic is fine
|
# when not creating, normal logic is fine
|
||||||
return super().objectify(form)
|
return super().objectify(form)
|
||||||
|
|
||||||
def redirect_after_create(self, obj):
|
def redirect_after_create(self, batch):
|
||||||
"""
|
"""
|
||||||
If the new batch requires initial population, we launch a
|
If the new batch requires initial population, we launch a
|
||||||
thread for that and show the "progress" page.
|
thread for that and show the "progress" page.
|
||||||
|
@ -266,8 +263,6 @@ class BatchMasterView(MasterView):
|
||||||
Otherwise this will do the normal thing of redirecting to the
|
Otherwise this will do the normal thing of redirecting to the
|
||||||
"view" page for the new batch.
|
"view" page for the new batch.
|
||||||
"""
|
"""
|
||||||
batch = obj
|
|
||||||
|
|
||||||
# just view batch if should not populate
|
# just view batch if should not populate
|
||||||
if not self.batch_handler.should_populate(batch):
|
if not self.batch_handler.should_populate(batch):
|
||||||
return self.redirect(self.get_action_url("view", batch))
|
return self.redirect(self.get_action_url("view", batch))
|
||||||
|
@ -288,7 +283,7 @@ class BatchMasterView(MasterView):
|
||||||
thread.start()
|
thread.start()
|
||||||
return self.render_progress(progress)
|
return self.render_progress(progress)
|
||||||
|
|
||||||
def delete_instance(self, obj):
|
def delete_instance(self, batch):
|
||||||
"""
|
"""
|
||||||
Delete the given batch instance.
|
Delete the given batch instance.
|
||||||
|
|
||||||
|
@ -296,7 +291,6 @@ class BatchMasterView(MasterView):
|
||||||
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()`
|
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()`
|
||||||
on the :attr:`batch_handler`.
|
on the :attr:`batch_handler`.
|
||||||
"""
|
"""
|
||||||
batch = obj
|
|
||||||
self.batch_handler.do_delete(batch, self.request.user)
|
self.batch_handler.do_delete(batch, self.request.user)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -332,22 +326,29 @@ class BatchMasterView(MasterView):
|
||||||
raise RuntimeError("can't find the batch")
|
raise RuntimeError("can't find the batch")
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def onerror():
|
try:
|
||||||
|
# populate the batch
|
||||||
|
self.batch_handler.do_populate(batch, progress=progress)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
except Exception as error: # pylint: disable=broad-exception-caught
|
||||||
|
session.rollback()
|
||||||
log.warning(
|
log.warning(
|
||||||
"failed to populate %s: %s",
|
"failed to populate %s: %s",
|
||||||
self.get_model_title(),
|
self.get_model_title(),
|
||||||
batch,
|
batch,
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
if progress:
|
||||||
|
progress.handle_error(error)
|
||||||
|
|
||||||
self.do_thread_body(
|
else:
|
||||||
self.batch_handler.do_populate,
|
session.commit()
|
||||||
(batch,),
|
if progress:
|
||||||
{"progress": progress},
|
progress.handle_success()
|
||||||
onerror,
|
|
||||||
session=session,
|
finally:
|
||||||
progress=progress,
|
session.close()
|
||||||
)
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# execute methods
|
# execute methods
|
||||||
|
@ -387,21 +388,20 @@ class BatchMasterView(MasterView):
|
||||||
model_class = cls.get_model_class()
|
model_class = cls.get_model_class()
|
||||||
return model_class.__row_class__
|
return model_class.__row_class__
|
||||||
|
|
||||||
def get_row_grid_data(self, obj):
|
def get_row_grid_data(self, batch):
|
||||||
"""
|
"""
|
||||||
Returns the base query for the batch
|
Returns the base query for the batch
|
||||||
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
|
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
|
||||||
data.
|
data.
|
||||||
"""
|
"""
|
||||||
session = self.Session()
|
|
||||||
batch = obj
|
|
||||||
row_model_class = self.get_row_model_class()
|
row_model_class = self.get_row_model_class()
|
||||||
query = session.query(row_model_class).filter(row_model_class.batch == batch)
|
query = self.Session.query(row_model_class).filter(
|
||||||
|
row_model_class.batch == batch
|
||||||
|
)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_row_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_row_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_row_grid(g)
|
super().configure_row_grid(g)
|
||||||
|
|
||||||
g.set_label("sequence", "Seq.", column_only=True)
|
g.set_label("sequence", "Seq.", column_only=True)
|
||||||
|
@ -413,3 +413,36 @@ class BatchMasterView(MasterView):
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
return row.STATUS.get(value, value)
|
return row.STATUS.get(value, value)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# configuration
|
||||||
|
##############################
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
cls._defaults(config)
|
||||||
|
cls._batch_defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _batch_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
model_title = cls.get_model_title()
|
||||||
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
|
||||||
|
# execute
|
||||||
|
config.add_route(
|
||||||
|
f"{route_prefix}.execute",
|
||||||
|
f"{instance_url_prefix}/execute",
|
||||||
|
request_method="POST",
|
||||||
|
)
|
||||||
|
config.add_view(
|
||||||
|
cls,
|
||||||
|
attr="execute",
|
||||||
|
route_name=f"{route_prefix}.execute",
|
||||||
|
permission=f"{permission_prefix}.execute",
|
||||||
|
)
|
||||||
|
config.add_wutta_permission(
|
||||||
|
permission_prefix, f"{permission_prefix}.execute", f"Execute {model_title}"
|
||||||
|
)
|
||||||
|
|
|
@ -135,7 +135,7 @@ class CommonView(View):
|
||||||
""" """
|
""" """
|
||||||
self.app.send_email("feedback", context)
|
self.app.send_email("feedback", context)
|
||||||
|
|
||||||
def setup(self, session=None): # pylint: disable=too-many-locals
|
def setup(self, session=None):
|
||||||
"""
|
"""
|
||||||
View for first-time app setup, to create admin user.
|
View for first-time app setup, to create admin user.
|
||||||
|
|
||||||
|
@ -294,8 +294,7 @@ class CommonView(View):
|
||||||
return self.redirect(referrer)
|
return self.redirect(referrer)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config): # pylint: disable=empty-docstring
|
def defaults(cls, config):
|
||||||
""" """
|
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -343,14 +342,14 @@ class CommonView(View):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
CommonView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"CommonView", base["CommonView"]
|
"CommonView", base["CommonView"]
|
||||||
)
|
)
|
||||||
CommonView.defaults(config)
|
CommonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -30,7 +30,7 @@ from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import EmailRecipients
|
from wuttaweb.forms.schema import EmailRecipients
|
||||||
|
|
||||||
|
|
||||||
class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
class EmailSettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for :term:`email settings <email setting>`.
|
Master view for :term:`email settings <email setting>`.
|
||||||
"""
|
"""
|
||||||
|
@ -107,9 +107,8 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
"enabled": self.email_handler.is_enabled(key),
|
"enabled": self.email_handler.is_enabled(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# key
|
# key
|
||||||
|
@ -137,9 +136,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
recips = ", ".join(recips[:2])
|
recips = ", ".join(recips[:2])
|
||||||
return f"{recips}, ..."
|
return f"{recips}, ..."
|
||||||
|
|
||||||
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
|
def get_instance(self): # pylint: disable=empty-docstring
|
||||||
self, **kwargs
|
|
||||||
):
|
|
||||||
""" """
|
""" """
|
||||||
key = self.request.matchdict["key"]
|
key = self.request.matchdict["key"]
|
||||||
setting = self.email_handler.get_email_setting(key, instance=False)
|
setting = self.email_handler.get_email_setting(key, instance=False)
|
||||||
|
@ -148,14 +145,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
raise self.notfound()
|
raise self.notfound()
|
||||||
|
|
||||||
def get_instance_title(self, instance): # pylint: disable=empty-docstring
|
def get_instance_title(self, setting): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
setting = instance
|
|
||||||
return setting["subject"]
|
return setting["subject"]
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# description
|
# description
|
||||||
|
@ -180,9 +175,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
# enabled
|
# enabled
|
||||||
f.set_node("enabled", colander.Boolean())
|
f.set_node("enabled", colander.Boolean())
|
||||||
|
|
||||||
def persist( # pylint: disable=too-many-branches,empty-docstring,arguments-differ,unused-argument
|
def persist(self, setting): # pylint: disable=too-many-branches,empty-docstring
|
||||||
self, setting, **kwargs
|
|
||||||
):
|
|
||||||
""" """
|
""" """
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
key = self.request.matchdict["key"]
|
key = self.request.matchdict["key"]
|
||||||
|
@ -307,14 +300,14 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
EmailSettingView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"EmailSettingView", base["EmailSettingView"]
|
"EmailSettingView", base["EmailSettingView"]
|
||||||
)
|
)
|
||||||
EmailSettingView.defaults(config)
|
EmailSettingView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -41,7 +41,7 @@ That will in turn include the following modules:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
|
|
||||||
def mod(spec):
|
def mod(spec):
|
||||||
return kwargs.get(spec, spec)
|
return kwargs.get(spec, spec)
|
||||||
|
@ -57,5 +57,5 @@ def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
config.include(mod("wuttaweb.views.upgrades"))
|
config.include(mod("wuttaweb.views.upgrades"))
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
"""
|
"""
|
||||||
Base Logic for Master Views
|
Base Logic for Master Views
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -45,7 +44,7 @@ from wuttaweb.progress import SessionProgress
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View): # pylint: disable=too-many-public-methods
|
class MasterView(View):
|
||||||
"""
|
"""
|
||||||
Base class for "master" views.
|
Base class for "master" views.
|
||||||
|
|
||||||
|
@ -399,8 +398,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
# attributes
|
# attributes
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
model_class = None
|
|
||||||
|
|
||||||
# features
|
# features
|
||||||
listable = True
|
listable = True
|
||||||
has_grid = True
|
has_grid = True
|
||||||
|
@ -441,7 +438,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
viewing = False
|
viewing = False
|
||||||
editing = False
|
editing = False
|
||||||
deleting = False
|
deleting = False
|
||||||
executing = False
|
|
||||||
configuring = False
|
configuring = False
|
||||||
|
|
||||||
# default DB session
|
# default DB session
|
||||||
|
@ -528,12 +524,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
* :meth:`redirect_after_create()`
|
* :meth:`redirect_after_create()`
|
||||||
"""
|
"""
|
||||||
self.creating = True
|
self.creating = True
|
||||||
session = self.Session()
|
|
||||||
form = self.make_model_form(cancel_url_fallback=self.get_index_url())
|
form = self.make_model_form(cancel_url_fallback=self.get_index_url())
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
obj = self.create_save_form(form)
|
obj = self.create_save_form(form)
|
||||||
session.flush()
|
self.Session.flush()
|
||||||
return self.redirect_after_create(obj)
|
return self.redirect_after_create(obj)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -822,25 +817,33 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
self, query, progress=None
|
self, query, progress=None
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
|
model_title_plural = self.get_model_title_plural()
|
||||||
|
|
||||||
|
# nb. use new session, separate from web transaction
|
||||||
session = self.app.make_session()
|
session = self.app.make_session()
|
||||||
records = query.with_session(session).all()
|
records = query.with_session(session).all()
|
||||||
|
|
||||||
def onerror():
|
try:
|
||||||
|
self.delete_bulk_action(records, progress=progress)
|
||||||
|
|
||||||
|
except Exception as error: # pylint: disable=broad-exception-caught
|
||||||
|
session.rollback()
|
||||||
log.warning(
|
log.warning(
|
||||||
"failed to delete %s results for %s",
|
"failed to delete %s results for %s",
|
||||||
len(records),
|
len(records),
|
||||||
self.get_model_title_plural(),
|
model_title_plural,
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
if progress:
|
||||||
|
progress.handle_error(error)
|
||||||
|
|
||||||
self.do_thread_body(
|
else:
|
||||||
self.delete_bulk_action,
|
session.commit()
|
||||||
(records,),
|
if progress:
|
||||||
{"progress": progress},
|
progress.handle_success()
|
||||||
onerror,
|
|
||||||
session=session,
|
finally:
|
||||||
progress=progress,
|
session.close()
|
||||||
)
|
|
||||||
|
|
||||||
def delete_bulk_action(self, data, progress=None):
|
def delete_bulk_action(self, data, progress=None):
|
||||||
"""
|
"""
|
||||||
|
@ -914,7 +917,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
data = self.autocomplete_data(term) # pylint: disable=assignment-from-none
|
data = self.autocomplete_data(term)
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -928,7 +931,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def autocomplete_data(self, term): # pylint: disable=unused-argument
|
def autocomplete_data(self, term):
|
||||||
"""
|
"""
|
||||||
Should return the data/query for the "matching" model records,
|
Should return the data/query for the "matching" model records,
|
||||||
based on autocomplete search term. This is called by
|
based on autocomplete search term. This is called by
|
||||||
|
@ -940,7 +943,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
:returns: List of data records, or SQLAlchemy query.
|
:returns: List of data records, or SQLAlchemy query.
|
||||||
"""
|
"""
|
||||||
return None
|
|
||||||
|
|
||||||
def autocomplete_normalize(self, obj):
|
def autocomplete_normalize(self, obj):
|
||||||
"""
|
"""
|
||||||
|
@ -1004,13 +1006,13 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
obj = self.get_instance()
|
obj = self.get_instance()
|
||||||
filename = self.request.GET.get("filename", None)
|
filename = self.request.GET.get("filename", None)
|
||||||
|
|
||||||
path = self.download_path(obj, filename) # pylint: disable=assignment-from-none
|
path = self.download_path(obj, filename)
|
||||||
if not path or not os.path.exists(path):
|
if not path or not os.path.exists(path):
|
||||||
return self.notfound()
|
return self.notfound()
|
||||||
|
|
||||||
return self.file_response(path)
|
return self.file_response(path)
|
||||||
|
|
||||||
def download_path(self, obj, filename): # pylint: disable=unused-argument
|
def download_path(self, obj, filename):
|
||||||
"""
|
"""
|
||||||
Should return absolute path on disk, for the given object and
|
Should return absolute path on disk, for the given object and
|
||||||
filename. Result will be used to return a file response to
|
filename. Result will be used to return a file response to
|
||||||
|
@ -1031,7 +1033,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
the :meth:`download()` view will return a 404 not found
|
the :meth:`download()` view will return a 404 not found
|
||||||
response.
|
response.
|
||||||
"""
|
"""
|
||||||
return None
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# execute methods
|
# execute methods
|
||||||
|
@ -1303,7 +1304,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
Note that their order does not matter since the template
|
Note that their order does not matter since the template
|
||||||
must explicitly define field layout etc.
|
must explicitly define field layout etc.
|
||||||
"""
|
"""
|
||||||
return []
|
|
||||||
|
|
||||||
def configure_gather_settings(
|
def configure_gather_settings(
|
||||||
self,
|
self,
|
||||||
|
@ -2244,9 +2244,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
:returns: The dict of route kwargs for the object.
|
:returns: The dict of route kwargs for the object.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return {key: obj[key] for key in self.get_model_key()}
|
return dict([(key, obj[key]) for key in self.get_model_key()])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return {key: getattr(obj, key) for key in self.get_model_key()}
|
return dict([(key, getattr(obj, key)) for key in self.get_model_key()])
|
||||||
|
|
||||||
def get_action_url(self, action, obj, **kwargs):
|
def get_action_url(self, action, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -2477,60 +2477,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
session = session or self.Session()
|
session = session or self.Session()
|
||||||
session.add(obj)
|
session.add(obj)
|
||||||
|
|
||||||
def do_thread_body( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
||||||
self, func, args, kwargs, onerror=None, session=None, progress=None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Generic method to invoke for thread operations.
|
|
||||||
|
|
||||||
:param func: Callable which performs the actual logic. This
|
|
||||||
will be wrapped with a try/except statement for error
|
|
||||||
handling.
|
|
||||||
|
|
||||||
:param args: Tuple of positional arguments to pass to the
|
|
||||||
``func`` callable.
|
|
||||||
|
|
||||||
:param kwargs: Dict of keyword arguments to pass to the
|
|
||||||
``func`` callable.
|
|
||||||
|
|
||||||
:param onerror: Optional callback to invoke if ``func`` raises
|
|
||||||
an error. It should not expect any arguments.
|
|
||||||
|
|
||||||
:param session: Optional :term:`db session` in effect. Note
|
|
||||||
that if supplied, it will be *committed* (or rolled back on
|
|
||||||
error) and *closed* by this method. If you need more
|
|
||||||
specialized handling, do not use this method (or don't
|
|
||||||
specify the ``session``).
|
|
||||||
|
|
||||||
:param progress: Optional progress factory. If supplied, this
|
|
||||||
is assumed to be a
|
|
||||||
:class:`~wuttaweb.progress.SessionProgress` instance, and
|
|
||||||
it will be updated per success or failure of ``func``
|
|
||||||
invocation.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
func(*args, **kwargs)
|
|
||||||
|
|
||||||
except Exception as error: # pylint: disable=broad-exception-caught
|
|
||||||
if session:
|
|
||||||
session.rollback()
|
|
||||||
if onerror:
|
|
||||||
onerror()
|
|
||||||
else:
|
|
||||||
log.warning("failed to invoke thread callable: %s", func, exc_info=True)
|
|
||||||
if progress:
|
|
||||||
progress.handle_error(error)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if session:
|
|
||||||
session.commit()
|
|
||||||
if progress:
|
|
||||||
progress.handle_success()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if session:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# row methods
|
# row methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -2751,7 +2697,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
do not set the :attr:`model_class`, then you *must* set the
|
do not set the :attr:`model_class`, then you *must* set the
|
||||||
:attr:`model_name`.
|
:attr:`model_name`.
|
||||||
"""
|
"""
|
||||||
return cls.model_class
|
if hasattr(cls, "model_class"):
|
||||||
|
return cls.model_class
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_model_name(cls):
|
def get_model_name(cls):
|
||||||
|
@ -2860,9 +2808,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
inspector = sa.inspect(model_class)
|
inspector = sa.inspect(model_class)
|
||||||
keys = [col.name for col in inspector.primary_key]
|
keys = [col.name for col in inspector.primary_key]
|
||||||
return tuple(
|
return tuple(
|
||||||
prop.key
|
[
|
||||||
for prop in inspector.column_attrs
|
prop.key
|
||||||
if all(col.name in keys for col in prop.columns)
|
for prop in inspector.column_attrs
|
||||||
|
if all(col.name in keys for col in prop.columns)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
raise AttributeError(f"you must define model_key for view class: {cls}")
|
raise AttributeError(f"you must define model_key for view class: {cls}")
|
||||||
|
|
|
@ -28,10 +28,9 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
from wuttjamaican.db.model import Person
|
from wuttjamaican.db.model import Person
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.util import make_users_grid
|
|
||||||
|
|
||||||
|
|
||||||
class PersonView(MasterView): # pylint: disable=abstract-method
|
class PersonView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for people.
|
Master view for people.
|
||||||
|
|
||||||
|
@ -71,9 +70,8 @@ class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
"users",
|
"users",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# full_name
|
# full_name
|
||||||
|
@ -85,9 +83,8 @@ class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
# last_name
|
# last_name
|
||||||
g.set_link("last_name")
|
g.set_link("last_name")
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
person = f.model_instance
|
person = f.model_instance
|
||||||
|
|
||||||
|
@ -108,9 +105,12 @@ class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
return make_users_grid(
|
model = self.app.model
|
||||||
self.request,
|
route_prefix = self.get_route_prefix()
|
||||||
route_prefix=self.get_route_prefix(),
|
|
||||||
|
grid = self.make_grid(
|
||||||
|
key=f"{route_prefix}.view.users",
|
||||||
|
model_class=model.User,
|
||||||
data=person.users,
|
data=person.users,
|
||||||
columns=[
|
columns=[
|
||||||
"username",
|
"username",
|
||||||
|
@ -118,6 +118,23 @@ class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.request.has_perm("users.view"):
|
||||||
|
|
||||||
|
def view_url(user, i): # pylint: disable=unused-argument
|
||||||
|
return self.request.route_url("users.view", uuid=user.uuid)
|
||||||
|
|
||||||
|
grid.add_action("view", icon="eye", url=view_url)
|
||||||
|
grid.set_link("username")
|
||||||
|
|
||||||
|
if self.request.has_perm("users.edit"):
|
||||||
|
|
||||||
|
def edit_url(user, i): # pylint: disable=unused-argument
|
||||||
|
return self.request.route_url("users.edit", uuid=user.uuid)
|
||||||
|
|
||||||
|
grid.add_action("edit", url=edit_url)
|
||||||
|
|
||||||
|
return grid
|
||||||
|
|
||||||
def objectify(self, form): # pylint: disable=empty-docstring
|
def objectify(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
person = super().objectify(form)
|
person = super().objectify(form)
|
||||||
|
@ -196,14 +213,14 @@ class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
PersonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
PersonView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"PersonView", base["PersonView"]
|
"PersonView", base["PersonView"]
|
||||||
)
|
)
|
||||||
PersonView.defaults(config)
|
PersonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -63,15 +63,13 @@ def progress(request):
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
progress = kwargs.get( # pylint: disable=redefined-outer-name
|
progress = kwargs.get("progress", base["progress"])
|
||||||
"progress", base["progress"]
|
|
||||||
)
|
|
||||||
config.add_route("progress", "/progress/{key}")
|
config.add_route("progress", "/progress/{key}")
|
||||||
config.add_view(progress, route_name="progress", renderer="json")
|
config.add_view(progress, route_name="progress", renderer="json")
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -37,7 +37,7 @@ from wuttaweb.views import MasterView
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ReportView(MasterView): # pylint: disable=abstract-method
|
class ReportView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for :term:`reports <report>`; route prefix is
|
Master view for :term:`reports <report>`; route prefix is
|
||||||
``reports``.
|
``reports``.
|
||||||
|
@ -89,9 +89,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
"help_text": report.__doc__,
|
"help_text": report.__doc__,
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# report_key
|
# report_key
|
||||||
|
@ -104,9 +103,7 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
# help_text
|
# help_text
|
||||||
g.set_searchable("help_text")
|
g.set_searchable("help_text")
|
||||||
|
|
||||||
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
|
def get_instance(self): # pylint: disable=empty-docstring
|
||||||
self, **kwargs
|
|
||||||
):
|
|
||||||
""" """
|
""" """
|
||||||
key = self.request.matchdict["report_key"]
|
key = self.request.matchdict["report_key"]
|
||||||
report = self.report_handler.get_report(key)
|
report = self.report_handler.get_report(key)
|
||||||
|
@ -115,9 +112,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
raise self.notfound()
|
raise self.notfound()
|
||||||
|
|
||||||
def get_instance_title(self, instance): # pylint: disable=empty-docstring
|
def get_instance_title(self, report): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
report = instance
|
|
||||||
return report["report_title"]
|
return report["report_title"]
|
||||||
|
|
||||||
def view(self):
|
def view(self):
|
||||||
|
@ -155,9 +151,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return self.render_to_response("view", context)
|
return self.render_to_response("view", context)
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
key = self.request.matchdict["report_key"]
|
key = self.request.matchdict["report_key"]
|
||||||
report = self.report_handler.get_report(key)
|
report = self.report_handler.get_report(key)
|
||||||
|
@ -266,14 +261,14 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
ReportView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"ReportView", base["ReportView"]
|
"ReportView", base["ReportView"]
|
||||||
)
|
)
|
||||||
ReportView.defaults(config)
|
ReportView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -29,10 +29,9 @@ from wuttaweb.views import MasterView
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
from wuttaweb.forms.schema import Permissions, RoleRef
|
from wuttaweb.forms.schema import Permissions, RoleRef
|
||||||
from wuttaweb.util import make_users_grid
|
|
||||||
|
|
||||||
|
|
||||||
class RoleView(MasterView): # pylint: disable=abstract-method
|
class RoleView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for roles.
|
Master view for roles.
|
||||||
|
|
||||||
|
@ -59,8 +58,6 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
}
|
}
|
||||||
sort_defaults = "name"
|
sort_defaults = "name"
|
||||||
|
|
||||||
wutta_permissions = None
|
|
||||||
|
|
||||||
# TODO: master should handle this, possibly via configure_form()
|
# TODO: master should handle this, possibly via configure_form()
|
||||||
def get_query(self, session=None): # pylint: disable=empty-docstring
|
def get_query(self, session=None): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
@ -68,9 +65,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
query = super().get_query(session=session)
|
query = super().get_query(session=session)
|
||||||
return query.order_by(model.Role.name)
|
return query.order_by(model.Role.name)
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
|
@ -79,9 +75,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
# notes
|
# notes
|
||||||
g.set_renderer("notes", self.grid_render_notes)
|
g.set_renderer("notes", self.grid_render_notes)
|
||||||
|
|
||||||
def is_editable(self, obj): # pylint: disable=empty-docstring
|
def is_editable(self, role): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
role = obj
|
|
||||||
session = self.app.get_session(role)
|
session = self.app.get_session(role)
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
||||||
|
@ -98,9 +93,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_deletable(self, obj): # pylint: disable=empty-docstring
|
def is_deletable(self, role): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
role = obj
|
|
||||||
session = self.app.get_session(role)
|
session = self.app.get_session(role)
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
||||||
|
@ -114,9 +108,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
role = f.model_instance
|
role = f.model_instance
|
||||||
|
|
||||||
|
@ -152,9 +145,12 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
return make_users_grid(
|
model = self.app.model
|
||||||
self.request,
|
route_prefix = self.get_route_prefix()
|
||||||
route_prefix=self.get_route_prefix(),
|
|
||||||
|
grid = self.make_grid(
|
||||||
|
key=f"{route_prefix}.view.users",
|
||||||
|
model_class=model.User,
|
||||||
data=role.users,
|
data=role.users,
|
||||||
columns=[
|
columns=[
|
||||||
"username",
|
"username",
|
||||||
|
@ -163,6 +159,24 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.request.has_perm("users.view"):
|
||||||
|
|
||||||
|
def view_url(user, i): # pylint: disable=unused-argument
|
||||||
|
return self.request.route_url("users.view", uuid=user.uuid)
|
||||||
|
|
||||||
|
grid.add_action("view", icon="eye", url=view_url)
|
||||||
|
grid.set_link("person")
|
||||||
|
grid.set_link("username")
|
||||||
|
|
||||||
|
if self.request.has_perm("users.edit"):
|
||||||
|
|
||||||
|
def edit_url(user, i): # pylint: disable=unused-argument
|
||||||
|
return self.request.route_url("users.edit", uuid=user.uuid)
|
||||||
|
|
||||||
|
grid.add_action("edit", url=edit_url)
|
||||||
|
|
||||||
|
return grid
|
||||||
|
|
||||||
def unique_name(self, node, value): # pylint: disable=empty-docstring
|
def unique_name(self, node, value): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -303,7 +317,7 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionView(MasterView): # pylint: disable=abstract-method
|
class PermissionView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for permissions.
|
Master view for permissions.
|
||||||
|
|
||||||
|
@ -332,7 +346,7 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
|
||||||
"permission",
|
"permission",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ
|
def get_query(self, **kwargs): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
query = super().get_query(**kwargs)
|
query = super().get_query(**kwargs)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -342,9 +356,8 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -356,28 +369,25 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
|
||||||
# permission
|
# permission
|
||||||
g.set_link("permission")
|
g.set_link("permission")
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# role
|
# role
|
||||||
f.set_node("role", RoleRef(self.request))
|
f.set_node("role", RoleRef(self.request))
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
RoleView = kwargs.get("RoleView", base["RoleView"]) # pylint: disable=invalid-name
|
||||||
"RoleView", base["RoleView"]
|
|
||||||
)
|
|
||||||
RoleView.defaults(config)
|
RoleView.defaults(config)
|
||||||
|
|
||||||
PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
PermissionView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"PermissionView", base["PermissionView"]
|
"PermissionView", base["PermissionView"]
|
||||||
)
|
)
|
||||||
PermissionView.defaults(config)
|
PermissionView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -35,7 +35,7 @@ from wuttaweb.views import MasterView
|
||||||
from wuttaweb.util import get_libver, get_liburl
|
from wuttaweb.util import get_libver, get_liburl
|
||||||
|
|
||||||
|
|
||||||
class AppInfoView(MasterView): # pylint: disable=abstract-method
|
class AppInfoView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the core app info, to show/edit config etc.
|
Master view for the core app info, to show/edit config etc.
|
||||||
|
|
||||||
|
@ -93,9 +93,8 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
g.sort_multiple = False
|
g.sort_multiple = False
|
||||||
|
@ -174,9 +173,7 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return simple_settings
|
return simple_settings
|
||||||
|
|
||||||
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
|
def configure_get_context(self, **kwargs): # pylint: disable=empty-docstring
|
||||||
self, **kwargs
|
|
||||||
):
|
|
||||||
""" """
|
""" """
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
|
|
||||||
|
@ -223,7 +220,7 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SettingView(MasterView): # pylint: disable=abstract-method
|
class SettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the "raw" settings table.
|
Master view for the "raw" settings table.
|
||||||
|
|
||||||
|
@ -245,17 +242,15 @@ class SettingView(MasterView): # pylint: disable=abstract-method
|
||||||
sort_defaults = "name"
|
sort_defaults = "name"
|
||||||
|
|
||||||
# TODO: master should handle this (per model key)
|
# TODO: master should handle this (per model key)
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.set_link("name")
|
g.set_link("name")
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
|
@ -280,19 +275,19 @@ class SettingView(MasterView): # pylint: disable=abstract-method
|
||||||
node.raise_invalid("Setting name must be unique")
|
node.raise_invalid("Setting name must be unique")
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
AppInfoView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"AppInfoView", base["AppInfoView"]
|
"AppInfoView", base["AppInfoView"]
|
||||||
)
|
)
|
||||||
AppInfoView.defaults(config)
|
AppInfoView.defaults(config)
|
||||||
|
|
||||||
SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
SettingView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"SettingView", base["SettingView"]
|
"SettingView", base["SettingView"]
|
||||||
)
|
)
|
||||||
SettingView.defaults(config)
|
SettingView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -41,7 +41,7 @@ from wuttaweb.progress import get_progress_session
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UpgradeView(MasterView): # pylint: disable=abstract-method
|
class UpgradeView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for upgrades.
|
Master view for upgrades.
|
||||||
|
|
||||||
|
@ -72,9 +72,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
sort_defaults = ("created", "desc")
|
sort_defaults = ("created", "desc")
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -122,9 +121,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
return "has-background-warning"
|
return "has-background-warning"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
upgrade = f.model_instance
|
upgrade = f.model_instance
|
||||||
|
@ -206,12 +204,11 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
|
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_instance(self, obj):
|
def delete_instance(self, upgrade):
|
||||||
"""
|
"""
|
||||||
We override this method to delete any files associated with
|
We override this method to delete any files associated with
|
||||||
the upgrade, in addition to deleting the upgrade proper.
|
the upgrade, in addition to deleting the upgrade proper.
|
||||||
"""
|
"""
|
||||||
upgrade = obj
|
|
||||||
path = self.get_upgrade_filepath(upgrade, create=False)
|
path = self.get_upgrade_filepath(upgrade, create=False)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
@ -230,9 +227,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return upgrade
|
return upgrade
|
||||||
|
|
||||||
def download_path(self, obj, filename): # pylint: disable=empty-docstring
|
def download_path(self, upgrade, filename): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
upgrade = obj
|
|
||||||
if filename:
|
if filename:
|
||||||
return self.get_upgrade_filepath(upgrade, filename)
|
return self.get_upgrade_filepath(upgrade, filename)
|
||||||
return None
|
return None
|
||||||
|
@ -249,7 +245,7 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
path = os.path.join(path, filename)
|
path = os.path.join(path, filename)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def execute_instance(self, obj, user, progress=None):
|
def execute_instance(self, upgrade, user, progress=None):
|
||||||
"""
|
"""
|
||||||
This method runs the actual upgrade.
|
This method runs the actual upgrade.
|
||||||
|
|
||||||
|
@ -262,7 +258,6 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
The upgrade itself is marked as "executed" with status of
|
The upgrade itself is marked as "executed" with status of
|
||||||
either ``SUCCESS`` or ``FAILURE``.
|
either ``SUCCESS`` or ``FAILURE``.
|
||||||
"""
|
"""
|
||||||
upgrade = obj
|
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
|
||||||
# locate file paths
|
# locate file paths
|
||||||
|
@ -382,14 +377,14 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
UpgradeView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
UpgradeView = kwargs.get( # pylint: disable=invalid-name
|
||||||
"UpgradeView", base["UpgradeView"]
|
"UpgradeView", base["UpgradeView"]
|
||||||
)
|
)
|
||||||
UpgradeView.defaults(config)
|
UpgradeView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -30,7 +30,7 @@ from wuttaweb.forms import widgets
|
||||||
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
||||||
|
|
||||||
|
|
||||||
class UserView(MasterView): # pylint: disable=abstract-method
|
class UserView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for users.
|
Master view for users.
|
||||||
|
|
||||||
|
@ -82,9 +82,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
def configure_grid(self, g): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -107,9 +106,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
return "has-background-warning"
|
return "has-background-warning"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_editable(self, obj): # pylint: disable=empty-docstring
|
def is_editable(self, user): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
user = obj
|
|
||||||
|
|
||||||
# only root can edit certain users
|
# only root can edit certain users
|
||||||
if user.prevent_edit and not self.request.is_root:
|
if user.prevent_edit and not self.request.is_root:
|
||||||
|
@ -117,9 +115,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def configure_form(self, form): # pylint: disable=empty-docstring
|
def configure_form(self, f): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
user = f.model_instance
|
user = f.model_instance
|
||||||
|
|
||||||
|
@ -236,7 +233,7 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
||||||
old_roles = {role.uuid for role in user.roles}
|
old_roles = set([role.uuid for role in user.roles])
|
||||||
new_roles = data["roles"]
|
new_roles = data["roles"]
|
||||||
|
|
||||||
admin = auth.get_role_administrator(session)
|
admin = auth.get_role_administrator(session)
|
||||||
|
@ -420,14 +417,12 @@ class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
UserView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
UserView = kwargs.get("UserView", base["UserView"]) # pylint: disable=invalid-name
|
||||||
"UserView", base["UserView"]
|
|
||||||
)
|
|
||||||
UserView.defaults(config)
|
UserView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config): # pylint: disable=missing-function-docstring
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -363,7 +363,7 @@ class TestForm(TestCase):
|
||||||
|
|
||||||
# basic
|
# basic
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema)
|
||||||
self.assertIsNone(form.deform_form)
|
self.assertFalse(hasattr(form, "deform_form"))
|
||||||
dform = form.get_deform()
|
dform = form.get_deform()
|
||||||
self.assertIsInstance(dform, deform.Form)
|
self.assertIsInstance(dform, deform.Form)
|
||||||
self.assertIs(form.deform_form, dform)
|
self.assertIs(form.deform_form, dform)
|
||||||
|
@ -684,7 +684,7 @@ class TestForm(TestCase):
|
||||||
def test_validate(self):
|
def test_validate(self):
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema)
|
||||||
self.assertIsNone(form.validated)
|
self.assertFalse(hasattr(form, "validated"))
|
||||||
|
|
||||||
# will not validate unless request is POST
|
# will not validate unless request is POST
|
||||||
self.request.POST = {"foo": "blarg", "bar": "baz"}
|
self.request.POST = {"foo": "blarg", "bar": "baz"}
|
||||||
|
|
|
@ -497,7 +497,7 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
# settings are loaded, applied, saved
|
# settings are loaded, applied, saved
|
||||||
self.assertEqual(grid.sort_defaults, [])
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
self.assertIsNone(grid.active_sorters)
|
self.assertFalse(hasattr(grid, "active_sorters"))
|
||||||
self.request.GET = {"sort1key": "name", "sort1dir": "desc"}
|
self.request.GET = {"sort1key": "name", "sort1dir": "desc"}
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
|
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
|
||||||
|
@ -525,7 +525,7 @@ class TestGrid(WebTestCase):
|
||||||
sort_on_backend=True,
|
sort_on_backend=True,
|
||||||
sort_defaults="name",
|
sort_defaults="name",
|
||||||
)
|
)
|
||||||
self.assertIsNone(grid.active_sorters)
|
self.assertFalse(hasattr(grid, "active_sorters"))
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
|
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
|
||||||
|
|
||||||
|
@ -537,7 +537,7 @@ class TestGrid(WebTestCase):
|
||||||
mod.SortInfo("name", "asc"),
|
mod.SortInfo("name", "asc"),
|
||||||
mod.SortInfo("value", "desc"),
|
mod.SortInfo("value", "desc"),
|
||||||
]
|
]
|
||||||
self.assertIsNone(grid.active_sorters)
|
self.assertFalse(hasattr(grid, "active_sorters"))
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
|
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
|
||||||
|
|
||||||
|
@ -556,7 +556,7 @@ class TestGrid(WebTestCase):
|
||||||
paginated=True,
|
paginated=True,
|
||||||
paginate_on_backend=True,
|
paginate_on_backend=True,
|
||||||
)
|
)
|
||||||
self.assertIsNone(grid.active_sorters)
|
self.assertFalse(hasattr(grid, "active_sorters"))
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
|
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ class TestGridFilter(WebTestCase):
|
||||||
|
|
||||||
# verb is not set by default, but can be set
|
# verb is not set by default, but can be set
|
||||||
filtr = self.make_filter(model.Setting.name)
|
filtr = self.make_filter(model.Setting.name)
|
||||||
self.assertIsNone(filtr.verb)
|
self.assertFalse(hasattr(filtr, "verb"))
|
||||||
filtr = self.make_filter(model.Setting.name, verb="foo")
|
filtr = self.make_filter(model.Setting.name, verb="foo")
|
||||||
self.assertEqual(filtr.verb, "foo")
|
self.assertEqual(filtr.verb, "foo")
|
||||||
|
|
||||||
|
|
|
@ -76,23 +76,23 @@ class TestNewRequest(TestCase):
|
||||||
subscribers.new_request(event)
|
subscribers.new_request(event)
|
||||||
|
|
||||||
# component tracking dict is missing at first
|
# component tracking dict is missing at first
|
||||||
self.assertFalse(hasattr(self.request, "wuttaweb_registered_components"))
|
self.assertFalse(hasattr(self.request, "_wuttaweb_registered_components"))
|
||||||
|
|
||||||
# registering a component
|
# registering a component
|
||||||
self.request.register_component("foo-example", "FooExample")
|
self.request.register_component("foo-example", "FooExample")
|
||||||
self.assertTrue(hasattr(self.request, "wuttaweb_registered_components"))
|
self.assertTrue(hasattr(self.request, "_wuttaweb_registered_components"))
|
||||||
self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
|
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
|
||||||
self.assertIn("foo-example", self.request.wuttaweb_registered_components)
|
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.request.wuttaweb_registered_components["foo-example"], "FooExample"
|
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
|
||||||
)
|
)
|
||||||
|
|
||||||
# re-registering same name
|
# re-registering same name
|
||||||
self.request.register_component("foo-example", "FooExample")
|
self.request.register_component("foo-example", "FooExample")
|
||||||
self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
|
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
|
||||||
self.assertIn("foo-example", self.request.wuttaweb_registered_components)
|
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.request.wuttaweb_registered_components["foo-example"], "FooExample"
|
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_referrer(self):
|
def test_get_referrer(self):
|
||||||
|
|
|
@ -16,7 +16,6 @@ from wuttjamaican.util import resource_path
|
||||||
|
|
||||||
from wuttaweb import util as mod
|
from wuttaweb import util as mod
|
||||||
from wuttaweb.app import establish_theme
|
from wuttaweb.app import establish_theme
|
||||||
from wuttaweb.grids import Grid
|
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -666,52 +665,6 @@ class TestMakeJsonSafe(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestRenderVueFinalize(TestCase):
|
|
||||||
|
|
||||||
def basic(self):
|
|
||||||
html = mod.render_vue_finalize("wutta-grid", "WuttaGrid")
|
|
||||||
self.assertIn("<script>", html)
|
|
||||||
self.assertIn("Vue.component('wutta-grid', WuttaGrid)", html)
|
|
||||||
|
|
||||||
|
|
||||||
class TestMakeUsersGrid(WebTestCase):
|
|
||||||
|
|
||||||
def test_make_users_grid(self):
|
|
||||||
self.pyramid_config.add_route("users.view", "/users/{uuid}/view")
|
|
||||||
self.pyramid_config.add_route("users.edit", "/users/{uuid}/edit")
|
|
||||||
model = self.app.model
|
|
||||||
person = model.Person(full_name="John Doe")
|
|
||||||
self.session.add(person)
|
|
||||||
user = model.User(username="john", person=person)
|
|
||||||
self.session.add(user)
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
# basic (no actions because not prvileged)
|
|
||||||
grid = mod.make_users_grid(self.request, key="blah.users", data=person.users)
|
|
||||||
self.assertIsInstance(grid, Grid)
|
|
||||||
self.assertFalse(grid.linked_columns)
|
|
||||||
self.assertFalse(grid.actions)
|
|
||||||
|
|
||||||
# key may be derived from route_prefix
|
|
||||||
grid = mod.make_users_grid(self.request, route_prefix="foo")
|
|
||||||
self.assertIsInstance(grid, Grid)
|
|
||||||
self.assertEqual(grid.key, "foo.view.users")
|
|
||||||
|
|
||||||
# view + edit actions (because root)
|
|
||||||
with patch.object(self.request, "is_root", new=True):
|
|
||||||
grid = mod.make_users_grid(
|
|
||||||
self.request, key="blah.users", data=person.users
|
|
||||||
)
|
|
||||||
self.assertIsInstance(grid, Grid)
|
|
||||||
self.assertIn("username", grid.linked_columns)
|
|
||||||
self.assertEqual(len(grid.actions), 2)
|
|
||||||
self.assertEqual(grid.actions[0].key, "view")
|
|
||||||
self.assertEqual(grid.actions[1].key, "edit")
|
|
||||||
|
|
||||||
# render grid to ensure coverage for link urls
|
|
||||||
grid.render_vue_template()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAvailableThemes(TestCase):
|
class TestGetAvailableThemes(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -624,7 +624,7 @@ class TestMasterView(WebTestCase):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
# empty by default
|
# empty by default
|
||||||
self.assertIsNone(mod.MasterView.model_class)
|
self.assertFalse(hasattr(mod.MasterView, "model_class"))
|
||||||
data = view.get_grid_data(session=self.session)
|
data = view.get_grid_data(session=self.session)
|
||||||
self.assertEqual(data, [])
|
self.assertEqual(data, [])
|
||||||
|
|
||||||
|
@ -1371,25 +1371,6 @@ class TestMasterView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 7)
|
self.assertEqual(self.session.query(model.Setting).count(), 7)
|
||||||
|
|
||||||
def test_do_thread_body(self):
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
# nb. so far this is just proving coverage, in case caller
|
|
||||||
# does not specify an error handler
|
|
||||||
|
|
||||||
def func():
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# with error handler
|
|
||||||
onerror = MagicMock()
|
|
||||||
view.do_thread_body(func, (), {}, onerror)
|
|
||||||
onerror.assert_called_once_with()
|
|
||||||
|
|
||||||
# without error handler
|
|
||||||
onerror.reset_mock()
|
|
||||||
view.do_thread_body(func, (), {})
|
|
||||||
onerror.assert_not_called()
|
|
||||||
|
|
||||||
def test_delete_bulk_thread(self):
|
def test_delete_bulk_thread(self):
|
||||||
self.pyramid_config.add_route("settings", "/settings/")
|
self.pyramid_config.add_route("settings", "/settings/")
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -1675,11 +1656,6 @@ class TestMasterView(WebTestCase):
|
||||||
count = self.session.query(model.Setting).count()
|
count = self.session.query(model.Setting).count()
|
||||||
self.assertEqual(count, 0)
|
self.assertEqual(count, 0)
|
||||||
|
|
||||||
def test_configure_get_simple_settings(self):
|
|
||||||
view = self.make_view()
|
|
||||||
settings = view.configure_get_simple_settings()
|
|
||||||
self.assertEqual(settings, [])
|
|
||||||
|
|
||||||
def test_configure_gather_settings(self):
|
def test_configure_gather_settings(self):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue