Compare commits
	
		
			32 commits
		
	
	
		
			601dec7777
			...
			0a08918657
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0a08918657 | |||
| a1868e1f44 | |||
| ad74bede04 | |||
| dd25d98e7d | |||
| 92754a64c4 | |||
| 459c16ba4f | |||
| e123b12cd9 | |||
| deaf1976f3 | |||
| 1dd184622f | |||
| 95aeb87899 | |||
| 07e90229ce | |||
| 2624f9dce8 | |||
| 2bcdeb42cd | |||
| 48494ee5e4 | |||
| 6c66c8d57b | |||
| e38f7ba293 | |||
| ab35847f23 | |||
| ebd44c55f5 | |||
| 4b4d81c4f3 | |||
| ec982fe168 | |||
| 564ac318bc | |||
| 65849ad82d | |||
| d6f7c19f71 | |||
| 11ec57387e | |||
| 5a6ed6135a | |||
| d01c343a7c | |||
| d7eacf0f52 | |||
| 0dc6b615e7 | |||
| 282fc61e50 | |||
| 1fec99bc92 | |||
| d39f5afd4b | |||
| 5c23120795 | 
					 38 changed files with 584 additions and 397 deletions
				
			
		
							
								
								
									
										38
									
								
								.pylintrc
									
										
									
									
									
								
							
							
						
						
									
										38
									
								
								.pylintrc
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -2,37 +2,7 @@
 | 
			
		|||
 | 
			
		||||
[MESSAGES CONTROL]
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,7 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
"""
 | 
			
		||||
Package Version
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from importlib.metadata import version
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}"
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"}])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue