Compare commits
	
		
			32 commits
		
	
	
		
			601dec7777
			...
			0a08918657
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0a08918657 | |||
| a1868e1f44 | |||
| ad74bede04 | |||
| dd25d98e7d | |||
| 92754a64c4 | |||
| 459c16ba4f | |||
| e123b12cd9 | |||
| deaf1976f3 | |||
| 1dd184622f | |||
| 95aeb87899 | |||
| 07e90229ce | |||
| 2624f9dce8 | |||
| 2bcdeb42cd | |||
| 48494ee5e4 | |||
| 6c66c8d57b | |||
| e38f7ba293 | |||
| ab35847f23 | |||
| ebd44c55f5 | |||
| 4b4d81c4f3 | |||
| ec982fe168 | |||
| 564ac318bc | |||
| 65849ad82d | |||
| d6f7c19f71 | |||
| 11ec57387e | |||
| 5a6ed6135a | |||
| d01c343a7c | |||
| d7eacf0f52 | |||
| 0dc6b615e7 | |||
| 282fc61e50 | |||
| 1fec99bc92 | |||
| d39f5afd4b | |||
| 5c23120795 | 
					 38 changed files with 584 additions and 397 deletions
				
			
		
							
								
								
									
										38
									
								
								.pylintrc
									
										
									
									
									
								
							
							
						
						
									
										38
									
								
								.pylintrc
									
										
									
									
									
								
							| 
						 | 
					@ -2,37 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[MESSAGES CONTROL]
 | 
					[MESSAGES CONTROL]
 | 
				
			||||||
disable=fixme,
 | 
					disable=fixme,
 | 
				
			||||||
        abstract-method,
 | 
					
 | 
				
			||||||
        arguments-differ,
 | 
					[SIMILARITIES]
 | 
				
			||||||
        arguments-renamed,
 | 
					# nb. cuts out some noise for duplicate-code
 | 
				
			||||||
        assignment-from-no-return,
 | 
					min-similarity-lines=5
 | 
				
			||||||
        attribute-defined-outside-init,
 | 
					 | 
				
			||||||
        consider-using-dict-comprehension,
 | 
					 | 
				
			||||||
        consider-using-dict-items,
 | 
					 | 
				
			||||||
        consider-using-generator,
 | 
					 | 
				
			||||||
        consider-using-get,
 | 
					 | 
				
			||||||
        consider-using-set-comprehension,
 | 
					 | 
				
			||||||
        duplicate-code,
 | 
					 | 
				
			||||||
        isinstance-second-argument-not-valid-type,
 | 
					 | 
				
			||||||
        keyword-arg-before-vararg,
 | 
					 | 
				
			||||||
        missing-function-docstring,
 | 
					 | 
				
			||||||
        missing-module-docstring,
 | 
					 | 
				
			||||||
        no-else-raise,
 | 
					 | 
				
			||||||
        no-member,
 | 
					 | 
				
			||||||
        not-callable,
 | 
					 | 
				
			||||||
        protected-access,
 | 
					 | 
				
			||||||
        redefined-outer-name,
 | 
					 | 
				
			||||||
        simplifiable-if-expression,
 | 
					 | 
				
			||||||
        singleton-comparison,
 | 
					 | 
				
			||||||
        super-init-not-called,
 | 
					 | 
				
			||||||
        too-few-public-methods,
 | 
					 | 
				
			||||||
        too-many-arguments,
 | 
					 | 
				
			||||||
        too-many-lines,
 | 
					 | 
				
			||||||
        too-many-locals,
 | 
					 | 
				
			||||||
        too-many-nested-blocks,
 | 
					 | 
				
			||||||
        too-many-positional-arguments,
 | 
					 | 
				
			||||||
        too-many-public-methods,
 | 
					 | 
				
			||||||
        too-many-statements,
 | 
					 | 
				
			||||||
        ungrouped-imports,
 | 
					 | 
				
			||||||
        unidiomatic-typecheck,
 | 
					 | 
				
			||||||
        unnecessary-comprehension,
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,9 @@ project.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
 | 
					.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen
 | 
				
			||||||
 | 
					    :target: https://github.com/pylint-dev/pylint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
 | 
					.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
 | 
				
			||||||
   :target: https://github.com/psf/black
 | 
					   :target: https://github.com/psf/black
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,7 @@
 | 
				
			||||||
# -*- coding: utf-8; -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Package Version
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from importlib.metadata import version
 | 
					from importlib.metadata import version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,13 +65,13 @@ class WebAppProvider(AppProvider):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
 | 
					        :returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if "web_handler" not in self.__dict__:
 | 
					        if "web" not in self.app.handlers:
 | 
				
			||||||
            spec = self.config.get(
 | 
					            spec = self.config.get(
 | 
				
			||||||
                f"{self.appname}.web.handler_spec",
 | 
					                f"{self.appname}.web.handler_spec",
 | 
				
			||||||
                default="wuttaweb.handler:WebHandler",
 | 
					                default="wuttaweb.handler:WebHandler",
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            self.web_handler = self.app.load_object(spec)(self.config)
 | 
					            self.app.handlers["web"] = self.app.load_object(spec)(self.config)
 | 
				
			||||||
        return self.web_handler
 | 
					        return self.app.handlers["web"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def make_wutta_config(settings, config_maker=None, **kwargs):
 | 
					def make_wutta_config(settings, config_maker=None, **kwargs):
 | 
				
			||||||
| 
						 | 
					@ -239,14 +239,14 @@ def make_wsgi_app(main_app=None, config=None):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # determine the app factory
 | 
					    # determine the app factory
 | 
				
			||||||
    if isinstance(main_app, str):
 | 
					    if isinstance(main_app, str):
 | 
				
			||||||
        make_wsgi_app = app.load_object(main_app)
 | 
					        factory = app.load_object(main_app)
 | 
				
			||||||
    elif callable(main_app):
 | 
					    elif callable(main_app):
 | 
				
			||||||
        make_wsgi_app = main_app
 | 
					        factory = main_app
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        raise ValueError("main_app must be spec or callable")
 | 
					        raise ValueError("main_app must be spec or callable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # construct a pyramid app "per usual"
 | 
					    # construct a pyramid app "per usual"
 | 
				
			||||||
    return make_wsgi_app({}, **settings)
 | 
					    return factory({}, **settings)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def make_asgi_app(main_app=None, config=None):
 | 
					def make_asgi_app(main_app=None, config=None):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,8 @@ class WuttaSecurityPolicy:
 | 
				
			||||||
        self.identity_cache = RequestLocalCache(self.load_identity)
 | 
					        self.identity_cache = RequestLocalCache(self.load_identity)
 | 
				
			||||||
        self.db_session = db_session or Session()
 | 
					        self.db_session = db_session or Session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def load_identity(self, request):
 | 
					    def load_identity(self, request):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        config = request.registry.settings["wutta_config"]
 | 
					        config = request.registry.settings["wutta_config"]
 | 
				
			||||||
        app = config.get_app()
 | 
					        app = config.get_app()
 | 
				
			||||||
        model = app.model
 | 
					        model = app.model
 | 
				
			||||||
| 
						 | 
					@ -122,22 +123,29 @@ class WuttaSecurityPolicy:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def identity(self, request):
 | 
					    def identity(self, request):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        return self.identity_cache.get_or_create(request)
 | 
					        return self.identity_cache.get_or_create(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def authenticated_userid(self, request):
 | 
					    def authenticated_userid(self, request):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        user = self.identity(request)
 | 
					        user = self.identity(request)
 | 
				
			||||||
        if user is not None:
 | 
					        if user is not None:
 | 
				
			||||||
            return user.uuid
 | 
					            return user.uuid
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remember(self, request, userid, **kw):
 | 
					    def remember(self, request, userid, **kw):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        return self.session_helper.remember(request, userid, **kw)
 | 
					        return self.session_helper.remember(request, userid, **kw)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def forget(self, request, **kw):
 | 
					    def forget(self, request, **kw):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        return self.session_helper.forget(request, **kw)
 | 
					        return self.session_helper.forget(request, **kw)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def permits(self, request, context, permission):  # pylint: disable=unused-argument
 | 
					    def permits(  # pylint: disable=unused-argument,empty-docstring
 | 
				
			||||||
 | 
					        self, request, context, permission
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # nb. root user can do anything
 | 
					        # nb. root user can do anything
 | 
				
			||||||
        if getattr(request, "is_root", False):
 | 
					        if getattr(request, "is_root", False):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@
 | 
				
			||||||
from wuttjamaican.email import EmailSetting
 | 
					from wuttjamaican.email import EmailSetting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class feedback(EmailSetting):  # pylint: disable=invalid-name
 | 
					class feedback(EmailSetting):  # pylint: disable=invalid-name,too-few-public-methods
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Sent when user submits feedback via the web app.
 | 
					    Sent when user submits feedback via the web app.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Base form classes
 | 
					Base form classes
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					# pylint: disable=too-many-lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from collections import OrderedDict
 | 
					from collections import OrderedDict
 | 
				
			||||||
| 
						 | 
					@ -36,13 +37,19 @@ from colanderalchemy import SQLAlchemySchemaNode
 | 
				
			||||||
from pyramid.renderers import render
 | 
					from pyramid.renderers import render
 | 
				
			||||||
from webhelpers2.html import HTML
 | 
					from webhelpers2.html import HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.util import FieldList, get_form_data, get_model_fields, make_json_safe
 | 
					from wuttaweb.util import (
 | 
				
			||||||
 | 
					    FieldList,
 | 
				
			||||||
 | 
					    get_form_data,
 | 
				
			||||||
 | 
					    get_model_fields,
 | 
				
			||||||
 | 
					    make_json_safe,
 | 
				
			||||||
 | 
					    render_vue_finalize,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Form:  # pylint: disable=too-many-instance-attributes
 | 
					class Form:  # pylint: disable=too-many-instance-attributes,too-many-public-methods
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Base class for all forms.
 | 
					    Base class for all forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -263,11 +270,12 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       If the :meth:`validate()` method was called, and it succeeded,
 | 
					       If the :meth:`validate()` method was called, and it succeeded,
 | 
				
			||||||
       this will be set to the validated data dict.
 | 
					       this will be set to the validated data dict.
 | 
				
			||||||
 | 
					 | 
				
			||||||
       Note that in all other cases, this attribute may not exist.
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    deform_form = None
 | 
				
			||||||
 | 
					    validated = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        request,
 | 
					        request,
 | 
				
			||||||
        fields=None,
 | 
					        fields=None,
 | 
				
			||||||
| 
						 | 
					@ -330,7 +338,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        self.model_class = model_class
 | 
					        self.model_class = model_class
 | 
				
			||||||
        self.model_instance = model_instance
 | 
					        self.model_instance = model_instance
 | 
				
			||||||
        if self.model_instance and not self.model_class:
 | 
					        if self.model_instance and not self.model_class:
 | 
				
			||||||
            if type(self.model_instance) is not dict:
 | 
					            if not isinstance(self.model_instance, dict):
 | 
				
			||||||
                self.model_class = type(self.model_instance)
 | 
					                self.model_class = type(self.model_instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.set_fields(fields or self.get_fields())
 | 
					        self.set_fields(fields or self.get_fields())
 | 
				
			||||||
| 
						 | 
					@ -873,7 +881,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        Return the :class:`deform:deform.Form` instance for the form,
 | 
					        Return the :class:`deform:deform.Form` instance for the form,
 | 
				
			||||||
        generating it automatically if necessary.
 | 
					        generating it automatically if necessary.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, "deform_form"):
 | 
					        if not self.deform_form:
 | 
				
			||||||
            schema = self.get_schema()
 | 
					            schema = self.get_schema()
 | 
				
			||||||
            kwargs = {}
 | 
					            kwargs = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -982,7 +990,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        output = render(template, context)
 | 
					        output = render(template, context)
 | 
				
			||||||
        return HTML.literal(output)
 | 
					        return HTML.literal(output)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_vue_field(  # pylint: disable=unused-argument
 | 
					    def render_vue_field(  # pylint: disable=unused-argument,too-many-locals
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        fieldname,
 | 
					        fieldname,
 | 
				
			||||||
        readonly=None,
 | 
					        readonly=None,
 | 
				
			||||||
| 
						 | 
					@ -1093,12 +1101,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        The actual output may depend on various form attributes, in
 | 
					        The actual output may depend on various form attributes, in
 | 
				
			||||||
        particular :attr:`vue_tagname`.
 | 
					        particular :attr:`vue_tagname`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
 | 
					        return render_vue_finalize(self.vue_tagname, self.vue_component)
 | 
				
			||||||
        make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
 | 
					 | 
				
			||||||
        return HTML.tag(
 | 
					 | 
				
			||||||
            "script",
 | 
					 | 
				
			||||||
            c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_vue_model_data(self):
 | 
					    def get_vue_model_data(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -1124,7 +1127,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
            # for now we explicitly translate here, ugh.  also
 | 
					            # for now we explicitly translate here, ugh.  also
 | 
				
			||||||
            # note this does not yet allow for null values.. :(
 | 
					            # note this does not yet allow for null values.. :(
 | 
				
			||||||
            if isinstance(field.typ, colander.Boolean):
 | 
					            if isinstance(field.typ, colander.Boolean):
 | 
				
			||||||
                value = True if value == field.typ.true_val else False
 | 
					                value = value == field.typ.true_val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            model_data[field.oid] = make_json_safe(value)
 | 
					            model_data[field.oid] = make_json_safe(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1173,9 +1176,9 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        :attr:`validated` attribute.
 | 
					        :attr:`validated` attribute.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        However if the data is not valid, ``False`` is returned, and
 | 
					        However if the data is not valid, ``False`` is returned, and
 | 
				
			||||||
        there will be no :attr:`validated` attribute.  In that case
 | 
					        the :attr:`validated` attribute will be ``None``.  In that
 | 
				
			||||||
        you should inspect the form errors to learn/display what went
 | 
					        case you should inspect the form errors to learn/display what
 | 
				
			||||||
        wrong for the user's sake.  See also
 | 
					        went wrong for the user's sake.  See also
 | 
				
			||||||
        :meth:`get_field_errors()`.
 | 
					        :meth:`get_field_errors()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This uses :meth:`deform:deform.Field.validate()` under the
 | 
					        This uses :meth:`deform:deform.Field.validate()` under the
 | 
				
			||||||
| 
						 | 
					@ -1191,8 +1194,7 @@ class Form:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: Data dict, or ``False``.
 | 
					        :returns: Data dict, or ``False``.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if hasattr(self, "validated"):
 | 
					        self.validated = None
 | 
				
			||||||
            del self.validated
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.request.method != "POST":
 | 
					        if self.request.method != "POST":
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ class WuttaDateTime(colander.DateTime):
 | 
				
			||||||
        node.raise_invalid("Invalid date and/or time")
 | 
					        node.raise_invalid("Invalid date and/or time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ObjectNode(colander.SchemaNode):
 | 
					class ObjectNode(colander.SchemaNode):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Custom schema node class which adds methods for compatibility with
 | 
					    Custom schema node class which adds methods for compatibility with
 | 
				
			||||||
    ColanderAlchemy.  This is a direct subclass of
 | 
					    ColanderAlchemy.  This is a direct subclass of
 | 
				
			||||||
| 
						 | 
					@ -183,7 +183,7 @@ class WuttaDictEnum(colander.String):
 | 
				
			||||||
    def widget_maker(self, **kwargs):  # pylint: disable=empty-docstring
 | 
					    def widget_maker(self, **kwargs):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        if "values" not in kwargs:
 | 
					        if "values" not in kwargs:
 | 
				
			||||||
            kwargs["values"] = [(k, v) for k, v in self.enum_dct.items()]
 | 
					            kwargs["values"] = list(self.enum_dct.items())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return widgets.SelectWidget(**kwargs)
 | 
					        return widgets.SelectWidget(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -288,13 +288,8 @@ class ObjectRef(colander.SchemaType):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    default_empty_option = ("", "(none)")
 | 
					    default_empty_option = ("", "(none)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(self, request, *args, **kwargs):
 | 
				
			||||||
        self,
 | 
					        empty_option = kwargs.pop("empty_option", None)
 | 
				
			||||||
        request,
 | 
					 | 
				
			||||||
        empty_option=None,
 | 
					 | 
				
			||||||
        *args,
 | 
					 | 
				
			||||||
        **kwargs,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        # nb. allow session injection for tests
 | 
					        # nb. allow session injection for tests
 | 
				
			||||||
        self.session = kwargs.pop("session", Session())
 | 
					        self.session = kwargs.pop("session", Session())
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
| 
						 | 
					@ -381,7 +376,9 @@ class ObjectRef(colander.SchemaType):
 | 
				
			||||||
        if not value:
 | 
					        if not value:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if isinstance(value, self.model_class):
 | 
					        if isinstance(  # pylint: disable=isinstance-second-argument-not-valid-type
 | 
				
			||||||
 | 
					            value, self.model_class
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            return value
 | 
					            return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # fetch object from DB
 | 
					        # fetch object from DB
 | 
				
			||||||
| 
						 | 
					@ -475,8 +472,9 @@ class PersonRef(ObjectRef):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        return query.order_by(self.model_class.full_name)
 | 
					        return query.order_by(self.model_class.full_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object_url(self, person):  # pylint: disable=empty-docstring
 | 
					    def get_object_url(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        person = obj
 | 
				
			||||||
        return self.request.route_url("people.view", uuid=person.uuid)
 | 
					        return self.request.route_url("people.view", uuid=person.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -499,8 +497,9 @@ class RoleRef(ObjectRef):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        return query.order_by(self.model_class.name)
 | 
					        return query.order_by(self.model_class.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object_url(self, role):  # pylint: disable=empty-docstring
 | 
					    def get_object_url(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        role = obj
 | 
				
			||||||
        return self.request.route_url("roles.view", uuid=role.uuid)
 | 
					        return self.request.route_url("roles.view", uuid=role.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -523,8 +522,9 @@ class UserRef(ObjectRef):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        return query.order_by(self.model_class.username)
 | 
					        return query.order_by(self.model_class.username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object_url(self, user):  # pylint: disable=empty-docstring
 | 
					    def get_object_url(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        user = obj
 | 
				
			||||||
        return self.request.route_url("users.view", uuid=user.uuid)
 | 
					        return self.request.route_url("users.view", uuid=user.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -557,7 +557,7 @@ class RoleRefs(WuttaSet):
 | 
				
			||||||
                auth.get_role_authenticated(session),
 | 
					                auth.get_role_authenticated(session),
 | 
				
			||||||
                auth.get_role_anonymous(session),
 | 
					                auth.get_role_anonymous(session),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            avoid = set([role.uuid for role in avoid])
 | 
					            avoid = {role.uuid for role in avoid}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # also avoid admin unless current user is root
 | 
					            # also avoid admin unless current user is root
 | 
				
			||||||
            if not self.request.is_root:
 | 
					            if not self.request.is_root:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,7 +103,8 @@ class ObjectRefWidget(SelectWidget):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    readonly_template = "readonly/objectref"
 | 
					    readonly_template = "readonly/objectref"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, request, url=None, *args, **kwargs):
 | 
					    def __init__(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        url = kwargs.pop("url", None)
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self.request = request
 | 
					        self.request = request
 | 
				
			||||||
        self.url = url
 | 
					        self.url = url
 | 
				
			||||||
| 
						 | 
					@ -299,7 +300,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
 | 
				
			||||||
        return super().serialize(field, cstruct, **kw)
 | 
					        return super().serialize(field, cstruct, **kw)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileDownloadWidget(Widget):
 | 
					class FileDownloadWidget(Widget):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
 | 
					    Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
 | 
				
			||||||
    fields.
 | 
					    fields.
 | 
				
			||||||
| 
						 | 
					@ -357,7 +358,7 @@ class FileDownloadWidget(Widget):
 | 
				
			||||||
        return humanize.naturalsize(size)
 | 
					        return humanize.naturalsize(size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GridWidget(Widget):
 | 
					class GridWidget(Widget):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Widget for fields whose data is represented by a :term:`grid`.
 | 
					    Widget for fields whose data is represented by a :term:`grid`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -406,6 +407,7 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    readonly_template = "readonly/rolerefs"
 | 
					    readonly_template = "readonly/rolerefs"
 | 
				
			||||||
 | 
					    session = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def serialize(self, field, cstruct, **kw):  # pylint: disable=empty-docstring
 | 
					    def serialize(self, field, cstruct, **kw):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
| 
						 | 
					@ -462,6 +464,7 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template = "permissions"
 | 
					    template = "permissions"
 | 
				
			||||||
    readonly_template = "readonly/permissions"
 | 
					    readonly_template = "readonly/permissions"
 | 
				
			||||||
 | 
					    permissions = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def serialize(self, field, cstruct, **kw):  # pylint: disable=empty-docstring
 | 
					    def serialize(self, field, cstruct, **kw):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
| 
						 | 
					@ -512,7 +515,7 @@ class EmailRecipientsWidget(TextAreaWidget):
 | 
				
			||||||
        return ", ".join(values)
 | 
					        return ", ".join(values)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BatchIdWidget(Widget):
 | 
					class BatchIdWidget(Widget):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Widget for use with the
 | 
					    Widget for use with the
 | 
				
			||||||
    :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
 | 
					    :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Base grid classes
 | 
					Base grid classes
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					# pylint: disable=too-many-lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import functools
 | 
					import functools
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
| 
						 | 
					@ -38,7 +39,12 @@ from pyramid.renderers import render
 | 
				
			||||||
from webhelpers2.html import HTML
 | 
					from webhelpers2.html import HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican.db.util import UUID
 | 
					from wuttjamaican.db.util import UUID
 | 
				
			||||||
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
 | 
					from wuttaweb.util import (
 | 
				
			||||||
 | 
					    FieldList,
 | 
				
			||||||
 | 
					    get_model_fields,
 | 
				
			||||||
 | 
					    make_json_safe,
 | 
				
			||||||
 | 
					    render_vue_finalize,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
 | 
					from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,7 +59,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Grid:  # pylint: disable=too-many-instance-attributes
 | 
					class Grid:  # pylint: disable=too-many-instance-attributes,too-many-public-methods
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Base class for all :term:`grids <grid>`.
 | 
					    Base class for all :term:`grids <grid>`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -263,7 +269,7 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
       ``active_sorters`` defines the "current/effective" sorters.
 | 
					       ``active_sorters`` defines the "current/effective" sorters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       This attribute is set by :meth:`load_settings()`; until that is
 | 
					       This attribute is set by :meth:`load_settings()`; until that is
 | 
				
			||||||
       called it will not exist.
 | 
					       called its value will be ``None``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       This is conceptually a "subset" of :attr:`sorters` although a
 | 
					       This is conceptually a "subset" of :attr:`sorters` although a
 | 
				
			||||||
       different format is used here::
 | 
					       different format is used here::
 | 
				
			||||||
| 
						 | 
					@ -371,7 +377,11 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
       See also :meth:`add_tool()` and :meth:`set_tools()`.
 | 
					       See also :meth:`add_tool()` and :meth:`set_tools()`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    active_sorters = None
 | 
				
			||||||
 | 
					    joined = None
 | 
				
			||||||
 | 
					    pager = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        request,
 | 
					        request,
 | 
				
			||||||
        vue_tagname="wutta-grid",
 | 
					        vue_tagname="wutta-grid",
 | 
				
			||||||
| 
						 | 
					@ -688,7 +698,7 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
            "percent": self.render_percent,
 | 
					            "percent": self.render_percent,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if renderer in builtins:
 | 
					        if renderer in builtins:  # pylint: disable=consider-using-get
 | 
				
			||||||
            renderer = builtins[renderer]
 | 
					            renderer = builtins[renderer]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if kwargs:
 | 
					        if kwargs:
 | 
				
			||||||
| 
						 | 
					@ -1086,8 +1096,8 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        # TODO: this should be improved; is needed in tailbone for
 | 
					        # TODO: this should be improved; is needed in tailbone for
 | 
				
			||||||
        # multi-column sorting with sqlalchemy queries
 | 
					        # multi-column sorting with sqlalchemy queries
 | 
				
			||||||
        if model_property:
 | 
					        if model_property:
 | 
				
			||||||
            sorter._class = model_class
 | 
					            sorter._class = model_class  # pylint: disable=protected-access
 | 
				
			||||||
            sorter._column = model_property
 | 
					            sorter._column = model_property  # pylint: disable=protected-access
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return sorter
 | 
					        return sorter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1372,10 +1382,10 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        if filterinfo and callable(filterinfo):
 | 
					        if filterinfo and callable(filterinfo):
 | 
				
			||||||
            # filtr = filterinfo
 | 
					            # filtr = filterinfo
 | 
				
			||||||
            raise NotImplementedError
 | 
					            raise NotImplementedError
 | 
				
			||||||
        else:
 | 
					
 | 
				
			||||||
            kwargs["key"] = key
 | 
					        kwargs["key"] = key
 | 
				
			||||||
            kwargs.setdefault("label", self.get_label(key))
 | 
					        kwargs.setdefault("label", self.get_label(key))
 | 
				
			||||||
            filtr = self.make_filter(filterinfo or key, **kwargs)
 | 
					        filtr = self.make_filter(filterinfo or key, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.filters[key] = filtr
 | 
					        self.filters[key] = filtr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1473,7 +1483,9 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
    # configuration methods
 | 
					    # configuration methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def load_settings(self, persist=True):  # pylint: disable=too-many-branches
 | 
					    def load_settings(  # pylint: disable=too-many-branches,too-many-statements
 | 
				
			||||||
 | 
					        self, persist=True
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Load all effective settings for the grid.
 | 
					        Load all effective settings for the grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1626,7 +1638,7 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_setting(  # pylint: disable=empty-docstring
 | 
					    def get_setting(  # pylint: disable=empty-docstring,too-many-arguments,too-many-positional-arguments
 | 
				
			||||||
        self, settings, key, src="session", default=None, normalize=lambda v: v
 | 
					        self, settings, key, src="session", default=None, normalize=lambda v: v
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
| 
						 | 
					@ -2205,12 +2217,7 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        The actual output may depend on various grid attributes, in
 | 
					        The actual output may depend on various grid attributes, in
 | 
				
			||||||
        particular :attr:`vue_tagname`.
 | 
					        particular :attr:`vue_tagname`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
 | 
					        return render_vue_finalize(self.vue_tagname, self.vue_component)
 | 
				
			||||||
        make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
 | 
					 | 
				
			||||||
        return HTML.tag(
 | 
					 | 
				
			||||||
            "script",
 | 
					 | 
				
			||||||
            c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_vue_columns(self):
 | 
					    def get_vue_columns(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -2290,12 +2297,11 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        :returns: The first sorter in format ``[sortkey, sortdir]``,
 | 
					        :returns: The first sorter in format ``[sortkey, sortdir]``,
 | 
				
			||||||
           or ``None``.
 | 
					           or ``None``.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if hasattr(self, "active_sorters"):
 | 
					        if self.active_sorters:
 | 
				
			||||||
            if self.active_sorters:
 | 
					            sorter = self.active_sorters[0]
 | 
				
			||||||
                sorter = self.active_sorters[0]
 | 
					            return [sorter["key"], sorter["dir"]]
 | 
				
			||||||
                return [sorter["key"], sorter["dir"]]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        elif self.sort_defaults:
 | 
					        if self.sort_defaults:
 | 
				
			||||||
            sorter = self.sort_defaults[0]
 | 
					            sorter = self.sort_defaults[0]
 | 
				
			||||||
            return [sorter.sortkey, sorter.sortdir]
 | 
					            return [sorter.sortkey, sorter.sortdir]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2386,9 +2392,9 @@ class Grid:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
            record = make_json_safe(record, warn=False)
 | 
					            record = make_json_safe(record, warn=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # customize value rendering where applicable
 | 
					            # customize value rendering where applicable
 | 
				
			||||||
            for key in self.renderers:
 | 
					            for key, renderer in self.renderers.items():
 | 
				
			||||||
                value = record.get(key, None)
 | 
					                value = record.get(key, None)
 | 
				
			||||||
                record[key] = self.renderers[key](original_record, key, value)
 | 
					                record[key] = renderer(original_record, key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # add action urls to each record
 | 
					            # add action urls to each record
 | 
				
			||||||
            for action in self.actions:
 | 
					            for action in self.actions:
 | 
				
			||||||
| 
						 | 
					@ -2537,7 +2543,7 @@ class GridAction:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
       Optional HTML class attribute for the action's ``<a>`` tag.
 | 
					       Optional HTML class attribute for the action's ``<a>`` tag.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        request,
 | 
					        request,
 | 
				
			||||||
        key,
 | 
					        key,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,9 +59,10 @@ class GridFilter:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param request: Current :term:`request` object.
 | 
					    :param request: Current :term:`request` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param model_property: Property of a model class, representing the
 | 
					    :param nullable: Boolean indicating whether the filter should
 | 
				
			||||||
       column by which to filter.  For instance,
 | 
					       include ``is_null`` and ``is_not_null`` verbs.  If not
 | 
				
			||||||
       ``model.Person.full_name``.
 | 
					       specified, the column will be inspected (if possible) and use
 | 
				
			||||||
 | 
					       its nullable flag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param \\**kwargs: Any additional kwargs will be set as attributes
 | 
					    :param \\**kwargs: Any additional kwargs will be set as attributes
 | 
				
			||||||
       on the filter instance.
 | 
					       on the filter instance.
 | 
				
			||||||
| 
						 | 
					@ -169,13 +170,14 @@ class GridFilter:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        "is_not_null",
 | 
					        "is_not_null",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        request,
 | 
					        request,
 | 
				
			||||||
        key,
 | 
					        key,
 | 
				
			||||||
        label=None,
 | 
					        label=None,
 | 
				
			||||||
        verbs=None,
 | 
					        verbs=None,
 | 
				
			||||||
        choices=None,
 | 
					        choices=None,
 | 
				
			||||||
 | 
					        nullable=None,
 | 
				
			||||||
        default_active=False,
 | 
					        default_active=False,
 | 
				
			||||||
        default_verb=None,
 | 
					        default_verb=None,
 | 
				
			||||||
        default_value=None,
 | 
					        default_value=None,
 | 
				
			||||||
| 
						 | 
					@ -196,10 +198,14 @@ class GridFilter:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
            self.verbs = verbs
 | 
					            self.verbs = verbs
 | 
				
			||||||
        if default_verb:
 | 
					        if default_verb:
 | 
				
			||||||
            self.default_verb = default_verb
 | 
					            self.default_verb = default_verb
 | 
				
			||||||
 | 
					        self.verb = None  # active verb is set later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # choices
 | 
					        # choices
 | 
				
			||||||
        self.set_choices(choices or {})
 | 
					        self.set_choices(choices or {})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # nullable
 | 
				
			||||||
 | 
					        self.nullable = nullable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # value
 | 
					        # value
 | 
				
			||||||
        self.default_value = default_value
 | 
					        self.default_value = default_value
 | 
				
			||||||
        self.value = self.default_value
 | 
					        self.value = self.default_value
 | 
				
			||||||
| 
						 | 
					@ -248,7 +254,7 @@ class GridFilter:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
        Returns a dict of all defined verb labels.
 | 
					        Returns a dict of all defined verb labels.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # TODO: should traverse hierarchy
 | 
					        # TODO: should traverse hierarchy
 | 
				
			||||||
        labels = dict([(verb, verb) for verb in self.get_verbs()])
 | 
					        labels = {verb: verb for verb in self.get_verbs()}
 | 
				
			||||||
        labels.update(self.default_verb_labels)
 | 
					        labels.update(self.default_verb_labels)
 | 
				
			||||||
        return labels
 | 
					        return labels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -378,7 +384,7 @@ class GridFilter:  # pylint: disable=too-many-instance-attributes
 | 
				
			||||||
            raise VerbNotSupported(verb)
 | 
					            raise VerbNotSupported(verb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # invoke filter method
 | 
					        # invoke filter method
 | 
				
			||||||
        return func(data, value)
 | 
					        return func(data, value)  # pylint: disable=not-callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_is_any(self, data, value):  # pylint: disable=unused-argument
 | 
					    def filter_is_any(self, data, value):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -398,18 +404,12 @@ class AlchemyFilter(GridFilter):
 | 
				
			||||||
    :param model_property: Property of a model class, representing the
 | 
					    :param model_property: Property of a model class, representing the
 | 
				
			||||||
       column by which to filter.  For instance,
 | 
					       column by which to filter.  For instance,
 | 
				
			||||||
       ``model.Person.full_name``.
 | 
					       ``model.Person.full_name``.
 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param nullable: Boolean indicating whether the filter should
 | 
					 | 
				
			||||||
       include ``is_null`` and ``is_not_null`` verbs.  If not
 | 
					 | 
				
			||||||
       specified, the column will be inspected and use its nullable
 | 
					 | 
				
			||||||
       flag.
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        nullable = kwargs.pop("nullable", None)
 | 
					        self.model_property = kwargs.pop("model_property")
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.nullable = nullable
 | 
					 | 
				
			||||||
        if self.nullable is None:
 | 
					        if self.nullable is None:
 | 
				
			||||||
            columns = self.model_property.prop.columns
 | 
					            columns = self.model_property.prop.columns
 | 
				
			||||||
            if len(columns) == 1:
 | 
					            if len(columns) == 1:
 | 
				
			||||||
| 
						 | 
					@ -446,7 +446,7 @@ class AlchemyFilter(GridFilter):
 | 
				
			||||||
        # probably does not expect that, so explicitly include them.
 | 
					        # probably does not expect that, so explicitly include them.
 | 
				
			||||||
        return query.filter(
 | 
					        return query.filter(
 | 
				
			||||||
            sa.or_(
 | 
					            sa.or_(
 | 
				
			||||||
                self.model_property == None,
 | 
					                self.model_property == None,  # pylint: disable=singleton-comparison
 | 
				
			||||||
                self.model_property != value,
 | 
					                self.model_property != value,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -491,14 +491,18 @@ class AlchemyFilter(GridFilter):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Filter data with an ``IS NULL`` query.  The value is ignored.
 | 
					        Filter data with an ``IS NULL`` query.  The value is ignored.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return query.filter(self.model_property == None)
 | 
					        return query.filter(
 | 
				
			||||||
 | 
					            self.model_property == None  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_is_not_null(self, query, value):  # pylint: disable=unused-argument
 | 
					    def filter_is_not_null(self, query, value):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Filter data with an ``IS NOT NULL`` query.  The value is
 | 
					        Filter data with an ``IS NOT NULL`` query.  The value is
 | 
				
			||||||
        ignored.
 | 
					        ignored.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return query.filter(self.model_property != None)
 | 
					        return query.filter(
 | 
				
			||||||
 | 
					            self.model_property != None  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StringAlchemyFilter(AlchemyFilter):
 | 
					class StringAlchemyFilter(AlchemyFilter):
 | 
				
			||||||
| 
						 | 
					@ -550,7 +554,12 @@ class StringAlchemyFilter(AlchemyFilter):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # sql probably excludes null values from results, but user
 | 
					        # sql probably excludes null values from results, but user
 | 
				
			||||||
        # probably does not expect that, so explicitly include them.
 | 
					        # probably does not expect that, so explicitly include them.
 | 
				
			||||||
        return query.filter(sa.or_(self.model_property == None, sa.and_(*criteria)))
 | 
					        return query.filter(
 | 
				
			||||||
 | 
					            sa.or_(
 | 
				
			||||||
 | 
					                self.model_property == None,  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					                sa.and_(*criteria),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NumericAlchemyFilter(AlchemyFilter):
 | 
					class NumericAlchemyFilter(AlchemyFilter):
 | 
				
			||||||
| 
						 | 
					@ -628,14 +637,18 @@ class BooleanAlchemyFilter(AlchemyFilter):
 | 
				
			||||||
        Filter data with an "is true" condition.  The value is
 | 
					        Filter data with an "is true" condition.  The value is
 | 
				
			||||||
        ignored.
 | 
					        ignored.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return query.filter(self.model_property == True)
 | 
					        return query.filter(
 | 
				
			||||||
 | 
					            self.model_property == True  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_is_false(self, query, value):  # pylint: disable=unused-argument
 | 
					    def filter_is_false(self, query, value):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Filter data with an "is false" condition.  The value is
 | 
					        Filter data with an "is false" condition.  The value is
 | 
				
			||||||
        ignored.
 | 
					        ignored.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return query.filter(self.model_property == False)
 | 
					        return query.filter(
 | 
				
			||||||
 | 
					            self.model_property == False  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_is_false_null(self, query, value):  # pylint: disable=unused-argument
 | 
					    def filter_is_false_null(self, query, value):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -643,7 +656,10 @@ class BooleanAlchemyFilter(AlchemyFilter):
 | 
				
			||||||
        ignored.
 | 
					        ignored.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return query.filter(
 | 
					        return query.filter(
 | 
				
			||||||
            sa.or_(self.model_property == False, self.model_property == None)
 | 
					            sa.or_(
 | 
				
			||||||
 | 
					                self.model_property == False,  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					                self.model_property == None,  # pylint: disable=singleton-comparison
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -229,7 +229,7 @@ class MenuHandler(GenericHandler):
 | 
				
			||||||
        # that somewhat to produce our final menus
 | 
					        # that somewhat to produce our final menus
 | 
				
			||||||
        self._mark_allowed(request, raw_menus)
 | 
					        self._mark_allowed(request, raw_menus)
 | 
				
			||||||
        final_menus = []
 | 
					        final_menus = []
 | 
				
			||||||
        for topitem in raw_menus:
 | 
					        for topitem in raw_menus:  # pylint: disable=too-many-nested-blocks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if topitem["allowed"]:
 | 
					            if topitem["allowed"]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -323,7 +323,7 @@ class MenuHandler(GenericHandler):
 | 
				
			||||||
        Traverse the menu set, and mark each item as "allowed" (or
 | 
					        Traverse the menu set, and mark each item as "allowed" (or
 | 
				
			||||||
        not) based on current user permissions.
 | 
					        not) based on current user permissions.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for topitem in menus:
 | 
					        for topitem in menus:  # pylint: disable=too-many-nested-blocks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if topitem.get("type", "menu") == "link":
 | 
					            if topitem.get("type", "menu") == "link":
 | 
				
			||||||
                topitem["allowed"] = True
 | 
					                topitem["allowed"] = True
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +91,7 @@ class SessionProgress(ProgressBase):  # pylint: disable=too-many-instance-attrib
 | 
				
			||||||
       :attr:`success_url`.
 | 
					       :attr:`success_url`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments,super-init-not-called
 | 
				
			||||||
        self, request, key, success_msg=None, success_url=None, error_url=None
 | 
					        self, request, key, success_msg=None, success_url=None, error_url=None
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        self.request = request
 | 
					        self.request = request
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  wuttaweb -- Web App for Wutta Framework
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -70,5 +70,5 @@ testing = Resource(img, "testing.png", renderer=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: should consider deprecating this?
 | 
					# TODO: should consider deprecating this?
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    config.add_static_view("wuttaweb", "wuttaweb:static")
 | 
					    config.add_static_view("wuttaweb", "wuttaweb:static")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -159,20 +159,20 @@ def new_request(event):
 | 
				
			||||||
        Register a Vue 3 component, so the base template knows to
 | 
					        Register a Vue 3 component, so the base template knows to
 | 
				
			||||||
        declare it for use within the app (page).
 | 
					        declare it for use within the app (page).
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(request, "_wuttaweb_registered_components"):
 | 
					        if not hasattr(request, "wuttaweb_registered_components"):
 | 
				
			||||||
            request._wuttaweb_registered_components = OrderedDict()
 | 
					            request.wuttaweb_registered_components = OrderedDict()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if tagname in request._wuttaweb_registered_components:
 | 
					        if tagname in request.wuttaweb_registered_components:
 | 
				
			||||||
            log.warning(
 | 
					            log.warning(
 | 
				
			||||||
                "component with tagname '%s' already registered "
 | 
					                "component with tagname '%s' already registered "
 | 
				
			||||||
                "with class '%s' but we are replacing that "
 | 
					                "with class '%s' but we are replacing that "
 | 
				
			||||||
                "with class '%s'",
 | 
					                "with class '%s'",
 | 
				
			||||||
                tagname,
 | 
					                tagname,
 | 
				
			||||||
                request._wuttaweb_registered_components[tagname],
 | 
					                request.wuttaweb_registered_components[tagname],
 | 
				
			||||||
                classname,
 | 
					                classname,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request._wuttaweb_registered_components[tagname] = classname
 | 
					        request.wuttaweb_registered_components[tagname] = classname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    request.register_component = register_component
 | 
					    request.register_component = register_component
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -411,7 +411,7 @@ def before_render(event):
 | 
				
			||||||
        context["available_themes"] = get_available_themes(config)
 | 
					        context["available_themes"] = get_available_themes(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    config.add_subscriber(new_request, "pyramid.events.NewRequest")
 | 
					    config.add_subscriber(new_request, "pyramid.events.NewRequest")
 | 
				
			||||||
    config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
 | 
					    config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
 | 
				
			||||||
    config.add_subscriber(before_render, "pyramid.events.BeforeRender")
 | 
					    config.add_subscriber(before_render, "pyramid.events.BeforeRender")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,8 +58,8 @@
 | 
				
			||||||
    const app = createApp()
 | 
					    const app = createApp()
 | 
				
			||||||
    app.component('vue-fontawesome', FontAwesomeIcon)
 | 
					    app.component('vue-fontawesome', FontAwesomeIcon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    % if hasattr(request, '_wuttaweb_registered_components'):
 | 
					    % if hasattr(request, 'wuttaweb_registered_components'):
 | 
				
			||||||
        % for tagname, classname in request._wuttaweb_registered_components.items():
 | 
					        % for tagname, classname in request.wuttaweb_registered_components.items():
 | 
				
			||||||
            app.component('${tagname}', ${classname})
 | 
					            app.component('${tagname}', ${classname})
 | 
				
			||||||
        % endfor
 | 
					        % endfor
 | 
				
			||||||
    % endif
 | 
					    % endif
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  wuttaweb -- Web App for Wutta Framework
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -39,10 +39,14 @@ class WebTestCase(DataTestCase):
 | 
				
			||||||
    Base class for test suites requiring a full (typical) web app.
 | 
					    Base class for test suites requiring a full (typical) web app.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        self.setup_web()
 | 
					        self.setup_web()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setup_web(self):
 | 
					    def setup_web(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Perform setup for the testing web app.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        self.setup_db()
 | 
					        self.setup_db()
 | 
				
			||||||
        self.request = self.make_request()
 | 
					        self.request = self.make_request()
 | 
				
			||||||
        self.pyramid_config = testing.setUp(
 | 
					        self.pyramid_config = testing.setUp(
 | 
				
			||||||
| 
						 | 
					@ -87,8 +91,14 @@ class WebTestCase(DataTestCase):
 | 
				
			||||||
        self.teardown_web()
 | 
					        self.teardown_web()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def teardown_web(self):
 | 
					    def teardown_web(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Perform teardown for the testing web app.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        testing.tearDown()
 | 
					        testing.tearDown()
 | 
				
			||||||
        self.teardown_db()
 | 
					        self.teardown_db()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_request(self):
 | 
					    def make_request(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Make and return a new dummy request object.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        return testing.DummyRequest()
 | 
					        return testing.DummyRequest()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -618,6 +618,71 @@ def make_json_safe(value, key=None, warn=True):
 | 
				
			||||||
    return value
 | 
					    return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def render_vue_finalize(vue_tagname, vue_component):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Render the Vue "finalize" script for a form or grid component.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is a convenience for shared logic; it returns e.g.:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       <script>
 | 
				
			||||||
 | 
					         WuttaGrid.data = function() { return WuttaGridData }
 | 
				
			||||||
 | 
					         Vue.component('wutta-grid', WuttaGrid)
 | 
				
			||||||
 | 
					       </script>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    set_data = f"{vue_component}.data = function() {{ return {vue_component}Data }}"
 | 
				
			||||||
 | 
					    make_component = f"Vue.component('{vue_tagname}', {vue_component})"
 | 
				
			||||||
 | 
					    return HTML.tag(
 | 
				
			||||||
 | 
					        "script",
 | 
				
			||||||
 | 
					        c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_users_grid(request, **kwargs):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Make and return a users (sub)grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This grid is shown for the Users field when viewing a Person or
 | 
				
			||||||
 | 
					    Role, for instance.  It is called by the following methods:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * :meth:`wuttaweb.views.people.PersonView.make_users_grid()`
 | 
				
			||||||
 | 
					    * :meth:`wuttaweb.views.roles.RoleView.make_users_grid()`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
 | 
					       instance.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    config = request.wutta_config
 | 
				
			||||||
 | 
					    app = config.get_app()
 | 
				
			||||||
 | 
					    model = app.model
 | 
				
			||||||
 | 
					    web = app.get_web_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if "key" not in kwargs:
 | 
				
			||||||
 | 
					        route_prefix = kwargs.pop("route_prefix")
 | 
				
			||||||
 | 
					        kwargs["key"] = f"{route_prefix}.view.users"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    kwargs.setdefault("model_class", model.User)
 | 
				
			||||||
 | 
					    grid = web.make_grid(request, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if request.has_perm("users.view"):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def view_url(user, i):  # pylint: disable=unused-argument
 | 
				
			||||||
 | 
					            return request.route_url("users.view", uuid=user.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        grid.add_action("view", icon="eye", url=view_url)
 | 
				
			||||||
 | 
					        grid.set_link("person")
 | 
				
			||||||
 | 
					        grid.set_link("username")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if request.has_perm("users.edit"):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def edit_url(user, i):  # pylint: disable=unused-argument
 | 
				
			||||||
 | 
					            return request.route_url("users.edit", uuid=user.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        grid.add_action("edit", url=edit_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##############################
 | 
					##############################
 | 
				
			||||||
# theme functions
 | 
					# theme functions
 | 
				
			||||||
##############################
 | 
					##############################
 | 
				
			||||||
| 
						 | 
					@ -757,14 +822,14 @@ def set_app_theme(request, theme, session=None):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # there's only one global template lookup; can get to it via any renderer
 | 
					    # there's only one global template lookup; can get to it via any renderer
 | 
				
			||||||
    # but should *not* use /base.mako since that one is about to get volatile
 | 
					    # but should *not* use /base.mako since that one is about to get volatile
 | 
				
			||||||
    renderer = get_renderer("/menu.mako")
 | 
					    renderer = get_renderer("/page.mako")
 | 
				
			||||||
    lookup = renderer.lookup
 | 
					    lookup = renderer.lookup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # overwrite first entry in lookup's directory list
 | 
					    # overwrite first entry in lookup's directory list
 | 
				
			||||||
    lookup.directories[0] = theme_path
 | 
					    lookup.directories[0] = theme_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # clear template cache for lookup object, so it will reload each (as needed)
 | 
					    # clear template cache for lookup object, so it will reload each (as needed)
 | 
				
			||||||
    lookup._collection.clear()
 | 
					    lookup._collection.clear()  # pylint: disable=protected-access
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # persist current theme in db settings
 | 
					    # persist current theme in db settings
 | 
				
			||||||
    with app.short_session(session=session) as s:
 | 
					    with app.short_session(session=session) as s:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  wuttaweb -- Web App for Wutta Framework
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -34,5 +34,5 @@ from .base import View
 | 
				
			||||||
from .master import MasterView
 | 
					from .master import MasterView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    config.include("wuttaweb.views.essential")
 | 
					    config.include("wuttaweb.views.essential")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,8 @@ class AuthView(View):
 | 
				
			||||||
            # 'referrer': referrer,
 | 
					            # 'referrer': referrer,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def login_make_schema(self):
 | 
					    def login_make_schema(self):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        schema = colander.Schema()
 | 
					        schema = colander.Schema()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # nb. we must explicitly declare the widgets in order to also
 | 
					        # nb. we must explicitly declare the widgets in order to also
 | 
				
			||||||
| 
						 | 
					@ -220,13 +221,19 @@ class AuthView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return schema
 | 
					        return schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def change_password_validate_current_password(self, node, value):
 | 
					    def change_password_validate_current_password(  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        self, node, value
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        auth = self.app.get_auth_handler()
 | 
					        auth = self.app.get_auth_handler()
 | 
				
			||||||
        user = self.request.user
 | 
					        user = self.request.user
 | 
				
			||||||
        if not auth.check_user_password(user, value):
 | 
					        if not auth.check_user_password(user, value):
 | 
				
			||||||
            node.raise_invalid("Current password is incorrect.")
 | 
					            node.raise_invalid("Current password is incorrect.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def change_password_validate_new_password(self, node, value):
 | 
					    def change_password_validate_new_password(  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        self, node, value
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        auth = self.app.get_auth_handler()
 | 
					        auth = self.app.get_auth_handler()
 | 
				
			||||||
        user = self.request.user
 | 
					        user = self.request.user
 | 
				
			||||||
        if auth.check_user_password(user, value):
 | 
					        if auth.check_user_password(user, value):
 | 
				
			||||||
| 
						 | 
					@ -277,7 +284,8 @@ class AuthView(View):
 | 
				
			||||||
        return self.redirect(url)
 | 
					        return self.redirect(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def defaults(cls, config):
 | 
					    def defaults(cls, config):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        cls._auth_defaults(config)
 | 
					        cls._auth_defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
| 
						 | 
					@ -311,12 +319,14 @@ class AuthView(View):
 | 
				
			||||||
        config.add_view(cls, attr="stop_root", route_name="stop_root")
 | 
					        config.add_view(cls, attr="stop_root", route_name="stop_root")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AuthView = kwargs.get("AuthView", base["AuthView"])  # pylint: disable=invalid-name
 | 
					    AuthView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
 | 
					        "AuthView", base["AuthView"]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    AuthView.defaults(config)
 | 
					    AuthView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,8 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
       from :meth:`get_batch_handler()`.
 | 
					       from :meth:`get_batch_handler()`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    executable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    labels = {
 | 
					    labels = {
 | 
				
			||||||
        "id": "Batch ID",
 | 
					        "id": "Batch ID",
 | 
				
			||||||
        "status_code": "Status",
 | 
					        "status_code": "Status",
 | 
				
			||||||
| 
						 | 
					@ -121,8 +123,9 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return super().render_to_response(template, context)
 | 
					        return super().render_to_response(template, context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,15 +156,17 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
            return f"{batch_id:08d}"
 | 
					            return f"{batch_id:08d}"
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance_title(self, batch):  # pylint: disable=empty-docstring
 | 
					    def get_instance_title(self, instance):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        batch = instance
 | 
				
			||||||
        if batch.description:
 | 
					        if batch.description:
 | 
				
			||||||
            return f"{batch.id_str} {batch.description}"
 | 
					            return f"{batch.id_str} {batch.description}"
 | 
				
			||||||
        return batch.id_str
 | 
					        return batch.id_str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=too-many-branches,empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=too-many-branches,empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(form)
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        batch = f.model_instance
 | 
					        batch = f.model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # id
 | 
					        # id
 | 
				
			||||||
| 
						 | 
					@ -235,13 +240,11 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
            batch = schema.objectify(form.validated, context=form.model_instance)
 | 
					            batch = schema.objectify(form.validated, context=form.model_instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # then we collect attributes from the new batch
 | 
					            # then we collect attributes from the new batch
 | 
				
			||||||
            kw = dict(
 | 
					            kw = {
 | 
				
			||||||
                [
 | 
					                key: getattr(batch, key)
 | 
				
			||||||
                    (key, getattr(batch, key))
 | 
					                for key in form.validated
 | 
				
			||||||
                    for key in form.validated
 | 
					                if hasattr(batch, key)
 | 
				
			||||||
                    if hasattr(batch, key)
 | 
					            }
 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # and set attribute for user creating the batch
 | 
					            # and set attribute for user creating the batch
 | 
				
			||||||
            kw["created_by"] = self.request.user
 | 
					            kw["created_by"] = self.request.user
 | 
				
			||||||
| 
						 | 
					@ -255,7 +258,7 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        # when not creating, normal logic is fine
 | 
					        # when not creating, normal logic is fine
 | 
				
			||||||
        return super().objectify(form)
 | 
					        return super().objectify(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def redirect_after_create(self, batch):
 | 
					    def redirect_after_create(self, obj):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        If the new batch requires initial population, we launch a
 | 
					        If the new batch requires initial population, we launch a
 | 
				
			||||||
        thread for that and show the "progress" page.
 | 
					        thread for that and show the "progress" page.
 | 
				
			||||||
| 
						 | 
					@ -263,6 +266,8 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        Otherwise this will do the normal thing of redirecting to the
 | 
					        Otherwise this will do the normal thing of redirecting to the
 | 
				
			||||||
        "view" page for the new batch.
 | 
					        "view" page for the new batch.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        batch = obj
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # just view batch if should not populate
 | 
					        # just view batch if should not populate
 | 
				
			||||||
        if not self.batch_handler.should_populate(batch):
 | 
					        if not self.batch_handler.should_populate(batch):
 | 
				
			||||||
            return self.redirect(self.get_action_url("view", batch))
 | 
					            return self.redirect(self.get_action_url("view", batch))
 | 
				
			||||||
| 
						 | 
					@ -283,7 +288,7 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        thread.start()
 | 
					        thread.start()
 | 
				
			||||||
        return self.render_progress(progress)
 | 
					        return self.render_progress(progress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete_instance(self, batch):
 | 
					    def delete_instance(self, obj):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Delete the given batch instance.
 | 
					        Delete the given batch instance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,6 +296,7 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()`
 | 
					        :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()`
 | 
				
			||||||
        on the :attr:`batch_handler`.
 | 
					        on the :attr:`batch_handler`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        batch = obj
 | 
				
			||||||
        self.batch_handler.do_delete(batch, self.request.user)
 | 
					        self.batch_handler.do_delete(batch, self.request.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -326,29 +332,22 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
                raise RuntimeError("can't find the batch")
 | 
					                raise RuntimeError("can't find the batch")
 | 
				
			||||||
            time.sleep(0.1)
 | 
					            time.sleep(0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        def onerror():
 | 
				
			||||||
            # populate the batch
 | 
					 | 
				
			||||||
            self.batch_handler.do_populate(batch, progress=progress)
 | 
					 | 
				
			||||||
            session.flush()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        except Exception as error:  # pylint: disable=broad-exception-caught
 | 
					 | 
				
			||||||
            session.rollback()
 | 
					 | 
				
			||||||
            log.warning(
 | 
					            log.warning(
 | 
				
			||||||
                "failed to populate %s: %s",
 | 
					                "failed to populate %s: %s",
 | 
				
			||||||
                self.get_model_title(),
 | 
					                self.get_model_title(),
 | 
				
			||||||
                batch,
 | 
					                batch,
 | 
				
			||||||
                exc_info=True,
 | 
					                exc_info=True,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if progress:
 | 
					 | 
				
			||||||
                progress.handle_error(error)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        else:
 | 
					        self.do_thread_body(
 | 
				
			||||||
            session.commit()
 | 
					            self.batch_handler.do_populate,
 | 
				
			||||||
            if progress:
 | 
					            (batch,),
 | 
				
			||||||
                progress.handle_success()
 | 
					            {"progress": progress},
 | 
				
			||||||
 | 
					            onerror,
 | 
				
			||||||
        finally:
 | 
					            session=session,
 | 
				
			||||||
            session.close()
 | 
					            progress=progress,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # execute methods
 | 
					    # execute methods
 | 
				
			||||||
| 
						 | 
					@ -388,20 +387,21 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        model_class = cls.get_model_class()
 | 
					        model_class = cls.get_model_class()
 | 
				
			||||||
        return model_class.__row_class__
 | 
					        return model_class.__row_class__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_row_grid_data(self, batch):
 | 
					    def get_row_grid_data(self, obj):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns the base query for the batch
 | 
					        Returns the base query for the batch
 | 
				
			||||||
        :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
 | 
					        :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
 | 
				
			||||||
        data.
 | 
					        data.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        session = self.Session()
 | 
				
			||||||
 | 
					        batch = obj
 | 
				
			||||||
        row_model_class = self.get_row_model_class()
 | 
					        row_model_class = self.get_row_model_class()
 | 
				
			||||||
        query = self.Session.query(row_model_class).filter(
 | 
					        query = session.query(row_model_class).filter(row_model_class.batch == batch)
 | 
				
			||||||
            row_model_class.batch == batch
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return query
 | 
					        return query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_row_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_row_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_row_grid(g)
 | 
					        super().configure_row_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        g.set_label("sequence", "Seq.", column_only=True)
 | 
					        g.set_label("sequence", "Seq.", column_only=True)
 | 
				
			||||||
| 
						 | 
					@ -413,36 +413,3 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        return row.STATUS.get(value, value)
 | 
					        return row.STATUS.get(value, value)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ##############################
 | 
					 | 
				
			||||||
    # configuration
 | 
					 | 
				
			||||||
    ##############################
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def defaults(cls, config):  # pylint: disable=empty-docstring
 | 
					 | 
				
			||||||
        """ """
 | 
					 | 
				
			||||||
        cls._defaults(config)
 | 
					 | 
				
			||||||
        cls._batch_defaults(config)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def _batch_defaults(cls, config):
 | 
					 | 
				
			||||||
        route_prefix = cls.get_route_prefix()
 | 
					 | 
				
			||||||
        permission_prefix = cls.get_permission_prefix()
 | 
					 | 
				
			||||||
        model_title = cls.get_model_title()
 | 
					 | 
				
			||||||
        instance_url_prefix = cls.get_instance_url_prefix()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # execute
 | 
					 | 
				
			||||||
        config.add_route(
 | 
					 | 
				
			||||||
            f"{route_prefix}.execute",
 | 
					 | 
				
			||||||
            f"{instance_url_prefix}/execute",
 | 
					 | 
				
			||||||
            request_method="POST",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        config.add_view(
 | 
					 | 
				
			||||||
            cls,
 | 
					 | 
				
			||||||
            attr="execute",
 | 
					 | 
				
			||||||
            route_name=f"{route_prefix}.execute",
 | 
					 | 
				
			||||||
            permission=f"{permission_prefix}.execute",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        config.add_wutta_permission(
 | 
					 | 
				
			||||||
            permission_prefix, f"{permission_prefix}.execute", f"Execute {model_title}"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -135,7 +135,7 @@ class CommonView(View):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        self.app.send_email("feedback", context)
 | 
					        self.app.send_email("feedback", context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setup(self, session=None):
 | 
					    def setup(self, session=None):  # pylint: disable=too-many-locals
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        View for first-time app setup, to create admin user.
 | 
					        View for first-time app setup, to create admin user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -294,7 +294,8 @@ class CommonView(View):
 | 
				
			||||||
        return self.redirect(referrer)
 | 
					        return self.redirect(referrer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def defaults(cls, config):
 | 
					    def defaults(cls, config):  # pylint: disable=empty-docstring
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
        cls._defaults(config)
 | 
					        cls._defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
| 
						 | 
					@ -342,14 +343,14 @@ class CommonView(View):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CommonView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    CommonView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "CommonView", base["CommonView"]
 | 
					        "CommonView", base["CommonView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    CommonView.defaults(config)
 | 
					    CommonView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ from wuttaweb.views import MasterView
 | 
				
			||||||
from wuttaweb.forms.schema import EmailRecipients
 | 
					from wuttaweb.forms.schema import EmailRecipients
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmailSettingView(MasterView):
 | 
					class EmailSettingView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for :term:`email settings <email setting>`.
 | 
					    Master view for :term:`email settings <email setting>`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
| 
						 | 
					@ -107,8 +107,9 @@ class EmailSettingView(MasterView):
 | 
				
			||||||
            "enabled": self.email_handler.is_enabled(key),
 | 
					            "enabled": self.email_handler.is_enabled(key),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # key
 | 
					        # key
 | 
				
			||||||
| 
						 | 
					@ -136,7 +137,9 @@ class EmailSettingView(MasterView):
 | 
				
			||||||
        recips = ", ".join(recips[:2])
 | 
					        recips = ", ".join(recips[:2])
 | 
				
			||||||
        return f"{recips}, ..."
 | 
					        return f"{recips}, ..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance(self):  # pylint: disable=empty-docstring
 | 
					    def get_instance(  # pylint: disable=empty-docstring,arguments-differ,unused-argument
 | 
				
			||||||
 | 
					        self, **kwargs
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        key = self.request.matchdict["key"]
 | 
					        key = self.request.matchdict["key"]
 | 
				
			||||||
        setting = self.email_handler.get_email_setting(key, instance=False)
 | 
					        setting = self.email_handler.get_email_setting(key, instance=False)
 | 
				
			||||||
| 
						 | 
					@ -145,12 +148,14 @@ class EmailSettingView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        raise self.notfound()
 | 
					        raise self.notfound()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance_title(self, setting):  # pylint: disable=empty-docstring
 | 
					    def get_instance_title(self, instance):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        setting = instance
 | 
				
			||||||
        return setting["subject"]
 | 
					        return setting["subject"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # description
 | 
					        # description
 | 
				
			||||||
| 
						 | 
					@ -175,7 +180,9 @@ class EmailSettingView(MasterView):
 | 
				
			||||||
        # enabled
 | 
					        # enabled
 | 
				
			||||||
        f.set_node("enabled", colander.Boolean())
 | 
					        f.set_node("enabled", colander.Boolean())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def persist(self, setting):  # pylint: disable=too-many-branches,empty-docstring
 | 
					    def persist(  # pylint: disable=too-many-branches,empty-docstring,arguments-differ,unused-argument
 | 
				
			||||||
 | 
					        self, setting, **kwargs
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        session = self.Session()
 | 
					        session = self.Session()
 | 
				
			||||||
        key = self.request.matchdict["key"]
 | 
					        key = self.request.matchdict["key"]
 | 
				
			||||||
| 
						 | 
					@ -300,14 +307,14 @@ class EmailSettingView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    EmailSettingView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    EmailSettingView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "EmailSettingView", base["EmailSettingView"]
 | 
					        "EmailSettingView", base["EmailSettingView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    EmailSettingView.defaults(config)
 | 
					    EmailSettingView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  wuttaweb -- Web App for Wutta Framework
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ That will in turn include the following modules:
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mod(spec):
 | 
					    def mod(spec):
 | 
				
			||||||
        return kwargs.get(spec, spec)
 | 
					        return kwargs.get(spec, spec)
 | 
				
			||||||
| 
						 | 
					@ -57,5 +57,5 @@ def defaults(config, **kwargs):
 | 
				
			||||||
    config.include(mod("wuttaweb.views.upgrades"))
 | 
					    config.include(mod("wuttaweb.views.upgrades"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Base Logic for Master Views
 | 
					Base Logic for Master Views
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					# pylint: disable=too-many-lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
| 
						 | 
					@ -44,7 +45,7 @@ from wuttaweb.progress import SessionProgress
 | 
				
			||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MasterView(View):
 | 
					class MasterView(View):  # pylint: disable=too-many-public-methods
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Base class for "master" views.
 | 
					    Base class for "master" views.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -398,6 +399,8 @@ class MasterView(View):
 | 
				
			||||||
    # attributes
 | 
					    # attributes
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model_class = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # features
 | 
					    # features
 | 
				
			||||||
    listable = True
 | 
					    listable = True
 | 
				
			||||||
    has_grid = True
 | 
					    has_grid = True
 | 
				
			||||||
| 
						 | 
					@ -438,6 +441,7 @@ class MasterView(View):
 | 
				
			||||||
    viewing = False
 | 
					    viewing = False
 | 
				
			||||||
    editing = False
 | 
					    editing = False
 | 
				
			||||||
    deleting = False
 | 
					    deleting = False
 | 
				
			||||||
 | 
					    executing = False
 | 
				
			||||||
    configuring = False
 | 
					    configuring = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # default DB session
 | 
					    # default DB session
 | 
				
			||||||
| 
						 | 
					@ -524,11 +528,12 @@ class MasterView(View):
 | 
				
			||||||
        * :meth:`redirect_after_create()`
 | 
					        * :meth:`redirect_after_create()`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.creating = True
 | 
					        self.creating = True
 | 
				
			||||||
 | 
					        session = self.Session()
 | 
				
			||||||
        form = self.make_model_form(cancel_url_fallback=self.get_index_url())
 | 
					        form = self.make_model_form(cancel_url_fallback=self.get_index_url())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if form.validate():
 | 
					        if form.validate():
 | 
				
			||||||
            obj = self.create_save_form(form)
 | 
					            obj = self.create_save_form(form)
 | 
				
			||||||
            self.Session.flush()
 | 
					            session.flush()
 | 
				
			||||||
            return self.redirect_after_create(obj)
 | 
					            return self.redirect_after_create(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        context = {
 | 
					        context = {
 | 
				
			||||||
| 
						 | 
					@ -817,33 +822,25 @@ class MasterView(View):
 | 
				
			||||||
        self, query, progress=None
 | 
					        self, query, progress=None
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        model_title_plural = self.get_model_title_plural()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # nb. use new session, separate from web transaction
 | 
					 | 
				
			||||||
        session = self.app.make_session()
 | 
					        session = self.app.make_session()
 | 
				
			||||||
        records = query.with_session(session).all()
 | 
					        records = query.with_session(session).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        def onerror():
 | 
				
			||||||
            self.delete_bulk_action(records, progress=progress)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        except Exception as error:  # pylint: disable=broad-exception-caught
 | 
					 | 
				
			||||||
            session.rollback()
 | 
					 | 
				
			||||||
            log.warning(
 | 
					            log.warning(
 | 
				
			||||||
                "failed to delete %s results for %s",
 | 
					                "failed to delete %s results for %s",
 | 
				
			||||||
                len(records),
 | 
					                len(records),
 | 
				
			||||||
                model_title_plural,
 | 
					                self.get_model_title_plural(),
 | 
				
			||||||
                exc_info=True,
 | 
					                exc_info=True,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if progress:
 | 
					 | 
				
			||||||
                progress.handle_error(error)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        else:
 | 
					        self.do_thread_body(
 | 
				
			||||||
            session.commit()
 | 
					            self.delete_bulk_action,
 | 
				
			||||||
            if progress:
 | 
					            (records,),
 | 
				
			||||||
                progress.handle_success()
 | 
					            {"progress": progress},
 | 
				
			||||||
 | 
					            onerror,
 | 
				
			||||||
        finally:
 | 
					            session=session,
 | 
				
			||||||
            session.close()
 | 
					            progress=progress,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete_bulk_action(self, data, progress=None):
 | 
					    def delete_bulk_action(self, data, progress=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -917,7 +914,7 @@ class MasterView(View):
 | 
				
			||||||
        if not term:
 | 
					        if not term:
 | 
				
			||||||
            return []
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.autocomplete_data(term)
 | 
					        data = self.autocomplete_data(term)  # pylint: disable=assignment-from-none
 | 
				
			||||||
        if not data:
 | 
					        if not data:
 | 
				
			||||||
            return []
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -931,7 +928,7 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return results
 | 
					        return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def autocomplete_data(self, term):
 | 
					    def autocomplete_data(self, term):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Should return the data/query for the "matching" model records,
 | 
					        Should return the data/query for the "matching" model records,
 | 
				
			||||||
        based on autocomplete search term.  This is called by
 | 
					        based on autocomplete search term.  This is called by
 | 
				
			||||||
| 
						 | 
					@ -943,6 +940,7 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: List of data records, or SQLAlchemy query.
 | 
					        :returns: List of data records, or SQLAlchemy query.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def autocomplete_normalize(self, obj):
 | 
					    def autocomplete_normalize(self, obj):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -1006,13 +1004,13 @@ class MasterView(View):
 | 
				
			||||||
        obj = self.get_instance()
 | 
					        obj = self.get_instance()
 | 
				
			||||||
        filename = self.request.GET.get("filename", None)
 | 
					        filename = self.request.GET.get("filename", None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        path = self.download_path(obj, filename)
 | 
					        path = self.download_path(obj, filename)  # pylint: disable=assignment-from-none
 | 
				
			||||||
        if not path or not os.path.exists(path):
 | 
					        if not path or not os.path.exists(path):
 | 
				
			||||||
            return self.notfound()
 | 
					            return self.notfound()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.file_response(path)
 | 
					        return self.file_response(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def download_path(self, obj, filename):
 | 
					    def download_path(self, obj, filename):  # pylint: disable=unused-argument
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Should return absolute path on disk, for the given object and
 | 
					        Should return absolute path on disk, for the given object and
 | 
				
			||||||
        filename.  Result will be used to return a file response to
 | 
					        filename.  Result will be used to return a file response to
 | 
				
			||||||
| 
						 | 
					@ -1033,6 +1031,7 @@ class MasterView(View):
 | 
				
			||||||
        the :meth:`download()` view will return a 404 not found
 | 
					        the :meth:`download()` view will return a 404 not found
 | 
				
			||||||
        response.
 | 
					        response.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # execute methods
 | 
					    # execute methods
 | 
				
			||||||
| 
						 | 
					@ -1304,6 +1303,7 @@ class MasterView(View):
 | 
				
			||||||
           Note that their order does not matter since the template
 | 
					           Note that their order does not matter since the template
 | 
				
			||||||
           must explicitly define field layout etc.
 | 
					           must explicitly define field layout etc.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_gather_settings(
 | 
					    def configure_gather_settings(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
| 
						 | 
					@ -2244,9 +2244,9 @@ class MasterView(View):
 | 
				
			||||||
        :returns: The dict of route kwargs for the object.
 | 
					        :returns: The dict of route kwargs for the object.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return dict([(key, obj[key]) for key in self.get_model_key()])
 | 
					            return {key: obj[key] for key in self.get_model_key()}
 | 
				
			||||||
        except TypeError:
 | 
					        except TypeError:
 | 
				
			||||||
            return dict([(key, getattr(obj, key)) for key in self.get_model_key()])
 | 
					            return {key: getattr(obj, key) for key in self.get_model_key()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_action_url(self, action, obj, **kwargs):
 | 
					    def get_action_url(self, action, obj, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -2477,6 +2477,60 @@ class MasterView(View):
 | 
				
			||||||
            session = session or self.Session()
 | 
					            session = session or self.Session()
 | 
				
			||||||
            session.add(obj)
 | 
					            session.add(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def do_thread_body(  # pylint: disable=too-many-arguments,too-many-positional-arguments
 | 
				
			||||||
 | 
					        self, func, args, kwargs, onerror=None, session=None, progress=None
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Generic method to invoke for thread operations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param func: Callable which performs the actual logic.  This
 | 
				
			||||||
 | 
					           will be wrapped with a try/except statement for error
 | 
				
			||||||
 | 
					           handling.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param args: Tuple of positional arguments to pass to the
 | 
				
			||||||
 | 
					           ``func`` callable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param kwargs: Dict of keyword arguments to pass to the
 | 
				
			||||||
 | 
					           ``func`` callable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param onerror: Optional callback to invoke if ``func`` raises
 | 
				
			||||||
 | 
					           an error.  It should not expect any arguments.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param session: Optional :term:`db session` in effect.  Note
 | 
				
			||||||
 | 
					           that if supplied, it will be *committed* (or rolled back on
 | 
				
			||||||
 | 
					           error) and *closed* by this method.  If you need more
 | 
				
			||||||
 | 
					           specialized handling, do not use this method (or don't
 | 
				
			||||||
 | 
					           specify the ``session``).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param progress: Optional progress factory.  If supplied, this
 | 
				
			||||||
 | 
					           is assumed to be a
 | 
				
			||||||
 | 
					           :class:`~wuttaweb.progress.SessionProgress` instance, and
 | 
				
			||||||
 | 
					           it will be updated per success or failure of ``func``
 | 
				
			||||||
 | 
					           invocation.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            func(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        except Exception as error:  # pylint: disable=broad-exception-caught
 | 
				
			||||||
 | 
					            if session:
 | 
				
			||||||
 | 
					                session.rollback()
 | 
				
			||||||
 | 
					            if onerror:
 | 
				
			||||||
 | 
					                onerror()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                log.warning("failed to invoke thread callable: %s", func, exc_info=True)
 | 
				
			||||||
 | 
					            if progress:
 | 
				
			||||||
 | 
					                progress.handle_error(error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if session:
 | 
				
			||||||
 | 
					                session.commit()
 | 
				
			||||||
 | 
					            if progress:
 | 
				
			||||||
 | 
					                progress.handle_success()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        finally:
 | 
				
			||||||
 | 
					            if session:
 | 
				
			||||||
 | 
					                session.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # row methods
 | 
					    # row methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -2697,9 +2751,7 @@ class MasterView(View):
 | 
				
			||||||
        do not set the :attr:`model_class`, then you *must* set the
 | 
					        do not set the :attr:`model_class`, then you *must* set the
 | 
				
			||||||
        :attr:`model_name`.
 | 
					        :attr:`model_name`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if hasattr(cls, "model_class"):
 | 
					        return cls.model_class
 | 
				
			||||||
            return cls.model_class
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_model_name(cls):
 | 
					    def get_model_name(cls):
 | 
				
			||||||
| 
						 | 
					@ -2808,11 +2860,9 @@ class MasterView(View):
 | 
				
			||||||
            inspector = sa.inspect(model_class)
 | 
					            inspector = sa.inspect(model_class)
 | 
				
			||||||
            keys = [col.name for col in inspector.primary_key]
 | 
					            keys = [col.name for col in inspector.primary_key]
 | 
				
			||||||
            return tuple(
 | 
					            return tuple(
 | 
				
			||||||
                [
 | 
					                prop.key
 | 
				
			||||||
                    prop.key
 | 
					                for prop in inspector.column_attrs
 | 
				
			||||||
                    for prop in inspector.column_attrs
 | 
					                if all(col.name in keys for col in prop.columns)
 | 
				
			||||||
                    if all(col.name in keys for col in prop.columns)
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        raise AttributeError(f"you must define model_key for view class: {cls}")
 | 
					        raise AttributeError(f"you must define model_key for view class: {cls}")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,9 +28,10 @@ import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican.db.model import Person
 | 
					from wuttjamaican.db.model import Person
 | 
				
			||||||
from wuttaweb.views import MasterView
 | 
					from wuttaweb.views import MasterView
 | 
				
			||||||
 | 
					from wuttaweb.util import make_users_grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PersonView(MasterView):
 | 
					class PersonView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for people.
 | 
					    Master view for people.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,8 +71,9 @@ class PersonView(MasterView):
 | 
				
			||||||
        "users",
 | 
					        "users",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # full_name
 | 
					        # full_name
 | 
				
			||||||
| 
						 | 
					@ -83,8 +85,9 @@ class PersonView(MasterView):
 | 
				
			||||||
        # last_name
 | 
					        # last_name
 | 
				
			||||||
        g.set_link("last_name")
 | 
					        g.set_link("last_name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
        person = f.model_instance
 | 
					        person = f.model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,12 +108,9 @@ class PersonView(MasterView):
 | 
				
			||||||
        :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
 | 
					        :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
           instance.
 | 
					           instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        model = self.app.model
 | 
					        return make_users_grid(
 | 
				
			||||||
        route_prefix = self.get_route_prefix()
 | 
					            self.request,
 | 
				
			||||||
 | 
					            route_prefix=self.get_route_prefix(),
 | 
				
			||||||
        grid = self.make_grid(
 | 
					 | 
				
			||||||
            key=f"{route_prefix}.view.users",
 | 
					 | 
				
			||||||
            model_class=model.User,
 | 
					 | 
				
			||||||
            data=person.users,
 | 
					            data=person.users,
 | 
				
			||||||
            columns=[
 | 
					            columns=[
 | 
				
			||||||
                "username",
 | 
					                "username",
 | 
				
			||||||
| 
						 | 
					@ -118,23 +118,6 @@ class PersonView(MasterView):
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.request.has_perm("users.view"):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            def view_url(user, i):  # pylint: disable=unused-argument
 | 
					 | 
				
			||||||
                return self.request.route_url("users.view", uuid=user.uuid)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            grid.add_action("view", icon="eye", url=view_url)
 | 
					 | 
				
			||||||
            grid.set_link("username")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.request.has_perm("users.edit"):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            def edit_url(user, i):  # pylint: disable=unused-argument
 | 
					 | 
				
			||||||
                return self.request.route_url("users.edit", uuid=user.uuid)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            grid.add_action("edit", url=edit_url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return grid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def objectify(self, form):  # pylint: disable=empty-docstring
 | 
					    def objectify(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        person = super().objectify(form)
 | 
					        person = super().objectify(form)
 | 
				
			||||||
| 
						 | 
					@ -213,14 +196,14 @@ class PersonView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PersonView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    PersonView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "PersonView", base["PersonView"]
 | 
					        "PersonView", base["PersonView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    PersonView.defaults(config)
 | 
					    PersonView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  wuttaweb -- Web App for Wutta Framework
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
#  Copyright © 2024 Lance Edgar
 | 
					#  Copyright © 2024-2025 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -63,13 +63,15 @@ def progress(request):
 | 
				
			||||||
    return session
 | 
					    return session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    progress = kwargs.get("progress", base["progress"])
 | 
					    progress = kwargs.get(  # pylint: disable=redefined-outer-name
 | 
				
			||||||
 | 
					        "progress", base["progress"]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    config.add_route("progress", "/progress/{key}")
 | 
					    config.add_route("progress", "/progress/{key}")
 | 
				
			||||||
    config.add_view(progress, route_name="progress", renderer="json")
 | 
					    config.add_view(progress, route_name="progress", renderer="json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ from wuttaweb.views import MasterView
 | 
				
			||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportView(MasterView):
 | 
					class ReportView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for :term:`reports <report>`; route prefix is
 | 
					    Master view for :term:`reports <report>`; route prefix is
 | 
				
			||||||
    ``reports``.
 | 
					    ``reports``.
 | 
				
			||||||
| 
						 | 
					@ -89,8 +89,9 @@ class ReportView(MasterView):
 | 
				
			||||||
            "help_text": report.__doc__,
 | 
					            "help_text": report.__doc__,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # report_key
 | 
					        # report_key
 | 
				
			||||||
| 
						 | 
					@ -103,7 +104,9 @@ class ReportView(MasterView):
 | 
				
			||||||
        # help_text
 | 
					        # help_text
 | 
				
			||||||
        g.set_searchable("help_text")
 | 
					        g.set_searchable("help_text")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance(self):  # pylint: disable=empty-docstring
 | 
					    def get_instance(  # pylint: disable=empty-docstring,arguments-differ,unused-argument
 | 
				
			||||||
 | 
					        self, **kwargs
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        key = self.request.matchdict["report_key"]
 | 
					        key = self.request.matchdict["report_key"]
 | 
				
			||||||
        report = self.report_handler.get_report(key)
 | 
					        report = self.report_handler.get_report(key)
 | 
				
			||||||
| 
						 | 
					@ -112,8 +115,9 @@ class ReportView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        raise self.notfound()
 | 
					        raise self.notfound()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance_title(self, report):  # pylint: disable=empty-docstring
 | 
					    def get_instance_title(self, instance):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        report = instance
 | 
				
			||||||
        return report["report_title"]
 | 
					        return report["report_title"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def view(self):
 | 
					    def view(self):
 | 
				
			||||||
| 
						 | 
					@ -151,8 +155,9 @@ class ReportView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.render_to_response("view", context)
 | 
					        return self.render_to_response("view", context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
        key = self.request.matchdict["report_key"]
 | 
					        key = self.request.matchdict["report_key"]
 | 
				
			||||||
        report = self.report_handler.get_report(key)
 | 
					        report = self.report_handler.get_report(key)
 | 
				
			||||||
| 
						 | 
					@ -261,14 +266,14 @@ class ReportView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ReportView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    ReportView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "ReportView", base["ReportView"]
 | 
					        "ReportView", base["ReportView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    ReportView.defaults(config)
 | 
					    ReportView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,9 +29,10 @@ from wuttaweb.views import MasterView
 | 
				
			||||||
from wuttaweb.db import Session
 | 
					from wuttaweb.db import Session
 | 
				
			||||||
from wuttaweb.forms import widgets
 | 
					from wuttaweb.forms import widgets
 | 
				
			||||||
from wuttaweb.forms.schema import Permissions, RoleRef
 | 
					from wuttaweb.forms.schema import Permissions, RoleRef
 | 
				
			||||||
 | 
					from wuttaweb.util import make_users_grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RoleView(MasterView):
 | 
					class RoleView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for roles.
 | 
					    Master view for roles.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,6 +59,8 @@ class RoleView(MasterView):
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    sort_defaults = "name"
 | 
					    sort_defaults = "name"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    wutta_permissions = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO: master should handle this, possibly via configure_form()
 | 
					    # TODO: master should handle this, possibly via configure_form()
 | 
				
			||||||
    def get_query(self, session=None):  # pylint: disable=empty-docstring
 | 
					    def get_query(self, session=None):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
| 
						 | 
					@ -65,8 +68,9 @@ class RoleView(MasterView):
 | 
				
			||||||
        query = super().get_query(session=session)
 | 
					        query = super().get_query(session=session)
 | 
				
			||||||
        return query.order_by(model.Role.name)
 | 
					        return query.order_by(model.Role.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # name
 | 
					        # name
 | 
				
			||||||
| 
						 | 
					@ -75,8 +79,9 @@ class RoleView(MasterView):
 | 
				
			||||||
        # notes
 | 
					        # notes
 | 
				
			||||||
        g.set_renderer("notes", self.grid_render_notes)
 | 
					        g.set_renderer("notes", self.grid_render_notes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_editable(self, role):  # pylint: disable=empty-docstring
 | 
					    def is_editable(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        role = obj
 | 
				
			||||||
        session = self.app.get_session(role)
 | 
					        session = self.app.get_session(role)
 | 
				
			||||||
        auth = self.app.get_auth_handler()
 | 
					        auth = self.app.get_auth_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,8 +98,9 @@ class RoleView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_deletable(self, role):  # pylint: disable=empty-docstring
 | 
					    def is_deletable(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        role = obj
 | 
				
			||||||
        session = self.app.get_session(role)
 | 
					        session = self.app.get_session(role)
 | 
				
			||||||
        auth = self.app.get_auth_handler()
 | 
					        auth = self.app.get_auth_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,8 +114,9 @@ class RoleView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
        role = f.model_instance
 | 
					        role = f.model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,12 +152,9 @@ class RoleView(MasterView):
 | 
				
			||||||
        :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
 | 
					        :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
           instance.
 | 
					           instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        model = self.app.model
 | 
					        return make_users_grid(
 | 
				
			||||||
        route_prefix = self.get_route_prefix()
 | 
					            self.request,
 | 
				
			||||||
 | 
					            route_prefix=self.get_route_prefix(),
 | 
				
			||||||
        grid = self.make_grid(
 | 
					 | 
				
			||||||
            key=f"{route_prefix}.view.users",
 | 
					 | 
				
			||||||
            model_class=model.User,
 | 
					 | 
				
			||||||
            data=role.users,
 | 
					            data=role.users,
 | 
				
			||||||
            columns=[
 | 
					            columns=[
 | 
				
			||||||
                "username",
 | 
					                "username",
 | 
				
			||||||
| 
						 | 
					@ -159,24 +163,6 @@ class RoleView(MasterView):
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.request.has_perm("users.view"):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            def view_url(user, i):  # pylint: disable=unused-argument
 | 
					 | 
				
			||||||
                return self.request.route_url("users.view", uuid=user.uuid)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            grid.add_action("view", icon="eye", url=view_url)
 | 
					 | 
				
			||||||
            grid.set_link("person")
 | 
					 | 
				
			||||||
            grid.set_link("username")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.request.has_perm("users.edit"):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            def edit_url(user, i):  # pylint: disable=unused-argument
 | 
					 | 
				
			||||||
                return self.request.route_url("users.edit", uuid=user.uuid)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            grid.add_action("edit", url=edit_url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return grid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def unique_name(self, node, value):  # pylint: disable=empty-docstring
 | 
					    def unique_name(self, node, value):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
| 
						 | 
					@ -317,7 +303,7 @@ class RoleView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PermissionView(MasterView):
 | 
					class PermissionView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for permissions.
 | 
					    Master view for permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -346,7 +332,7 @@ class PermissionView(MasterView):
 | 
				
			||||||
        "permission",
 | 
					        "permission",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_query(self, **kwargs):  # pylint: disable=empty-docstring
 | 
					    def get_query(self, **kwargs):  # pylint: disable=empty-docstring,arguments-differ
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        query = super().get_query(**kwargs)
 | 
					        query = super().get_query(**kwargs)
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
| 
						 | 
					@ -356,8 +342,9 @@ class PermissionView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return query
 | 
					        return query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -369,25 +356,28 @@ class PermissionView(MasterView):
 | 
				
			||||||
        # permission
 | 
					        # permission
 | 
				
			||||||
        g.set_link("permission")
 | 
					        g.set_link("permission")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # role
 | 
					        # role
 | 
				
			||||||
        f.set_node("role", RoleRef(self.request))
 | 
					        f.set_node("role", RoleRef(self.request))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    RoleView = kwargs.get("RoleView", base["RoleView"])  # pylint: disable=invalid-name
 | 
					    RoleView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
 | 
					        "RoleView", base["RoleView"]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    RoleView.defaults(config)
 | 
					    RoleView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PermissionView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    PermissionView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "PermissionView", base["PermissionView"]
 | 
					        "PermissionView", base["PermissionView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    PermissionView.defaults(config)
 | 
					    PermissionView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ from wuttaweb.views import MasterView
 | 
				
			||||||
from wuttaweb.util import get_libver, get_liburl
 | 
					from wuttaweb.util import get_libver, get_liburl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppInfoView(MasterView):
 | 
					class AppInfoView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for the core app info, to show/edit config etc.
 | 
					    Master view for the core app info, to show/edit config etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,8 +93,9 @@ class AppInfoView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        g.sort_multiple = False
 | 
					        g.sort_multiple = False
 | 
				
			||||||
| 
						 | 
					@ -173,7 +174,9 @@ class AppInfoView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return simple_settings
 | 
					        return simple_settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_get_context(self, **kwargs):  # pylint: disable=empty-docstring
 | 
					    def configure_get_context(  # pylint: disable=empty-docstring,arguments-differ
 | 
				
			||||||
 | 
					        self, **kwargs
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
        context = super().configure_get_context(**kwargs)
 | 
					        context = super().configure_get_context(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -220,7 +223,7 @@ class AppInfoView(MasterView):
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingView(MasterView):
 | 
					class SettingView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for the "raw" settings table.
 | 
					    Master view for the "raw" settings table.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -242,15 +245,17 @@ class SettingView(MasterView):
 | 
				
			||||||
    sort_defaults = "name"
 | 
					    sort_defaults = "name"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO: master should handle this (per model key)
 | 
					    # TODO: master should handle this (per model key)
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # name
 | 
					        # name
 | 
				
			||||||
        g.set_link("name")
 | 
					        g.set_link("name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # name
 | 
					        # name
 | 
				
			||||||
| 
						 | 
					@ -275,19 +280,19 @@ class SettingView(MasterView):
 | 
				
			||||||
            node.raise_invalid("Setting name must be unique")
 | 
					            node.raise_invalid("Setting name must be unique")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AppInfoView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    AppInfoView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "AppInfoView", base["AppInfoView"]
 | 
					        "AppInfoView", base["AppInfoView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    AppInfoView.defaults(config)
 | 
					    AppInfoView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SettingView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    SettingView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "SettingView", base["SettingView"]
 | 
					        "SettingView", base["SettingView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    SettingView.defaults(config)
 | 
					    SettingView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ from wuttaweb.progress import get_progress_session
 | 
				
			||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UpgradeView(MasterView):
 | 
					class UpgradeView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for upgrades.
 | 
					    Master view for upgrades.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -72,8 +72,9 @@ class UpgradeView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sort_defaults = ("created", "desc")
 | 
					    sort_defaults = ("created", "desc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
        enum = self.app.enum
 | 
					        enum = self.app.enum
 | 
				
			||||||
| 
						 | 
					@ -121,8 +122,9 @@ class UpgradeView(MasterView):
 | 
				
			||||||
            return "has-background-warning"
 | 
					            return "has-background-warning"
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
        enum = self.app.enum
 | 
					        enum = self.app.enum
 | 
				
			||||||
        upgrade = f.model_instance
 | 
					        upgrade = f.model_instance
 | 
				
			||||||
| 
						 | 
					@ -204,11 +206,12 @@ class UpgradeView(MasterView):
 | 
				
			||||||
                "stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
 | 
					                "stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete_instance(self, upgrade):
 | 
					    def delete_instance(self, obj):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        We override this method to delete any files associated with
 | 
					        We override this method to delete any files associated with
 | 
				
			||||||
        the upgrade, in addition to deleting the upgrade proper.
 | 
					        the upgrade, in addition to deleting the upgrade proper.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        upgrade = obj
 | 
				
			||||||
        path = self.get_upgrade_filepath(upgrade, create=False)
 | 
					        path = self.get_upgrade_filepath(upgrade, create=False)
 | 
				
			||||||
        if os.path.exists(path):
 | 
					        if os.path.exists(path):
 | 
				
			||||||
            shutil.rmtree(path)
 | 
					            shutil.rmtree(path)
 | 
				
			||||||
| 
						 | 
					@ -227,8 +230,9 @@ class UpgradeView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return upgrade
 | 
					        return upgrade
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def download_path(self, upgrade, filename):  # pylint: disable=empty-docstring
 | 
					    def download_path(self, obj, filename):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        upgrade = obj
 | 
				
			||||||
        if filename:
 | 
					        if filename:
 | 
				
			||||||
            return self.get_upgrade_filepath(upgrade, filename)
 | 
					            return self.get_upgrade_filepath(upgrade, filename)
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
| 
						 | 
					@ -245,7 +249,7 @@ class UpgradeView(MasterView):
 | 
				
			||||||
            path = os.path.join(path, filename)
 | 
					            path = os.path.join(path, filename)
 | 
				
			||||||
        return path
 | 
					        return path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def execute_instance(self, upgrade, user, progress=None):
 | 
					    def execute_instance(self, obj, user, progress=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        This method runs the actual upgrade.
 | 
					        This method runs the actual upgrade.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -258,6 +262,7 @@ class UpgradeView(MasterView):
 | 
				
			||||||
        The upgrade itself is marked as "executed" with status of
 | 
					        The upgrade itself is marked as "executed" with status of
 | 
				
			||||||
        either ``SUCCESS`` or ``FAILURE``.
 | 
					        either ``SUCCESS`` or ``FAILURE``.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        upgrade = obj
 | 
				
			||||||
        enum = self.app.enum
 | 
					        enum = self.app.enum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # locate file paths
 | 
					        # locate file paths
 | 
				
			||||||
| 
						 | 
					@ -377,14 +382,14 @@ class UpgradeView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    UpgradeView = kwargs.get(  # pylint: disable=invalid-name
 | 
					    UpgradeView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
        "UpgradeView", base["UpgradeView"]
 | 
					        "UpgradeView", base["UpgradeView"]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    UpgradeView.defaults(config)
 | 
					    UpgradeView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ from wuttaweb.forms import widgets
 | 
				
			||||||
from wuttaweb.forms.schema import PersonRef, RoleRefs
 | 
					from wuttaweb.forms.schema import PersonRef, RoleRefs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserView(MasterView):
 | 
					class UserView(MasterView):  # pylint: disable=abstract-method
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for users.
 | 
					    Master view for users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,8 +82,9 @@ class UserView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return query
 | 
					        return query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):  # pylint: disable=empty-docstring
 | 
					    def configure_grid(self, grid):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        g = grid
 | 
				
			||||||
        super().configure_grid(g)
 | 
					        super().configure_grid(g)
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,8 +107,9 @@ class UserView(MasterView):
 | 
				
			||||||
            return "has-background-warning"
 | 
					            return "has-background-warning"
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_editable(self, user):  # pylint: disable=empty-docstring
 | 
					    def is_editable(self, obj):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        user = obj
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # only root can edit certain users
 | 
					        # only root can edit certain users
 | 
				
			||||||
        if user.prevent_edit and not self.request.is_root:
 | 
					        if user.prevent_edit and not self.request.is_root:
 | 
				
			||||||
| 
						 | 
					@ -115,8 +117,9 @@ class UserView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_form(self, f):  # pylint: disable=empty-docstring
 | 
					    def configure_form(self, form):  # pylint: disable=empty-docstring
 | 
				
			||||||
        """ """
 | 
					        """ """
 | 
				
			||||||
 | 
					        f = form
 | 
				
			||||||
        super().configure_form(f)
 | 
					        super().configure_form(f)
 | 
				
			||||||
        user = f.model_instance
 | 
					        user = f.model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,7 +236,7 @@ class UserView(MasterView):
 | 
				
			||||||
        session = self.Session()
 | 
					        session = self.Session()
 | 
				
			||||||
        auth = self.app.get_auth_handler()
 | 
					        auth = self.app.get_auth_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        old_roles = set([role.uuid for role in user.roles])
 | 
					        old_roles = {role.uuid for role in user.roles}
 | 
				
			||||||
        new_roles = data["roles"]
 | 
					        new_roles = data["roles"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        admin = auth.get_role_administrator(session)
 | 
					        admin = auth.get_role_administrator(session)
 | 
				
			||||||
| 
						 | 
					@ -417,12 +420,14 @@ class UserView(MasterView):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    UserView = kwargs.get("UserView", base["UserView"])  # pylint: disable=invalid-name
 | 
					    UserView = kwargs.get(  # pylint: disable=invalid-name,redefined-outer-name
 | 
				
			||||||
 | 
					        "UserView", base["UserView"]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    UserView.defaults(config)
 | 
					    UserView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):  # pylint: disable=missing-function-docstring
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -363,7 +363,7 @@ class TestForm(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # basic
 | 
					        # basic
 | 
				
			||||||
        form = self.make_form(schema=schema)
 | 
					        form = self.make_form(schema=schema)
 | 
				
			||||||
        self.assertFalse(hasattr(form, "deform_form"))
 | 
					        self.assertIsNone(form.deform_form)
 | 
				
			||||||
        dform = form.get_deform()
 | 
					        dform = form.get_deform()
 | 
				
			||||||
        self.assertIsInstance(dform, deform.Form)
 | 
					        self.assertIsInstance(dform, deform.Form)
 | 
				
			||||||
        self.assertIs(form.deform_form, dform)
 | 
					        self.assertIs(form.deform_form, dform)
 | 
				
			||||||
| 
						 | 
					@ -684,7 +684,7 @@ class TestForm(TestCase):
 | 
				
			||||||
    def test_validate(self):
 | 
					    def test_validate(self):
 | 
				
			||||||
        schema = self.make_schema()
 | 
					        schema = self.make_schema()
 | 
				
			||||||
        form = self.make_form(schema=schema)
 | 
					        form = self.make_form(schema=schema)
 | 
				
			||||||
        self.assertFalse(hasattr(form, "validated"))
 | 
					        self.assertIsNone(form.validated)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # will not validate unless request is POST
 | 
					        # will not validate unless request is POST
 | 
				
			||||||
        self.request.POST = {"foo": "blarg", "bar": "baz"}
 | 
					        self.request.POST = {"foo": "blarg", "bar": "baz"}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -497,7 +497,7 @@ class TestGrid(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # settings are loaded, applied, saved
 | 
					        # settings are loaded, applied, saved
 | 
				
			||||||
        self.assertEqual(grid.sort_defaults, [])
 | 
					        self.assertEqual(grid.sort_defaults, [])
 | 
				
			||||||
        self.assertFalse(hasattr(grid, "active_sorters"))
 | 
					        self.assertIsNone(grid.active_sorters)
 | 
				
			||||||
        self.request.GET = {"sort1key": "name", "sort1dir": "desc"}
 | 
					        self.request.GET = {"sort1key": "name", "sort1dir": "desc"}
 | 
				
			||||||
        grid.load_settings()
 | 
					        grid.load_settings()
 | 
				
			||||||
        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
 | 
					        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
 | 
				
			||||||
| 
						 | 
					@ -525,7 +525,7 @@ class TestGrid(WebTestCase):
 | 
				
			||||||
            sort_on_backend=True,
 | 
					            sort_on_backend=True,
 | 
				
			||||||
            sort_defaults="name",
 | 
					            sort_defaults="name",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertFalse(hasattr(grid, "active_sorters"))
 | 
					        self.assertIsNone(grid.active_sorters)
 | 
				
			||||||
        grid.load_settings()
 | 
					        grid.load_settings()
 | 
				
			||||||
        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
 | 
					        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -537,7 +537,7 @@ class TestGrid(WebTestCase):
 | 
				
			||||||
            mod.SortInfo("name", "asc"),
 | 
					            mod.SortInfo("name", "asc"),
 | 
				
			||||||
            mod.SortInfo("value", "desc"),
 | 
					            mod.SortInfo("value", "desc"),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        self.assertFalse(hasattr(grid, "active_sorters"))
 | 
					        self.assertIsNone(grid.active_sorters)
 | 
				
			||||||
        grid.load_settings()
 | 
					        grid.load_settings()
 | 
				
			||||||
        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
 | 
					        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "asc"}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -556,7 +556,7 @@ class TestGrid(WebTestCase):
 | 
				
			||||||
            paginated=True,
 | 
					            paginated=True,
 | 
				
			||||||
            paginate_on_backend=True,
 | 
					            paginate_on_backend=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertFalse(hasattr(grid, "active_sorters"))
 | 
					        self.assertIsNone(grid.active_sorters)
 | 
				
			||||||
        grid.load_settings()
 | 
					        grid.load_settings()
 | 
				
			||||||
        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
 | 
					        self.assertEqual(grid.active_sorters, [{"key": "name", "dir": "desc"}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ class TestGridFilter(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # verb is not set by default, but can be set
 | 
					        # verb is not set by default, but can be set
 | 
				
			||||||
        filtr = self.make_filter(model.Setting.name)
 | 
					        filtr = self.make_filter(model.Setting.name)
 | 
				
			||||||
        self.assertFalse(hasattr(filtr, "verb"))
 | 
					        self.assertIsNone(filtr.verb)
 | 
				
			||||||
        filtr = self.make_filter(model.Setting.name, verb="foo")
 | 
					        filtr = self.make_filter(model.Setting.name, verb="foo")
 | 
				
			||||||
        self.assertEqual(filtr.verb, "foo")
 | 
					        self.assertEqual(filtr.verb, "foo")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,23 +76,23 @@ class TestNewRequest(TestCase):
 | 
				
			||||||
        subscribers.new_request(event)
 | 
					        subscribers.new_request(event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # component tracking dict is missing at first
 | 
					        # component tracking dict is missing at first
 | 
				
			||||||
        self.assertFalse(hasattr(self.request, "_wuttaweb_registered_components"))
 | 
					        self.assertFalse(hasattr(self.request, "wuttaweb_registered_components"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # registering a component
 | 
					        # registering a component
 | 
				
			||||||
        self.request.register_component("foo-example", "FooExample")
 | 
					        self.request.register_component("foo-example", "FooExample")
 | 
				
			||||||
        self.assertTrue(hasattr(self.request, "_wuttaweb_registered_components"))
 | 
					        self.assertTrue(hasattr(self.request, "wuttaweb_registered_components"))
 | 
				
			||||||
        self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
 | 
					        self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
 | 
				
			||||||
        self.assertIn("foo-example", self.request._wuttaweb_registered_components)
 | 
					        self.assertIn("foo-example", self.request.wuttaweb_registered_components)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            self.request._wuttaweb_registered_components["foo-example"], "FooExample"
 | 
					            self.request.wuttaweb_registered_components["foo-example"], "FooExample"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # re-registering same name
 | 
					        # re-registering same name
 | 
				
			||||||
        self.request.register_component("foo-example", "FooExample")
 | 
					        self.request.register_component("foo-example", "FooExample")
 | 
				
			||||||
        self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
 | 
					        self.assertEqual(len(self.request.wuttaweb_registered_components), 1)
 | 
				
			||||||
        self.assertIn("foo-example", self.request._wuttaweb_registered_components)
 | 
					        self.assertIn("foo-example", self.request.wuttaweb_registered_components)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            self.request._wuttaweb_registered_components["foo-example"], "FooExample"
 | 
					            self.request.wuttaweb_registered_components["foo-example"], "FooExample"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_referrer(self):
 | 
					    def test_get_referrer(self):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ from wuttjamaican.util import resource_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb import util as mod
 | 
					from wuttaweb import util as mod
 | 
				
			||||||
from wuttaweb.app import establish_theme
 | 
					from wuttaweb.app import establish_theme
 | 
				
			||||||
 | 
					from wuttaweb.grids import Grid
 | 
				
			||||||
from wuttaweb.testing import WebTestCase
 | 
					from wuttaweb.testing import WebTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -665,6 +666,52 @@ class TestMakeJsonSafe(TestCase):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestRenderVueFinalize(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def basic(self):
 | 
				
			||||||
 | 
					        html = mod.render_vue_finalize("wutta-grid", "WuttaGrid")
 | 
				
			||||||
 | 
					        self.assertIn("<script>", html)
 | 
				
			||||||
 | 
					        self.assertIn("Vue.component('wutta-grid', WuttaGrid)", html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestMakeUsersGrid(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_make_users_grid(self):
 | 
				
			||||||
 | 
					        self.pyramid_config.add_route("users.view", "/users/{uuid}/view")
 | 
				
			||||||
 | 
					        self.pyramid_config.add_route("users.edit", "/users/{uuid}/edit")
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        person = model.Person(full_name="John Doe")
 | 
				
			||||||
 | 
					        self.session.add(person)
 | 
				
			||||||
 | 
					        user = model.User(username="john", person=person)
 | 
				
			||||||
 | 
					        self.session.add(user)
 | 
				
			||||||
 | 
					        self.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # basic (no actions because not prvileged)
 | 
				
			||||||
 | 
					        grid = mod.make_users_grid(self.request, key="blah.users", data=person.users)
 | 
				
			||||||
 | 
					        self.assertIsInstance(grid, Grid)
 | 
				
			||||||
 | 
					        self.assertFalse(grid.linked_columns)
 | 
				
			||||||
 | 
					        self.assertFalse(grid.actions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # key may be derived from route_prefix
 | 
				
			||||||
 | 
					        grid = mod.make_users_grid(self.request, route_prefix="foo")
 | 
				
			||||||
 | 
					        self.assertIsInstance(grid, Grid)
 | 
				
			||||||
 | 
					        self.assertEqual(grid.key, "foo.view.users")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # view + edit actions (because root)
 | 
				
			||||||
 | 
					        with patch.object(self.request, "is_root", new=True):
 | 
				
			||||||
 | 
					            grid = mod.make_users_grid(
 | 
				
			||||||
 | 
					                self.request, key="blah.users", data=person.users
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            self.assertIsInstance(grid, Grid)
 | 
				
			||||||
 | 
					            self.assertIn("username", grid.linked_columns)
 | 
				
			||||||
 | 
					            self.assertEqual(len(grid.actions), 2)
 | 
				
			||||||
 | 
					            self.assertEqual(grid.actions[0].key, "view")
 | 
				
			||||||
 | 
					            self.assertEqual(grid.actions[1].key, "edit")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # render grid to ensure coverage for link urls
 | 
				
			||||||
 | 
					            grid.render_vue_template()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestGetAvailableThemes(TestCase):
 | 
					class TestGetAvailableThemes(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -624,7 +624,7 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
        view = self.make_view()
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # empty by default
 | 
					        # empty by default
 | 
				
			||||||
        self.assertFalse(hasattr(mod.MasterView, "model_class"))
 | 
					        self.assertIsNone(mod.MasterView.model_class)
 | 
				
			||||||
        data = view.get_grid_data(session=self.session)
 | 
					        data = view.get_grid_data(session=self.session)
 | 
				
			||||||
        self.assertEqual(data, [])
 | 
					        self.assertEqual(data, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1371,6 +1371,25 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
            self.session.commit()
 | 
					            self.session.commit()
 | 
				
			||||||
            self.assertEqual(self.session.query(model.Setting).count(), 7)
 | 
					            self.assertEqual(self.session.query(model.Setting).count(), 7)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_do_thread_body(self):
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # nb. so far this is just proving coverage, in case caller
 | 
				
			||||||
 | 
					        # does not specify an error handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def func():
 | 
				
			||||||
 | 
					            raise RuntimeError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # with error handler
 | 
				
			||||||
 | 
					        onerror = MagicMock()
 | 
				
			||||||
 | 
					        view.do_thread_body(func, (), {}, onerror)
 | 
				
			||||||
 | 
					        onerror.assert_called_once_with()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # without error handler
 | 
				
			||||||
 | 
					        onerror.reset_mock()
 | 
				
			||||||
 | 
					        view.do_thread_body(func, (), {})
 | 
				
			||||||
 | 
					        onerror.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete_bulk_thread(self):
 | 
					    def test_delete_bulk_thread(self):
 | 
				
			||||||
        self.pyramid_config.add_route("settings", "/settings/")
 | 
					        self.pyramid_config.add_route("settings", "/settings/")
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
| 
						 | 
					@ -1656,6 +1675,11 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
                        count = self.session.query(model.Setting).count()
 | 
					                        count = self.session.query(model.Setting).count()
 | 
				
			||||||
                        self.assertEqual(count, 0)
 | 
					                        self.assertEqual(count, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_configure_get_simple_settings(self):
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        settings = view.configure_get_simple_settings()
 | 
				
			||||||
 | 
					        self.assertEqual(settings, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_configure_gather_settings(self):
 | 
					    def test_configure_gather_settings(self):
 | 
				
			||||||
        view = self.make_view()
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue