Compare commits
32 commits
601dec7777
...
0a08918657
Author | SHA1 | Date | |
---|---|---|---|
0a08918657 | |||
a1868e1f44 | |||
ad74bede04 | |||
dd25d98e7d | |||
92754a64c4 | |||
459c16ba4f | |||
e123b12cd9 | |||
deaf1976f3 | |||
1dd184622f | |||
95aeb87899 | |||
07e90229ce | |||
2624f9dce8 | |||
2bcdeb42cd | |||
48494ee5e4 | |||
6c66c8d57b | |||
e38f7ba293 | |||
ab35847f23 | |||
ebd44c55f5 | |||
4b4d81c4f3 | |||
ec982fe168 | |||
564ac318bc | |||
65849ad82d | |||
d6f7c19f71 | |||
11ec57387e | |||
5a6ed6135a | |||
d01c343a7c | |||
d7eacf0f52 | |||
0dc6b615e7 | |||
282fc61e50 | |||
1fec99bc92 | |||
d39f5afd4b | |||
5c23120795 |
38 changed files with 584 additions and 397 deletions
38
.pylintrc
38
.pylintrc
|
@ -2,37 +2,7 @@
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
disable=fixme,
|
disable=fixme,
|
||||||
abstract-method,
|
|
||||||
arguments-differ,
|
[SIMILARITIES]
|
||||||
arguments-renamed,
|
# nb. cuts out some noise for duplicate-code
|
||||||
assignment-from-no-return,
|
min-similarity-lines=5
|
||||||
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,6 +11,9 @@ 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,4 +1,7 @@
|
||||||
# -*- 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_handler" not in self.__dict__:
|
if "web" not in self.app.handlers:
|
||||||
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.web_handler = self.app.load_object(spec)(self.config)
|
self.app.handlers["web"] = self.app.load_object(spec)(self.config)
|
||||||
return self.web_handler
|
return self.app.handlers["web"]
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
make_wsgi_app = app.load_object(main_app)
|
factory = app.load_object(main_app)
|
||||||
elif callable(main_app):
|
elif callable(main_app):
|
||||||
make_wsgi_app = main_app
|
factory = 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 make_wsgi_app({}, **settings)
|
return factory({}, **settings)
|
||||||
|
|
||||||
|
|
||||||
def make_asgi_app(main_app=None, config=None):
|
def make_asgi_app(main_app=None, config=None):
|
||||||
|
|
|
@ -105,7 +105,8 @@ 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):
|
def load_identity(self, request): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
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
|
||||||
|
@ -122,22 +123,29 @@ class WuttaSecurityPolicy:
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def identity(self, request):
|
def identity(self, request): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
return self.identity_cache.get_or_create(request)
|
return self.identity_cache.get_or_create(request)
|
||||||
|
|
||||||
def authenticated_userid(self, request):
|
def authenticated_userid(self, request): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
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):
|
def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
return self.session_helper.remember(request, userid, **kw)
|
return self.session_helper.remember(request, userid, **kw)
|
||||||
|
|
||||||
def forget(self, request, **kw):
|
def forget(self, request, **kw): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
return self.session_helper.forget(request, **kw)
|
return self.session_helper.forget(request, **kw)
|
||||||
|
|
||||||
def permits(self, request, context, permission): # pylint: disable=unused-argument
|
def permits( # pylint: disable=unused-argument,empty-docstring
|
||||||
|
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
|
class feedback(EmailSetting): # pylint: disable=invalid-name,too-few-public-methods
|
||||||
"""
|
"""
|
||||||
Sent when user submits feedback via the web app.
|
Sent when user submits feedback via the web app.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"""
|
"""
|
||||||
Base form classes
|
Base form classes
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -36,13 +37,19 @@ 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 FieldList, get_form_data, get_model_fields, make_json_safe
|
from wuttaweb.util import (
|
||||||
|
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
|
class Form: # pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||||
"""
|
"""
|
||||||
Base class for all forms.
|
Base class for all forms.
|
||||||
|
|
||||||
|
@ -263,11 +270,12 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
deform_form = None
|
||||||
|
validated = None
|
||||||
|
|
||||||
|
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
fields=None,
|
fields=None,
|
||||||
|
@ -330,7 +338,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
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 type(self.model_instance) is not dict:
|
if not isinstance(self.model_instance, 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())
|
||||||
|
@ -873,7 +881,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
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 hasattr(self, "deform_form"):
|
if not self.deform_form:
|
||||||
schema = self.get_schema()
|
schema = self.get_schema()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
@ -982,7 +990,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
output = render(template, context)
|
output = render(template, context)
|
||||||
return HTML.literal(output)
|
return HTML.literal(output)
|
||||||
|
|
||||||
def render_vue_field( # pylint: disable=unused-argument
|
def render_vue_field( # pylint: disable=unused-argument,too-many-locals
|
||||||
self,
|
self,
|
||||||
fieldname,
|
fieldname,
|
||||||
readonly=None,
|
readonly=None,
|
||||||
|
@ -1093,12 +1101,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
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`.
|
||||||
"""
|
"""
|
||||||
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
|
return render_vue_finalize(self.vue_tagname, self.vue_component)
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -1124,7 +1127,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
# 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 = True if value == field.typ.true_val else False
|
value = value == field.typ.true_val
|
||||||
|
|
||||||
model_data[field.oid] = make_json_safe(value)
|
model_data[field.oid] = make_json_safe(value)
|
||||||
|
|
||||||
|
@ -1173,9 +1176,9 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
: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
|
||||||
there will be no :attr:`validated` attribute. In that case
|
the :attr:`validated` attribute will be ``None``. In that
|
||||||
you should inspect the form errors to learn/display what went
|
case you should inspect the form errors to learn/display what
|
||||||
wrong for the user's sake. See also
|
went 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
|
||||||
|
@ -1191,8 +1194,7 @@ class Form: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
:returns: Data dict, or ``False``.
|
:returns: Data dict, or ``False``.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "validated"):
|
self.validated = None
|
||||||
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):
|
class ObjectNode(colander.SchemaNode): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
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"] = [(k, v) for k, v in self.enum_dct.items()]
|
kwargs["values"] = list(self.enum_dct.items())
|
||||||
|
|
||||||
return widgets.SelectWidget(**kwargs)
|
return widgets.SelectWidget(**kwargs)
|
||||||
|
|
||||||
|
@ -288,13 +288,8 @@ class ObjectRef(colander.SchemaType):
|
||||||
|
|
||||||
default_empty_option = ("", "(none)")
|
default_empty_option = ("", "(none)")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, request, *args, **kwargs):
|
||||||
self,
|
empty_option = kwargs.pop("empty_option", None)
|
||||||
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)
|
||||||
|
@ -381,7 +376,9 @@ class ObjectRef(colander.SchemaType):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(value, self.model_class):
|
if isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
|
||||||
|
value, self.model_class
|
||||||
|
):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# fetch object from DB
|
# fetch object from DB
|
||||||
|
@ -475,8 +472,9 @@ 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, person): # pylint: disable=empty-docstring
|
def get_object_url(self, obj): # 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -499,8 +497,9 @@ class RoleRef(ObjectRef):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.name)
|
return query.order_by(self.model_class.name)
|
||||||
|
|
||||||
def get_object_url(self, role): # pylint: disable=empty-docstring
|
def get_object_url(self, obj): # 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -523,8 +522,9 @@ class UserRef(ObjectRef):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.username)
|
return query.order_by(self.model_class.username)
|
||||||
|
|
||||||
def get_object_url(self, user): # pylint: disable=empty-docstring
|
def get_object_url(self, obj): # 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 = set([role.uuid for role in avoid])
|
avoid = {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,7 +103,8 @@ class ObjectRefWidget(SelectWidget):
|
||||||
|
|
||||||
readonly_template = "readonly/objectref"
|
readonly_template = "readonly/objectref"
|
||||||
|
|
||||||
def __init__(self, request, url=None, *args, **kwargs):
|
def __init__(self, request, *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
|
||||||
|
@ -299,7 +300,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class FileDownloadWidget(Widget):
|
class FileDownloadWidget(Widget): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
||||||
fields.
|
fields.
|
||||||
|
@ -357,7 +358,7 @@ class FileDownloadWidget(Widget):
|
||||||
return humanize.naturalsize(size)
|
return humanize.naturalsize(size)
|
||||||
|
|
||||||
|
|
||||||
class GridWidget(Widget):
|
class GridWidget(Widget): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Widget for fields whose data is represented by a :term:`grid`.
|
Widget for fields whose data is represented by a :term:`grid`.
|
||||||
|
|
||||||
|
@ -406,6 +407,7 @@ 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
|
||||||
""" """
|
""" """
|
||||||
|
@ -462,6 +464,7 @@ 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
|
||||||
""" """
|
""" """
|
||||||
|
@ -512,7 +515,7 @@ class EmailRecipientsWidget(TextAreaWidget):
|
||||||
return ", ".join(values)
|
return ", ".join(values)
|
||||||
|
|
||||||
|
|
||||||
class BatchIdWidget(Widget):
|
class BatchIdWidget(Widget): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
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,6 +23,7 @@
|
||||||
"""
|
"""
|
||||||
Base grid classes
|
Base grid classes
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
@ -38,7 +39,12 @@ 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 FieldList, get_model_fields, make_json_safe
|
from wuttaweb.util import (
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +59,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Grid: # pylint: disable=too-many-instance-attributes
|
class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||||
"""
|
"""
|
||||||
Base class for all :term:`grids <grid>`.
|
Base class for all :term:`grids <grid>`.
|
||||||
|
|
||||||
|
@ -263,7 +269,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
``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 it will not exist.
|
called its value will be ``None``.
|
||||||
|
|
||||||
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::
|
||||||
|
@ -371,7 +377,11 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
See also :meth:`add_tool()` and :meth:`set_tools()`.
|
See also :meth:`add_tool()` and :meth:`set_tools()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
active_sorters = None
|
||||||
|
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",
|
||||||
|
@ -688,7 +698,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
"percent": self.render_percent,
|
"percent": self.render_percent,
|
||||||
}
|
}
|
||||||
|
|
||||||
if renderer in builtins:
|
if renderer in builtins: # pylint: disable=consider-using-get
|
||||||
renderer = builtins[renderer]
|
renderer = builtins[renderer]
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
@ -1086,8 +1096,8 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
# 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
|
sorter._class = model_class # pylint: disable=protected-access
|
||||||
sorter._column = model_property
|
sorter._column = model_property # pylint: disable=protected-access
|
||||||
|
|
||||||
return sorter
|
return sorter
|
||||||
|
|
||||||
|
@ -1372,7 +1382,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
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)
|
||||||
|
@ -1473,7 +1483,9 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
# configuration methods
|
# configuration methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def load_settings(self, persist=True): # pylint: disable=too-many-branches
|
def load_settings( # pylint: disable=too-many-branches,too-many-statements
|
||||||
|
self, persist=True
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Load all effective settings for the grid.
|
Load all effective settings for the grid.
|
||||||
|
|
||||||
|
@ -1626,7 +1638,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_setting( # pylint: disable=empty-docstring
|
def get_setting( # pylint: disable=empty-docstring,too-many-arguments,too-many-positional-arguments
|
||||||
self, settings, key, src="session", default=None, normalize=lambda v: v
|
self, settings, key, src="session", default=None, normalize=lambda v: v
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
|
@ -2205,12 +2217,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
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`.
|
||||||
"""
|
"""
|
||||||
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
|
return render_vue_finalize(self.vue_tagname, self.vue_component)
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -2290,12 +2297,11 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
:returns: The first sorter in format ``[sortkey, sortdir]``,
|
:returns: The first sorter in format ``[sortkey, sortdir]``,
|
||||||
or ``None``.
|
or ``None``.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "active_sorters"):
|
|
||||||
if self.active_sorters:
|
if self.active_sorters:
|
||||||
sorter = self.active_sorters[0]
|
sorter = self.active_sorters[0]
|
||||||
return [sorter["key"], sorter["dir"]]
|
return [sorter["key"], sorter["dir"]]
|
||||||
|
|
||||||
elif self.sort_defaults:
|
if self.sort_defaults:
|
||||||
sorter = self.sort_defaults[0]
|
sorter = self.sort_defaults[0]
|
||||||
return [sorter.sortkey, sorter.sortdir]
|
return [sorter.sortkey, sorter.sortdir]
|
||||||
|
|
||||||
|
@ -2386,9 +2392,9 @@ class Grid: # pylint: disable=too-many-instance-attributes
|
||||||
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 in self.renderers:
|
for key, renderer in self.renderers.items():
|
||||||
value = record.get(key, None)
|
value = record.get(key, None)
|
||||||
record[key] = self.renderers[key](original_record, key, value)
|
record[key] = renderer(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:
|
||||||
|
@ -2537,7 +2543,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__(
|
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -59,9 +59,10 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
:param request: Current :term:`request` object.
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
:param model_property: Property of a model class, representing the
|
:param nullable: Boolean indicating whether the filter should
|
||||||
column by which to filter. For instance,
|
include ``is_null`` and ``is_not_null`` verbs. If not
|
||||||
``model.Person.full_name``.
|
specified, the column will be inspected (if possible) and use
|
||||||
|
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.
|
||||||
|
@ -169,13 +170,14 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
|
||||||
"is_not_null",
|
"is_not_null",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||||
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,
|
||||||
|
@ -196,10 +198,14 @@ 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
|
||||||
|
@ -248,7 +254,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 = dict([(verb, verb) for verb in self.get_verbs()])
|
labels = {verb: verb for verb in self.get_verbs()}
|
||||||
labels.update(self.default_verb_labels)
|
labels.update(self.default_verb_labels)
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
|
@ -378,7 +384,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)
|
return func(data, value) # pylint: disable=not-callable
|
||||||
|
|
||||||
def filter_is_any(self, data, value): # pylint: disable=unused-argument
|
def filter_is_any(self, data, value): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
|
@ -398,18 +404,12 @@ 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):
|
||||||
nullable = kwargs.pop("nullable", None)
|
self.model_property = kwargs.pop("model_property")
|
||||||
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,
|
self.model_property == None, # pylint: disable=singleton-comparison
|
||||||
self.model_property != value,
|
self.model_property != value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -491,14 +491,18 @@ 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(self.model_property == None)
|
return query.filter(
|
||||||
|
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(self.model_property != None)
|
return query.filter(
|
||||||
|
self.model_property != None # pylint: disable=singleton-comparison
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StringAlchemyFilter(AlchemyFilter):
|
class StringAlchemyFilter(AlchemyFilter):
|
||||||
|
@ -550,7 +554,12 @@ 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(sa.or_(self.model_property == None, sa.and_(*criteria)))
|
return query.filter(
|
||||||
|
sa.or_(
|
||||||
|
self.model_property == None, # pylint: disable=singleton-comparison
|
||||||
|
sa.and_(*criteria),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NumericAlchemyFilter(AlchemyFilter):
|
class NumericAlchemyFilter(AlchemyFilter):
|
||||||
|
@ -628,14 +637,18 @@ 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(self.model_property == True)
|
return query.filter(
|
||||||
|
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(self.model_property == False)
|
return query.filter(
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
|
@ -643,7 +656,10 @@ class BooleanAlchemyFilter(AlchemyFilter):
|
||||||
ignored.
|
ignored.
|
||||||
"""
|
"""
|
||||||
return query.filter(
|
return query.filter(
|
||||||
sa.or_(self.model_property == False, self.model_property == None)
|
sa.or_(
|
||||||
|
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:
|
for topitem in raw_menus: # pylint: disable=too-many-nested-blocks
|
||||||
|
|
||||||
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:
|
for topitem in menus: # pylint: disable=too-many-nested-blocks
|
||||||
|
|
||||||
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__(
|
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,super-init-not-called
|
||||||
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 Lance Edgar
|
# Copyright © 2024-2025 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):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -39,10 +39,14 @@ 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):
|
def setUp(self): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
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(
|
||||||
|
@ -87,8 +91,14 @@ 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,6 +618,71 @@ 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
|
||||||
##############################
|
##############################
|
||||||
|
@ -757,14 +822,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("/menu.mako")
|
renderer = get_renderer("/page.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()
|
lookup._collection.clear() # pylint: disable=protected-access
|
||||||
|
|
||||||
# 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 Lance Edgar
|
# Copyright © 2024-2025 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):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
config.include("wuttaweb.views.essential")
|
config.include("wuttaweb.views.essential")
|
||||||
|
|
|
@ -95,7 +95,8 @@ class AuthView(View):
|
||||||
# 'referrer': referrer,
|
# 'referrer': referrer,
|
||||||
}
|
}
|
||||||
|
|
||||||
def login_make_schema(self):
|
def login_make_schema(self): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
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
|
||||||
|
@ -220,13 +221,19 @@ class AuthView(View):
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def change_password_validate_current_password(self, node, value):
|
def change_password_validate_current_password( # pylint: disable=empty-docstring
|
||||||
|
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(self, node, value):
|
def change_password_validate_new_password( # pylint: disable=empty-docstring
|
||||||
|
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):
|
||||||
|
@ -277,7 +284,8 @@ class AuthView(View):
|
||||||
return self.redirect(url)
|
return self.redirect(url)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
cls._auth_defaults(config)
|
cls._auth_defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -311,12 +319,14 @@ 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):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
AuthView = kwargs.get("AuthView", base["AuthView"]) # pylint: disable=invalid-name
|
AuthView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
|
"AuthView", base["AuthView"]
|
||||||
|
)
|
||||||
AuthView.defaults(config)
|
AuthView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -51,6 +51,8 @@ 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",
|
||||||
|
@ -121,8 +123,9 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
return super().render_to_response(template, context)
|
return super().render_to_response(template, context)
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -153,15 +156,17 @@ class BatchMasterView(MasterView):
|
||||||
return f"{batch_id:08d}"
|
return f"{batch_id:08d}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_instance_title(self, batch): # pylint: disable=empty-docstring
|
def get_instance_title(self, instance): # 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, f): # pylint: disable=too-many-branches,empty-docstring
|
def configure_form(self, form): # pylint: disable=too-many-branches,empty-docstring
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(form)
|
||||||
|
f = form
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
|
|
||||||
# id
|
# id
|
||||||
|
@ -235,13 +240,11 @@ 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 = dict(
|
kw = {
|
||||||
[
|
key: getattr(batch, key)
|
||||||
(key, getattr(batch, key))
|
|
||||||
for key in form.validated
|
for key in form.validated
|
||||||
if hasattr(batch, key)
|
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
|
||||||
|
@ -255,7 +258,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, batch):
|
def redirect_after_create(self, obj):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
@ -263,6 +266,8 @@ 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))
|
||||||
|
@ -283,7 +288,7 @@ class BatchMasterView(MasterView):
|
||||||
thread.start()
|
thread.start()
|
||||||
return self.render_progress(progress)
|
return self.render_progress(progress)
|
||||||
|
|
||||||
def delete_instance(self, batch):
|
def delete_instance(self, obj):
|
||||||
"""
|
"""
|
||||||
Delete the given batch instance.
|
Delete the given batch instance.
|
||||||
|
|
||||||
|
@ -291,6 +296,7 @@ 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)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -326,29 +332,22 @@ 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)
|
||||||
|
|
||||||
try:
|
def onerror():
|
||||||
# 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)
|
|
||||||
|
|
||||||
else:
|
self.do_thread_body(
|
||||||
session.commit()
|
self.batch_handler.do_populate,
|
||||||
if progress:
|
(batch,),
|
||||||
progress.handle_success()
|
{"progress": progress},
|
||||||
|
onerror,
|
||||||
finally:
|
session=session,
|
||||||
session.close()
|
progress=progress,
|
||||||
|
)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# execute methods
|
# execute methods
|
||||||
|
@ -388,20 +387,21 @@ 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, batch):
|
def get_row_grid_data(self, obj):
|
||||||
"""
|
"""
|
||||||
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 = self.Session.query(row_model_class).filter(
|
query = session.query(row_model_class).filter(row_model_class.batch == batch)
|
||||||
row_model_class.batch == batch
|
|
||||||
)
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_row_grid(self, g): # pylint: disable=empty-docstring
|
def configure_row_grid(self, grid): # 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,36 +413,3 @@ 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):
|
def setup(self, session=None): # pylint: disable=too-many-locals
|
||||||
"""
|
"""
|
||||||
View for first-time app setup, to create admin user.
|
View for first-time app setup, to create admin user.
|
||||||
|
|
||||||
|
@ -294,7 +294,8 @@ class CommonView(View):
|
||||||
return self.redirect(referrer)
|
return self.redirect(referrer)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -342,14 +343,14 @@ class CommonView(View):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
CommonView = kwargs.get( # pylint: disable=invalid-name
|
CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"CommonView", base["CommonView"]
|
"CommonView", base["CommonView"]
|
||||||
)
|
)
|
||||||
CommonView.defaults(config)
|
CommonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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):
|
class EmailSettingView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for :term:`email settings <email setting>`.
|
Master view for :term:`email settings <email setting>`.
|
||||||
"""
|
"""
|
||||||
|
@ -107,8 +107,9 @@ class EmailSettingView(MasterView):
|
||||||
"enabled": self.email_handler.is_enabled(key),
|
"enabled": self.email_handler.is_enabled(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# key
|
# key
|
||||||
|
@ -136,7 +137,9 @@ class EmailSettingView(MasterView):
|
||||||
recips = ", ".join(recips[:2])
|
recips = ", ".join(recips[:2])
|
||||||
return f"{recips}, ..."
|
return f"{recips}, ..."
|
||||||
|
|
||||||
def get_instance(self): # pylint: disable=empty-docstring
|
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
|
||||||
|
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)
|
||||||
|
@ -145,12 +148,14 @@ class EmailSettingView(MasterView):
|
||||||
|
|
||||||
raise self.notfound()
|
raise self.notfound()
|
||||||
|
|
||||||
def get_instance_title(self, setting): # pylint: disable=empty-docstring
|
def get_instance_title(self, instance): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
setting = instance
|
||||||
return setting["subject"]
|
return setting["subject"]
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# description
|
# description
|
||||||
|
@ -175,7 +180,9 @@ class EmailSettingView(MasterView):
|
||||||
# enabled
|
# enabled
|
||||||
f.set_node("enabled", colander.Boolean())
|
f.set_node("enabled", colander.Boolean())
|
||||||
|
|
||||||
def persist(self, setting): # pylint: disable=too-many-branches,empty-docstring
|
def persist( # pylint: disable=too-many-branches,empty-docstring,arguments-differ,unused-argument
|
||||||
|
self, setting, **kwargs
|
||||||
|
):
|
||||||
""" """
|
""" """
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
key = self.request.matchdict["key"]
|
key = self.request.matchdict["key"]
|
||||||
|
@ -300,14 +307,14 @@ class EmailSettingView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
EmailSettingView = kwargs.get( # pylint: disable=invalid-name
|
EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"EmailSettingView", base["EmailSettingView"]
|
"EmailSettingView", base["EmailSettingView"]
|
||||||
)
|
)
|
||||||
EmailSettingView.defaults(config)
|
EmailSettingView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024 Lance Edgar
|
# Copyright © 2024-2025 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):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
|
|
||||||
def mod(spec):
|
def mod(spec):
|
||||||
return kwargs.get(spec, spec)
|
return kwargs.get(spec, spec)
|
||||||
|
@ -57,5 +57,5 @@ def defaults(config, **kwargs):
|
||||||
config.include(mod("wuttaweb.views.upgrades"))
|
config.include(mod("wuttaweb.views.upgrades"))
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"""
|
"""
|
||||||
Base Logic for Master Views
|
Base Logic for Master Views
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -44,7 +45,7 @@ from wuttaweb.progress import SessionProgress
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View):
|
class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
Base class for "master" views.
|
Base class for "master" views.
|
||||||
|
|
||||||
|
@ -398,6 +399,8 @@ class MasterView(View):
|
||||||
# attributes
|
# attributes
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
model_class = None
|
||||||
|
|
||||||
# features
|
# features
|
||||||
listable = True
|
listable = True
|
||||||
has_grid = True
|
has_grid = True
|
||||||
|
@ -438,6 +441,7 @@ class MasterView(View):
|
||||||
viewing = False
|
viewing = False
|
||||||
editing = False
|
editing = False
|
||||||
deleting = False
|
deleting = False
|
||||||
|
executing = False
|
||||||
configuring = False
|
configuring = False
|
||||||
|
|
||||||
# default DB session
|
# default DB session
|
||||||
|
@ -524,11 +528,12 @@ class MasterView(View):
|
||||||
* :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)
|
||||||
self.Session.flush()
|
session.flush()
|
||||||
return self.redirect_after_create(obj)
|
return self.redirect_after_create(obj)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -817,33 +822,25 @@ class MasterView(View):
|
||||||
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()
|
||||||
|
|
||||||
try:
|
def onerror():
|
||||||
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),
|
||||||
model_title_plural,
|
self.get_model_title_plural(),
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
if progress:
|
|
||||||
progress.handle_error(error)
|
|
||||||
|
|
||||||
else:
|
self.do_thread_body(
|
||||||
session.commit()
|
self.delete_bulk_action,
|
||||||
if progress:
|
(records,),
|
||||||
progress.handle_success()
|
{"progress": progress},
|
||||||
|
onerror,
|
||||||
finally:
|
session=session,
|
||||||
session.close()
|
progress=progress,
|
||||||
|
)
|
||||||
|
|
||||||
def delete_bulk_action(self, data, progress=None):
|
def delete_bulk_action(self, data, progress=None):
|
||||||
"""
|
"""
|
||||||
|
@ -917,7 +914,7 @@ class MasterView(View):
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
data = self.autocomplete_data(term)
|
data = self.autocomplete_data(term) # pylint: disable=assignment-from-none
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -931,7 +928,7 @@ class MasterView(View):
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def autocomplete_data(self, term):
|
def autocomplete_data(self, term): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
@ -943,6 +940,7 @@ class MasterView(View):
|
||||||
|
|
||||||
: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):
|
||||||
"""
|
"""
|
||||||
|
@ -1006,13 +1004,13 @@ class MasterView(View):
|
||||||
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)
|
path = self.download_path(obj, filename) # pylint: disable=assignment-from-none
|
||||||
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):
|
def download_path(self, obj, filename): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
@ -1033,6 +1031,7 @@ class MasterView(View):
|
||||||
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
|
||||||
|
@ -1304,6 +1303,7 @@ class MasterView(View):
|
||||||
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):
|
||||||
:returns: The dict of route kwargs for the object.
|
:returns: The dict of route kwargs for the object.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return dict([(key, obj[key]) for key in self.get_model_key()])
|
return {key: obj[key] for key in self.get_model_key()}
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return dict([(key, getattr(obj, key)) for key in self.get_model_key()])
|
return {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,6 +2477,60 @@ class MasterView(View):
|
||||||
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
|
||||||
##############################
|
##############################
|
||||||
|
@ -2697,9 +2751,7 @@ class MasterView(View):
|
||||||
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`.
|
||||||
"""
|
"""
|
||||||
if hasattr(cls, "model_class"):
|
|
||||||
return cls.model_class
|
return cls.model_class
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_model_name(cls):
|
def get_model_name(cls):
|
||||||
|
@ -2808,11 +2860,9 @@ class MasterView(View):
|
||||||
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
|
prop.key
|
||||||
for prop in inspector.column_attrs
|
for prop in inspector.column_attrs
|
||||||
if all(col.name in keys for col in prop.columns)
|
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,9 +28,10 @@ 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):
|
class PersonView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for people.
|
Master view for people.
|
||||||
|
|
||||||
|
@ -70,8 +71,9 @@ class PersonView(MasterView):
|
||||||
"users",
|
"users",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# full_name
|
# full_name
|
||||||
|
@ -83,8 +85,9 @@ class PersonView(MasterView):
|
||||||
# last_name
|
# last_name
|
||||||
g.set_link("last_name")
|
g.set_link("last_name")
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
person = f.model_instance
|
person = f.model_instance
|
||||||
|
|
||||||
|
@ -105,12 +108,9 @@ class PersonView(MasterView):
|
||||||
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
return make_users_grid(
|
||||||
route_prefix = self.get_route_prefix()
|
self.request,
|
||||||
|
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,23 +118,6 @@ class PersonView(MasterView):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -213,14 +196,14 @@ class PersonView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
PersonView = kwargs.get( # pylint: disable=invalid-name
|
PersonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"PersonView", base["PersonView"]
|
"PersonView", base["PersonView"]
|
||||||
)
|
)
|
||||||
PersonView.defaults(config)
|
PersonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
@ -63,13 +63,15 @@ def progress(request):
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
progress = kwargs.get("progress", base["progress"])
|
progress = kwargs.get( # pylint: disable=redefined-outer-name
|
||||||
|
"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):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -37,7 +37,7 @@ from wuttaweb.views import MasterView
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ReportView(MasterView):
|
class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for :term:`reports <report>`; route prefix is
|
Master view for :term:`reports <report>`; route prefix is
|
||||||
``reports``.
|
``reports``.
|
||||||
|
@ -89,8 +89,9 @@ class ReportView(MasterView):
|
||||||
"help_text": report.__doc__,
|
"help_text": report.__doc__,
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# report_key
|
# report_key
|
||||||
|
@ -103,7 +104,9 @@ class ReportView(MasterView):
|
||||||
# help_text
|
# help_text
|
||||||
g.set_searchable("help_text")
|
g.set_searchable("help_text")
|
||||||
|
|
||||||
def get_instance(self): # pylint: disable=empty-docstring
|
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
|
||||||
|
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)
|
||||||
|
@ -112,8 +115,9 @@ class ReportView(MasterView):
|
||||||
|
|
||||||
raise self.notfound()
|
raise self.notfound()
|
||||||
|
|
||||||
def get_instance_title(self, report): # pylint: disable=empty-docstring
|
def get_instance_title(self, instance): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
report = instance
|
||||||
return report["report_title"]
|
return report["report_title"]
|
||||||
|
|
||||||
def view(self):
|
def view(self):
|
||||||
|
@ -151,8 +155,9 @@ class ReportView(MasterView):
|
||||||
|
|
||||||
return self.render_to_response("view", context)
|
return self.render_to_response("view", context)
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # 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)
|
||||||
|
@ -261,14 +266,14 @@ class ReportView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
ReportView = kwargs.get( # pylint: disable=invalid-name
|
ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"ReportView", base["ReportView"]
|
"ReportView", base["ReportView"]
|
||||||
)
|
)
|
||||||
ReportView.defaults(config)
|
ReportView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -29,9 +29,10 @@ 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):
|
class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for roles.
|
Master view for roles.
|
||||||
|
|
||||||
|
@ -58,6 +59,8 @@ class RoleView(MasterView):
|
||||||
}
|
}
|
||||||
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
|
||||||
""" """
|
""" """
|
||||||
|
@ -65,8 +68,9 @@ class RoleView(MasterView):
|
||||||
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, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
|
@ -75,8 +79,9 @@ class RoleView(MasterView):
|
||||||
# notes
|
# notes
|
||||||
g.set_renderer("notes", self.grid_render_notes)
|
g.set_renderer("notes", self.grid_render_notes)
|
||||||
|
|
||||||
def is_editable(self, role): # pylint: disable=empty-docstring
|
def is_editable(self, obj): # 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()
|
||||||
|
|
||||||
|
@ -93,8 +98,9 @@ class RoleView(MasterView):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_deletable(self, role): # pylint: disable=empty-docstring
|
def is_deletable(self, obj): # 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()
|
||||||
|
|
||||||
|
@ -108,8 +114,9 @@ class RoleView(MasterView):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
role = f.model_instance
|
role = f.model_instance
|
||||||
|
|
||||||
|
@ -145,12 +152,9 @@ class RoleView(MasterView):
|
||||||
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
|
||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
return make_users_grid(
|
||||||
route_prefix = self.get_route_prefix()
|
self.request,
|
||||||
|
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",
|
||||||
|
@ -159,24 +163,6 @@ class RoleView(MasterView):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
@ -317,7 +303,7 @@ class RoleView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionView(MasterView):
|
class PermissionView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for permissions.
|
Master view for permissions.
|
||||||
|
|
||||||
|
@ -346,7 +332,7 @@ class PermissionView(MasterView):
|
||||||
"permission",
|
"permission",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_query(self, **kwargs): # pylint: disable=empty-docstring
|
def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ
|
||||||
""" """
|
""" """
|
||||||
query = super().get_query(**kwargs)
|
query = super().get_query(**kwargs)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -356,8 +342,9 @@ class PermissionView(MasterView):
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -369,25 +356,28 @@ class PermissionView(MasterView):
|
||||||
# permission
|
# permission
|
||||||
g.set_link("permission")
|
g.set_link("permission")
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # 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):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
RoleView = kwargs.get("RoleView", base["RoleView"]) # pylint: disable=invalid-name
|
RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
|
"RoleView", base["RoleView"]
|
||||||
|
)
|
||||||
RoleView.defaults(config)
|
RoleView.defaults(config)
|
||||||
|
|
||||||
PermissionView = kwargs.get( # pylint: disable=invalid-name
|
PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"PermissionView", base["PermissionView"]
|
"PermissionView", base["PermissionView"]
|
||||||
)
|
)
|
||||||
PermissionView.defaults(config)
|
PermissionView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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):
|
class AppInfoView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
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,8 +93,9 @@ class AppInfoView(MasterView):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
g.sort_multiple = False
|
g.sort_multiple = False
|
||||||
|
@ -173,7 +174,9 @@ class AppInfoView(MasterView):
|
||||||
|
|
||||||
return simple_settings
|
return simple_settings
|
||||||
|
|
||||||
def configure_get_context(self, **kwargs): # pylint: disable=empty-docstring
|
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
|
||||||
|
self, **kwargs
|
||||||
|
):
|
||||||
""" """
|
""" """
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
|
|
||||||
|
@ -220,7 +223,7 @@ class AppInfoView(MasterView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SettingView(MasterView):
|
class SettingView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for the "raw" settings table.
|
Master view for the "raw" settings table.
|
||||||
|
|
||||||
|
@ -242,15 +245,17 @@ class SettingView(MasterView):
|
||||||
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, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # 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, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
|
@ -275,19 +280,19 @@ class SettingView(MasterView):
|
||||||
node.raise_invalid("Setting name must be unique")
|
node.raise_invalid("Setting name must be unique")
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
AppInfoView = kwargs.get( # pylint: disable=invalid-name
|
AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"AppInfoView", base["AppInfoView"]
|
"AppInfoView", base["AppInfoView"]
|
||||||
)
|
)
|
||||||
AppInfoView.defaults(config)
|
AppInfoView.defaults(config)
|
||||||
|
|
||||||
SettingView = kwargs.get( # pylint: disable=invalid-name
|
SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"SettingView", base["SettingView"]
|
"SettingView", base["SettingView"]
|
||||||
)
|
)
|
||||||
SettingView.defaults(config)
|
SettingView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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):
|
class UpgradeView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for upgrades.
|
Master view for upgrades.
|
||||||
|
|
||||||
|
@ -72,8 +72,9 @@ class UpgradeView(MasterView):
|
||||||
|
|
||||||
sort_defaults = ("created", "desc")
|
sort_defaults = ("created", "desc")
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # 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
|
||||||
|
@ -121,8 +122,9 @@ class UpgradeView(MasterView):
|
||||||
return "has-background-warning"
|
return "has-background-warning"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # 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
|
||||||
|
@ -204,11 +206,12 @@ class UpgradeView(MasterView):
|
||||||
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
|
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_instance(self, upgrade):
|
def delete_instance(self, obj):
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
@ -227,8 +230,9 @@ class UpgradeView(MasterView):
|
||||||
|
|
||||||
return upgrade
|
return upgrade
|
||||||
|
|
||||||
def download_path(self, upgrade, filename): # pylint: disable=empty-docstring
|
def download_path(self, obj, 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
|
||||||
|
@ -245,7 +249,7 @@ class UpgradeView(MasterView):
|
||||||
path = os.path.join(path, filename)
|
path = os.path.join(path, filename)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def execute_instance(self, upgrade, user, progress=None):
|
def execute_instance(self, obj, user, progress=None):
|
||||||
"""
|
"""
|
||||||
This method runs the actual upgrade.
|
This method runs the actual upgrade.
|
||||||
|
|
||||||
|
@ -258,6 +262,7 @@ class UpgradeView(MasterView):
|
||||||
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
|
||||||
|
@ -377,14 +382,14 @@ class UpgradeView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
UpgradeView = kwargs.get( # pylint: disable=invalid-name
|
UpgradeView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
"UpgradeView", base["UpgradeView"]
|
"UpgradeView", base["UpgradeView"]
|
||||||
)
|
)
|
||||||
UpgradeView.defaults(config)
|
UpgradeView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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):
|
class UserView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view for users.
|
Master view for users.
|
||||||
|
|
||||||
|
@ -82,8 +82,9 @@ class UserView(MasterView):
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_grid(self, g): # pylint: disable=empty-docstring
|
def configure_grid(self, grid): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -106,8 +107,9 @@ class UserView(MasterView):
|
||||||
return "has-background-warning"
|
return "has-background-warning"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_editable(self, user): # pylint: disable=empty-docstring
|
def is_editable(self, obj): # 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:
|
||||||
|
@ -115,8 +117,9 @@ class UserView(MasterView):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def configure_form(self, f): # pylint: disable=empty-docstring
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
user = f.model_instance
|
user = f.model_instance
|
||||||
|
|
||||||
|
@ -233,7 +236,7 @@ class UserView(MasterView):
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
||||||
old_roles = set([role.uuid for role in user.roles])
|
old_roles = {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)
|
||||||
|
@ -417,12 +420,14 @@ class UserView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
UserView = kwargs.get("UserView", base["UserView"]) # pylint: disable=invalid-name
|
UserView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
|
||||||
|
"UserView", base["UserView"]
|
||||||
|
)
|
||||||
UserView.defaults(config)
|
UserView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config): # pylint: disable=missing-function-docstring
|
||||||
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.assertFalse(hasattr(form, "deform_form"))
|
self.assertIsNone(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.assertFalse(hasattr(form, "validated"))
|
self.assertIsNone(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.assertFalse(hasattr(grid, "active_sorters"))
|
self.assertIsNone(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.assertFalse(hasattr(grid, "active_sorters"))
|
self.assertIsNone(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.assertFalse(hasattr(grid, "active_sorters"))
|
self.assertIsNone(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.assertFalse(hasattr(grid, "active_sorters"))
|
self.assertIsNone(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.assertFalse(hasattr(filtr, "verb"))
|
self.assertIsNone(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,6 +16,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -665,6 +666,52 @@ 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.assertFalse(hasattr(mod.MasterView, "model_class"))
|
self.assertIsNone(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,6 +1371,25 @@ 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
|
||||||
|
@ -1656,6 +1675,11 @@ 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