3
0
Fork 0

Compare commits

..

No commits in common. "0a08918657845b36dbbe9bf0a67ace5ecc2c1ee9" and "601dec777713c3aa4ceeffddc0fde61ac27a8438" have entirely different histories.

38 changed files with 397 additions and 584 deletions

View file

@ -2,7 +2,37 @@
[MESSAGES CONTROL]
disable=fixme,
[SIMILARITIES]
# nb. cuts out some noise for duplicate-code
min-similarity-lines=5
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,

View file

@ -11,9 +11,6 @@ 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,7 +1,4 @@
# -*- 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" not in self.app.handlers:
if "web_handler" not in self.__dict__:
spec = self.config.get(
f"{self.appname}.web.handler_spec",
default="wuttaweb.handler:WebHandler",
)
self.app.handlers["web"] = self.app.load_object(spec)(self.config)
return self.app.handlers["web"]
self.web_handler = self.app.load_object(spec)(self.config)
return self.web_handler
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):
factory = app.load_object(main_app)
make_wsgi_app = app.load_object(main_app)
elif callable(main_app):
factory = main_app
make_wsgi_app = main_app
else:
raise ValueError("main_app must be spec or callable")
# construct a pyramid app "per usual"
return factory({}, **settings)
return make_wsgi_app({}, **settings)
def make_asgi_app(main_app=None, config=None):

View file

@ -105,8 +105,7 @@ class WuttaSecurityPolicy:
self.identity_cache = RequestLocalCache(self.load_identity)
self.db_session = db_session or Session()
def load_identity(self, request): # pylint: disable=empty-docstring
""" """
def load_identity(self, request):
config = request.registry.settings["wutta_config"]
app = config.get_app()
model = app.model
@ -123,29 +122,22 @@ class WuttaSecurityPolicy:
return user
def identity(self, request): # pylint: disable=empty-docstring
""" """
def identity(self, request):
return self.identity_cache.get_or_create(request)
def authenticated_userid(self, request): # pylint: disable=empty-docstring
""" """
def authenticated_userid(self, request):
user = self.identity(request)
if user is not None:
return user.uuid
return None
def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
""" """
def remember(self, request, userid, **kw):
return self.session_helper.remember(request, userid, **kw)
def forget(self, request, **kw): # pylint: disable=empty-docstring
""" """
def forget(self, request, **kw):
return self.session_helper.forget(request, **kw)
def permits( # pylint: disable=unused-argument,empty-docstring
self, request, context, permission
):
""" """
def permits(self, request, context, permission): # pylint: disable=unused-argument
# 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,too-few-public-methods
class feedback(EmailSetting): # pylint: disable=invalid-name
"""
Sent when user submits feedback via the web app.
"""

View file

@ -23,7 +23,6 @@
"""
Base form classes
"""
# pylint: disable=too-many-lines
import logging
from collections import OrderedDict
@ -37,19 +36,13 @@ 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,
render_vue_finalize,
)
from wuttaweb.util import FieldList, get_form_data, get_model_fields, make_json_safe
log = logging.getLogger(__name__)
class Form: # pylint: disable=too-many-instance-attributes,too-many-public-methods
class Form: # pylint: disable=too-many-instance-attributes
"""
Base class for all forms.
@ -270,12 +263,11 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
If the :meth:`validate()` method was called, and it succeeded,
this will be set to the validated data dict.
Note that in all other cases, this attribute may not exist.
"""
deform_form = None
validated = None
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
def __init__(
self,
request,
fields=None,
@ -338,7 +330,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
self.model_class = model_class
self.model_instance = model_instance
if self.model_instance and not self.model_class:
if not isinstance(self.model_instance, dict):
if type(self.model_instance) is not dict:
self.model_class = type(self.model_instance)
self.set_fields(fields or self.get_fields())
@ -881,7 +873,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
Return the :class:`deform:deform.Form` instance for the form,
generating it automatically if necessary.
"""
if not self.deform_form:
if not hasattr(self, "deform_form"):
schema = self.get_schema()
kwargs = {}
@ -990,7 +982,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
output = render(template, context)
return HTML.literal(output)
def render_vue_field( # pylint: disable=unused-argument,too-many-locals
def render_vue_field( # pylint: disable=unused-argument
self,
fieldname,
readonly=None,
@ -1101,7 +1093,12 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
The actual output may depend on various form attributes, in
particular :attr:`vue_tagname`.
"""
return render_vue_finalize(self.vue_tagname, self.vue_component)
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
def get_vue_model_data(self):
"""
@ -1127,7 +1124,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# for now we explicitly translate here, ugh. also
# note this does not yet allow for null values.. :(
if isinstance(field.typ, colander.Boolean):
value = value == field.typ.true_val
value = True if value == field.typ.true_val else False
model_data[field.oid] = make_json_safe(value)
@ -1176,9 +1173,9 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
:attr:`validated` attribute.
However if the data is not valid, ``False`` is returned, and
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
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
:meth:`get_field_errors()`.
This uses :meth:`deform:deform.Field.validate()` under the
@ -1194,7 +1191,8 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
:returns: Data dict, or ``False``.
"""
self.validated = None
if hasattr(self, "validated"):
del self.validated
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): # pylint: disable=abstract-method
class ObjectNode(colander.SchemaNode):
"""
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"] = list(self.enum_dct.items())
kwargs["values"] = [(k, v) for k, v in self.enum_dct.items()]
return widgets.SelectWidget(**kwargs)
@ -288,8 +288,13 @@ class ObjectRef(colander.SchemaType):
default_empty_option = ("", "(none)")
def __init__(self, request, *args, **kwargs):
empty_option = kwargs.pop("empty_option", None)
def __init__(
self,
request,
empty_option=None,
*args,
**kwargs,
):
# nb. allow session injection for tests
self.session = kwargs.pop("session", Session())
super().__init__(*args, **kwargs)
@ -376,9 +381,7 @@ class ObjectRef(colander.SchemaType):
if not value:
return None
if isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
value, self.model_class
):
if isinstance(value, self.model_class):
return value
# fetch object from DB
@ -472,9 +475,8 @@ class PersonRef(ObjectRef):
""" """
return query.order_by(self.model_class.full_name)
def get_object_url(self, obj): # pylint: disable=empty-docstring
def get_object_url(self, person): # pylint: disable=empty-docstring
""" """
person = obj
return self.request.route_url("people.view", uuid=person.uuid)
@ -497,9 +499,8 @@ class RoleRef(ObjectRef):
""" """
return query.order_by(self.model_class.name)
def get_object_url(self, obj): # pylint: disable=empty-docstring
def get_object_url(self, role): # pylint: disable=empty-docstring
""" """
role = obj
return self.request.route_url("roles.view", uuid=role.uuid)
@ -522,9 +523,8 @@ class UserRef(ObjectRef):
""" """
return query.order_by(self.model_class.username)
def get_object_url(self, obj): # pylint: disable=empty-docstring
def get_object_url(self, user): # pylint: disable=empty-docstring
""" """
user = obj
return self.request.route_url("users.view", uuid=user.uuid)
@ -557,7 +557,7 @@ class RoleRefs(WuttaSet):
auth.get_role_authenticated(session),
auth.get_role_anonymous(session),
}
avoid = {role.uuid for role in avoid}
avoid = set([role.uuid for role in avoid])
# also avoid admin unless current user is root
if not self.request.is_root:

View file

@ -103,8 +103,7 @@ class ObjectRefWidget(SelectWidget):
readonly_template = "readonly/objectref"
def __init__(self, request, *args, **kwargs):
url = kwargs.pop("url", None)
def __init__(self, request, url=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.url = url
@ -300,7 +299,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
return super().serialize(field, cstruct, **kw)
class FileDownloadWidget(Widget): # pylint: disable=abstract-method
class FileDownloadWidget(Widget):
"""
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
fields.
@ -358,7 +357,7 @@ class FileDownloadWidget(Widget): # pylint: disable=abstract-method
return humanize.naturalsize(size)
class GridWidget(Widget): # pylint: disable=abstract-method
class GridWidget(Widget):
"""
Widget for fields whose data is represented by a :term:`grid`.
@ -407,7 +406,6 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
"""
readonly_template = "readonly/rolerefs"
session = None
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
""" """
@ -464,7 +462,6 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
template = "permissions"
readonly_template = "readonly/permissions"
permissions = None
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
""" """
@ -515,7 +512,7 @@ class EmailRecipientsWidget(TextAreaWidget):
return ", ".join(values)
class BatchIdWidget(Widget): # pylint: disable=abstract-method
class BatchIdWidget(Widget):
"""
Widget for use with the
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`

View file

@ -23,7 +23,6 @@
"""
Base grid classes
"""
# pylint: disable=too-many-lines
import functools
import logging
@ -39,12 +38,7 @@ 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,
render_vue_finalize,
)
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
@ -59,7 +53,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
"""
class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-methods
class Grid: # pylint: disable=too-many-instance-attributes
"""
Base class for all :term:`grids <grid>`.
@ -269,7 +263,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
``active_sorters`` defines the "current/effective" sorters.
This attribute is set by :meth:`load_settings()`; until that is
called its value will be ``None``.
called it will not exist.
This is conceptually a "subset" of :attr:`sorters` although a
different format is used here::
@ -377,11 +371,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
See also :meth:`add_tool()` and :meth:`set_tools()`.
"""
active_sorters = None
joined = None
pager = None
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
def __init__(
self,
request,
vue_tagname="wutta-grid",
@ -698,7 +688,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
"percent": self.render_percent,
}
if renderer in builtins: # pylint: disable=consider-using-get
if renderer in builtins:
renderer = builtins[renderer]
if kwargs:
@ -1096,8 +1086,8 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# TODO: this should be improved; is needed in tailbone for
# multi-column sorting with sqlalchemy queries
if model_property:
sorter._class = model_class # pylint: disable=protected-access
sorter._column = model_property # pylint: disable=protected-access
sorter._class = model_class
sorter._column = model_property
return sorter
@ -1382,10 +1372,10 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
if filterinfo and callable(filterinfo):
# filtr = filterinfo
raise NotImplementedError
kwargs["key"] = key
kwargs.setdefault("label", self.get_label(key))
filtr = self.make_filter(filterinfo or key, **kwargs)
else:
kwargs["key"] = key
kwargs.setdefault("label", self.get_label(key))
filtr = self.make_filter(filterinfo or key, **kwargs)
self.filters[key] = filtr
@ -1483,9 +1473,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# configuration methods
##############################
def load_settings( # pylint: disable=too-many-branches,too-many-statements
self, persist=True
):
def load_settings(self, persist=True): # pylint: disable=too-many-branches
"""
Load all effective settings for the grid.
@ -1638,7 +1626,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
return False
def get_setting( # pylint: disable=empty-docstring,too-many-arguments,too-many-positional-arguments
def get_setting( # pylint: disable=empty-docstring
self, settings, key, src="session", default=None, normalize=lambda v: v
):
""" """
@ -2217,7 +2205,12 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
The actual output may depend on various grid attributes, in
particular :attr:`vue_tagname`.
"""
return render_vue_finalize(self.vue_tagname, self.vue_component)
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
def get_vue_columns(self):
"""
@ -2297,11 +2290,12 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
:returns: The first sorter in format ``[sortkey, sortdir]``,
or ``None``.
"""
if self.active_sorters:
sorter = self.active_sorters[0]
return [sorter["key"], sorter["dir"]]
if hasattr(self, "active_sorters"):
if self.active_sorters:
sorter = self.active_sorters[0]
return [sorter["key"], sorter["dir"]]
if self.sort_defaults:
elif self.sort_defaults:
sorter = self.sort_defaults[0]
return [sorter.sortkey, sorter.sortdir]
@ -2392,9 +2386,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
record = make_json_safe(record, warn=False)
# customize value rendering where applicable
for key, renderer in self.renderers.items():
for key in self.renderers:
value = record.get(key, None)
record[key] = renderer(original_record, key, value)
record[key] = self.renderers[key](original_record, key, value)
# add action urls to each record
for action in self.actions:
@ -2543,7 +2537,7 @@ class GridAction: # pylint: disable=too-many-instance-attributes
Optional HTML class attribute for the action's ``<a>`` tag.
"""
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
def __init__(
self,
request,
key,

View file

@ -59,10 +59,9 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
:param request: Current :term:`request` object.
: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 model_property: Property of a model class, representing the
column by which to filter. For instance,
``model.Person.full_name``.
:param \\**kwargs: Any additional kwargs will be set as attributes
on the filter instance.
@ -170,14 +169,13 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
"is_not_null",
]
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
def __init__(
self,
request,
key,
label=None,
verbs=None,
choices=None,
nullable=None,
default_active=False,
default_verb=None,
default_value=None,
@ -198,14 +196,10 @@ 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
@ -254,7 +248,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
Returns a dict of all defined verb labels.
"""
# TODO: should traverse hierarchy
labels = {verb: verb for verb in self.get_verbs()}
labels = dict([(verb, verb) for verb in self.get_verbs()])
labels.update(self.default_verb_labels)
return labels
@ -384,7 +378,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes
raise VerbNotSupported(verb)
# invoke filter method
return func(data, value) # pylint: disable=not-callable
return func(data, value)
def filter_is_any(self, data, value): # pylint: disable=unused-argument
"""
@ -404,12 +398,18 @@ 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):
self.model_property = kwargs.pop("model_property")
nullable = kwargs.pop("nullable", None)
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, # pylint: disable=singleton-comparison
self.model_property == None,
self.model_property != value,
)
)
@ -491,18 +491,14 @@ class AlchemyFilter(GridFilter):
"""
Filter data with an ``IS NULL`` query. The value is ignored.
"""
return query.filter(
self.model_property == None # pylint: disable=singleton-comparison
)
return query.filter(self.model_property == None)
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 # pylint: disable=singleton-comparison
)
return query.filter(self.model_property != None)
class StringAlchemyFilter(AlchemyFilter):
@ -554,12 +550,7 @@ 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, # pylint: disable=singleton-comparison
sa.and_(*criteria),
)
)
return query.filter(sa.or_(self.model_property == None, sa.and_(*criteria)))
class NumericAlchemyFilter(AlchemyFilter):
@ -637,18 +628,14 @@ class BooleanAlchemyFilter(AlchemyFilter):
Filter data with an "is true" condition. The value is
ignored.
"""
return query.filter(
self.model_property == True # pylint: disable=singleton-comparison
)
return query.filter(self.model_property == True)
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 # pylint: disable=singleton-comparison
)
return query.filter(self.model_property == False)
def filter_is_false_null(self, query, value): # pylint: disable=unused-argument
"""
@ -656,10 +643,7 @@ class BooleanAlchemyFilter(AlchemyFilter):
ignored.
"""
return query.filter(
sa.or_(
self.model_property == False, # pylint: disable=singleton-comparison
self.model_property == None, # pylint: disable=singleton-comparison
)
sa.or_(self.model_property == False, self.model_property == None)
)

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: # pylint: disable=too-many-nested-blocks
for topitem in raw_menus:
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: # pylint: disable=too-many-nested-blocks
for topitem in menus:
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__( # pylint: disable=too-many-arguments,too-many-positional-arguments,super-init-not-called
def __init__(
self, request, key, success_msg=None, success_url=None, error_url=None
):
self.request = request

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024-2025 Lance Edgar
# Copyright © 2024 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): # pylint: disable=missing-function-docstring
def includeme(config):
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): # pylint: disable=missing-function-docstring
def includeme(config):
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-2025 Lance Edgar
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -39,14 +39,10 @@ class WebTestCase(DataTestCase):
Base class for test suites requiring a full (typical) web app.
"""
def setUp(self): # pylint: disable=empty-docstring
""" """
def setUp(self):
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(
@ -91,14 +87,8 @@ 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,71 +618,6 @@ 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
##############################
@ -822,14 +757,14 @@ def set_app_theme(request, theme, session=None):
# there's only one global template lookup; can get to it via any renderer
# but should *not* use /base.mako since that one is about to get volatile
renderer = get_renderer("/page.mako")
renderer = get_renderer("/menu.mako")
lookup = renderer.lookup
# 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() # pylint: disable=protected-access
lookup._collection.clear()
# 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-2025 Lance Edgar
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -34,5 +34,5 @@ from .base import View
from .master import MasterView
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
config.include("wuttaweb.views.essential")

View file

@ -95,8 +95,7 @@ class AuthView(View):
# 'referrer': referrer,
}
def login_make_schema(self): # pylint: disable=empty-docstring
""" """
def login_make_schema(self):
schema = colander.Schema()
# nb. we must explicitly declare the widgets in order to also
@ -221,19 +220,13 @@ class AuthView(View):
return schema
def change_password_validate_current_password( # pylint: disable=empty-docstring
self, node, value
):
""" """
def change_password_validate_current_password(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( # pylint: disable=empty-docstring
self, node, value
):
""" """
def change_password_validate_new_password(self, node, value):
auth = self.app.get_auth_handler()
user = self.request.user
if auth.check_user_password(user, value):
@ -284,8 +277,7 @@ class AuthView(View):
return self.redirect(url)
@classmethod
def defaults(cls, config): # pylint: disable=empty-docstring
""" """
def defaults(cls, config):
cls._auth_defaults(config)
@classmethod
@ -319,14 +311,12 @@ class AuthView(View):
config.add_view(cls, attr="stop_root", route_name="stop_root")
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
AuthView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"AuthView", base["AuthView"]
)
AuthView = kwargs.get("AuthView", base["AuthView"]) # pylint: disable=invalid-name
AuthView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -51,8 +51,6 @@ class BatchMasterView(MasterView):
from :meth:`get_batch_handler()`.
"""
executable = True
labels = {
"id": "Batch ID",
"status_code": "Status",
@ -123,9 +121,8 @@ class BatchMasterView(MasterView):
return super().render_to_response(template, context)
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
model = self.app.model
@ -156,17 +153,15 @@ class BatchMasterView(MasterView):
return f"{batch_id:08d}"
return None
def get_instance_title(self, instance): # pylint: disable=empty-docstring
def get_instance_title(self, batch): # pylint: disable=empty-docstring
""" """
batch = instance
if batch.description:
return f"{batch.id_str} {batch.description}"
return batch.id_str
def configure_form(self, form): # pylint: disable=too-many-branches,empty-docstring
def configure_form(self, f): # pylint: disable=too-many-branches,empty-docstring
""" """
super().configure_form(form)
f = form
super().configure_form(f)
batch = f.model_instance
# id
@ -240,11 +235,13 @@ class BatchMasterView(MasterView):
batch = schema.objectify(form.validated, context=form.model_instance)
# then we collect attributes from the new batch
kw = {
key: getattr(batch, key)
for key in form.validated
if hasattr(batch, key)
}
kw = dict(
[
(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
@ -258,7 +255,7 @@ class BatchMasterView(MasterView):
# when not creating, normal logic is fine
return super().objectify(form)
def redirect_after_create(self, obj):
def redirect_after_create(self, batch):
"""
If the new batch requires initial population, we launch a
thread for that and show the "progress" page.
@ -266,8 +263,6 @@ class BatchMasterView(MasterView):
Otherwise this will do the normal thing of redirecting to the
"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))
@ -288,7 +283,7 @@ class BatchMasterView(MasterView):
thread.start()
return self.render_progress(progress)
def delete_instance(self, obj):
def delete_instance(self, batch):
"""
Delete the given batch instance.
@ -296,7 +291,6 @@ 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)
##############################
@ -332,22 +326,29 @@ class BatchMasterView(MasterView):
raise RuntimeError("can't find the batch")
time.sleep(0.1)
def onerror():
try:
# populate the batch
self.batch_handler.do_populate(batch, progress=progress)
session.flush()
except Exception as error: # pylint: disable=broad-exception-caught
session.rollback()
log.warning(
"failed to populate %s: %s",
self.get_model_title(),
batch,
exc_info=True,
)
if progress:
progress.handle_error(error)
self.do_thread_body(
self.batch_handler.do_populate,
(batch,),
{"progress": progress},
onerror,
session=session,
progress=progress,
)
else:
session.commit()
if progress:
progress.handle_success()
finally:
session.close()
##############################
# execute methods
@ -387,21 +388,20 @@ class BatchMasterView(MasterView):
model_class = cls.get_model_class()
return model_class.__row_class__
def get_row_grid_data(self, obj):
def get_row_grid_data(self, batch):
"""
Returns the base query for the batch
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
data.
"""
session = self.Session()
batch = obj
row_model_class = self.get_row_model_class()
query = session.query(row_model_class).filter(row_model_class.batch == batch)
query = self.Session.query(row_model_class).filter(
row_model_class.batch == batch
)
return query
def configure_row_grid(self, grid): # pylint: disable=empty-docstring
def configure_row_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_row_grid(g)
g.set_label("sequence", "Seq.", column_only=True)
@ -413,3 +413,36 @@ 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): # pylint: disable=too-many-locals
def setup(self, session=None):
"""
View for first-time app setup, to create admin user.
@ -294,8 +294,7 @@ class CommonView(View):
return self.redirect(referrer)
@classmethod
def defaults(cls, config): # pylint: disable=empty-docstring
""" """
def defaults(cls, config):
cls._defaults(config)
@classmethod
@ -343,14 +342,14 @@ class CommonView(View):
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
CommonView = kwargs.get( # pylint: disable=invalid-name
"CommonView", base["CommonView"]
)
CommonView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -30,7 +30,7 @@ from wuttaweb.views import MasterView
from wuttaweb.forms.schema import EmailRecipients
class EmailSettingView(MasterView): # pylint: disable=abstract-method
class EmailSettingView(MasterView):
"""
Master view for :term:`email settings <email setting>`.
"""
@ -107,9 +107,8 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
"enabled": self.email_handler.is_enabled(key),
}
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
# key
@ -137,9 +136,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
recips = ", ".join(recips[:2])
return f"{recips}, ..."
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
self, **kwargs
):
def get_instance(self): # pylint: disable=empty-docstring
""" """
key = self.request.matchdict["key"]
setting = self.email_handler.get_email_setting(key, instance=False)
@ -148,14 +145,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
raise self.notfound()
def get_instance_title(self, instance): # pylint: disable=empty-docstring
def get_instance_title(self, setting): # pylint: disable=empty-docstring
""" """
setting = instance
return setting["subject"]
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
# description
@ -180,9 +175,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
# enabled
f.set_node("enabled", colander.Boolean())
def persist( # pylint: disable=too-many-branches,empty-docstring,arguments-differ,unused-argument
self, setting, **kwargs
):
def persist(self, setting): # pylint: disable=too-many-branches,empty-docstring
""" """
session = self.Session()
key = self.request.matchdict["key"]
@ -307,14 +300,14 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
EmailSettingView = kwargs.get( # pylint: disable=invalid-name
"EmailSettingView", base["EmailSettingView"]
)
EmailSettingView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

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

View file

@ -23,7 +23,6 @@
"""
Base Logic for Master Views
"""
# pylint: disable=too-many-lines
import logging
import os
@ -45,7 +44,7 @@ from wuttaweb.progress import SessionProgress
log = logging.getLogger(__name__)
class MasterView(View): # pylint: disable=too-many-public-methods
class MasterView(View):
"""
Base class for "master" views.
@ -399,8 +398,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
# attributes
##############################
model_class = None
# features
listable = True
has_grid = True
@ -441,7 +438,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
viewing = False
editing = False
deleting = False
executing = False
configuring = False
# default DB session
@ -528,12 +524,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods
* :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)
session.flush()
self.Session.flush()
return self.redirect_after_create(obj)
context = {
@ -822,25 +817,33 @@ class MasterView(View): # pylint: disable=too-many-public-methods
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()
def onerror():
try:
self.delete_bulk_action(records, progress=progress)
except Exception as error: # pylint: disable=broad-exception-caught
session.rollback()
log.warning(
"failed to delete %s results for %s",
len(records),
self.get_model_title_plural(),
model_title_plural,
exc_info=True,
)
if progress:
progress.handle_error(error)
self.do_thread_body(
self.delete_bulk_action,
(records,),
{"progress": progress},
onerror,
session=session,
progress=progress,
)
else:
session.commit()
if progress:
progress.handle_success()
finally:
session.close()
def delete_bulk_action(self, data, progress=None):
"""
@ -914,7 +917,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
if not term:
return []
data = self.autocomplete_data(term) # pylint: disable=assignment-from-none
data = self.autocomplete_data(term)
if not data:
return []
@ -928,7 +931,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
return results
def autocomplete_data(self, term): # pylint: disable=unused-argument
def autocomplete_data(self, term):
"""
Should return the data/query for the "matching" model records,
based on autocomplete search term. This is called by
@ -940,7 +943,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
:returns: List of data records, or SQLAlchemy query.
"""
return None
def autocomplete_normalize(self, obj):
"""
@ -1004,13 +1006,13 @@ class MasterView(View): # pylint: disable=too-many-public-methods
obj = self.get_instance()
filename = self.request.GET.get("filename", None)
path = self.download_path(obj, filename) # pylint: disable=assignment-from-none
path = self.download_path(obj, filename)
if not path or not os.path.exists(path):
return self.notfound()
return self.file_response(path)
def download_path(self, obj, filename): # pylint: disable=unused-argument
def download_path(self, obj, filename):
"""
Should return absolute path on disk, for the given object and
filename. Result will be used to return a file response to
@ -1031,7 +1033,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
the :meth:`download()` view will return a 404 not found
response.
"""
return None
##############################
# execute methods
@ -1303,7 +1304,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
Note that their order does not matter since the template
must explicitly define field layout etc.
"""
return []
def configure_gather_settings(
self,
@ -2244,9 +2244,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
:returns: The dict of route kwargs for the object.
"""
try:
return {key: obj[key] for key in self.get_model_key()}
return dict([(key, obj[key]) for key in self.get_model_key()])
except TypeError:
return {key: getattr(obj, key) for key in self.get_model_key()}
return dict([(key, getattr(obj, key)) for key in self.get_model_key()])
def get_action_url(self, action, obj, **kwargs):
"""
@ -2477,60 +2477,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
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
##############################
@ -2751,7 +2697,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
do not set the :attr:`model_class`, then you *must* set the
:attr:`model_name`.
"""
return cls.model_class
if hasattr(cls, "model_class"):
return cls.model_class
return None
@classmethod
def get_model_name(cls):
@ -2860,9 +2808,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods
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)
[
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,10 +28,9 @@ 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): # pylint: disable=abstract-method
class PersonView(MasterView):
"""
Master view for people.
@ -71,9 +70,8 @@ class PersonView(MasterView): # pylint: disable=abstract-method
"users",
]
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
# full_name
@ -85,9 +83,8 @@ class PersonView(MasterView): # pylint: disable=abstract-method
# last_name
g.set_link("last_name")
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
person = f.model_instance
@ -108,9 +105,12 @@ class PersonView(MasterView): # pylint: disable=abstract-method
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
return make_users_grid(
self.request,
route_prefix=self.get_route_prefix(),
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
data=person.users,
columns=[
"username",
@ -118,6 +118,23 @@ class PersonView(MasterView): # pylint: disable=abstract-method
],
)
if self.request.has_perm("users.view"):
def view_url(user, i): # pylint: disable=unused-argument
return self.request.route_url("users.view", uuid=user.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("username")
if self.request.has_perm("users.edit"):
def edit_url(user, i): # pylint: disable=unused-argument
return self.request.route_url("users.edit", uuid=user.uuid)
grid.add_action("edit", url=edit_url)
return grid
def objectify(self, form): # pylint: disable=empty-docstring
""" """
person = super().objectify(form)
@ -196,14 +213,14 @@ class PersonView(MasterView): # pylint: disable=abstract-method
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
PersonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
PersonView = kwargs.get( # pylint: disable=invalid-name
"PersonView", base["PersonView"]
)
PersonView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024-2025 Lance Edgar
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -63,15 +63,13 @@ def progress(request):
return session
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
progress = kwargs.get( # pylint: disable=redefined-outer-name
"progress", base["progress"]
)
progress = kwargs.get("progress", base["progress"])
config.add_route("progress", "/progress/{key}")
config.add_view(progress, route_name="progress", renderer="json")
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -37,7 +37,7 @@ from wuttaweb.views import MasterView
log = logging.getLogger(__name__)
class ReportView(MasterView): # pylint: disable=abstract-method
class ReportView(MasterView):
"""
Master view for :term:`reports <report>`; route prefix is
``reports``.
@ -89,9 +89,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
"help_text": report.__doc__,
}
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
# report_key
@ -104,9 +103,7 @@ class ReportView(MasterView): # pylint: disable=abstract-method
# help_text
g.set_searchable("help_text")
def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument
self, **kwargs
):
def get_instance(self): # pylint: disable=empty-docstring
""" """
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
@ -115,9 +112,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
raise self.notfound()
def get_instance_title(self, instance): # pylint: disable=empty-docstring
def get_instance_title(self, report): # pylint: disable=empty-docstring
""" """
report = instance
return report["report_title"]
def view(self):
@ -155,9 +151,8 @@ class ReportView(MasterView): # pylint: disable=abstract-method
return self.render_to_response("view", context)
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
@ -266,14 +261,14 @@ class ReportView(MasterView): # pylint: disable=abstract-method
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
ReportView = kwargs.get( # pylint: disable=invalid-name
"ReportView", base["ReportView"]
)
ReportView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -29,10 +29,9 @@ 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): # pylint: disable=abstract-method
class RoleView(MasterView):
"""
Master view for roles.
@ -59,8 +58,6 @@ class RoleView(MasterView): # pylint: disable=abstract-method
}
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
""" """
@ -68,9 +65,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
query = super().get_query(session=session)
return query.order_by(model.Role.name)
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
# name
@ -79,9 +75,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
# notes
g.set_renderer("notes", self.grid_render_notes)
def is_editable(self, obj): # pylint: disable=empty-docstring
def is_editable(self, role): # pylint: disable=empty-docstring
""" """
role = obj
session = self.app.get_session(role)
auth = self.app.get_auth_handler()
@ -98,9 +93,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
return True
def is_deletable(self, obj): # pylint: disable=empty-docstring
def is_deletable(self, role): # pylint: disable=empty-docstring
""" """
role = obj
session = self.app.get_session(role)
auth = self.app.get_auth_handler()
@ -114,9 +108,8 @@ class RoleView(MasterView): # pylint: disable=abstract-method
return True
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
role = f.model_instance
@ -152,9 +145,12 @@ class RoleView(MasterView): # pylint: disable=abstract-method
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
return make_users_grid(
self.request,
route_prefix=self.get_route_prefix(),
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
data=role.users,
columns=[
"username",
@ -163,6 +159,24 @@ class RoleView(MasterView): # pylint: disable=abstract-method
],
)
if self.request.has_perm("users.view"):
def view_url(user, i): # pylint: disable=unused-argument
return self.request.route_url("users.view", uuid=user.uuid)
grid.add_action("view", icon="eye", url=view_url)
grid.set_link("person")
grid.set_link("username")
if self.request.has_perm("users.edit"):
def edit_url(user, i): # pylint: disable=unused-argument
return self.request.route_url("users.edit", uuid=user.uuid)
grid.add_action("edit", url=edit_url)
return grid
def unique_name(self, node, value): # pylint: disable=empty-docstring
""" """
model = self.app.model
@ -303,7 +317,7 @@ class RoleView(MasterView): # pylint: disable=abstract-method
)
class PermissionView(MasterView): # pylint: disable=abstract-method
class PermissionView(MasterView):
"""
Master view for permissions.
@ -332,7 +346,7 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
"permission",
]
def get_query(self, **kwargs): # pylint: disable=empty-docstring,arguments-differ
def get_query(self, **kwargs): # pylint: disable=empty-docstring
""" """
query = super().get_query(**kwargs)
model = self.app.model
@ -342,9 +356,8 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
return query
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
model = self.app.model
@ -356,28 +369,25 @@ class PermissionView(MasterView): # pylint: disable=abstract-method
# permission
g.set_link("permission")
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
# role
f.set_node("role", RoleRef(self.request))
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
RoleView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"RoleView", base["RoleView"]
)
RoleView = kwargs.get("RoleView", base["RoleView"]) # pylint: disable=invalid-name
RoleView.defaults(config)
PermissionView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
PermissionView = kwargs.get( # pylint: disable=invalid-name
"PermissionView", base["PermissionView"]
)
PermissionView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -35,7 +35,7 @@ from wuttaweb.views import MasterView
from wuttaweb.util import get_libver, get_liburl
class AppInfoView(MasterView): # pylint: disable=abstract-method
class AppInfoView(MasterView):
"""
Master view for the core app info, to show/edit config etc.
@ -93,9 +93,8 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
return data
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
g.sort_multiple = False
@ -174,9 +173,7 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
return simple_settings
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
self, **kwargs
):
def configure_get_context(self, **kwargs): # pylint: disable=empty-docstring
""" """
context = super().configure_get_context(**kwargs)
@ -223,7 +220,7 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
return context
class SettingView(MasterView): # pylint: disable=abstract-method
class SettingView(MasterView):
"""
Master view for the "raw" settings table.
@ -245,17 +242,15 @@ class SettingView(MasterView): # pylint: disable=abstract-method
sort_defaults = "name"
# TODO: master should handle this (per model key)
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
# name
g.set_link("name")
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
# name
@ -280,19 +275,19 @@ class SettingView(MasterView): # pylint: disable=abstract-method
node.raise_invalid("Setting name must be unique")
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
AppInfoView = kwargs.get( # pylint: disable=invalid-name
"AppInfoView", base["AppInfoView"]
)
AppInfoView.defaults(config)
SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
SettingView = kwargs.get( # pylint: disable=invalid-name
"SettingView", base["SettingView"]
)
SettingView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -41,7 +41,7 @@ from wuttaweb.progress import get_progress_session
log = logging.getLogger(__name__)
class UpgradeView(MasterView): # pylint: disable=abstract-method
class UpgradeView(MasterView):
"""
Master view for upgrades.
@ -72,9 +72,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
sort_defaults = ("created", "desc")
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
model = self.app.model
enum = self.app.enum
@ -122,9 +121,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
return "has-background-warning"
return None
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
enum = self.app.enum
upgrade = f.model_instance
@ -206,12 +204,11 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
)
def delete_instance(self, obj):
def delete_instance(self, upgrade):
"""
We override this method to delete any files associated with
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)
@ -230,9 +227,8 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
return upgrade
def download_path(self, obj, filename): # pylint: disable=empty-docstring
def download_path(self, upgrade, filename): # pylint: disable=empty-docstring
""" """
upgrade = obj
if filename:
return self.get_upgrade_filepath(upgrade, filename)
return None
@ -249,7 +245,7 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
path = os.path.join(path, filename)
return path
def execute_instance(self, obj, user, progress=None):
def execute_instance(self, upgrade, user, progress=None):
"""
This method runs the actual upgrade.
@ -262,7 +258,6 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
The upgrade itself is marked as "executed" with status of
either ``SUCCESS`` or ``FAILURE``.
"""
upgrade = obj
enum = self.app.enum
# locate file paths
@ -382,14 +377,14 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
UpgradeView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
UpgradeView = kwargs.get( # pylint: disable=invalid-name
"UpgradeView", base["UpgradeView"]
)
UpgradeView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -30,7 +30,7 @@ from wuttaweb.forms import widgets
from wuttaweb.forms.schema import PersonRef, RoleRefs
class UserView(MasterView): # pylint: disable=abstract-method
class UserView(MasterView):
"""
Master view for users.
@ -82,9 +82,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
return query
def configure_grid(self, grid): # pylint: disable=empty-docstring
def configure_grid(self, g): # pylint: disable=empty-docstring
""" """
g = grid
super().configure_grid(g)
model = self.app.model
@ -107,9 +106,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
return "has-background-warning"
return None
def is_editable(self, obj): # pylint: disable=empty-docstring
def is_editable(self, user): # pylint: disable=empty-docstring
""" """
user = obj
# only root can edit certain users
if user.prevent_edit and not self.request.is_root:
@ -117,9 +115,8 @@ class UserView(MasterView): # pylint: disable=abstract-method
return True
def configure_form(self, form): # pylint: disable=empty-docstring
def configure_form(self, f): # pylint: disable=empty-docstring
""" """
f = form
super().configure_form(f)
user = f.model_instance
@ -236,7 +233,7 @@ class UserView(MasterView): # pylint: disable=abstract-method
session = self.Session()
auth = self.app.get_auth_handler()
old_roles = {role.uuid for role in user.roles}
old_roles = set([role.uuid for role in user.roles])
new_roles = data["roles"]
admin = auth.get_role_administrator(session)
@ -420,14 +417,12 @@ class UserView(MasterView): # pylint: disable=abstract-method
)
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
def defaults(config, **kwargs):
base = globals()
UserView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
"UserView", base["UserView"]
)
UserView = kwargs.get("UserView", base["UserView"]) # pylint: disable=invalid-name
UserView.defaults(config)
def includeme(config): # pylint: disable=missing-function-docstring
def includeme(config):
defaults(config)

View file

@ -363,7 +363,7 @@ class TestForm(TestCase):
# basic
form = self.make_form(schema=schema)
self.assertIsNone(form.deform_form)
self.assertFalse(hasattr(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.assertIsNone(form.validated)
self.assertFalse(hasattr(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.assertIsNone(grid.active_sorters)
self.assertFalse(hasattr(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.assertIsNone(grid.active_sorters)
self.assertFalse(hasattr(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.assertIsNone(grid.active_sorters)
self.assertFalse(hasattr(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.assertIsNone(grid.active_sorters)
self.assertFalse(hasattr(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.assertIsNone(filtr.verb)
self.assertFalse(hasattr(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,7 +16,6 @@ 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
@ -666,52 +665,6 @@ class TestMakeJsonSafe(TestCase):
)
class TestRenderVueFinalize(TestCase):
def basic(self):
html = mod.render_vue_finalize("wutta-grid", "WuttaGrid")
self.assertIn("<script>", html)
self.assertIn("Vue.component('wutta-grid', WuttaGrid)", html)
class TestMakeUsersGrid(WebTestCase):
def test_make_users_grid(self):
self.pyramid_config.add_route("users.view", "/users/{uuid}/view")
self.pyramid_config.add_route("users.edit", "/users/{uuid}/edit")
model = self.app.model
person = model.Person(full_name="John Doe")
self.session.add(person)
user = model.User(username="john", person=person)
self.session.add(user)
self.session.commit()
# basic (no actions because not prvileged)
grid = mod.make_users_grid(self.request, key="blah.users", data=person.users)
self.assertIsInstance(grid, Grid)
self.assertFalse(grid.linked_columns)
self.assertFalse(grid.actions)
# key may be derived from route_prefix
grid = mod.make_users_grid(self.request, route_prefix="foo")
self.assertIsInstance(grid, Grid)
self.assertEqual(grid.key, "foo.view.users")
# view + edit actions (because root)
with patch.object(self.request, "is_root", new=True):
grid = mod.make_users_grid(
self.request, key="blah.users", data=person.users
)
self.assertIsInstance(grid, Grid)
self.assertIn("username", grid.linked_columns)
self.assertEqual(len(grid.actions), 2)
self.assertEqual(grid.actions[0].key, "view")
self.assertEqual(grid.actions[1].key, "edit")
# render grid to ensure coverage for link urls
grid.render_vue_template()
class TestGetAvailableThemes(TestCase):
def setUp(self):

View file

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