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]
disable=fixme,
abstract-method,
arguments-differ,
arguments-renamed,
assignment-from-no-return,
attribute-defined-outside-init,
consider-using-dict-comprehension,
consider-using-dict-items,
consider-using-generator,
consider-using-get,
consider-using-set-comprehension,
duplicate-code,
isinstance-second-argument-not-valid-type,
keyword-arg-before-vararg,
missing-function-docstring,
missing-module-docstring,
no-else-raise,
no-member,
not-callable,
protected-access,
redefined-outer-name,
simplifiable-if-expression,
singleton-comparison,
super-init-not-called,
too-few-public-methods,
too-many-arguments,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-positional-arguments,
too-many-public-methods,
too-many-statements,
ungrouped-imports,
unidiomatic-typecheck,
unnecessary-comprehension,
[SIMILARITIES]
# nb. cuts out some noise for duplicate-code
min-similarity-lines=5

View file

@ -11,6 +11,9 @@ project.
.. _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
:target: https://github.com/psf/black

View file

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

View file

@ -65,13 +65,13 @@ class WebAppProvider(AppProvider):
: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(
f"{self.appname}.web.handler_spec",
default="wuttaweb.handler:WebHandler",
)
self.web_handler = self.app.load_object(spec)(self.config)
return self.web_handler
self.app.handlers["web"] = self.app.load_object(spec)(self.config)
return self.app.handlers["web"]
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
if isinstance(main_app, str):
make_wsgi_app = app.load_object(main_app)
factory = app.load_object(main_app)
elif callable(main_app):
make_wsgi_app = main_app
factory = main_app
else:
raise ValueError("main_app must be spec or callable")
# construct a pyramid app "per usual"
return make_wsgi_app({}, **settings)
return factory({}, **settings)
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.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"]
app = config.get_app()
model = app.model
@ -122,22 +123,29 @@ class WuttaSecurityPolicy:
return user
def identity(self, request):
def identity(self, request): # pylint: disable=empty-docstring
""" """
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)
if user is not None:
return user.uuid
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)
def forget(self, request, **kw):
def forget(self, request, **kw): # pylint: disable=empty-docstring
""" """
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
if getattr(request, "is_root", False):

View file

@ -27,7 +27,7 @@
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.
"""

View file

@ -23,6 +23,7 @@
"""
Base form classes
"""
# pylint: disable=too-many-lines
import logging
from collections import OrderedDict
@ -36,13 +37,19 @@ from colanderalchemy import SQLAlchemySchemaNode
from pyramid.renderers import render
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__)
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.
@ -263,11 +270,12 @@ class Form: # pylint: disable=too-many-instance-attributes
If the :meth:`validate()` method was called, and it succeeded,
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,
request,
fields=None,
@ -330,7 +338,7 @@ class Form: # pylint: disable=too-many-instance-attributes
self.model_class = model_class
self.model_instance = model_instance
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.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,
generating it automatically if necessary.
"""
if not hasattr(self, "deform_form"):
if not self.deform_form:
schema = self.get_schema()
kwargs = {}
@ -982,7 +990,7 @@ class Form: # pylint: disable=too-many-instance-attributes
output = render(template, context)
return HTML.literal(output)
def render_vue_field( # pylint: disable=unused-argument
def render_vue_field( # pylint: disable=unused-argument,too-many-locals
self,
fieldname,
readonly=None,
@ -1093,12 +1101,7 @@ class Form: # pylint: disable=too-many-instance-attributes
The actual output may depend on various form attributes, in
particular :attr:`vue_tagname`.
"""
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
return render_vue_finalize(self.vue_tagname, self.vue_component)
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
# note this does not yet allow for null values.. :(
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)
@ -1173,9 +1176,9 @@ class Form: # pylint: disable=too-many-instance-attributes
:attr:`validated` attribute.
However if the data is not valid, ``False`` is returned, and
there will be no :attr:`validated` attribute. In that case
you should inspect the form errors to learn/display what went
wrong for the user's sake. See also
the :attr:`validated` attribute will be ``None``. In that
case you should inspect the form errors to learn/display what
went wrong for the user's sake. See also
:meth:`get_field_errors()`.
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``.
"""
if hasattr(self, "validated"):
del self.validated
self.validated = None
if self.request.method != "POST":
return False

View file

@ -69,7 +69,7 @@ class WuttaDateTime(colander.DateTime):
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
ColanderAlchemy. This is a direct subclass of
@ -183,7 +183,7 @@ class WuttaDictEnum(colander.String):
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
""" """
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)
@ -288,13 +288,8 @@ class ObjectRef(colander.SchemaType):
default_empty_option = ("", "(none)")
def __init__(
self,
request,
empty_option=None,
*args,
**kwargs,
):
def __init__(self, request, *args, **kwargs):
empty_option = kwargs.pop("empty_option", None)
# nb. allow session injection for tests
self.session = kwargs.pop("session", Session())
super().__init__(*args, **kwargs)
@ -381,7 +376,9 @@ class ObjectRef(colander.SchemaType):
if not value:
return None
if isinstance(value, self.model_class):
if isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
value, self.model_class
):
return value
# fetch object from DB
@ -475,8 +472,9 @@ class PersonRef(ObjectRef):
""" """
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)
@ -499,8 +497,9 @@ class RoleRef(ObjectRef):
""" """
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)
@ -523,8 +522,9 @@ class UserRef(ObjectRef):
""" """
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)
@ -557,7 +557,7 @@ class RoleRefs(WuttaSet):
auth.get_role_authenticated(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
if not self.request.is_root:

View file

@ -103,7 +103,8 @@ class ObjectRefWidget(SelectWidget):
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)
self.request = request
self.url = url
@ -299,7 +300,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
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`
fields.
@ -357,7 +358,7 @@ class FileDownloadWidget(Widget):
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`.
@ -406,6 +407,7 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
"""
readonly_template = "readonly/rolerefs"
session = None
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
""" """
@ -462,6 +464,7 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
template = "permissions"
readonly_template = "readonly/permissions"
permissions = None
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
""" """
@ -512,7 +515,7 @@ class EmailRecipientsWidget(TextAreaWidget):
return ", ".join(values)
class BatchIdWidget(Widget):
class BatchIdWidget(Widget): # pylint: disable=abstract-method
"""
Widget for use with the
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`

View file

@ -23,6 +23,7 @@
"""
Base grid classes
"""
# pylint: disable=too-many-lines
import functools
import logging
@ -38,7 +39,12 @@ from pyramid.renderers import render
from webhelpers2.html import HTML
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
@ -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>`.
@ -263,7 +269,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
``active_sorters`` defines the "current/effective" sorters.
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
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()`.
"""
def __init__(
active_sorters = None
joined = None
pager = None
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
self,
request,
vue_tagname="wutta-grid",
@ -688,7 +698,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
"percent": self.render_percent,
}
if renderer in builtins:
if renderer in builtins: # pylint: disable=consider-using-get
renderer = builtins[renderer]
if kwargs:
@ -1086,8 +1096,8 @@ class Grid: # pylint: disable=too-many-instance-attributes
# TODO: this should be improved; is needed in tailbone for
# multi-column sorting with sqlalchemy queries
if model_property:
sorter._class = model_class
sorter._column = model_property
sorter._class = model_class # pylint: disable=protected-access
sorter._column = model_property # pylint: disable=protected-access
return sorter
@ -1372,7 +1382,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
if filterinfo and callable(filterinfo):
# filtr = filterinfo
raise NotImplementedError
else:
kwargs["key"] = key
kwargs.setdefault("label", self.get_label(key))
filtr = self.make_filter(filterinfo or key, **kwargs)
@ -1473,7 +1483,9 @@ class Grid: # pylint: disable=too-many-instance-attributes
# 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.
@ -1626,7 +1638,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
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
):
""" """
@ -2205,12 +2217,7 @@ class Grid: # pylint: disable=too-many-instance-attributes
The actual output may depend on various grid attributes, in
particular :attr:`vue_tagname`.
"""
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
return render_vue_finalize(self.vue_tagname, self.vue_component)
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]``,
or ``None``.
"""
if hasattr(self, "active_sorters"):
if self.active_sorters:
sorter = self.active_sorters[0]
return [sorter["key"], sorter["dir"]]
elif self.sort_defaults:
if self.sort_defaults:
sorter = self.sort_defaults[0]
return [sorter.sortkey, sorter.sortdir]
@ -2386,9 +2392,9 @@ class Grid: # pylint: disable=too-many-instance-attributes
record = make_json_safe(record, warn=False)
# customize value rendering where applicable
for key in self.renderers:
for key, renderer in self.renderers.items():
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
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.
"""
def __init__(
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
self,
request,
key,

View file

@ -59,9 +59,10 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
:param request: Current :term:`request` object.
:param model_property: Property of a model class, representing the
column by which to filter. For instance,
``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 (if possible) and use
its nullable flag.
:param \\**kwargs: Any additional kwargs will be set as attributes
on the filter instance.
@ -169,13 +170,14 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
"is_not_null",
]
def __init__(
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
self,
request,
key,
label=None,
verbs=None,
choices=None,
nullable=None,
default_active=False,
default_verb=None,
default_value=None,
@ -196,10 +198,14 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
self.verbs = verbs
if default_verb:
self.default_verb = default_verb
self.verb = None # active verb is set later
# choices
self.set_choices(choices or {})
# nullable
self.nullable = nullable
# value
self.default_value = 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.
"""
# 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)
return labels
@ -378,7 +384,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
raise VerbNotSupported(verb)
# 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
"""
@ -398,18 +404,12 @@ class AlchemyFilter(GridFilter):
:param model_property: Property of a model class, representing the
column by which to filter. For instance,
``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):
nullable = kwargs.pop("nullable", None)
self.model_property = kwargs.pop("model_property")
super().__init__(*args, **kwargs)
self.nullable = nullable
if self.nullable is None:
columns = self.model_property.prop.columns
if len(columns) == 1:
@ -446,7 +446,7 @@ class AlchemyFilter(GridFilter):
# probably does not expect that, so explicitly include them.
return query.filter(
sa.or_(
self.model_property == None,
self.model_property == None, # pylint: disable=singleton-comparison
self.model_property != value,
)
)
@ -491,14 +491,18 @@ class AlchemyFilter(GridFilter):
"""
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
"""
Filter data with an ``IS NOT NULL`` query. The value is
ignored.
"""
return query.filter(self.model_property != None)
return query.filter(
self.model_property != None # pylint: disable=singleton-comparison
)
class StringAlchemyFilter(AlchemyFilter):
@ -550,7 +554,12 @@ class StringAlchemyFilter(AlchemyFilter):
# sql probably excludes null values from results, but user
# 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):
@ -628,14 +637,18 @@ class BooleanAlchemyFilter(AlchemyFilter):
Filter data with an "is true" condition. The value is
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
"""
Filter data with an "is false" condition. The value is
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
"""
@ -643,7 +656,10 @@ class BooleanAlchemyFilter(AlchemyFilter):
ignored.
"""
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
self._mark_allowed(request, raw_menus)
final_menus = []
for topitem in raw_menus:
for topitem in raw_menus: # pylint: disable=too-many-nested-blocks
if topitem["allowed"]:
@ -323,7 +323,7 @@ class MenuHandler(GenericHandler):
Traverse the menu set, and mark each item as "allowed" (or
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":
topitem["allowed"] = True

View file

@ -91,7 +91,7 @@ class SessionProgress(ProgressBase): # pylint: disable=too-many-instance-attrib
: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 = request

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -70,5 +70,5 @@ testing = Resource(img, "testing.png", renderer=True)
# TODO: should consider deprecating this?
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
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
declare it for use within the app (page).
"""
if not hasattr(request, "_wuttaweb_registered_components"):
request._wuttaweb_registered_components = OrderedDict()
if not hasattr(request, "wuttaweb_registered_components"):
request.wuttaweb_registered_components = OrderedDict()
if tagname in request._wuttaweb_registered_components:
if tagname in request.wuttaweb_registered_components:
log.warning(
"component with tagname '%s' already registered "
"with class '%s' but we are replacing that "
"with class '%s'",
tagname,
request._wuttaweb_registered_components[tagname],
request.wuttaweb_registered_components[tagname],
classname,
)
request._wuttaweb_registered_components[tagname] = classname
request.wuttaweb_registered_components[tagname] = classname
request.register_component = register_component
@ -411,7 +411,7 @@ def before_render(event):
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_set_user, "pyramid.events.NewRequest")
config.add_subscriber(before_render, "pyramid.events.BeforeRender")

View file

@ -58,8 +58,8 @@
const app = createApp()
app.component('vue-fontawesome', FontAwesomeIcon)
% if hasattr(request, '_wuttaweb_registered_components'):
% for tagname, classname in request._wuttaweb_registered_components.items():
% if hasattr(request, 'wuttaweb_registered_components'):
% for tagname, classname in request.wuttaweb_registered_components.items():
app.component('${tagname}', ${classname})
% endfor
% endif

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
# Copyright © 2024-2025 Lance Edgar
#
# 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.
"""
def setUp(self):
def setUp(self): # pylint: disable=empty-docstring
""" """
self.setup_web()
def setup_web(self):
"""
Perform setup for the testing web app.
"""
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(
@ -87,8 +91,14 @@ class WebTestCase(DataTestCase):
self.teardown_web()
def teardown_web(self):
"""
Perform teardown for the testing web app.
"""
testing.tearDown()
self.teardown_db()
def make_request(self):
"""
Make and return a new dummy request object.
"""
return testing.DummyRequest()

View file

@ -618,6 +618,71 @@ def make_json_safe(value, key=None, warn=True):
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
##############################
@ -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
# 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
# overwrite first entry in lookup's directory list
lookup.directories[0] = theme_path
# 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
with app.short_session(session=session) as s:

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -34,5 +34,5 @@ from .base import View
from .master import MasterView
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
config.include("wuttaweb.views.essential")

View file

@ -95,7 +95,8 @@ class AuthView(View):
# 'referrer': referrer,
}
def login_make_schema(self):
def login_make_schema(self): # pylint: disable=empty-docstring
""" """
schema = colander.Schema()
# nb. we must explicitly declare the widgets in order to also
@ -220,13 +221,19 @@ class AuthView(View):
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()
user = self.request.user
if not auth.check_user_password(user, value):
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()
user = self.request.user
if auth.check_user_password(user, value):
@ -277,7 +284,8 @@ class AuthView(View):
return self.redirect(url)
@classmethod
def defaults(cls, config):
def defaults(cls, config): # pylint: disable=empty-docstring
""" """
cls._auth_defaults(config)
@classmethod
@ -311,12 +319,14 @@ class AuthView(View):
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()
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)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -51,6 +51,8 @@ class BatchMasterView(MasterView):
from :meth:`get_batch_handler()`.
"""
executable = True
labels = {
"id": "Batch ID",
"status_code": "Status",
@ -121,8 +123,9 @@ class BatchMasterView(MasterView):
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)
model = self.app.model
@ -153,15 +156,17 @@ class BatchMasterView(MasterView):
return f"{batch_id:08d}"
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:
return f"{batch.id_str} {batch.description}"
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
# id
@ -235,13 +240,11 @@ class BatchMasterView(MasterView):
batch = schema.objectify(form.validated, context=form.model_instance)
# then we collect attributes from the new batch
kw = dict(
[
(key, getattr(batch, key))
kw = {
key: getattr(batch, key)
for key in form.validated
if hasattr(batch, key)
]
)
}
# and set attribute for user creating the batch
kw["created_by"] = self.request.user
@ -255,7 +258,7 @@ class BatchMasterView(MasterView):
# when not creating, normal logic is fine
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
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
"view" page for the new batch.
"""
batch = obj
# just view batch if should not populate
if not self.batch_handler.should_populate(batch):
return self.redirect(self.get_action_url("view", batch))
@ -283,7 +288,7 @@ class BatchMasterView(MasterView):
thread.start()
return self.render_progress(progress)
def delete_instance(self, batch):
def delete_instance(self, obj):
"""
Delete the given batch instance.
@ -291,6 +296,7 @@ class BatchMasterView(MasterView):
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()`
on the :attr:`batch_handler`.
"""
batch = obj
self.batch_handler.do_delete(batch, self.request.user)
##############################
@ -326,29 +332,22 @@ class BatchMasterView(MasterView):
raise RuntimeError("can't find the batch")
time.sleep(0.1)
try:
# populate the batch
self.batch_handler.do_populate(batch, progress=progress)
session.flush()
except Exception as error: # pylint: disable=broad-exception-caught
session.rollback()
def onerror():
log.warning(
"failed to populate %s: %s",
self.get_model_title(),
batch,
exc_info=True,
)
if progress:
progress.handle_error(error)
else:
session.commit()
if progress:
progress.handle_success()
finally:
session.close()
self.do_thread_body(
self.batch_handler.do_populate,
(batch,),
{"progress": progress},
onerror,
session=session,
progress=progress,
)
##############################
# execute methods
@ -388,20 +387,21 @@ class BatchMasterView(MasterView):
model_class = cls.get_model_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
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
data.
"""
session = self.Session()
batch = obj
row_model_class = self.get_row_model_class()
query = self.Session.query(row_model_class).filter(
row_model_class.batch == batch
)
query = session.query(row_model_class).filter(row_model_class.batch == batch)
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)
g.set_label("sequence", "Seq.", column_only=True)
@ -413,36 +413,3 @@ class BatchMasterView(MasterView):
):
""" """
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)
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.
@ -294,7 +294,8 @@ class CommonView(View):
return self.redirect(referrer)
@classmethod
def defaults(cls, config):
def defaults(cls, config): # pylint: disable=empty-docstring
""" """
cls._defaults(config)
@classmethod
@ -342,14 +343,14 @@ class CommonView(View):
)
def defaults(config, **kwargs):
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
base = globals()
CommonView = kwargs.get( # pylint: disable=invalid-name
CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"CommonView", base["CommonView"]
)
CommonView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -30,7 +30,7 @@ from wuttaweb.views import MasterView
from wuttaweb.forms.schema import EmailRecipients
class EmailSettingView(MasterView):
class EmailSettingView(MasterView): # pylint: disable=abstract-method
"""
Master view for :term:`email settings <email setting>`.
"""
@ -107,8 +107,9 @@ class EmailSettingView(MasterView):
"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)
# key
@ -136,7 +137,9 @@ class EmailSettingView(MasterView):
recips = ", ".join(recips[:2])
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"]
setting = self.email_handler.get_email_setting(key, instance=False)
@ -145,12 +148,14 @@ class EmailSettingView(MasterView):
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"]
def configure_form(self, f): # pylint: disable=empty-docstring
def configure_form(self, form): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
# description
@ -175,7 +180,9 @@ class EmailSettingView(MasterView):
# enabled
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()
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()
EmailSettingView = kwargs.get( # pylint: disable=invalid-name
EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"EmailSettingView", base["EmailSettingView"]
)
EmailSettingView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
# Copyright © 2024-2025 Lance Edgar
#
# 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):
return kwargs.get(spec, spec)
@ -57,5 +57,5 @@ def defaults(config, **kwargs):
config.include(mod("wuttaweb.views.upgrades"))
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -23,6 +23,7 @@
"""
Base Logic for Master Views
"""
# pylint: disable=too-many-lines
import logging
import os
@ -44,7 +45,7 @@ from wuttaweb.progress import SessionProgress
log = logging.getLogger(__name__)
class MasterView(View):
class MasterView(View): # pylint: disable=too-many-public-methods
"""
Base class for "master" views.
@ -398,6 +399,8 @@ class MasterView(View):
# attributes
##############################
model_class = None
# features
listable = True
has_grid = True
@ -438,6 +441,7 @@ class MasterView(View):
viewing = False
editing = False
deleting = False
executing = False
configuring = False
# default DB session
@ -524,11 +528,12 @@ class MasterView(View):
* :meth:`redirect_after_create()`
"""
self.creating = True
session = self.Session()
form = self.make_model_form(cancel_url_fallback=self.get_index_url())
if form.validate():
obj = self.create_save_form(form)
self.Session.flush()
session.flush()
return self.redirect_after_create(obj)
context = {
@ -817,33 +822,25 @@ class MasterView(View):
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()
records = query.with_session(session).all()
try:
self.delete_bulk_action(records, progress=progress)
except Exception as error: # pylint: disable=broad-exception-caught
session.rollback()
def onerror():
log.warning(
"failed to delete %s results for %s",
len(records),
model_title_plural,
self.get_model_title_plural(),
exc_info=True,
)
if progress:
progress.handle_error(error)
else:
session.commit()
if progress:
progress.handle_success()
finally:
session.close()
self.do_thread_body(
self.delete_bulk_action,
(records,),
{"progress": progress},
onerror,
session=session,
progress=progress,
)
def delete_bulk_action(self, data, progress=None):
"""
@ -917,7 +914,7 @@ class MasterView(View):
if not term:
return []
data = self.autocomplete_data(term)
data = self.autocomplete_data(term) # pylint: disable=assignment-from-none
if not data:
return []
@ -931,7 +928,7 @@ class MasterView(View):
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,
based on autocomplete search term. This is called by
@ -943,6 +940,7 @@ class MasterView(View):
:returns: List of data records, or SQLAlchemy query.
"""
return None
def autocomplete_normalize(self, obj):
"""
@ -1006,13 +1004,13 @@ class MasterView(View):
obj = self.get_instance()
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):
return self.notfound()
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
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
response.
"""
return None
##############################
# execute methods
@ -1304,6 +1303,7 @@ class MasterView(View):
Note that their order does not matter since the template
must explicitly define field layout etc.
"""
return []
def configure_gather_settings(
self,
@ -2244,9 +2244,9 @@ class MasterView(View):
:returns: The dict of route kwargs for the object.
"""
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:
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):
"""
@ -2477,6 +2477,60 @@ class MasterView(View):
session = session or self.Session()
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
##############################
@ -2697,9 +2751,7 @@ class MasterView(View):
do not set the :attr:`model_class`, then you *must* set the
:attr:`model_name`.
"""
if hasattr(cls, "model_class"):
return cls.model_class
return None
@classmethod
def get_model_name(cls):
@ -2808,11 +2860,9 @@ class MasterView(View):
inspector = sa.inspect(model_class)
keys = [col.name for col in inspector.primary_key]
return tuple(
[
prop.key
for prop in inspector.column_attrs
if all(col.name in keys for col in prop.columns)
]
)
raise AttributeError(f"you must define model_key for view class: {cls}")

View file

@ -28,9 +28,10 @@ import sqlalchemy as sa
from wuttjamaican.db.model import Person
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.
@ -70,8 +71,9 @@ class PersonView(MasterView):
"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)
# full_name
@ -83,8 +85,9 @@ class PersonView(MasterView):
# 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)
person = f.model_instance
@ -105,12 +108,9 @@ class PersonView(MasterView):
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
return make_users_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=person.users,
columns=[
"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
""" """
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()
PersonView = kwargs.get( # pylint: disable=invalid-name
PersonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"PersonView", base["PersonView"]
)
PersonView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -63,13 +63,15 @@ def progress(request):
return session
def defaults(config, **kwargs):
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
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_view(progress, route_name="progress", renderer="json")
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -37,7 +37,7 @@ from wuttaweb.views import MasterView
log = logging.getLogger(__name__)
class ReportView(MasterView):
class ReportView(MasterView): # pylint: disable=abstract-method
"""
Master view for :term:`reports <report>`; route prefix is
``reports``.
@ -89,8 +89,9 @@ class ReportView(MasterView):
"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)
# report_key
@ -103,7 +104,9 @@ class ReportView(MasterView):
# 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"]
report = self.report_handler.get_report(key)
@ -112,8 +115,9 @@ class ReportView(MasterView):
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"]
def view(self):
@ -151,8 +155,9 @@ class ReportView(MasterView):
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)
key = self.request.matchdict["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()
ReportView = kwargs.get( # pylint: disable=invalid-name
ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"ReportView", base["ReportView"]
)
ReportView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -29,9 +29,10 @@ from wuttaweb.views import MasterView
from wuttaweb.db import Session
from wuttaweb.forms import widgets
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.
@ -58,6 +59,8 @@ class RoleView(MasterView):
}
sort_defaults = "name"
wutta_permissions = None
# TODO: master should handle this, possibly via configure_form()
def get_query(self, session=None): # pylint: disable=empty-docstring
""" """
@ -65,8 +68,9 @@ class RoleView(MasterView):
query = super().get_query(session=session)
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)
# name
@ -75,8 +79,9 @@ class RoleView(MasterView):
# 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)
auth = self.app.get_auth_handler()
@ -93,8 +98,9 @@ class RoleView(MasterView):
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)
auth = self.app.get_auth_handler()
@ -108,8 +114,9 @@ class RoleView(MasterView):
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)
role = f.model_instance
@ -145,12 +152,9 @@ class RoleView(MasterView):
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
return make_users_grid(
self.request,
route_prefix=self.get_route_prefix(),
data=role.users,
columns=[
"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
""" """
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.
@ -346,7 +332,7 @@ class PermissionView(MasterView):
"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)
model = self.app.model
@ -356,8 +342,9 @@ class PermissionView(MasterView):
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)
model = self.app.model
@ -369,25 +356,28 @@ class PermissionView(MasterView):
# 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)
# role
f.set_node("role", RoleRef(self.request))
def defaults(config, **kwargs):
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
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)
PermissionView = kwargs.get( # pylint: disable=invalid-name
PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"PermissionView", base["PermissionView"]
)
PermissionView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -35,7 +35,7 @@ from wuttaweb.views import MasterView
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.
@ -93,8 +93,9 @@ class AppInfoView(MasterView):
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)
g.sort_multiple = False
@ -173,7 +174,9 @@ class AppInfoView(MasterView):
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)
@ -220,7 +223,7 @@ class AppInfoView(MasterView):
return context
class SettingView(MasterView):
class SettingView(MasterView): # pylint: disable=abstract-method
"""
Master view for the "raw" settings table.
@ -242,15 +245,17 @@ class SettingView(MasterView):
sort_defaults = "name"
# 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)
# 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)
# name
@ -275,19 +280,19 @@ class SettingView(MasterView):
node.raise_invalid("Setting name must be unique")
def defaults(config, **kwargs):
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
base = globals()
AppInfoView = kwargs.get( # pylint: disable=invalid-name
AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"AppInfoView", base["AppInfoView"]
)
AppInfoView.defaults(config)
SettingView = kwargs.get( # pylint: disable=invalid-name
SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"SettingView", base["SettingView"]
)
SettingView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -41,7 +41,7 @@ from wuttaweb.progress import get_progress_session
log = logging.getLogger(__name__)
class UpgradeView(MasterView):
class UpgradeView(MasterView): # pylint: disable=abstract-method
"""
Master view for upgrades.
@ -72,8 +72,9 @@ class UpgradeView(MasterView):
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)
model = self.app.model
enum = self.app.enum
@ -121,8 +122,9 @@ class UpgradeView(MasterView):
return "has-background-warning"
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)
enum = self.app.enum
upgrade = f.model_instance
@ -204,11 +206,12 @@ class UpgradeView(MasterView):
"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
the upgrade, in addition to deleting the upgrade proper.
"""
upgrade = obj
path = self.get_upgrade_filepath(upgrade, create=False)
if os.path.exists(path):
shutil.rmtree(path)
@ -227,8 +230,9 @@ class UpgradeView(MasterView):
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:
return self.get_upgrade_filepath(upgrade, filename)
return None
@ -245,7 +249,7 @@ class UpgradeView(MasterView):
path = os.path.join(path, filename)
return path
def execute_instance(self, upgrade, user, progress=None):
def execute_instance(self, obj, user, progress=None):
"""
This method runs the actual upgrade.
@ -258,6 +262,7 @@ class UpgradeView(MasterView):
The upgrade itself is marked as "executed" with status of
either ``SUCCESS`` or ``FAILURE``.
"""
upgrade = obj
enum = self.app.enum
# 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()
UpgradeView = kwargs.get( # pylint: disable=invalid-name
UpgradeView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"UpgradeView", base["UpgradeView"]
)
UpgradeView.defaults(config)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -30,7 +30,7 @@ from wuttaweb.forms import widgets
from wuttaweb.forms.schema import PersonRef, RoleRefs
class UserView(MasterView):
class UserView(MasterView): # pylint: disable=abstract-method
"""
Master view for users.
@ -82,8 +82,9 @@ class UserView(MasterView):
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)
model = self.app.model
@ -106,8 +107,9 @@ class UserView(MasterView):
return "has-background-warning"
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
if user.prevent_edit and not self.request.is_root:
@ -115,8 +117,9 @@ class UserView(MasterView):
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)
user = f.model_instance
@ -233,7 +236,7 @@ class UserView(MasterView):
session = self.Session()
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"]
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()
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)
def includeme(config):
def includeme(config): # pylint: disable=missing-function-docstring
defaults(config)

View file

@ -363,7 +363,7 @@ class TestForm(TestCase):
# basic
form = self.make_form(schema=schema)
self.assertFalse(hasattr(form, "deform_form"))
self.assertIsNone(form.deform_form)
dform = form.get_deform()
self.assertIsInstance(dform, deform.Form)
self.assertIs(form.deform_form, dform)
@ -684,7 +684,7 @@ class TestForm(TestCase):
def test_validate(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
self.assertFalse(hasattr(form, "validated"))
self.assertIsNone(form.validated)
# will not validate unless request is POST
self.request.POST = {"foo": "blarg", "bar": "baz"}

View file

@ -497,7 +497,7 @@ class TestGrid(WebTestCase):
# settings are loaded, applied, saved
self.assertEqual(grid.sort_defaults, [])
self.assertFalse(hasattr(grid, "active_sorters"))
self.assertIsNone(grid.active_sorters)
self.request.GET = {"sort1key": "name", "sort1dir": "desc"}
grid.load_settings()
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
@ -525,7 +525,7 @@ class TestGrid(WebTestCase):
sort_on_backend=True,
sort_defaults="name",
)
self.assertFalse(hasattr(grid, "active_sorters"))
self.assertIsNone(grid.active_sorters)
grid.load_settings()
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
@ -537,7 +537,7 @@ class TestGrid(WebTestCase):
mod.SortInfo("name", "asc"),
mod.SortInfo("value", "desc"),
]
self.assertFalse(hasattr(grid, "active_sorters"))
self.assertIsNone(grid.active_sorters)
grid.load_settings()
self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
@ -556,7 +556,7 @@ class TestGrid(WebTestCase):
paginated=True,
paginate_on_backend=True,
)
self.assertFalse(hasattr(grid, "active_sorters"))
self.assertIsNone(grid.active_sorters)
grid.load_settings()
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
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")
self.assertEqual(filtr.verb, "foo")

View file

@ -76,23 +76,23 @@ class TestNewRequest(TestCase):
subscribers.new_request(event)
# 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
self.request.register_component("foo-example", "FooExample")
self.assertTrue(hasattr(self.request, "_wuttaweb_registered_components"))
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
self.assertTrue(hasattr(self.request, "wuttaweb_registered_components"))
self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
self.assertIn("foo-example", self.request.wuttaweb_registered_components)
self.assertEqual(
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
self.request.wuttaweb_registered_components["foo-example"], "FooExample"
)
# re-registering same name
self.request.register_component("foo-example", "FooExample")
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
self.assertIn("foo-example", self.request.wuttaweb_registered_components)
self.assertEqual(
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
self.request.wuttaweb_registered_components["foo-example"], "FooExample"
)
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.app import establish_theme
from wuttaweb.grids import Grid
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):
def setUp(self):

View file

@ -624,7 +624,7 @@ class TestMasterView(WebTestCase):
view = self.make_view()
# empty by default
self.assertFalse(hasattr(mod.MasterView, "model_class"))
self.assertIsNone(mod.MasterView.model_class)
data = view.get_grid_data(session=self.session)
self.assertEqual(data, [])
@ -1371,6 +1371,25 @@ class TestMasterView(WebTestCase):
self.session.commit()
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):
self.pyramid_config.add_route("settings", "/settings/")
model = self.app.model
@ -1656,6 +1675,11 @@ class TestMasterView(WebTestCase):
count = self.session.query(model.Setting).count()
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):
view = self.make_view()