3
0
Fork 0

Compare commits

...

32 commits

Author SHA1 Message Date
0a08918657 docs: add badge for pylint 2025-09-01 13:32:40 -05:00
a1868e1f44 fix: fix 'duplicate-code' for pylint 2025-09-01 13:31:33 -05:00
ad74bede04 fix: fix 'no-member' for pylint 2025-09-01 12:10:12 -05:00
dd25d98e7d fix: fix 'attribute-defined-outside-init' for pylint 2025-09-01 11:53:50 -05:00
92754a64c4 fix: fix 'arguments-renamed' for pylint 2025-09-01 11:33:43 -05:00
459c16ba4f fix: fix 'arguments-differ' for pylint 2025-09-01 11:21:44 -05:00
e123b12cd9 fix: fix 'keyword-arg-before-vararg' for pylint 2025-09-01 11:06:47 -05:00
deaf1976f3 fix: fix 'too-many-nested-blocks' for pylint 2025-09-01 11:04:31 -05:00
1dd184622f fix: fix 'too-many-locals' for pylint 2025-09-01 11:03:10 -05:00
95aeb87899 fix: fix 'consider-using-generator' for pylint 2025-09-01 11:01:26 -05:00
07e90229ce fix: fix 'missing-function-docstring' and 'missing-module-docstring' for pylint 2025-09-01 10:59:58 -05:00
2624f9dce8 fix: fix 'super-init-not-called' for pylint 2025-09-01 10:48:29 -05:00
2bcdeb42cd fix: fix 'singleton-comparison' for pylint 2025-09-01 10:46:35 -05:00
48494ee5e4 fix: fix 'simplifiable-if-expression' for pylint 2025-09-01 10:39:30 -05:00
6c66c8d57b fix: fix 'redefined-outer-name' for pylint 2025-09-01 10:37:56 -05:00
e38f7ba293 fix: fix 'protected-access' for pylint 2025-09-01 10:34:08 -05:00
ab35847f23 fix: fix 'not-callable' for pylint 2025-09-01 10:26:33 -05:00
ebd44c55f5 fix: fix 'no-else-raise' for pylint 2025-09-01 10:25:21 -05:00
4b4d81c4f3 fix: fix 'isinstance-second-argument-not-valid-type' for pylint 2025-09-01 10:24:29 -05:00
ec982fe168 fix: fix 'consider-using-set-comprehension' for pylint 2025-09-01 10:22:29 -05:00
564ac318bc fix: fix 'consider-using-get' for pylint 2025-09-01 10:21:32 -05:00
65849ad82d fix: fix 'consider-using-dict-items' for pylint 2025-09-01 10:19:54 -05:00
d6f7c19f71 fix: fix 'consider-using-dict-comprehension' for pylint 2025-09-01 10:19:42 -05:00
11ec57387e fix: fix 'assignment-from-no-return' for pylint 2025-09-01 10:19:40 -05:00
5a6ed6135a fix: fix 'abstract-method' for pylint 2025-09-01 10:09:08 -05:00
d01c343a7c fix: fix 'too-few-public-methods' for pylint 2025-09-01 10:00:54 -05:00
d7eacf0f52 fix: fix 'too-many-lines' for pylint 2025-09-01 10:00:06 -05:00
0dc6b615e7 fix: fix 'too-many-arguments' for pylint 2025-09-01 09:58:56 -05:00
282fc61e50 fix: fix 'too-many-public-methods' for pylint 2025-09-01 09:54:48 -05:00
1fec99bc92 fix: fix 'too-many-statements' for pylint 2025-09-01 09:53:33 -05:00
d39f5afd4b fix: fix 'unidiomatic-typecheck' for pylint 2025-09-01 09:52:11 -05:00
5c23120795 fix: fix 'unnecessary-comprehension' for pylint 2025-09-01 09:51:20 -05:00
38 changed files with 584 additions and 397 deletions

View file

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

View file

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

View file

@ -1,4 +1,7 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
"""
Package Version
"""
from importlib.metadata import version from importlib.metadata import version

View file

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

View file

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

View file

@ -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.
""" """

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"}

View file

@ -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"}])

View file

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

View file

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

View file

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

View file

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