From 4d0693862dedf8400eb610a29a84c324d169d0c0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 31 Aug 2025 12:26:43 -0500 Subject: [PATCH] fix: format all code with black and from now on should not deviate from that... --- docs/conf.py | 46 +- src/wuttaweb/_version.py | 2 +- src/wuttaweb/app.py | 80 +- src/wuttaweb/auth.py | 34 +- src/wuttaweb/cli/webapp.py | 44 +- src/wuttaweb/conf.py | 9 +- src/wuttaweb/db/continuum.py | 2 +- src/wuttaweb/emails.py | 15 +- src/wuttaweb/forms/base.py | 177 ++-- src/wuttaweb/forms/schema.py | 97 +- src/wuttaweb/forms/widgets.py | 118 ++- src/wuttaweb/grids/base.py | 539 +++++----- src/wuttaweb/grids/filters.py | 212 ++-- src/wuttaweb/handler.py | 34 +- src/wuttaweb/menus.py | 252 ++--- src/wuttaweb/progress.py | 34 +- src/wuttaweb/static/__init__.py | 10 +- src/wuttaweb/subscribers.py | 95 +- src/wuttaweb/testing.py | 44 +- src/wuttaweb/util.py | 267 ++--- src/wuttaweb/views/__init__.py | 2 +- src/wuttaweb/views/auth.py | 165 +-- src/wuttaweb/views/base.py | 2 +- src/wuttaweb/views/batch.py | 169 ++-- src/wuttaweb/views/common.py | 234 +++-- src/wuttaweb/views/email.py | 195 ++-- src/wuttaweb/views/essential.py | 18 +- src/wuttaweb/views/master.py | 746 ++++++++------ src/wuttaweb/views/people.py | 120 ++- src/wuttaweb/views/progress.py | 18 +- src/wuttaweb/views/reports.py | 116 +-- src/wuttaweb/views/roles.py | 130 +-- src/wuttaweb/views/settings.py | 181 ++-- src/wuttaweb/views/upgrades.py | 195 ++-- src/wuttaweb/views/users.py | 223 +++-- tasks.py | 14 +- tests/cli/test_webapp.py | 85 +- tests/db/test_continuum.py | 14 +- tests/forms/test_base.py | 545 +++++----- tests/forms/test_schema.py | 134 +-- tests/forms/test_widgets.py | 157 +-- tests/grids/test_base.py | 1666 +++++++++++++++++-------------- tests/grids/test_filters.py | 454 +++++---- tests/test_app.py | 122 ++- tests/test_auth.py | 76 +- tests/test_emails.py | 6 +- tests/test_handler.py | 98 +- tests/test_menus.py | 282 +++--- tests/test_progress.py | 18 +- tests/test_static.py | 2 +- tests/test_subscribers.py | 191 ++-- tests/test_util.py | 597 +++++------ tests/util.py | 9 +- tests/views/test___init__.py | 2 +- tests/views/test_auth.py | 107 +- tests/views/test_base.py | 14 +- tests/views/test_batch.py | 246 +++-- tests/views/test_common.py | 130 ++- tests/views/test_email.py | 246 +++-- tests/views/test_essential.py | 2 +- tests/views/test_master.py | 1379 +++++++++++++------------ tests/views/test_people.py | 54 +- tests/views/test_progress.py | 52 +- tests/views/test_reports.py | 261 +++-- tests/views/test_roles.py | 146 +-- tests/views/test_settings.py | 30 +- tests/views/test_upgrades.py | 270 ++--- tests/views/test_users.py | 318 +++--- 68 files changed, 6693 insertions(+), 5659 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 72faf60..6bcd169 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,41 +8,41 @@ from importlib.metadata import version as get_version -project = 'WuttaWeb' -copyright = '2024, Lance Edgar' -author = 'Lance Edgar' -release = get_version('WuttaWeb') +project = "WuttaWeb" +copyright = "2024, Lance Edgar" +author = "Lance Edgar" +release = get_version("WuttaWeb") # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', - 'sphinxcontrib.programoutput', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.todo", + "sphinxcontrib.programoutput", ] -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = { - 'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None), - 'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None), - 'fanstatic': ('https://www.fanstatic.org/en/latest/', None), - 'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None), - 'python': ('https://docs.python.org/3/', None), - 'rattail-manual': ('https://docs.wuttaproject.org/rattail-manual/', None), - 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None), - 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), - 'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), - 'wutta-continuum': ('https://docs.wuttaproject.org/wutta-continuum/', None), + "colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None), + "deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None), + "fanstatic": ("https://www.fanstatic.org/en/latest/", None), + "pyramid": ("https://docs.pylonsproject.org/projects/pyramid/en/latest/", None), + "python": ("https://docs.python.org/3/", None), + "rattail-manual": ("https://docs.wuttaproject.org/rattail-manual/", None), + "sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None), + "webhelpers2": ("https://webhelpers2.readthedocs.io/en/latest/", None), + "wuttjamaican": ("https://docs.wuttaproject.org/wuttjamaican/", None), + "wutta-continuum": ("https://docs.wuttaproject.org/wutta-continuum/", None), } # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'furo' -html_static_path = ['_static'] +html_theme = "furo" +html_static_path = ["_static"] diff --git a/src/wuttaweb/_version.py b/src/wuttaweb/_version.py index 27aaeb4..8423504 100644 --- a/src/wuttaweb/_version.py +++ b/src/wuttaweb/_version.py @@ -3,4 +3,4 @@ from importlib.metadata import version -__version__ = version('wuttaweb') +__version__ = version("wuttaweb") diff --git a/src/wuttaweb/app.py b/src/wuttaweb/app.py index 7c65f86..1c96d40 100644 --- a/src/wuttaweb/app.py +++ b/src/wuttaweb/app.py @@ -48,8 +48,9 @@ class WebAppProvider(AppProvider): registers some :term:`email templates ` for the app, etc. """ - email_modules = ['wuttaweb.emails'] - email_templates = ['wuttaweb:email-templates'] + + email_modules = ["wuttaweb.emails"] + email_templates = ["wuttaweb:email-templates"] def get_web_handler(self, **kwargs): """ @@ -64,9 +65,11 @@ class WebAppProvider(AppProvider): :returns: Instance of :class:`~wuttaweb.handler.WebHandler`. """ - if 'web_handler' not in self.__dict__: - spec = self.config.get(f'{self.appname}.web.handler_spec', - default='wuttaweb.handler:WebHandler') + if "web_handler" not in self.__dict__: + spec = self.config.get( + f"{self.appname}.web.handler_spec", + default="wuttaweb.handler:WebHandler", + ) self.web_handler = self.app.load_object(spec)(self.config) return self.web_handler @@ -96,23 +99,25 @@ def make_wutta_config(settings, config_maker=None, **kwargs): If this config file path cannot be discovered, an error is raised. """ - wutta_config = settings.get('wutta_config') + wutta_config = settings.get("wutta_config") if not wutta_config: # validate config file path - path = settings.get('wutta.config') + path = settings.get("wutta.config") if not path or not os.path.exists(path): - raise ValueError("Please set 'wutta.config' in [app:main] " - "section of config to the path of your " - "config file. Lame, but necessary.") + raise ValueError( + "Please set 'wutta.config' in [app:main] " + "section of config to the path of your " + "config file. Lame, but necessary." + ) # make config, add to settings config_maker = config_maker or make_config wutta_config = config_maker(path, **kwargs) - settings['wutta_config'] = wutta_config + settings["wutta_config"] = wutta_config # configure database sessions - if hasattr(wutta_config, 'appdb_engine'): + if hasattr(wutta_config, "appdb_engine"): wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine) return wutta_config @@ -128,10 +133,11 @@ def make_pyramid_config(settings): :returns: Instance of :class:`pyramid:pyramid.config.Configurator`. """ - settings.setdefault('fanstatic.versioning', 'true') - settings.setdefault('mako.directories', ['wuttaweb:templates']) - settings.setdefault('pyramid_deform.template_search_path', - 'wuttaweb:templates/deform') + settings.setdefault("fanstatic.versioning", "true") + settings.setdefault("mako.directories", ["wuttaweb:templates"]) + settings.setdefault( + "pyramid_deform.template_search_path", "wuttaweb:templates/deform" + ) # update settings per current theme establish_theme(settings) @@ -142,21 +148,21 @@ def make_pyramid_config(settings): pyramid_config.set_security_policy(WuttaSecurityPolicy()) # require CSRF token for POST - pyramid_config.set_default_csrf_options(require_csrf=True, - token='_csrf', - header='X-CSRF-TOKEN') + pyramid_config.set_default_csrf_options( + require_csrf=True, token="_csrf", header="X-CSRF-TOKEN" + ) - pyramid_config.include('pyramid_beaker') - pyramid_config.include('pyramid_deform') - pyramid_config.include('pyramid_fanstatic') - pyramid_config.include('pyramid_mako') - pyramid_config.include('pyramid_tm') + pyramid_config.include("pyramid_beaker") + pyramid_config.include("pyramid_deform") + pyramid_config.include("pyramid_fanstatic") + pyramid_config.include("pyramid_mako") + pyramid_config.include("pyramid_tm") # add some permissions magic - pyramid_config.add_directive('add_wutta_permission_group', - 'wuttaweb.auth.add_permission_group') - pyramid_config.add_directive('add_wutta_permission', - 'wuttaweb.auth.add_permission') + pyramid_config.add_directive( + "add_wutta_permission_group", "wuttaweb.auth.add_permission_group" + ) + pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission") return pyramid_config @@ -179,9 +185,9 @@ def main(global_config, **settings): wutta_config = make_wutta_config(settings) pyramid_config = make_pyramid_config(settings) - pyramid_config.include('wuttaweb.static') - pyramid_config.include('wuttaweb.subscribers') - pyramid_config.include('wuttaweb.views') + pyramid_config.include("wuttaweb.static") + pyramid_config.include("wuttaweb.subscribers") + pyramid_config.include("wuttaweb.views") return pyramid_config.make_wsgi_app() @@ -226,10 +232,10 @@ def make_wsgi_app(main_app=None, config=None): app = config.get_app() # extract pyramid settings - settings = config.get_dict('app:main') + settings = config.get_dict("app:main") # keep same config object - settings['wutta_config'] = config + settings["wutta_config"] = config # determine the app factory if isinstance(main_app, str): @@ -270,15 +276,15 @@ def establish_theme(settings): will update ``settings['mako.directories']`` such that the theme's template path is listed first. """ - config = settings['wutta_config'] + config = settings["wutta_config"] theme = get_effective_theme(config) - settings['wuttaweb.theme'] = theme + settings["wuttaweb.theme"] = theme - directories = settings['mako.directories'] + directories = settings["mako.directories"] if isinstance(directories, str): directories = config.parse_list(directories) path = get_theme_template_path(config) directories.insert(0, path) - settings['mako.directories'] = directories + settings["mako.directories"] = directories diff --git a/src/wuttaweb/auth.py b/src/wuttaweb/auth.py index d99b142..6bc83d5 100644 --- a/src/wuttaweb/auth.py +++ b/src/wuttaweb/auth.py @@ -108,7 +108,7 @@ class WuttaSecurityPolicy: self.db_session = db_session or Session() def load_identity(self, request): - config = request.registry.settings['wutta_config'] + config = request.registry.settings["wutta_config"] app = config.get_app() model = app.model @@ -141,10 +141,10 @@ class WuttaSecurityPolicy: def permits(self, request, context, permission): # nb. root user can do anything - if getattr(request, 'is_root', False): + if getattr(request, "is_root", False): return True - config = request.registry.settings['wutta_config'] + config = request.registry.settings["wutta_config"] app = config.get_app() auth = app.get_auth_handler() user = self.identity(request) @@ -183,14 +183,16 @@ def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True): See also :func:`add_permission()`. """ - config = pyramid_config.get_settings()['wutta_config'] + config = pyramid_config.get_settings()["wutta_config"] app = config.get_app() + def action(): - perms = pyramid_config.get_settings().get('wutta_permissions', {}) + perms = pyramid_config.get_settings().get("wutta_permissions", {}) if overwrite or groupkey not in perms: - group = perms.setdefault(groupkey, {'key': groupkey}) - group['label'] = label or app.make_title(groupkey) - pyramid_config.add_settings({'wutta_permissions': perms}) + group = perms.setdefault(groupkey, {"key": groupkey}) + group["label"] = label or app.make_title(groupkey) + pyramid_config.add_settings({"wutta_permissions": perms}) + pyramid_config.action(None, action) @@ -229,13 +231,15 @@ def add_permission(pyramid_config, groupkey, key, label=None): See also :func:`add_permission_group()`. """ + def action(): - config = pyramid_config.get_settings()['wutta_config'] + config = pyramid_config.get_settings()["wutta_config"] app = config.get_app() - perms = pyramid_config.get_settings().get('wutta_permissions', {}) - group = perms.setdefault(groupkey, {'key': groupkey}) - group.setdefault('label', app.make_title(groupkey)) - perm = group.setdefault('perms', {}).setdefault(key, {'key': key}) - perm['label'] = label or app.make_title(key) - pyramid_config.add_settings({'wutta_permissions': perms}) + perms = pyramid_config.get_settings().get("wutta_permissions", {}) + group = perms.setdefault(groupkey, {"key": groupkey}) + group.setdefault("label", app.make_title(groupkey)) + perm = group.setdefault("perms", {}).setdefault(key, {"key": key}) + perm["label"] = label or app.make_title(key) + pyramid_config.add_settings({"wutta_permissions": perms}) + pyramid_config.action(None, action) diff --git a/src/wuttaweb/cli/webapp.py b/src/wuttaweb/cli/webapp.py index cf84682..6ec377c 100644 --- a/src/wuttaweb/cli/webapp.py +++ b/src/wuttaweb/cli/webapp.py @@ -36,11 +36,11 @@ from wuttjamaican.cli import wutta_typer @wutta_typer.command() def webapp( - ctx: typer.Context, - auto_reload: Annotated[ - bool, - typer.Option('--reload', '-r', - help="Auto-reload web app when files change.")] = False, + ctx: typer.Context, + auto_reload: Annotated[ + bool, + typer.Option("--reload", "-r", help="Auto-reload web app when files change."), + ] = False, ): """ Run the configured web app @@ -52,34 +52,38 @@ def webapp( sys.stderr.write("no config files found!\n") sys.exit(1) - runner = config.get(f'{config.appname}.web.app.runner', default='pserve') - if runner == 'pserve': + runner = config.get(f"{config.appname}.web.app.runner", default="pserve") + if runner == "pserve": # run pserve - argv = ['pserve', f'file+ini:{config.files_read[0]}'] - if ctx.params['auto_reload']: - argv.append('--reload') + argv = ["pserve", f"file+ini:{config.files_read[0]}"] + if ctx.params["auto_reload"]: + argv.append("--reload") pserve.main(argv=argv) - elif runner == 'uvicorn': + elif runner == "uvicorn": import uvicorn # need service details from config - spec = config.require(f'{config.appname}.web.app.spec') + spec = config.require(f"{config.appname}.web.app.spec") kw = { - 'host': config.get(f'{config.appname}.web.app.host', default='127.0.0.1'), - 'port': config.get_int(f'{config.appname}.web.app.port', default=8000), - 'reload': ctx.params['auto_reload'], - 'reload_dirs': config.get_list(f'{config.appname}.web.app.reload_dirs'), - 'factory': config.get_bool(f'{config.appname}.web.app.factory', default=False), - 'interface': config.get(f'{config.appname}.web.app.interface', default='auto'), - 'root_path': config.get(f'{config.appname}.web.app.root_path', default=''), + "host": config.get(f"{config.appname}.web.app.host", default="127.0.0.1"), + "port": config.get_int(f"{config.appname}.web.app.port", default=8000), + "reload": ctx.params["auto_reload"], + "reload_dirs": config.get_list(f"{config.appname}.web.app.reload_dirs"), + "factory": config.get_bool( + f"{config.appname}.web.app.factory", default=False + ), + "interface": config.get( + f"{config.appname}.web.app.interface", default="auto" + ), + "root_path": config.get(f"{config.appname}.web.app.root_path", default=""), } # also must inject our config files to env, since there is no # other way to specify when running via uvicorn - os.environ['WUTTA_CONFIG_FILES'] = os.pathsep.join(config.files_read) + os.environ["WUTTA_CONFIG_FILES"] = os.pathsep.join(config.files_read) # run uvicorn uvicorn.run(spec, **kw) diff --git a/src/wuttaweb/conf.py b/src/wuttaweb/conf.py index 7fb04ec..ed0a889 100644 --- a/src/wuttaweb/conf.py +++ b/src/wuttaweb/conf.py @@ -35,9 +35,12 @@ class WuttaWebConfigExtension(WuttaConfigExtension): only relevant if Wutta-Continuum is installed and enabled. For more info see :doc:`wutta-continuum:index`. """ - key = 'wuttaweb' + + key = "wuttaweb" def configure(self, config): """ """ - config.setdefault('wutta_continuum.wutta_plugin_spec', - 'wuttaweb.db.continuum:WuttaWebContinuumPlugin') + config.setdefault( + "wutta_continuum.wutta_plugin_spec", + "wuttaweb.db.continuum:WuttaWebContinuumPlugin", + ) diff --git a/src/wuttaweb/db/continuum.py b/src/wuttaweb/db/continuum.py index 08c4151..920a9a3 100644 --- a/src/wuttaweb/db/continuum.py +++ b/src/wuttaweb/db/continuum.py @@ -28,7 +28,7 @@ from pyramid.threadlocal import get_current_request try: from wutta_continuum.conf import WuttaContinuumPlugin -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover pass else: diff --git a/src/wuttaweb/emails.py b/src/wuttaweb/emails.py index 4954bec..4ca27e1 100644 --- a/src/wuttaweb/emails.py +++ b/src/wuttaweb/emails.py @@ -31,18 +31,19 @@ class feedback(EmailSetting): """ Sent when user submits feedback via the web app. """ + default_subject = "User Feedback" def sample_data(self): """ """ model = self.app.model person = model.Person(full_name="Barney Rubble") - user = model.User(username='barney', person=person) + user = model.User(username="barney", person=person) return { - 'user': user, - 'user_name': str(person), - 'user_url': '#', - 'referrer': 'http://example.com/', - 'client_ip': '127.0.0.1', - 'message': "This app is cool but needs a new feature.\n\nAllow me to describe...", + "user": user, + "user_name": str(person), + "user_url": "#", + "referrer": "http://example.com/", + "client_ip": "127.0.0.1", + "message": "This app is cool but needs a new feature.\n\nAllow me to describe...", } diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index 504e012..e5eafbd 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -268,35 +268,35 @@ class Form: """ def __init__( - self, - request, - fields=None, - schema=None, - model_class=None, - model_instance=None, - nodes={}, - widgets={}, - validators={}, - defaults={}, - readonly=False, - readonly_fields=[], - required_fields={}, - labels={}, - action_method='post', - action_url=None, - reset_url=None, - cancel_url=None, - cancel_url_fallback=None, - vue_tagname='wutta-form', - align_buttons_right=False, - auto_disable_submit=True, - button_label_submit="Save", - button_icon_submit='save', - button_type_submit='is-primary', - show_button_reset=False, - show_button_cancel=True, - button_label_cancel="Cancel", - auto_disable_cancel=True, + self, + request, + fields=None, + schema=None, + model_class=None, + model_instance=None, + nodes={}, + widgets={}, + validators={}, + defaults={}, + readonly=False, + readonly_fields=[], + required_fields={}, + labels={}, + action_method="post", + action_url=None, + reset_url=None, + cancel_url=None, + cancel_url_fallback=None, + vue_tagname="wutta-form", + align_buttons_right=False, + auto_disable_submit=True, + button_label_submit="Save", + button_icon_submit="save", + button_type_submit="is-primary", + show_button_reset=False, + show_button_cancel=True, + button_label_cancel="Cancel", + auto_disable_cancel=True, ): self.request = request self.schema = schema @@ -367,8 +367,8 @@ class Form: This is a generated value based on :attr:`vue_tagname`. """ - words = self.vue_tagname.split('-') - return ''.join([word.capitalize() for word in words]) + words = self.vue_tagname.split("-") + return "".join([word.capitalize() for word in words]) def get_cancel_url(self): """ @@ -392,8 +392,8 @@ class Form: # nb. use fake default to avoid normal default logic; # that way if we get something it's a real referrer - url = self.request.get_referrer(default='NOPE') - if url and url != 'NOPE': + url = self.request.get_referrer(default="NOPE") + if url and url != "NOPE": return url # use fallback URL if set @@ -471,8 +471,8 @@ class Form: # assume nodeinfo is a complete node node = nodeinfo - else: # assume nodeinfo is a schema type - kwargs.setdefault('name', key) + else: # assume nodeinfo is a schema type + kwargs.setdefault("name", key) node = ObjectNode(nodeinfo, **kwargs) self.nodes[key] = node @@ -534,7 +534,7 @@ class Form: """ from wuttaweb.forms import widgets - if widget_type == 'notes': + if widget_type == "notes": return widgets.NotesWidget(**kwargs) def set_default_widgets(self): @@ -565,7 +565,7 @@ class Form: attr = getattr(self.model_class, key, None) if attr: - prop = getattr(attr, 'prop', None) + prop = getattr(attr, "prop", None) if prop and isinstance(prop, orm.ColumnProperty): column = prop.columns[0] if isinstance(column.type, sa.Date): @@ -596,8 +596,10 @@ class Form: raise ValueError("grid must have a key!") if grid.key in self.grid_vue_context: - log.warning("grid data with key '%s' already registered, " - "but will be replaced", grid.key) + log.warning( + "grid data with key '%s' already registered, " "but will be replaced", + grid.key, + ) self.grid_vue_context[grid.key] = grid.get_vue_context() @@ -748,7 +750,7 @@ class Form: Otherwise ``None`` is returned. """ - if hasattr(self, 'fields') and self.fields: + if hasattr(self, "fields") and self.fields: return self.fields if self.schema: @@ -769,8 +771,9 @@ class Form: fields. If not set, the form's :attr:`model_class` is assumed. """ - return get_model_fields(self.config, - model_class=model_class or self.model_class) + return get_model_fields( + self.config, model_class=model_class or self.model_class + ) def get_schema(self): """ @@ -802,8 +805,7 @@ class Form: includes.append(key) # make initial schema with ColanderAlchemy magic - schema = SQLAlchemySchemaNode(self.model_class, - includes=includes) + schema = SQLAlchemySchemaNode(self.model_class, includes=includes) # fill in the blanks if anything got missed for key in fields: @@ -824,9 +826,7 @@ class Form: if not node: # otherwise make simple string node - node = colander.SchemaNode( - colander.String(), - name=key) + node = colander.SchemaNode(colander.String(), name=key) schema.add(node) @@ -844,7 +844,7 @@ class Form: if key is None: # nb. this one is form-wide schema.validator = validator - elif key in schema: # field-level + elif key in schema: # field-level schema[key].validator = validator # apply default value overrides @@ -867,7 +867,7 @@ class Form: Return the :class:`deform:deform.Form` instance for the form, generating it automatically if necessary. """ - if not hasattr(self, 'deform_form'): + if not hasattr(self, "deform_form"): model = self.app.model schema = self.get_schema() kwargs = {} @@ -897,9 +897,9 @@ class Form: # this is what we are trying currently... if isinstance(schema, SQLAlchemySchemaNode): - kwargs['appstruct'] = schema.dictify(self.model_instance) + kwargs["appstruct"] = schema.dictify(self.model_instance) else: - kwargs['appstruct'] = self.model_instance + kwargs["appstruct"] = self.model_instance # create the Deform instance # nb. must give a reference back to wutta form; this is @@ -926,10 +926,7 @@ class Form: """ return HTML.tag(self.vue_tagname, **kwargs) - def render_vue_template( - self, - template='/forms/vue_template.mako', - **context): + def render_vue_template(self, template="/forms/vue_template.mako", **context): """ Render the Vue template block for the form. @@ -962,29 +959,29 @@ class Form: :param template: Path to Mako template which is used to render the output. """ - context['form'] = self - context['dform'] = self.get_deform() - context.setdefault('request', self.request) - context['model_data'] = self.get_vue_model_data() + context["form"] = self + context["dform"] = self.get_deform() + context.setdefault("request", self.request) + context["model_data"] = self.get_vue_model_data() # set form method, enctype - context.setdefault('form_attrs', {}) - context['form_attrs'].setdefault('method', self.action_method) - if self.action_method == 'post': - context['form_attrs'].setdefault('enctype', 'multipart/form-data') + context.setdefault("form_attrs", {}) + context["form_attrs"].setdefault("method", self.action_method) + if self.action_method == "post": + context["form_attrs"].setdefault("enctype", "multipart/form-data") # auto disable button on submit if self.auto_disable_submit: - context['form_attrs']['@submit'] = 'formSubmitting = true' + context["form_attrs"]["@submit"] = "formSubmitting = true" output = render(template, context) return HTML.literal(output) def render_vue_field( - self, - fieldname, - readonly=None, - **kwargs, + self, + fieldname, + readonly=None, + **kwargs, ): """ Render the given field completely, i.e. ```` wrapper @@ -1026,7 +1023,7 @@ class Form: field = dform[fieldname] kw = {} if readonly: - kw['readonly'] = True + kw["readonly"] = True html = field.serialize(**kw) else: @@ -1034,20 +1031,20 @@ class Form: # TODO: need to abstract this somehow if self.model_instance: value = self.model_instance[fieldname] - html = str(value) if value is not None else '' + html = str(value) if value is not None else "" else: - html = '' + html = "" # mark all that as safe - html = HTML.literal(html or ' ') + html = HTML.literal(html or " ") # render field label label = self.get_label(fieldname) # b-field attrs attrs = { - ':horizontal': 'true', - 'label': label, + ":horizontal": "true", + "label": label, } # next we will build array of messages to display..some @@ -1059,22 +1056,21 @@ class Form: # show errors if present errors = self.get_field_errors(fieldname) if errors: - field_type = 'is-danger' + field_type = "is-danger" messages.extend(errors) # ..okay now we can declare the field messages and type if field_type: - attrs['type'] = field_type + attrs["type"] = field_type if messages: - cls = 'is-size-7' - if field_type == 'is-danger': - cls += ' has-text-danger' - messages = [HTML.tag('p', c=[msg], class_=cls) - for msg in messages] - slot = HTML.tag('slot', name='messages', c=messages) - html = HTML.tag('div', c=[html, slot]) + cls = "is-size-7" + if field_type == "is-danger": + cls += " has-text-danger" + messages = [HTML.tag("p", c=[msg], class_=cls) for msg in messages] + slot = HTML.tag("slot", name="messages", c=messages) + html = HTML.tag("div", c=[html, slot]) - return HTML.tag('b-field', c=[html], **attrs) + return HTML.tag("b-field", c=[html], **attrs) def render_vue_finalize(self): """ @@ -1094,11 +1090,10 @@ class Form: """ set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}" make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})" - return HTML.tag('script', c=['\n', - HTML.literal(set_data), - '\n', - HTML.literal(make_component), - '\n']) + return HTML.tag( + "script", + c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"], + ) def get_vue_model_data(self): """ @@ -1191,10 +1186,10 @@ class Form: :returns: Data dict, or ``False``. """ - if hasattr(self, 'validated'): + if hasattr(self, "validated"): del self.validated - if self.request.method != 'POST': + if self.request.method != "POST": return False # remove all readonly fields from deform / schema diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 605b2c5..a8a3f49 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -55,8 +55,8 @@ class WuttaDateTime(colander.DateTime): return colander.null formats = [ - '%Y-%m-%dT%H:%M:%S', - '%Y-%m-%dT%I:%M %p', + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%I:%M %p", ] for fmt in formats: @@ -98,7 +98,7 @@ class ObjectNode(colander.SchemaNode): If the node's type does not have a ``dictify()`` method, this will just convert the object to a string and return that. """ - if hasattr(self.typ, 'dictify'): + if hasattr(self.typ, "dictify"): return self.typ.dictify(obj) # TODO: this is better than raising an error, as it previously @@ -122,7 +122,7 @@ class ObjectNode(colander.SchemaNode): If the node's type does not have an ``objectify()`` method, this will raise ``NotImplementeError``. """ - if hasattr(self.typ, 'objectify'): + if hasattr(self.typ, "objectify"): return self.typ.objectify(value) class_name = self.typ.__class__.__name__ @@ -148,9 +148,10 @@ class WuttaEnum(colander.Enum): def widget_maker(self, **kwargs): """ """ - if 'values' not in kwargs: - kwargs['values'] = [(getattr(e, self.attr), getattr(e, self.attr)) - for e in self.enum_cls] + if "values" not in kwargs: + kwargs["values"] = [ + (getattr(e, self.attr), getattr(e, self.attr)) for e in self.enum_cls + ] return widgets.SelectWidget(**kwargs) @@ -180,8 +181,8 @@ class WuttaDictEnum(colander.String): def widget_maker(self, **kwargs): """ """ - if 'values' not in kwargs: - kwargs['values'] = [(k, v) for k, v in self.enum_dct.items()] + if "values" not in kwargs: + kwargs["values"] = [(k, v) for k, v in self.enum_dct.items()] return widgets.SelectWidget(**kwargs) @@ -201,7 +202,7 @@ class WuttaMoney(colander.Money): """ def __init__(self, request, *args, **kwargs): - self.scale = kwargs.pop('scale', None) + self.scale = kwargs.pop("scale", None) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -210,7 +211,7 @@ class WuttaMoney(colander.Money): def widget_maker(self, **kwargs): """ """ if self.scale: - kwargs.setdefault('scale', self.scale) + kwargs.setdefault("scale", self.scale) return widgets.WuttaMoneyInputWidget(self.request, **kwargs) @@ -284,17 +285,17 @@ class ObjectRef(colander.SchemaType): Note that in the latter, ``value`` must be a string. """ - default_empty_option = ('', "(none)") + default_empty_option = ("", "(none)") def __init__( - self, - request, - empty_option=None, - *args, - **kwargs, + self, + request, + empty_option=None, + *args, + **kwargs, ): # nb. allow session injection for tests - self.session = kwargs.pop('session', Session()) + self.session = kwargs.pop("session", Session()) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -307,7 +308,7 @@ class ObjectRef(colander.SchemaType): elif isinstance(empty_option, tuple) and len(empty_option) == 2: self.empty_option = empty_option else: - self.empty_option = ('', str(empty_option)) + self.empty_option = ("", str(empty_option)) else: self.empty_option = None @@ -427,17 +428,16 @@ class ObjectRef(colander.SchemaType): :class:`~wuttaweb.forms.widgets.ObjectRefWidget`. """ - if 'values' not in kwargs: + if "values" not in kwargs: query = self.get_query() objects = query.all() - values = [(self.serialize_object(obj), str(obj)) - for obj in objects] + values = [(self.serialize_object(obj), str(obj)) for obj in objects] if self.empty_option: values.insert(0, self.empty_option) - kwargs['values'] = values + kwargs["values"] = values - if 'url' not in kwargs: - kwargs['url'] = self.get_object_url + if "url" not in kwargs: + kwargs["url"] = self.get_object_url return widgets.ObjectRefWidget(self.request, **kwargs) @@ -475,7 +475,7 @@ class PersonRef(ObjectRef): def get_object_url(self, person): """ """ - return self.request.route_url('people.view', uuid=person.uuid) + return self.request.route_url("people.view", uuid=person.uuid) class RoleRef(ObjectRef): @@ -499,7 +499,7 @@ class RoleRef(ObjectRef): def get_object_url(self, role): """ """ - return self.request.route_url('roles.view', uuid=role.uuid) + return self.request.route_url("roles.view", uuid=role.uuid) class UserRef(ObjectRef): @@ -523,7 +523,7 @@ class UserRef(ObjectRef): def get_object_url(self, user): """ """ - return self.request.route_url('users.view', uuid=user.uuid) + return self.request.route_url("users.view", uuid=user.uuid) class RoleRefs(WuttaSet): @@ -544,9 +544,9 @@ class RoleRefs(WuttaSet): :returns: Instance of :class:`~wuttaweb.forms.widgets.RoleRefsWidget`. """ - session = kwargs.setdefault('session', Session()) + session = kwargs.setdefault("session", Session()) - if 'values' not in kwargs: + if "values" not in kwargs: model = self.app.model auth = self.app.get_auth_handler() @@ -562,12 +562,14 @@ class RoleRefs(WuttaSet): avoid.add(auth.get_role_administrator(session).uuid) # everything else can be (un)assigned for users - roles = session.query(model.Role)\ - .filter(~model.Role.uuid.in_(avoid))\ - .order_by(model.Role.name)\ - .all() + roles = ( + session.query(model.Role) + .filter(~model.Role.uuid.in_(avoid)) + .order_by(model.Role.name) + .all() + ) values = [(role.uuid.hex, role.name) for role in roles] - kwargs['values'] = values + kwargs["values"] = values return widgets.RoleRefsWidget(self.request, **kwargs) @@ -598,15 +600,15 @@ class Permissions(WuttaSet): :returns: Instance of :class:`~wuttaweb.forms.widgets.PermissionsWidget`. """ - kwargs.setdefault('session', Session()) - kwargs.setdefault('permissions', self.permissions) + kwargs.setdefault("session", Session()) + kwargs.setdefault("permissions", self.permissions) - if 'values' not in kwargs: + if "values" not in kwargs: values = [] for gkey, group in self.permissions.items(): - for pkey, perm in group['perms'].items(): - values.append((pkey, perm['label'])) - kwargs['values'] = values + for pkey, perm in group["perms"].items(): + values.append((pkey, perm["label"])) + kwargs["values"] = values return widgets.PermissionsWidget(self.request, **kwargs) @@ -631,7 +633,7 @@ class FileDownload(colander.String): """ def __init__(self, request, *args, **kwargs): - self.url = kwargs.pop('url', None) + self.url = kwargs.pop("url", None) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -639,7 +641,7 @@ class FileDownload(colander.String): def widget_maker(self, **kwargs): """ """ - kwargs.setdefault('url', self.url) + kwargs.setdefault("url", self.url) return widgets.FileDownloadWidget(self.request, **kwargs) @@ -653,16 +655,15 @@ class EmailRecipients(colander.String): if appstruct is colander.null: return colander.null - return '\n'.join(parse_list(appstruct)) + return "\n".join(parse_list(appstruct)) def deserialize(self, node, cstruct): """ """ if cstruct is colander.null: return colander.null - values = [value for value in parse_list(cstruct) - if value] - return ', '.join(values) + values = [value for value in parse_list(cstruct) if value] + return ", ".join(values) def widget_maker(self, **kwargs): """ @@ -675,4 +676,4 @@ class EmailRecipients(colander.String): # nb. colanderalchemy schema overrides -sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime} +sa.DateTime.__colanderalchemy_config__ = {"typ": WuttaDateTime} diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index ca53259..02377b1 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -47,10 +47,19 @@ import os import colander import humanize -from deform.widget import (Widget, TextInputWidget, TextAreaWidget, - PasswordWidget, CheckedPasswordWidget, - CheckboxWidget, SelectWidget, CheckboxChoiceWidget, - DateInputWidget, DateTimeInputWidget, MoneyInputWidget) +from deform.widget import ( + Widget, + TextInputWidget, + TextAreaWidget, + PasswordWidget, + CheckedPasswordWidget, + CheckboxWidget, + SelectWidget, + CheckboxChoiceWidget, + DateInputWidget, + DateTimeInputWidget, + MoneyInputWidget, +) from webhelpers2.html import HTML from wuttjamaican.conf import parse_list @@ -93,7 +102,8 @@ class ObjectRefWidget(SelectWidget): when the :class:`~wuttaweb.forms.schema.ObjectRef` type instance (associated with the node) is serialized. """ - readonly_template = 'readonly/objectref' + + readonly_template = "readonly/objectref" def __init__(self, request, url=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -105,10 +115,14 @@ class ObjectRefWidget(SelectWidget): values = super().get_template_values(field, cstruct, kw) # add url, only if rendering readonly - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if readonly: - if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None): - values['url'] = self.url(field.schema.model_instance) + if ( + "url" not in values + and self.url + and getattr(field.schema, "model_instance", None) + ): + values["url"] = self.url(field.schema.model_instance) return values @@ -128,7 +142,8 @@ class NotesWidget(TextAreaWidget): * ``textarea`` * ``readonly/notes`` """ - readonly_template = 'readonly/notes' + + readonly_template = "readonly/notes" class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): @@ -165,7 +180,8 @@ class WuttaCheckedPasswordWidget(PasswordWidget): * ``wutta_checked_password`` """ - template = 'wutta_checked_password' + + template = "wutta_checked_password" class WuttaDateWidget(DateInputWidget): @@ -197,7 +213,7 @@ class WuttaDateWidget(DateInputWidget): def serialize(self, field, cstruct, **kw): """ """ - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if readonly and cstruct: dt = datetime.datetime.fromisoformat(cstruct) return self.app.render_date(dt) @@ -234,7 +250,7 @@ class WuttaDateTimeWidget(DateTimeInputWidget): def serialize(self, field, cstruct, **kw): """ """ - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if readonly and cstruct: dt = datetime.datetime.fromisoformat(cstruct) return self.app.render_datetime(dt) @@ -264,7 +280,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget): """ def __init__(self, request, *args, **kwargs): - self.scale = kwargs.pop('scale', 2) + self.scale = kwargs.pop("scale", 2) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -272,13 +288,13 @@ class WuttaMoneyInputWidget(MoneyInputWidget): def serialize(self, field, cstruct, **kw): """ """ - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if readonly: if cstruct in (colander.null, None): - return HTML.tag('span') + return HTML.tag("span") cstruct = decimal.Decimal(cstruct) text = self.app.render_currency(cstruct, scale=self.scale) - return HTML.tag('span', c=[text]) + return HTML.tag("span", c=[text]) return super().serialize(field, cstruct, **kw) @@ -301,10 +317,11 @@ class FileDownloadWidget(Widget): :param url: Optional URL for hyperlink. If not specified, file name/size is shown with no hyperlink. """ - readonly_template = 'readonly/filedownload' + + readonly_template = "readonly/filedownload" def __init__(self, request, *args, **kwargs): - self.url = kwargs.pop('url', None) + self.url = kwargs.pop("url", None) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -313,21 +330,21 @@ class FileDownloadWidget(Widget): def serialize(self, field, cstruct, **kw): """ """ # nb. readonly is the only way this rolls - kw['readonly'] = True + kw["readonly"] = True template = self.readonly_template path = cstruct or None if path: - kw.setdefault('filename', os.path.basename(path)) - kw.setdefault('filesize', self.readable_size(path)) + kw.setdefault("filename", os.path.basename(path)) + kw.setdefault("filesize", self.readable_size(path)) if self.url: - kw.setdefault('url', self.url) + kw.setdefault("url", self.url) else: - kw.setdefault('filename', None) - kw.setdefault('filesize', None) + kw.setdefault("filename", None) + kw.setdefault("filesize", None) - kw.setdefault('url', None) + kw.setdefault("url", None) values = self.get_template_values(field, cstruct, kw) return field.renderer(template, **values) @@ -371,7 +388,7 @@ class GridWidget(Widget): :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on the ``grid`` to serialize. """ - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if not readonly: raise NotImplementedError("edit not allowed for this widget") @@ -387,14 +404,15 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget): This is a subclass of :class:`WuttaCheckboxChoiceWidget`. """ - readonly_template = 'readonly/rolerefs' + + readonly_template = "readonly/rolerefs" def serialize(self, field, cstruct, **kw): """ """ model = self.app.model # special logic when field is editable - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if not readonly: # but does not apply if current user is root @@ -404,12 +422,11 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget): # prune admin role from values list; it should not be # one of the options since current user is not admin - values = kw.get('values', self.values) - values = [val for val in values - if val[0] != admin.uuid] - kw['values'] = values + values = kw.get("values", self.values) + values = [val for val in values if val[0] != admin.uuid] + kw["values"] = values - else: # readonly + else: # readonly # roles roles = [] @@ -418,11 +435,11 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget): role = self.session.get(model.Role, uuid) if role: roles.append(role) - kw['roles'] = roles + kw["roles"] = roles # url - url = lambda role: self.request.route_url('roles.view', uuid=role.uuid) - kw['url'] = url + url = lambda role: self.request.route_url("roles.view", uuid=role.uuid) + kw["url"] = url # default logic from here return super().serialize(field, cstruct, **kw) @@ -440,19 +457,20 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget): * ``permissions`` * ``readonly/permissions`` """ - template = 'permissions' - readonly_template = 'readonly/permissions' + + template = "permissions" + readonly_template = "readonly/permissions" def serialize(self, field, cstruct, **kw): """ """ - kw.setdefault('permissions', self.permissions) + kw.setdefault("permissions", self.permissions) - if 'values' not in kw: + if "values" not in kw: values = [] for gkey, group in self.permissions.items(): - for pkey, perm in group['perms'].items(): - values.append((pkey, perm['label'])) - kw['values'] = values + for pkey, perm in group["perms"].items(): + values.append((pkey, perm["label"])) + kw["values"] = values return super().serialize(field, cstruct, **kw) @@ -472,13 +490,14 @@ class EmailRecipientsWidget(TextAreaWidget): See also the :class:`~wuttaweb.forms.schema.EmailRecipients` schema type, which uses this widget. """ - readonly_template = 'readonly/email_recips' + + readonly_template = "readonly/email_recips" def serialize(self, field, cstruct, **kw): """ """ - readonly = kw.get('readonly', self.readonly) + readonly = kw.get("readonly", self.readonly) if readonly: - kw['recips'] = parse_list(cstruct or '') + kw["recips"] = parse_list(cstruct or "") return super().serialize(field, cstruct, **kw) @@ -487,9 +506,8 @@ class EmailRecipientsWidget(TextAreaWidget): if pstruct is colander.null: return colander.null - values = [value for value in parse_list(pstruct) - if value] - return ', '.join(values) + values = [value for value in parse_list(pstruct) if value] + return ", ".join(values) class BatchIdWidget(Widget): @@ -508,4 +526,4 @@ class BatchIdWidget(Widget): return colander.null batch_id = int(cstruct) - return f'{batch_id:08d}' + return f"{batch_id:08d}" diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 6bf7274..739aa36 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -48,13 +48,14 @@ from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported log = logging.getLogger(__name__) -SortInfo = namedtuple('SortInfo', ['sortkey', 'sortdir']) +SortInfo = namedtuple("SortInfo", ["sortkey", "sortdir"]) SortInfo.__doc__ = """ Named tuple to track sorting info. Elements of :attr:`~Grid.sort_defaults` will be of this type. """ + class Grid: """ Base class for all :term:`grids `. @@ -374,37 +375,37 @@ class Grid: """ def __init__( - self, - request, - vue_tagname='wutta-grid', - model_class=None, - key=None, - columns=None, - data=None, - labels={}, - renderers={}, - enums={}, - checkable=False, - row_class=None, - actions=[], - linked_columns=[], - hidden_columns=[], - sortable=False, - sort_multiple=True, - sort_on_backend=True, - sorters=None, - sort_defaults=None, - paginated=False, - paginate_on_backend=True, - pagesize_options=None, - pagesize=None, - page=1, - searchable_columns=None, - filterable=False, - filters=None, - filter_defaults=None, - joiners=None, - tools=None, + self, + request, + vue_tagname="wutta-grid", + model_class=None, + key=None, + columns=None, + data=None, + labels={}, + renderers={}, + enums={}, + checkable=False, + row_class=None, + actions=[], + linked_columns=[], + hidden_columns=[], + sortable=False, + sort_multiple=True, + sort_on_backend=True, + sorters=None, + sort_defaults=None, + paginated=False, + paginate_on_backend=True, + pagesize_options=None, + pagesize=None, + page=1, + searchable_columns=None, + filterable=False, + filters=None, + filter_defaults=None, + joiners=None, + tools=None, ): self.request = request self.vue_tagname = vue_tagname @@ -434,7 +435,9 @@ class Grid: self.sortable = sortable self.sort_multiple = sort_multiple if self.sort_multiple and self.request.use_oruga: - log.warning("grid.sort_multiple is not implemented for Oruga-based templates") + log.warning( + "grid.sort_multiple is not implemented for Oruga-based templates" + ) self.sort_multiple = False self.sort_on_backend = sort_on_backend if sorters is not None: @@ -482,7 +485,7 @@ class Grid: Otherwise ``None`` is returned. """ - if hasattr(self, 'columns') and self.columns: + if hasattr(self, "columns") and self.columns: return self.columns columns = self.get_model_columns() @@ -500,8 +503,9 @@ class Grid: fields. If not set, the grid's :attr:`model_class` is assumed. """ - return get_model_fields(self.config, - model_class=model_class or self.model_class) + return get_model_fields( + self.config, model_class=model_class or self.model_class + ) @property def vue_component(self): @@ -510,8 +514,8 @@ class Grid: This is a generated value based on :attr:`vue_tagname`. """ - words = self.vue_tagname.split('-') - return ''.join([word.capitalize() for word in words]) + words = self.vue_tagname.split("-") + return "".join([word.capitalize() for word in words]) def set_columns(self, columns): """ @@ -576,7 +580,7 @@ class Grid: if hidden: if key not in self.hidden_columns: self.hidden_columns.append(key) - else: # un-hide + else: # un-hide if self.hidden_columns and key in self.hidden_columns: self.hidden_columns.remove(key) @@ -678,13 +682,13 @@ class Grid: Renderer overrides are tracked via :attr:`renderers`. """ builtins = { - 'batch_id': self.render_batch_id, - 'boolean': self.render_boolean, - 'currency': self.render_currency, - 'date': self.render_date, - 'datetime': self.render_datetime, - 'quantity': self.render_quantity, - 'percent': self.render_percent, + "batch_id": self.render_batch_id, + "boolean": self.render_boolean, + "currency": self.render_currency, + "date": self.render_date, + "datetime": self.render_datetime, + "quantity": self.render_quantity, + "percent": self.render_percent, } if renderer in builtins: @@ -722,7 +726,7 @@ class Grid: attr = getattr(self.model_class, key, None) if attr: - prop = getattr(attr, 'prop', None) + prop = getattr(attr, "prop", None) if prop and isinstance(prop, orm.ColumnProperty): column = prop.columns[0] if isinstance(column.type, sa.Date): @@ -780,7 +784,7 @@ class Grid: if link: if key not in self.linked_columns: self.linked_columns.append(key) - else: # unlink + else: # unlink if self.linked_columns and key in self.linked_columns: self.linked_columns.remove(key) @@ -946,8 +950,11 @@ class Grid: if key in sorters: continue prop = getattr(self.model_class, key, None) - if (prop and hasattr(prop, 'property') - and isinstance(prop.property, orm.ColumnProperty)): + if ( + prop + and hasattr(prop, "property") + and isinstance(prop.property, orm.ColumnProperty) + ): sorters[prop.key] = self.make_sorter(prop) return sorters @@ -1039,7 +1046,9 @@ class Grid: # query is sorted with order_by() if isinstance(data, orm.Query): if not model_property: - raise TypeError(f"grid sorter for '{key}' does not map to a model property") + raise TypeError( + f"grid sorter for '{key}' does not map to a model property" + ) query = data return query.order_by(getattr(model_property, direction)()) @@ -1052,16 +1061,16 @@ class Grid: # TODO: may need this for String etc. as well? if isinstance(model_property.type, sa.Text): if foldcase: - kfunc = lambda obj: (obj[key] or '').lower() + kfunc = lambda obj: (obj[key] or "").lower() else: - kfunc = lambda obj: obj[key] or '' + kfunc = lambda obj: obj[key] or "" if not kfunc: # nb. sorting with this can raise error if data # contains varying types, e.g. str and None kfunc = lambda obj: obj[key] # then sort the data and return - return sorted(data, key=kfunc, reverse=direction == 'desc') + return sorted(data, key=kfunc, reverse=direction == "desc") # TODO: this should be improved; is needed in tailbone for # multi-column sorting with sqlalchemy queries @@ -1168,13 +1177,15 @@ class Grid: sort_defaults = [] if len(args) == 1: if isinstance(args[0], str): - sort_defaults = [SortInfo(args[0], 'asc')] + sort_defaults = [SortInfo(args[0], "asc")] elif isinstance(args[0], tuple) and len(args[0]) == 2: sort_defaults = [SortInfo(*args[0])] elif isinstance(args[0], list): sort_defaults = [SortInfo(*tup) for tup in args[0]] else: - raise ValueError("for just one positional arg, must pass string, 2-tuple or list") + raise ValueError( + "for just one positional arg, must pass string, 2-tuple or list" + ) elif len(args) == 2: sort_defaults = [SortInfo(*args)] else: @@ -1182,9 +1193,12 @@ class Grid: # prune if multi-column requested but not supported if len(sort_defaults) > 1 and not self.sort_multiple: - log.warning("multi-column sorting is not enabled for the instance; " - "list will be pruned to first element for '%s' grid: %s", - self.key, sort_defaults) + log.warning( + "multi-column sorting is not enabled for the instance; " + "list will be pruned to first element for '%s' grid: %s", + self.key, + sort_defaults, + ) sort_defaults = [sort_defaults[0]] self.sort_defaults = sort_defaults @@ -1270,8 +1284,7 @@ class Grid: continue # do not create filter for UUID field - if (len(prop.columns) == 1 - and isinstance(prop.columns[0].type, UUID)): + if len(prop.columns) == 1 and isinstance(prop.columns[0].type, UUID): continue attr = getattr(self.model_class, prop.key) @@ -1294,12 +1307,12 @@ class Grid: :returns: A :class:`~wuttaweb.grids.filters.GridFilter` instance. """ - key = kwargs.pop('key', None) + key = kwargs.pop("key", None) # model_property is required model_property = None - if kwargs.get('model_property'): - model_property = kwargs['model_property'] + if kwargs.get("model_property"): + model_property = kwargs["model_property"] elif isinstance(columninfo, str): key = columninfo if self.model_class: @@ -1310,7 +1323,7 @@ class Grid: model_property = columninfo # optional factory override - factory = kwargs.pop('factory', None) + factory = kwargs.pop("factory", None) if not factory: typ = model_property.type factory = default_sqlalchemy_filters.get(type(typ)) @@ -1318,7 +1331,7 @@ class Grid: factory = default_sqlalchemy_filters[None] # make filter - kwargs['model_property'] = model_property + kwargs["model_property"] = model_property return factory(self.request, key or model_property.key, **kwargs) def set_filter(self, key, filterinfo=None, **kwargs): @@ -1349,8 +1362,8 @@ class Grid: # filtr = filterinfo raise NotImplementedError else: - kwargs['key'] = key - kwargs.setdefault('label', self.get_label(key)) + kwargs["key"] = key + kwargs.setdefault("label", self.get_label(key)) filtr = self.make_filter(filterinfo or key, **kwargs) self.filters[key] = filtr @@ -1384,7 +1397,7 @@ class Grid: Filter defaults are tracked via :attr:`filter_defaults`. """ - filter_defaults = dict(getattr(self, 'filter_defaults', {})) + filter_defaults = dict(getattr(self, "filter_defaults", {})) for key, values in defaults.items(): filtr = filter_defaults.setdefault(key, {}) @@ -1411,10 +1424,9 @@ class Grid: This method is intended for use in the constructor. Code can instead access :attr:`pagesize_options` directly. """ - options = self.config.get_list('wuttaweb.grids.default_pagesize_options') + options = self.config.get_list("wuttaweb.grids.default_pagesize_options") if options: - options = [int(size) for size in options - if size.isdigit()] + options = [int(size) for size in options if size.isdigit()] if options: return options @@ -1434,7 +1446,7 @@ class Grid: This method is intended for use in the constructor. Code can instead access :attr:`pagesize` directly. """ - size = self.config.get_int('wuttaweb.grids.default_pagesize') + size = self.config.get_int("wuttaweb.grids.default_pagesize") if size: return size @@ -1485,51 +1497,54 @@ class Grid: if self.filterable: for filtr in self.filters.values(): defaults = self.filter_defaults.get(filtr.key, {}) - settings[f'filter.{filtr.key}.active'] = defaults.get('active', - filtr.default_active) - settings[f'filter.{filtr.key}.verb'] = defaults.get('verb', - filtr.get_default_verb()) - settings[f'filter.{filtr.key}.value'] = defaults.get('value', - filtr.default_value) + settings[f"filter.{filtr.key}.active"] = defaults.get( + "active", filtr.default_active + ) + settings[f"filter.{filtr.key}.verb"] = defaults.get( + "verb", filtr.get_default_verb() + ) + settings[f"filter.{filtr.key}.value"] = defaults.get( + "value", filtr.default_value + ) if self.sortable: if self.sort_defaults: # nb. as of writing neither Buefy nor Oruga support a # multi-column *default* sort; so just use first sorter sortinfo = self.sort_defaults[0] - settings['sorters.length'] = 1 - settings['sorters.1.key'] = sortinfo.sortkey - settings['sorters.1.dir'] = sortinfo.sortdir + settings["sorters.length"] = 1 + settings["sorters.1.key"] = sortinfo.sortkey + settings["sorters.1.dir"] = sortinfo.sortdir else: - settings['sorters.length'] = 0 + settings["sorters.length"] = 0 if self.paginated and self.paginate_on_backend: - settings['pagesize'] = self.pagesize - settings['page'] = self.page + settings["pagesize"] = self.pagesize + settings["page"] = self.page # update settings dict based on what we find in the request # and/or user session. always prioritize the former. # nb. do not read settings if user wants a reset - if self.request.GET.get('reset-view'): + if self.request.GET.get("reset-view"): # at this point we only have default settings, and we want # to keep those *and* persist them for next time, below pass - elif self.request_has_settings('filter'): - self.update_filter_settings(settings, src='request') - if self.request_has_settings('sort'): - self.update_sort_settings(settings, src='request') + elif self.request_has_settings("filter"): + self.update_filter_settings(settings, src="request") + if self.request_has_settings("sort"): + self.update_sort_settings(settings, src="request") else: - self.update_sort_settings(settings, src='session') + self.update_sort_settings(settings, src="session") self.update_page_settings(settings) - elif self.request_has_settings('sort'): - self.update_filter_settings(settings, src='session') - self.update_sort_settings(settings, src='request') + elif self.request_has_settings("sort"): + self.update_filter_settings(settings, src="session") + self.update_sort_settings(settings, src="request") self.update_page_settings(settings) - elif self.request_has_settings('page'): - self.update_filter_settings(settings, src='session') - self.update_sort_settings(settings, src='session') + elif self.request_has_settings("page"): + self.update_filter_settings(settings, src="session") + self.update_sort_settings(settings, src="session") self.update_page_settings(settings) else: @@ -1537,32 +1552,36 @@ class Grid: persist = False # but still should load whatever is in user session - self.update_filter_settings(settings, src='session') - self.update_sort_settings(settings, src='session') + self.update_filter_settings(settings, src="session") + self.update_sort_settings(settings, src="session") self.update_page_settings(settings) # maybe save settings in user session, for next time if persist: - self.persist_settings(settings, dest='session') + self.persist_settings(settings, dest="session") # update ourself to reflect settings dict.. # filtering if self.filterable: for filtr in self.filters.values(): - filtr.active = settings[f'filter.{filtr.key}.active'] - filtr.verb = settings[f'filter.{filtr.key}.verb'] or filtr.get_default_verb() - filtr.value = settings[f'filter.{filtr.key}.value'] + filtr.active = settings[f"filter.{filtr.key}.active"] + filtr.verb = ( + settings[f"filter.{filtr.key}.verb"] or filtr.get_default_verb() + ) + filtr.value = settings[f"filter.{filtr.key}.value"] # sorting if self.sortable: # nb. doing this for frontend sorting also self.active_sorters = [] - for i in range(1, settings['sorters.length'] + 1): - self.active_sorters.append({ - 'key': settings[f'sorters.{i}.key'], - 'dir': settings[f'sorters.{i}.dir'], - }) + for i in range(1, settings["sorters.length"] + 1): + self.active_sorters.append( + { + "key": settings[f"sorters.{i}.key"], + "dir": settings[f"sorters.{i}.dir"], + } + ) # TODO: i thought this was needed, but now idk? # # nb. when showing full index page (i.e. not partial) # # this implies we must set the default sorter for Vue @@ -1572,35 +1591,36 @@ class Grid: # paging if self.paginated and self.paginate_on_backend: - self.pagesize = settings['pagesize'] - self.page = settings['page'] + self.pagesize = settings["pagesize"] + self.page = settings["page"] def request_has_settings(self, typ): """ """ - if typ == 'filter' and self.filterable: + if typ == "filter" and self.filterable: for filtr in self.filters.values(): if filtr.key in self.request.GET: return True - if 'filter' in self.request.GET: # user may be applying empty filters + if "filter" in self.request.GET: # user may be applying empty filters return True - elif typ == 'sort' and self.sortable and self.sort_on_backend: - if 'sort1key' in self.request.GET: + elif typ == "sort" and self.sortable and self.sort_on_backend: + if "sort1key" in self.request.GET: return True - elif typ == 'page' and self.paginated and self.paginate_on_backend: - for key in ['pagesize', 'page']: + elif typ == "page" and self.paginated and self.paginate_on_backend: + for key in ["pagesize", "page"]: if key in self.request.GET: return True return False - def get_setting(self, settings, key, src='session', default=None, - normalize=lambda v: v): + def get_setting( + self, settings, key, src="session", default=None, normalize=lambda v: v + ): """ """ - if src == 'request': + if src == "request": value = self.request.GET.get(key) if value is not None: try: @@ -1608,8 +1628,8 @@ class Grid: except ValueError: pass - elif src == 'session': - value = self.request.session.get(f'grid.{self.key}.{key}') + elif src == "session": + value = self.request.session.get(f"grid.{self.key}.{key}") if value is not None: return normalize(value) @@ -1627,52 +1647,62 @@ class Grid: return for filtr in self.filters.values(): - prefix = f'filter.{filtr.key}' + prefix = f"filter.{filtr.key}" - if src == 'request': + if src == "request": # consider filter active if query string contains a value for it - settings[f'{prefix}.active'] = filtr.key in self.request.GET - settings[f'{prefix}.verb'] = self.get_setting( - settings, f'{filtr.key}.verb', src='request', default='') - settings[f'{prefix}.value'] = self.get_setting( - settings, filtr.key, src='request', default='') + settings[f"{prefix}.active"] = filtr.key in self.request.GET + settings[f"{prefix}.verb"] = self.get_setting( + settings, f"{filtr.key}.verb", src="request", default="" + ) + settings[f"{prefix}.value"] = self.get_setting( + settings, filtr.key, src="request", default="" + ) - elif src == 'session': - settings[f'{prefix}.active'] = self.get_setting( - settings, f'{prefix}.active', src='session', - normalize=lambda v: str(v).lower() == 'true', default=False) - settings[f'{prefix}.verb'] = self.get_setting( - settings, f'{prefix}.verb', src='session', default='') - settings[f'{prefix}.value'] = self.get_setting( - settings, f'{prefix}.value', src='session', default='') + elif src == "session": + settings[f"{prefix}.active"] = self.get_setting( + settings, + f"{prefix}.active", + src="session", + normalize=lambda v: str(v).lower() == "true", + default=False, + ) + settings[f"{prefix}.verb"] = self.get_setting( + settings, f"{prefix}.verb", src="session", default="" + ) + settings[f"{prefix}.value"] = self.get_setting( + settings, f"{prefix}.value", src="session", default="" + ) def update_sort_settings(self, settings, src=None): """ """ if not (self.sortable and self.sort_on_backend): return - if src == 'request': + if src == "request": i = 1 while True: - skey = f'sort{i}key' + skey = f"sort{i}key" if skey in self.request.GET: - settings[f'sorters.{i}.key'] = self.get_setting(settings, skey, - src='request') - settings[f'sorters.{i}.dir'] = self.get_setting(settings, f'sort{i}dir', - src='request', - default='asc') + settings[f"sorters.{i}.key"] = self.get_setting( + settings, skey, src="request" + ) + settings[f"sorters.{i}.dir"] = self.get_setting( + settings, f"sort{i}dir", src="request", default="asc" + ) else: break i += 1 - settings['sorters.length'] = i - 1 + settings["sorters.length"] = i - 1 - elif src == 'session': - settings['sorters.length'] = self.get_setting(settings, 'sorters.length', - src='session', normalize=int) - for i in range(1, settings['sorters.length'] + 1): - for key in ('key', 'dir'): - skey = f'sorters.{i}.{key}' - settings[skey] = self.get_setting(settings, skey, src='session') + elif src == "session": + settings["sorters.length"] = self.get_setting( + settings, "sorters.length", src="session", normalize=int + ) + for i in range(1, settings["sorters.length"] + 1): + for key in ("key", "dir"): + skey = f"sorters.{i}.{key}" + settings[skey] = self.get_setting(settings, skey, src="session") def update_page_settings(self, settings): """ """ @@ -1682,34 +1712,34 @@ class Grid: # update the settings dict from request and/or user session # pagesize - pagesize = self.request.GET.get('pagesize') + pagesize = self.request.GET.get("pagesize") if pagesize is not None: if pagesize.isdigit(): - settings['pagesize'] = int(pagesize) + settings["pagesize"] = int(pagesize) else: - pagesize = self.request.session.get(f'grid.{self.key}.pagesize') + pagesize = self.request.session.get(f"grid.{self.key}.pagesize") if pagesize is not None: - settings['pagesize'] = pagesize + settings["pagesize"] = pagesize # page - page = self.request.GET.get('page') + page = self.request.GET.get("page") if page is not None: if page.isdigit(): - settings['page'] = int(page) + settings["page"] = int(page) else: - page = self.request.session.get(f'grid.{self.key}.page') + page = self.request.session.get(f"grid.{self.key}.page") if page is not None: - settings['page'] = int(page) + settings["page"] = int(page) def persist_settings(self, settings, dest=None): """ """ - if dest not in ('session',): + if dest not in ("session",): raise ValueError(f"invalid dest identifier: {dest}") # func to save a setting value to user session def persist(key, value=lambda k: settings.get(k)): - assert dest == 'session' - skey = f'grid.{self.key}.{key}' + assert dest == "session" + skey = f"grid.{self.key}.{key}" self.request.session[skey] = value(key) # filter settings @@ -1717,10 +1747,12 @@ class Grid: # always save all filters, with status for filtr in self.filters.values(): - persist(f'filter.{filtr.key}.active', - value=lambda k: 'true' if settings.get(k) else 'false') - persist(f'filter.{filtr.key}.verb') - persist(f'filter.{filtr.key}.value') + persist( + f"filter.{filtr.key}.active", + value=lambda k: "true" if settings.get(k) else "false", + ) + persist(f"filter.{filtr.key}.verb") + persist(f"filter.{filtr.key}.value") # sort settings if self.sortable and self.sort_on_backend: @@ -1729,26 +1761,26 @@ class Grid: # because number of sort settings will vary, so we delete # all and then write all - if dest == 'session': + if dest == "session": # remove sort settings from user session - prefix = f'grid.{self.key}.sorters.' + prefix = f"grid.{self.key}.sorters." for key in list(self.request.session): if key.startswith(prefix): del self.request.session[key] # now save sort settings to dest - if 'sorters.length' in settings: - persist('sorters.length') - for i in range(1, settings['sorters.length'] + 1): - persist(f'sorters.{i}.key') - persist(f'sorters.{i}.dir') + if "sorters.length" in settings: + persist("sorters.length") + for i in range(1, settings["sorters.length"] + 1): + persist(f"sorters.{i}.key") + persist(f"sorters.{i}.dir") # pagination settings if self.paginated and self.paginate_on_backend: # save to dest - persist('pagesize') - persist('page') + persist("pagesize") + persist("page") ############################## # data methods @@ -1794,8 +1826,7 @@ class Grid: This inspects each :class:`~wuttaweb.grids.filters.GridFilter` in :attr:`filters` and only returns the ones marked active. """ - return [filtr for filtr in self.filters.values() - if filtr.active] + return [filtr for filtr in self.filters.values() if filtr.active] def filter_data(self, data, filters=None): """ @@ -1848,8 +1879,8 @@ class Grid: sorters = reversed(sorters) for sorter in sorters: - sortkey = sorter['key'] - sortdir = sorter['dir'] + sortkey = sorter["key"] + sortdir = sorter["dir"] # cannot sort unless we have a sorter callable sortfunc = self.sorters.get(sortkey) @@ -1876,20 +1907,18 @@ class Grid: This method is called by :meth:`get_visible_data()`. """ if isinstance(data, orm.Query): - pager = SqlalchemyOrmPage(data, - items_per_page=self.pagesize, - page=self.page) + pager = SqlalchemyOrmPage( + data, items_per_page=self.pagesize, page=self.page + ) else: - pager = paginate.Page(data, - items_per_page=self.pagesize, - page=self.page) + pager = paginate.Page(data, items_per_page=self.pagesize, page=self.page) # pager may have detected that our current page is outside the # valid range. if so we should update ourself to match if pager.page != self.page: self.page = pager.page - key = f'grid.{self.key}.page' + key = f"grid.{self.key}.page" if key in self.request.session: self.request.session[key] = self.page @@ -1914,7 +1943,7 @@ class Grid: return "" batch_id = int(value) - return f'{batch_id:08d}' + return f"{batch_id:08d}" def render_boolean(self, obj, key, value): """ @@ -2034,10 +2063,8 @@ class Grid: return self.app.render_quantity(value) def render_table_element( - self, - form=None, - template='/grids/table_element.mako', - **context): + self, form=None, template="/grids/table_element.mako", **context + ): """ Render a simple Vue table element for the grid. @@ -2100,10 +2127,7 @@ class Grid: """ return HTML.tag(self.vue_tagname, **kwargs) - def render_vue_template( - self, - template='/grids/vue_template.mako', - **context): + def render_vue_template(self, template="/grids/vue_template.mako", **context): """ Render the Vue template block for the grid. @@ -2141,8 +2165,8 @@ class Grid: :param template: Path to Mako template which is used to render the output. """ - context['grid'] = self - context.setdefault('request', self.request) + context["grid"] = self + context.setdefault("request", self.request) output = render(template, context) return HTML.literal(output) @@ -2164,11 +2188,10 @@ class Grid: """ set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}" make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})" - return HTML.tag('script', c=['\n', - HTML.literal(set_data), - '\n', - HTML.literal(make_component), - '\n']) + return HTML.tag( + "script", + c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"], + ) def get_vue_columns(self): """ @@ -2195,13 +2218,15 @@ class Grid: columns = [] for name in self.columns: - columns.append({ - 'field': name, - 'label': self.get_label(name), - 'hidden': self.is_hidden(name), - 'sortable': self.is_sortable(name), - 'searchable': self.is_searchable(name), - }) + columns.append( + { + "field": name, + "label": self.get_label(name), + "hidden": self.is_hidden(name), + "sortable": self.is_sortable(name), + "searchable": self.is_searchable(name), + } + ) return columns def get_vue_active_sorters(self): @@ -2223,8 +2248,7 @@ class Grid: """ sorters = [] for sorter in self.active_sorters: - sorters.append({'field': sorter['key'], - 'order': sorter['dir']}) + sorters.append({"field": sorter["key"], "order": sorter["dir"]}) return sorters def get_vue_first_sorter(self): @@ -2247,10 +2271,10 @@ class Grid: :returns: The first sorter in format ``[sortkey, sortdir]``, or ``None``. """ - if hasattr(self, 'active_sorters'): + if hasattr(self, "active_sorters"): if self.active_sorters: sorter = self.active_sorters[0] - return [sorter['key'], sorter['dir']] + return [sorter["key"], sorter["dir"]] elif self.sort_defaults: sorter = self.sort_defaults[0] @@ -2272,20 +2296,22 @@ class Grid: choices = list(filtr.choices) choice_labels = dict(filtr.choices) - filters.append({ - 'key': filtr.key, - 'data_type': filtr.data_type, - 'active': filtr.active, - 'visible': filtr.active, - 'verbs': filtr.get_verbs(), - 'verb_labels': filtr.get_verb_labels(), - 'valueless_verbs': filtr.get_valueless_verbs(), - 'verb': filtr.verb, - 'choices': choices, - 'choice_labels': choice_labels, - 'value': filtr.value, - 'label': filtr.label, - }) + filters.append( + { + "key": filtr.key, + "data_type": filtr.data_type, + "active": filtr.active, + "visible": filtr.active, + "verbs": filtr.get_verbs(), + "verb_labels": filtr.get_verb_labels(), + "valueless_verbs": filtr.get_valueless_verbs(), + "verb": filtr.verb, + "choices": choices, + "choice_labels": choice_labels, + "value": filtr.value, + "label": filtr.label, + } + ) return filters def object_to_dict(self, obj): @@ -2294,7 +2320,7 @@ class Grid: dct = dict(obj) except TypeError: dct = dict(obj.__dict__) - dct.pop('_sa_instance_state', None) + dct.pop("_sa_instance_state", None) return dct def get_vue_context(self): @@ -2345,7 +2371,7 @@ class Grid: # add action urls to each record for action in self.actions: - key = f'_action_url_{action.key}' + key = f"_action_url_{action.key}" if key not in record: url = action.get_url(original_record, i) if url: @@ -2355,21 +2381,24 @@ class Grid: css_class = self.get_row_class(original_record, record, i) if css_class: # nb. use *string* zero-based index, for js compat - row_classes[str(i-1)] = css_class + row_classes[str(i - 1)] = css_class data.append(record) return { - 'data': data, - 'row_classes': row_classes, + "data": data, + "row_classes": row_classes, } def get_vue_data(self): """ """ - warnings.warn("grid.get_vue_data() is deprecated; " - "please use grid.get_vue_context() instead", - DeprecationWarning, stacklevel=2) - return self.get_vue_context()['data'] + warnings.warn( + "grid.get_vue_data() is deprecated; " + "please use grid.get_vue_context() instead", + DeprecationWarning, + stacklevel=2, + ) + return self.get_vue_context()["data"] def get_row_class(self, obj, data, i): """ @@ -2402,12 +2431,12 @@ class Grid: """ pager = self.pager return { - 'item_count': pager.item_count, - 'items_per_page': pager.items_per_page, - 'page': pager.page, - 'page_count': pager.page_count, - 'first_item': pager.first_item, - 'last_item': pager.last_item, + "item_count": pager.item_count, + "items_per_page": pager.items_per_page, + "page": pager.page, + "page_count": pager.page_count, + "first_item": pager.first_item, + "last_item": pager.last_item, } @@ -2487,15 +2516,15 @@ class GridAction: """ def __init__( - self, - request, - key, - label=None, - url=None, - target=None, - click_handler=None, - icon=None, - link_class=None, + self, + request, + key, + label=None, + url=None, + target=None, + click_handler=None, + icon=None, + link_class=None, ): self.request = request self.config = self.request.wutta_config @@ -2506,7 +2535,7 @@ class GridAction: self.click_handler = click_handler self.label = label or self.app.make_title(key) self.icon = icon or key - self.link_class = link_class or '' + self.link_class = link_class or "" def render_icon_and_label(self): """ @@ -2519,7 +2548,7 @@ class GridAction: self.render_icon(), self.render_label(), ] - return HTML.literal(' ').join(html) + return HTML.literal(" ").join(html) def render_icon(self): """ @@ -2535,9 +2564,9 @@ class GridAction: See also :meth:`render_icon_and_label()`. """ if self.request.use_oruga: - return HTML.tag('o-icon', icon=self.icon) + return HTML.tag("o-icon", icon=self.icon) - return HTML.tag('i', class_=f'fas fa-{self.icon}') + return HTML.tag("i", class_=f"fas fa-{self.icon}") def render_label(self): """ diff --git a/src/wuttaweb/grids/filters.py b/src/wuttaweb/grids/filters.py index dc6eb12..2e29177 100644 --- a/src/wuttaweb/grids/filters.py +++ b/src/wuttaweb/grids/filters.py @@ -27,9 +27,10 @@ Grid Filters import datetime import logging from collections import OrderedDict + try: from enum import EnumType -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover # nb. python <= 3.10 from enum import EnumMeta as EnumType @@ -137,47 +138,48 @@ class GridFilter: See also :attr:`default_verb`. """ - data_type = 'string' - default_verbs = ['equal', 'not_equal'] + + data_type = "string" + default_verbs = ["equal", "not_equal"] default_verb_labels = { - 'is_any': "is any", - 'equal': "equal to", - 'not_equal': "not equal to", - 'greater_than': "greater than", - 'greater_equal': "greater than or equal to", - 'less_than': "less than", - 'less_equal': "less than or equal to", + "is_any": "is any", + "equal": "equal to", + "not_equal": "not equal to", + "greater_than": "greater than", + "greater_equal": "greater than or equal to", + "less_than": "less than", + "less_equal": "less than or equal to", # 'between': "between", - 'is_true': "is true", - 'is_false': "is false", - 'is_false_null': "is false or null", - 'is_null': "is null", - 'is_not_null': "is not null", - 'contains': "contains", - 'does_not_contain': "does not contain", + "is_true": "is true", + "is_false": "is false", + "is_false_null": "is false or null", + "is_null": "is null", + "is_not_null": "is not null", + "contains": "contains", + "does_not_contain": "does not contain", } valueless_verbs = [ - 'is_any', - 'is_true', - 'is_false', - 'is_false_null', - 'is_null', - 'is_not_null', + "is_any", + "is_true", + "is_false", + "is_false_null", + "is_null", + "is_not_null", ] def __init__( - self, - request, - key, - label=None, - verbs=None, - choices={}, - default_active=False, - default_verb=None, - default_value=None, - **kwargs, + self, + request, + key, + label=None, + verbs=None, + choices={}, + default_active=False, + default_verb=None, + default_value=None, + **kwargs, ): self.request = request self.key = key @@ -205,12 +207,14 @@ class GridFilter: self.__dict__.update(kwargs) def __repr__(self): - verb = getattr(self, 'verb', None) - return (f"{self.__class__.__name__}(" - f"key='{self.key}', " - f"active={self.active}, " - f"verb={repr(verb)}, " - f"value={repr(self.value)})") + verb = getattr(self, "verb", None) + return ( + f"{self.__class__.__name__}(" + f"key='{self.key}', " + f"active={self.active}, " + f"verb={repr(verb)}, " + f"value={repr(self.value)})" + ) def get_verbs(self): """ @@ -218,7 +222,7 @@ class GridFilter: """ verbs = None - if hasattr(self, 'verbs'): + if hasattr(self, "verbs"): verbs = self.verbs else: @@ -229,13 +233,13 @@ class GridFilter: verbs = list(verbs) if self.nullable: - if 'is_null' not in verbs: - verbs.append('is_null') - if 'is_not_null' not in verbs: - verbs.append('is_not_null') + if "is_null" not in verbs: + verbs.append("is_null") + if "is_not_null" not in verbs: + verbs.append("is_not_null") - if 'is_any' not in verbs: - verbs.append('is_any') + if "is_any" not in verbs: + verbs.append("is_any") return verbs @@ -260,10 +264,10 @@ class GridFilter: """ verb = None - if hasattr(self, 'default_verb'): + if hasattr(self, "default_verb"): verb = self.default_verb - elif hasattr(self, 'verb'): + elif hasattr(self, "verb"): verb = self.verb if not verb: @@ -291,10 +295,10 @@ class GridFilter: """ if choices: self.choices = self.normalize_choices(choices) - self.data_type = 'choice' + self.data_type = "choice" else: self.choices = {} - self.data_type = 'string' + self.data_type = "string" def normalize_choices(self, choices): """ @@ -320,22 +324,18 @@ class GridFilter: normalized = choices if isinstance(choices, EnumType): - normalized = OrderedDict([ - (member.name, member.value) - for member in choices]) + normalized = OrderedDict( + [(member.name, member.value) for member in choices] + ) elif isinstance(choices, OrderedDict): normalized = choices elif isinstance(choices, dict): - normalized = OrderedDict([ - (key, choices[key]) - for key in sorted(choices)]) + normalized = OrderedDict([(key, choices[key]) for key in sorted(choices)]) elif isinstance(choices, list): - normalized = OrderedDict([ - (key, key) - for key in choices]) + normalized = OrderedDict([(key, key) for key in choices]) return normalized @@ -358,8 +358,11 @@ class GridFilter: verb = self.verb if not verb: verb = self.get_default_verb() - log.warn("missing verb for '%s' filter, will use default verb: %s", - self.key, verb) + log.warn( + "missing verb for '%s' filter, will use default verb: %s", + self.key, + verb, + ) # only attempt for known verbs if verb not in self.get_verbs(): @@ -370,7 +373,7 @@ class GridFilter: value = self.value # locate filter method - func = getattr(self, f'filter_{verb}', None) + func = getattr(self, f"filter_{verb}", None) if not func: raise VerbNotSupported(verb) @@ -403,7 +406,7 @@ class AlchemyFilter(GridFilter): """ def __init__(self, *args, **kwargs): - nullable = kwargs.pop('nullable', None) + nullable = kwargs.pop("nullable", None) super().__init__(*args, **kwargs) self.nullable = nullable @@ -441,10 +444,12 @@ class AlchemyFilter(GridFilter): # sql probably excludes null values from results, but user # probably does not expect that, so explicitly include them. - return query.filter(sa.or_( - self.model_property == None, - self.model_property != value, - )) + return query.filter( + sa.or_( + self.model_property == None, + self.model_property != value, + ) + ) def filter_greater_than(self, query, value): """ @@ -502,8 +507,8 @@ class StringAlchemyFilter(AlchemyFilter): Subclass of :class:`AlchemyFilter`. """ - default_verbs = ['contains', 'does_not_contain', - 'equal', 'not_equal'] + + default_verbs = ["contains", "does_not_contain", "equal", "not_equal"] def coerce_value(self, value): """ """ @@ -522,8 +527,8 @@ class StringAlchemyFilter(AlchemyFilter): criteria = [] for val in value.split(): - val = val.replace('_', r'\_') - val = f'%{val}%' + val = val.replace("_", r"\_") + val = f"%{val}%" criteria.append(self.model_property.ilike(val)) return query.filter(sa.and_(*criteria)) @@ -538,15 +543,13 @@ class StringAlchemyFilter(AlchemyFilter): criteria = [] for val in value.split(): - val = val.replace('_', r'\_') - val = f'%{val}%' + val = val.replace("_", r"\_") + val = f"%{val}%" criteria.append(~self.model_property.ilike(val)) # sql probably excludes null values from results, but user # probably does not expect that, so explicitly include them. - return query.filter(sa.or_( - self.model_property == None, - sa.and_(*criteria))) + return query.filter(sa.or_(self.model_property == None, sa.and_(*criteria))) class NumericAlchemyFilter(AlchemyFilter): @@ -555,9 +558,15 @@ class NumericAlchemyFilter(AlchemyFilter): Subclass of :class:`AlchemyFilter`. """ - default_verbs = ['equal', 'not_equal', - 'greater_than', 'greater_equal', - 'less_than', 'less_equal'] + + default_verbs = [ + "equal", + "not_equal", + "greater_than", + "greater_equal", + "less_than", + "less_equal", + ] class IntegerAlchemyFilter(NumericAlchemyFilter): @@ -582,26 +591,27 @@ class BooleanAlchemyFilter(AlchemyFilter): Subclass of :class:`AlchemyFilter`. """ - default_verbs = ['is_true', 'is_false'] + + default_verbs = ["is_true", "is_false"] def get_verbs(self): """ """ # get basic verbs from caller, or default list - verbs = getattr(self, 'verbs', self.default_verbs) + verbs = getattr(self, "verbs", self.default_verbs) if callable(verbs): verbs = verbs() verbs = list(verbs) # add some more if column is nullable if self.nullable: - for verb in ('is_false_null', 'is_null', 'is_not_null'): + for verb in ("is_false_null", "is_null", "is_not_null"): if verb not in verbs: verbs.append(verb) # add wildcard - if 'is_any' not in verbs: - verbs.append('is_any') + if "is_any" not in verbs: + verbs.append("is_any") return verbs @@ -629,8 +639,9 @@ class BooleanAlchemyFilter(AlchemyFilter): Filter data with "is false or null" condition. The value is ignored. """ - return query.filter(sa.or_(self.model_property == False, - self.model_property == None)) + return query.filter( + sa.or_(self.model_property == False, self.model_property == None) + ) class DateAlchemyFilter(AlchemyFilter): @@ -640,24 +651,25 @@ class DateAlchemyFilter(AlchemyFilter): Subclass of :class:`AlchemyFilter`. """ - data_type = 'date' + + data_type = "date" default_verbs = [ - 'equal', - 'not_equal', - 'greater_than', - 'greater_equal', - 'less_than', - 'less_equal', + "equal", + "not_equal", + "greater_than", + "greater_equal", + "less_than", + "less_equal", # 'between', ] default_verb_labels = { - 'equal': "on", - 'not_equal': "not on", - 'greater_than': "after", - 'greater_equal': "on or after", - 'less_than': "before", - 'less_equal': "on or before", + "equal": "on", + "not_equal": "not on", + "greater_than": "after", + "greater_equal": "on or after", + "less_than": "before", + "less_equal": "on or before", # 'between': "between", } @@ -668,7 +680,7 @@ class DateAlchemyFilter(AlchemyFilter): return value try: - dt = datetime.datetime.strptime(value, '%Y-%m-%d') + dt = datetime.datetime.strptime(value, "%Y-%m-%d") except ValueError: log.warning("invalid date value: %s", value) else: diff --git a/src/wuttaweb/handler.py b/src/wuttaweb/handler.py index e899268..04deab2 100644 --- a/src/wuttaweb/handler.py +++ b/src/wuttaweb/handler.py @@ -49,8 +49,8 @@ class WebHandler(GenericHandler): :param resource: :class:`fanstatic:fanstatic.Resource` instance representing an image file or other resource. """ - needed = request.environ['fanstatic.needed'] - url = needed.library_url(resource.library) + '/' + needed = request.environ["fanstatic.needed"] + url = needed.library_url(resource.library) + "/" if request.script_name: url = request.script_name + url return url + resource.relpath @@ -67,7 +67,7 @@ class WebHandler(GenericHandler): [wuttaweb] favicon_url = http://example.com/favicon.ico """ - url = self.config.get('wuttaweb.favicon_url') + url = self.config.get("wuttaweb.favicon_url") if url: return url return self.get_fanstatic_url(request, static.favicon) @@ -85,7 +85,7 @@ class WebHandler(GenericHandler): [wuttaweb] header_logo_url = http://example.com/logo.png """ - url = self.config.get('wuttaweb.header_logo_url') + url = self.config.get("wuttaweb.header_logo_url") if url: return url return self.get_favicon_url(request) @@ -102,7 +102,7 @@ class WebHandler(GenericHandler): [wuttaweb] logo_url = http://example.com/logo.png """ - url = self.config.get('wuttaweb.logo_url') + url = self.config.get("wuttaweb.logo_url") if url: return url return self.get_fanstatic_url(request, static.logo) @@ -120,16 +120,20 @@ class WebHandler(GenericHandler): :returns: Instance of :class:`~wuttaweb.menus.MenuHandler`. """ - spec = self.config.get(f'{self.appname}.web.menus.handler.spec') + spec = self.config.get(f"{self.appname}.web.menus.handler.spec") if not spec: - spec = self.config.get(f'{self.appname}.web.menus.handler_spec') + spec = self.config.get(f"{self.appname}.web.menus.handler_spec") if spec: - warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; " - f"please use '{self.appname}.web.menus.handler.spec' instead", - DeprecationWarning) + warnings.warn( + f"setting '{self.appname}.web.menus.handler_spec' is deprecated; " + f"please use '{self.appname}.web.menus.handler.spec' instead", + DeprecationWarning, + ) else: - spec = self.config.get(f'{self.appname}.web.menus.handler.default_spec', - default='wuttaweb.menus:MenuHandler') + spec = self.config.get( + f"{self.appname}.web.menus.handler.default_spec", + default="wuttaweb.menus:MenuHandler", + ) factory = self.app.load_object(spec) return factory(self.config) @@ -175,13 +179,15 @@ class WebHandler(GenericHandler): handlers.extend(default) # configured default, if applicable - default = self.config.get(f'{self.config.appname}.web.menus.handler.default_spec') + default = self.config.get( + f"{self.config.appname}.web.menus.handler.default_spec" + ) if default and default not in handlers: handlers.append(default) # registered via entry points registered = [] - for Handler in load_entry_points(f'{self.appname}.web.menus').values(): + for Handler in load_entry_points(f"{self.appname}.web.menus").values(): spec = Handler.get_spec() if spec not in handlers: registered.append(spec) diff --git a/src/wuttaweb/menus.py b/src/wuttaweb/menus.py index 3445659..507e6a7 100644 --- a/src/wuttaweb/menus.py +++ b/src/wuttaweb/menus.py @@ -121,13 +121,13 @@ class MenuHandler(GenericHandler): dicts as described in :class:`MenuHandler`. """ return { - 'title': "People", - 'type': 'menu', - 'items': [ + "title": "People", + "type": "menu", + "items": [ { - 'title': "All People", - 'route': 'people', - 'perm': 'people.list', + "title": "All People", + "route": "people", + "perm": "people.list", }, ], } @@ -150,59 +150,63 @@ class MenuHandler(GenericHandler): """ items = [] - if kwargs.get('include_people'): - items.extend([ - { - 'title': "All People", - 'route': 'people', - 'perm': 'people.list', - }, - ]) + if kwargs.get("include_people"): + items.extend( + [ + { + "title": "All People", + "route": "people", + "perm": "people.list", + }, + ] + ) - items.extend([ - { - 'title': "Users", - 'route': 'users', - 'perm': 'users.list', - }, - { - 'title': "Roles", - 'route': 'roles', - 'perm': 'roles.list', - }, - { - 'title': "Permissions", - 'route': 'permissions', - 'perm': 'permissions.list', - }, - {'type': 'sep'}, - { - 'title': "Email Settings", - 'route': 'email_settings', - 'perm': 'email_settings.list', - }, - {'type': 'sep'}, - { - 'title': "App Info", - 'route': 'appinfo', - 'perm': 'appinfo.list', - }, - { - 'title': "Raw Settings", - 'route': 'settings', - 'perm': 'settings.list', - }, - { - 'title': "Upgrades", - 'route': 'upgrades', - 'perm': 'upgrades.list', - }, - ]) + items.extend( + [ + { + "title": "Users", + "route": "users", + "perm": "users.list", + }, + { + "title": "Roles", + "route": "roles", + "perm": "roles.list", + }, + { + "title": "Permissions", + "route": "permissions", + "perm": "permissions.list", + }, + {"type": "sep"}, + { + "title": "Email Settings", + "route": "email_settings", + "perm": "email_settings.list", + }, + {"type": "sep"}, + { + "title": "App Info", + "route": "appinfo", + "perm": "appinfo.list", + }, + { + "title": "Raw Settings", + "route": "settings", + "perm": "settings.list", + }, + { + "title": "Upgrades", + "route": "upgrades", + "perm": "upgrades.list", + }, + ] + ) return { - 'title': kwargs.get('title', "Admin"), - 'type': 'menu', - 'items': items, + "title": kwargs.get("title", "Admin"), + "type": "menu", + "items": items, } ############################## @@ -227,64 +231,68 @@ class MenuHandler(GenericHandler): final_menus = [] for topitem in raw_menus: - if topitem['allowed']: + if topitem["allowed"]: - if topitem.get('type') == 'link': + if topitem.get("type") == "link": final_menus.append(self._make_menu_entry(request, topitem)) - else: # assuming 'menu' type + else: # assuming 'menu' type menu_items = [] - for item in topitem['items']: - if not item['allowed']: + for item in topitem["items"]: + if not item["allowed"]: continue # nested submenu - if item.get('type') == 'menu': + if item.get("type") == "menu": submenu_items = [] - for subitem in item['items']: - if subitem['allowed']: - submenu_items.append(self._make_menu_entry(request, subitem)) - menu_items.append({ - 'type': 'submenu', - 'title': item['title'], - 'items': submenu_items, - 'is_menu': True, - 'is_sep': False, - }) + for subitem in item["items"]: + if subitem["allowed"]: + submenu_items.append( + self._make_menu_entry(request, subitem) + ) + menu_items.append( + { + "type": "submenu", + "title": item["title"], + "items": submenu_items, + "is_menu": True, + "is_sep": False, + } + ) - elif item.get('type') == 'sep': + elif item.get("type") == "sep": # we only want to add a sep, *if* we already have some # menu items (i.e. there is something to separate) # *and* the last menu item is not a sep (avoid doubles) - if menu_items and not menu_items[-1]['is_sep']: + if menu_items and not menu_items[-1]["is_sep"]: menu_items.append(self._make_menu_entry(request, item)) - else: # standard menu item + else: # standard menu item menu_items.append(self._make_menu_entry(request, item)) # remove final separator if present - if menu_items and menu_items[-1]['is_sep']: + if menu_items and menu_items[-1]["is_sep"]: menu_items.pop() # only add if we wound up with something assert menu_items if menu_items: group = { - 'type': 'menu', - 'key': topitem.get('key'), - 'title': topitem['title'], - 'items': menu_items, - 'is_menu': True, - 'is_link': False, + "type": "menu", + "key": topitem.get("key"), + "title": topitem["title"], + "items": menu_items, + "is_menu": True, + "is_link": False, } # topitem w/ no key likely means it did not come # from config but rather explicit definition in # code. so we are free to "invent" a (safe) key # for it, since that is only for editing config - if not group['key']: - group['key'] = self._make_menu_key(topitem['title']) + if not group["key"]: + group["key"] = self._make_menu_key(topitem["title"]) final_menus.append(group) @@ -305,7 +313,7 @@ class MenuHandler(GenericHandler): Logic to determine if a given menu item is "allowed" for current user. """ - perm = item.get('perm') + perm = item.get("perm") if perm: return request.has_perm(perm) return True @@ -317,30 +325,30 @@ class MenuHandler(GenericHandler): """ for topitem in menus: - if topitem.get('type', 'menu') == 'link': - topitem['allowed'] = True + if topitem.get("type", "menu") == "link": + topitem["allowed"] = True - elif topitem.get('type', 'menu') == 'menu': - topitem['allowed'] = False + elif topitem.get("type", "menu") == "menu": + topitem["allowed"] = False - for item in topitem['items']: + for item in topitem["items"]: - if item.get('type') == 'menu': - for subitem in item['items']: - subitem['allowed'] = self._is_allowed(request, subitem) + if item.get("type") == "menu": + for subitem in item["items"]: + subitem["allowed"] = self._is_allowed(request, subitem) - item['allowed'] = False - for subitem in item['items']: - if subitem['allowed'] and subitem.get('type') != 'sep': - item['allowed'] = True + item["allowed"] = False + for subitem in item["items"]: + if subitem["allowed"] and subitem.get("type") != "sep": + item["allowed"] = True break else: - item['allowed'] = self._is_allowed(request, item) + item["allowed"] = self._is_allowed(request, item) - for item in topitem['items']: - if item['allowed'] and item.get('type') != 'sep': - topitem['allowed'] = True + for item in topitem["items"]: + if item["allowed"] and item.get("type") != "sep": + topitem["allowed"] = True break def _make_menu_entry(self, request, item): @@ -349,39 +357,39 @@ class MenuHandler(GenericHandler): object, for use in constructing final menu. """ # separator - if item.get('type') == 'sep': + if item.get("type") == "sep": return { - 'type': 'sep', - 'is_menu': False, - 'is_sep': True, + "type": "sep", + "is_menu": False, + "is_sep": True, } # standard menu item entry = { - 'type': 'item', - 'title': item['title'], - 'perm': item.get('perm'), - 'target': item.get('target'), - 'is_link': True, - 'is_menu': False, - 'is_sep': False, + "type": "item", + "title": item["title"], + "perm": item.get("perm"), + "target": item.get("target"), + "is_link": True, + "is_menu": False, + "is_sep": False, } - if item.get('route'): - entry['route'] = item['route'] + if item.get("route"): + entry["route"] = item["route"] try: - entry['url'] = request.route_url(entry['route']) - except KeyError: # happens if no such route + entry["url"] = request.route_url(entry["route"]) + except KeyError: # happens if no such route log.warning("invalid route name for menu entry: %s", entry) - entry['url'] = entry['route'] - entry['key'] = entry['route'] + entry["url"] = entry["route"] + entry["key"] = entry["route"] else: - if item.get('url'): - entry['url'] = item['url'] - entry['key'] = self._make_menu_key(entry['title']) + if item.get("url"): + entry["url"] = item["url"] + entry["key"] = self._make_menu_key(entry["title"]) return entry def _make_menu_key(self, value): """ Generate a normalized menu key for the given value. """ - return re.sub(r'\W', '', value.lower()) + return re.sub(r"\W", "", value.lower()) diff --git a/src/wuttaweb/progress.py b/src/wuttaweb/progress.py index 047be83..616cb44 100644 --- a/src/wuttaweb/progress.py +++ b/src/wuttaweb/progress.py @@ -33,7 +33,7 @@ def get_basic_session(request, **kwargs): """ Create/get a "basic" Beaker session object. """ - kwargs['use_cookies'] = False + kwargs["use_cookies"] = False return BeakerSession(request, **kwargs) @@ -41,7 +41,7 @@ def get_progress_session(request, key, **kwargs): """ Create/get a Beaker session object, to be used for progress. """ - kwargs['id'] = f'{request.session.id}.progress.{key}' + kwargs["id"] = f"{request.session.id}.progress.{key}" return get_basic_session(request, **kwargs) @@ -91,7 +91,9 @@ class SessionProgress(ProgressBase): :attr:`success_url`. """ - def __init__(self, request, key, success_msg=None, success_url=None, error_url=None): + def __init__( + self, request, key, success_msg=None, success_url=None, error_url=None + ): self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() @@ -104,24 +106,24 @@ class SessionProgress(ProgressBase): def __call__(self, message, maximum): self.clear() - self.session['message'] = message - self.session['maximum'] = maximum - self.session['maximum_display'] = f'{maximum:,d}' - self.session['value'] = 0 + self.session["message"] = message + self.session["maximum"] = maximum + self.session["maximum_display"] = f"{maximum:,d}" + self.session["value"] = 0 self.session.save() return self def clear(self): """ """ self.session.clear() - self.session['complete'] = False - self.session['error'] = False + self.session["complete"] = False + self.session["error"] = False self.session.save() def update(self, value): """ """ self.session.load() - self.session['value'] = value + self.session["value"] = value self.session.save() def handle_error(self, error, error_url=None): @@ -139,9 +141,9 @@ class SessionProgress(ProgressBase): :attr:`error_url` is used. """ self.session.load() - self.session['error'] = True - self.session['error_msg'] = self.app.render_error(error) - self.session['error_url'] = error_url or self.error_url + self.session["error"] = True + self.session["error_msg"] = self.app.render_error(error) + self.session["error_url"] = error_url or self.error_url self.session.save() def handle_success(self, success_msg=None, success_url=None): @@ -162,7 +164,7 @@ class SessionProgress(ProgressBase): :attr:`success_url` is used. """ self.session.load() - self.session['complete'] = True - self.session['success_msg'] = success_msg or self.success_msg - self.session['success_url'] = success_url or self.success_url + self.session["complete"] = True + self.session["success_msg"] = success_msg or self.success_msg + self.session["success_url"] = success_url or self.success_url self.session.save() diff --git a/src/wuttaweb/static/__init__.py b/src/wuttaweb/static/__init__.py index dd1ff45..8dcfdf5 100644 --- a/src/wuttaweb/static/__init__.py +++ b/src/wuttaweb/static/__init__.py @@ -62,13 +62,13 @@ from fanstatic import Library, Resource # fanstatic img library -img = Library('wuttaweb_img', 'img') -favicon = Resource(img, 'favicon.ico') +img = Library("wuttaweb_img", "img") +favicon = Resource(img, "favicon.ico") # nb. mock out the renderers here, to appease fanstatic -logo = Resource(img, 'logo.png', renderer=True) -testing = Resource(img, 'testing.png', renderer=True) +logo = Resource(img, "logo.png", renderer=True) +testing = Resource(img, "testing.png", renderer=True) # TODO: should consider deprecating this? def includeme(config): - config.add_static_view('wuttaweb', 'wuttaweb:static') + config.add_static_view("wuttaweb", "wuttaweb:static") diff --git a/src/wuttaweb/subscribers.py b/src/wuttaweb/subscribers.py index b0b1cc1..afdb1a5 100644 --- a/src/wuttaweb/subscribers.py +++ b/src/wuttaweb/subscribers.py @@ -120,32 +120,35 @@ def new_request(event): """ request = event.request - config = request.registry.settings['wutta_config'] + config = request.registry.settings["wutta_config"] app = config.get_app() request.wutta_config = config def get_referrer(default=None): - if request.params.get('referrer'): - return request.params['referrer'] - if request.session.get('referrer'): - return request.session.pop('referrer') - referrer = getattr(request, 'referrer', None) - if (not referrer or referrer == request.current_route_url() - or not referrer.startswith(request.host_url)): - referrer = default or request.route_url('home') + if request.params.get("referrer"): + return request.params["referrer"] + if request.session.get("referrer"): + return request.session.pop("referrer") + referrer = getattr(request, "referrer", None) + if ( + not referrer + or referrer == request.current_route_url() + or not referrer.startswith(request.host_url) + ): + referrer = default or request.route_url("home") return referrer request.get_referrer = get_referrer def use_oruga(request): - spec = config.get('wuttaweb.oruga_detector.spec') + spec = config.get("wuttaweb.oruga_detector.spec") if spec: func = app.load_object(spec) return func(request) - theme = request.registry.settings.get('wuttaweb.theme') - if theme == 'butterfly': + theme = request.registry.settings.get("wuttaweb.theme") + if theme == "butterfly": return True return False @@ -156,16 +159,18 @@ def new_request(event): Register a Vue 3 component, so the base template knows to declare it for use within the app (page). """ - if not hasattr(request, '_wuttaweb_registered_components'): + if not hasattr(request, "_wuttaweb_registered_components"): request._wuttaweb_registered_components = OrderedDict() if tagname in request._wuttaweb_registered_components: - log.warning("component with tagname '%s' already registered " - "with class '%s' but we are replacing that " - "with class '%s'", - tagname, - request._wuttaweb_registered_components[tagname], - classname) + log.warning( + "component with tagname '%s' already registered " + "with class '%s' but we are replacing that " + "with class '%s'", + tagname, + request._wuttaweb_registered_components[tagname], + classname, + ) request._wuttaweb_registered_components[tagname] = classname @@ -188,9 +193,9 @@ def default_user_getter(request, db_session=None): def new_request_set_user( - event, - user_getter=default_user_getter, - db_session=None, + event, + user_getter=default_user_getter, + db_session=None, ): """ Event hook called when processing a new :term:`request`, for sake @@ -258,32 +263,35 @@ def new_request_set_user( """ request = event.request - config = request.registry.settings['wutta_config'] + config = request.registry.settings["wutta_config"] app = config.get_app() auth = app.get_auth_handler() # request.user if db_session: user_getter = functools.partial(user_getter, db_session=db_session) - request.set_property(user_getter, name='user', reify=True) + request.set_property(user_getter, name="user", reify=True) # request.is_admin def is_admin(request): return auth.user_is_admin(request.user) + request.set_property(is_admin, reify=True) # request.is_root def is_root(request): if request.is_admin: - if request.session.get('is_root', False): + if request.session.get("is_root", False): return True return False + request.set_property(is_root, reify=True) # request.user_permissions def user_permissions(request): session = db_session or Session() return auth.get_permissions(session, request.user) + request.set_property(user_permissions, reify=True) # request.has_perm() @@ -293,6 +301,7 @@ def new_request_set_user( if name in request.user_permissions: return True return False + request.has_perm = has_perm # request.has_any_perm() @@ -301,6 +310,7 @@ def new_request_set_user( if request.has_perm(name): return True return False + request.has_any_perm = has_any_perm @@ -371,35 +381,36 @@ def before_render(event): allowed to change theme. Only set/relevant if ``expose_theme_picker`` is true (see above). """ - request = event.get('request') or threadlocal.get_current_request() + request = event.get("request") or threadlocal.get_current_request() config = request.wutta_config app = config.get_app() web = app.get_web_handler() context = event - context['config'] = config - context['app'] = app - context['web'] = web - context['h'] = helpers - context['url'] = request.route_url - context['json'] = json - context['b'] = 'o' if request.use_oruga else 'b' # for buefy + context["config"] = config + context["app"] = app + context["web"] = web + context["h"] = helpers + context["url"] = request.route_url + context["json"] = json + context["b"] = "o" if request.use_oruga else "b" # for buefy # TODO: this should be avoided somehow, for non-traditional web # apps, esp. "API" web apps. (in the meantime can configure the # app to use NullMenuHandler which avoids most of the overhead.) menus = web.get_menu_handler() - context['menus'] = menus.do_make_menus(request) + context["menus"] = menus.do_make_menus(request) # theme - context['theme'] = request.registry.settings.get('wuttaweb.theme', 'default') - context['expose_theme_picker'] = config.get_bool('wuttaweb.themes.expose_picker', - default=False) - if context['expose_theme_picker']: - context['available_themes'] = get_available_themes(config) + context["theme"] = request.registry.settings.get("wuttaweb.theme", "default") + context["expose_theme_picker"] = config.get_bool( + "wuttaweb.themes.expose_picker", default=False + ) + if context["expose_theme_picker"]: + context["available_themes"] = get_available_themes(config) def includeme(config): - config.add_subscriber(new_request, 'pyramid.events.NewRequest') - config.add_subscriber(new_request_set_user, 'pyramid.events.NewRequest') - config.add_subscriber(before_render, 'pyramid.events.BeforeRender') + config.add_subscriber(new_request, "pyramid.events.NewRequest") + config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest") + config.add_subscriber(before_render, "pyramid.events.BeforeRender") diff --git a/src/wuttaweb/testing.py b/src/wuttaweb/testing.py index 0a1916b..cced4ef 100644 --- a/src/wuttaweb/testing.py +++ b/src/wuttaweb/testing.py @@ -45,22 +45,28 @@ class WebTestCase(DataTestCase): def setup_web(self): self.setup_db() self.request = self.make_request() - self.pyramid_config = testing.setUp(request=self.request, settings={ - 'wutta_config': self.config, - 'mako.directories': ['wuttaweb:templates'], - 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform', - }) + self.pyramid_config = testing.setUp( + request=self.request, + settings={ + "wutta_config": self.config, + "mako.directories": ["wuttaweb:templates"], + "pyramid_deform.template_search_path": "wuttaweb:templates/deform", + }, + ) # init web - self.pyramid_config.include('pyramid_deform') - self.pyramid_config.include('pyramid_mako') - self.pyramid_config.add_directive('add_wutta_permission_group', - 'wuttaweb.auth.add_permission_group') - self.pyramid_config.add_directive('add_wutta_permission', - 'wuttaweb.auth.add_permission') - self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render', - 'pyramid.events.BeforeRender') - self.pyramid_config.include('wuttaweb.static') + self.pyramid_config.include("pyramid_deform") + self.pyramid_config.include("pyramid_mako") + self.pyramid_config.add_directive( + "add_wutta_permission_group", "wuttaweb.auth.add_permission_group" + ) + self.pyramid_config.add_directive( + "add_wutta_permission", "wuttaweb.auth.add_permission" + ) + self.pyramid_config.add_subscriber( + "wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender" + ) + self.pyramid_config.include("wuttaweb.static") # nb. mock out fanstatic env..good enough for now to avoid errors.. needed = fanstatic.init_needed() @@ -69,9 +75,13 @@ class WebTestCase(DataTestCase): # setup new request w/ anonymous user event = MagicMock(request=self.request) subscribers.new_request(event) - def user_getter(request, **kwargs): pass - subscribers.new_request_set_user(event, db_session=self.session, - user_getter=user_getter) + + def user_getter(request, **kwargs): + pass + + subscribers.new_request_set_user( + event, db_session=self.session, user_getter=user_getter + ) def tearDown(self): self.teardown_web() diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py index adf64ea..8723af6 100644 --- a/src/wuttaweb/util.py +++ b/src/wuttaweb/util.py @@ -69,8 +69,9 @@ class FieldList(list): i = self.index(field) self.insert(i, newfield) else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) + log.warning( + "field '%s' not found, will append new field: %s", field, newfield + ) self.append(newfield) def insert_after(self, field, newfield): @@ -86,8 +87,9 @@ class FieldList(list): i = self.index(field) self.insert(i + 1, newfield) else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) + log.warning( + "field '%s' not found, will append new field: %s", field, newfield + ) self.append(newfield) def set_sequence(self, fields): @@ -132,18 +134,19 @@ def get_form_data(request): # there is a better way? see also # https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr if not request.POST and ( - getattr(request, 'is_xhr', False) - or getattr(request, 'content_type', None) == 'application/json'): + getattr(request, "is_xhr", False) + or getattr(request, "content_type", None) == "application/json" + ): return request.json_body return request.POST def get_libver( - request, - key, - configured_only=False, - default_only=False, - prefix='wuttaweb', + request, + key, + configured_only=False, + default_only=False, + prefix="wuttaweb", ): """ Return the appropriate version string for the web resource library @@ -194,88 +197,94 @@ def get_libver( if not default_only: # nb. new/preferred setting - version = config.get(f'wuttaweb.libver.{key}') + version = config.get(f"wuttaweb.libver.{key}") if version: return version # fallback to caller-specified prefix - if prefix != 'wuttaweb': - version = config.get(f'{prefix}.libver.{key}') + if prefix != "wuttaweb": + version = config.get(f"{prefix}.libver.{key}") if version: - warnings.warn(f"config for {prefix}.libver.{key} is deprecated; " - f"please set wuttaweb.libver.{key} instead", - DeprecationWarning) + warnings.warn( + f"config for {prefix}.libver.{key} is deprecated; " + f"please set wuttaweb.libver.{key} instead", + DeprecationWarning, + ) return version - if key == 'buefy': + if key == "buefy": if not default_only: # nb. old/legacy setting - version = config.get(f'{prefix}.buefy_version') + version = config.get(f"{prefix}.buefy_version") if version: - warnings.warn(f"config for {prefix}.buefy_version is deprecated; " - "please set wuttaweb.libver.buefy instead", - DeprecationWarning) + warnings.warn( + f"config for {prefix}.buefy_version is deprecated; " + "please set wuttaweb.libver.buefy instead", + DeprecationWarning, + ) return version if not configured_only: - return '0.9.25' + return "0.9.25" - elif key == 'buefy.css': + elif key == "buefy.css": # nb. this always returns something - return get_libver(request, 'buefy', - default_only=default_only, - configured_only=configured_only) + return get_libver( + request, "buefy", default_only=default_only, configured_only=configured_only + ) - elif key == 'vue': + elif key == "vue": if not default_only: # nb. old/legacy setting - version = config.get(f'{prefix}.vue_version') + version = config.get(f"{prefix}.vue_version") if version: - warnings.warn(f"config for {prefix}.vue_version is deprecated; " - "please set wuttaweb.libver.vue instead", - DeprecationWarning) + warnings.warn( + f"config for {prefix}.vue_version is deprecated; " + "please set wuttaweb.libver.vue instead", + DeprecationWarning, + ) return version if not configured_only: - return '2.6.14' + return "2.6.14" - elif key == 'vue_resource': + elif key == "vue_resource": if not configured_only: - return '1.5.3' + return "1.5.3" - elif key == 'fontawesome': + elif key == "fontawesome": if not configured_only: - return '5.3.1' + return "5.3.1" - elif key == 'bb_vue': + elif key == "bb_vue": if not configured_only: - return '3.5.18' + return "3.5.18" - elif key == 'bb_oruga': + elif key == "bb_oruga": if not configured_only: - return '0.11.4' + return "0.11.4" - elif key in ('bb_oruga_bulma', 'bb_oruga_bulma_css'): + elif key in ("bb_oruga_bulma", "bb_oruga_bulma_css"): if not configured_only: - return '0.7.3' + return "0.7.3" - elif key == 'bb_fontawesome_svg_core': + elif key == "bb_fontawesome_svg_core": if not configured_only: - return '7.0.0' + return "7.0.0" - elif key == 'bb_free_solid_svg_icons': + elif key == "bb_free_solid_svg_icons": if not configured_only: - return '7.0.0' + return "7.0.0" - elif key == 'bb_vue_fontawesome': + elif key == "bb_vue_fontawesome": if not configured_only: - return '3.1.1' + return "3.1.1" def get_liburl( - request, - key, - configured_only=False, - default_only=False, - prefix='wuttaweb', + request, + key, + configured_only=False, + default_only=False, + prefix="wuttaweb", ): """ Return the appropriate URL for the web resource library identified @@ -346,100 +355,106 @@ def get_liburl( if not default_only: # nb. new/preferred setting - url = config.get(f'wuttaweb.liburl.{key}') + url = config.get(f"wuttaweb.liburl.{key}") if url: return url # fallback to caller-specified prefix - url = config.get(f'{prefix}.liburl.{key}') + url = config.get(f"{prefix}.liburl.{key}") if url: - warnings.warn(f"config for {prefix}.liburl.{key} is deprecated; " - f"please set wuttaweb.liburl.{key} instead", - DeprecationWarning) + warnings.warn( + f"config for {prefix}.liburl.{key} is deprecated; " + f"please set wuttaweb.liburl.{key} instead", + DeprecationWarning, + ) return url if configured_only: return - version = get_libver(request, key, prefix=prefix, - configured_only=False, - default_only=default_only) + version = get_libver( + request, key, prefix=prefix, configured_only=False, default_only=default_only + ) # load fanstatic libcache if configured - static = config.get('wuttaweb.static_libcache.module') + static = config.get("wuttaweb.static_libcache.module") if not static: - static = config.get(f'{prefix}.static_libcache.module') + static = config.get(f"{prefix}.static_libcache.module") if static: - warnings.warn(f"config for {prefix}.static_libcache.module is deprecated; " - "please set wuttaweb.static_libcache.module instead", - DeprecationWarning) + warnings.warn( + f"config for {prefix}.static_libcache.module is deprecated; " + "please set wuttaweb.static_libcache.module instead", + DeprecationWarning, + ) if static: static = importlib.import_module(static) - needed = request.environ['fanstatic.needed'] - liburl = needed.library_url(static.libcache) + '/' + needed = request.environ["fanstatic.needed"] + liburl = needed.library_url(static.libcache) + "/" # nb. add custom url prefix if needed, e.g. /wutta if request.script_name: liburl = request.script_name + liburl - if key == 'buefy': - if static and hasattr(static, 'buefy_js'): + if key == "buefy": + if static and hasattr(static, "buefy_js"): return liburl + static.buefy_js.relpath - return f'https://unpkg.com/buefy@{version}/dist/buefy.min.js' + return f"https://unpkg.com/buefy@{version}/dist/buefy.min.js" - elif key == 'buefy.css': - if static and hasattr(static, 'buefy_css'): + elif key == "buefy.css": + if static and hasattr(static, "buefy_css"): return liburl + static.buefy_css.relpath - return f'https://unpkg.com/buefy@{version}/dist/buefy.min.css' + return f"https://unpkg.com/buefy@{version}/dist/buefy.min.css" - elif key == 'vue': - if static and hasattr(static, 'vue_js'): + elif key == "vue": + if static and hasattr(static, "vue_js"): return liburl + static.vue_js.relpath - return f'https://unpkg.com/vue@{version}/dist/vue.min.js' + return f"https://unpkg.com/vue@{version}/dist/vue.min.js" - elif key == 'vue_resource': - if static and hasattr(static, 'vue_resource_js'): + elif key == "vue_resource": + if static and hasattr(static, "vue_resource_js"): return liburl + static.vue_resource_js.relpath - return f'https://cdn.jsdelivr.net/npm/vue-resource@{version}' + return f"https://cdn.jsdelivr.net/npm/vue-resource@{version}" - elif key == 'fontawesome': - if static and hasattr(static, 'fontawesome_js'): + elif key == "fontawesome": + if static and hasattr(static, "fontawesome_js"): return liburl + static.fontawesome_js.relpath - return f'https://use.fontawesome.com/releases/v{version}/js/all.js' + return f"https://use.fontawesome.com/releases/v{version}/js/all.js" - elif key == 'bb_vue': - if static and hasattr(static, 'bb_vue_js'): + elif key == "bb_vue": + if static and hasattr(static, "bb_vue_js"): return liburl + static.bb_vue_js.relpath - return f'https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js' + return f"https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js" - elif key == 'bb_oruga': - if static and hasattr(static, 'bb_oruga_js'): + elif key == "bb_oruga": + if static and hasattr(static, "bb_oruga_js"): return liburl + static.bb_oruga_js.relpath - return f'https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs' + return f"https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs" - elif key == 'bb_oruga_bulma': - if static and hasattr(static, 'bb_oruga_bulma_js'): + elif key == "bb_oruga_bulma": + if static and hasattr(static, "bb_oruga_bulma_js"): return liburl + static.bb_oruga_bulma_js.relpath - return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.js' + return f"https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.js" - elif key == 'bb_oruga_bulma_css': - if static and hasattr(static, 'bb_oruga_bulma_css'): + elif key == "bb_oruga_bulma_css": + if static and hasattr(static, "bb_oruga_bulma_css"): return liburl + static.bb_oruga_bulma_css.relpath - return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css' + return f"https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css" - elif key == 'bb_fontawesome_svg_core': - if static and hasattr(static, 'bb_fontawesome_svg_core_js'): + elif key == "bb_fontawesome_svg_core": + if static and hasattr(static, "bb_fontawesome_svg_core_js"): return liburl + static.bb_fontawesome_svg_core_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm' + return f"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm" - elif key == 'bb_free_solid_svg_icons': - if static and hasattr(static, 'bb_free_solid_svg_icons_js'): + elif key == "bb_free_solid_svg_icons": + if static and hasattr(static, "bb_free_solid_svg_icons_js"): return liburl + static.bb_free_solid_svg_icons_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm' + return f"https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm" - elif key == 'bb_vue_fontawesome': - if static and hasattr(static, 'bb_vue_fontawesome_js'): + elif key == "bb_vue_fontawesome": + if static and hasattr(static, "bb_vue_fontawesome_js"): return liburl + static.bb_vue_fontawesome_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm' + return ( + f"https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm" + ) def get_csrf_token(request): @@ -455,7 +470,7 @@ def get_csrf_token(request): return token -def render_csrf_token(request, name='_csrf'): +def render_csrf_token(request, name="_csrf"): """ Convenience function, returns CSRF hidden input inside hidden div, e.g.: @@ -480,7 +495,9 @@ def render_csrf_token(request, name='_csrf'): See also :func:`get_csrf_token()`. """ token = get_csrf_token(request) - return HTML.tag('div', tags.hidden(name, value=token, id=None), style='display:none;') + return HTML.tag( + "div", tags.hidden(name, value=token, id=None), style="display:none;" + ) def get_model_fields(config, model_class, include_fk=False): @@ -511,13 +528,16 @@ def get_model_fields(config, model_class, include_fk=False): if include_fk: fields = [prop.key for prop in mapper.iterate_properties] else: - fields = [prop.key for prop in mapper.iterate_properties - if not prop_is_fk(mapper, prop)] + fields = [ + prop.key + for prop in mapper.iterate_properties + if not prop_is_fk(mapper, prop) + ] # nb. we never want the continuum 'versions' prop app = config.get_app() - if app.continuum_is_enabled() and 'versions' in fields: - fields.remove('versions') + if app.continuum_is_enabled() and "versions" in fields: + fields.remove("versions") return fields @@ -599,6 +619,7 @@ def make_json_safe(value, key=None, warn=True): # theme functions ############################## + def get_available_themes(config): """ Returns the official list of theme names which are available for @@ -621,16 +642,17 @@ def get_available_themes(config): :param config: App :term:`config object`. """ # get available list from config, if it has one - available = config.get_list('wuttaweb.themes.keys', - default=['default', 'butterfly']) + available = config.get_list( + "wuttaweb.themes.keys", default=["default", "butterfly"] + ) # sort the list by name available.sort() # make default theme the first option - if 'default' in available: - available.remove('default') - available.insert(0, 'default') + if "default" in available: + available.remove("default") + available.insert(0, "default") return available @@ -663,7 +685,7 @@ def get_effective_theme(config, theme=None, session=None): if not theme: with app.short_session(session=session) as s: - theme = app.get_setting(s, 'wuttaweb.theme') or 'default' + theme = app.get_setting(s, "wuttaweb.theme") or "default" # confirm requested theme is available available = get_available_themes(config) @@ -701,8 +723,9 @@ def get_theme_template_path(config, theme=None, session=None): :returns: Path on disk to theme template folder. """ theme = get_effective_theme(config, theme=theme, session=session) - theme_path = config.get(f'wuttaweb.theme.{theme}', - default=f'wuttaweb:templates/themes/{theme}') + theme_path = config.get( + f"wuttaweb.theme.{theme}", default=f"wuttaweb:templates/themes/{theme}" + ) return resource_path(theme_path) @@ -731,7 +754,7 @@ def set_app_theme(request, theme, session=None): # there's only one global template lookup; can get to it via any renderer # but should *not* use /base.mako since that one is about to get volatile - renderer = get_renderer('/menu.mako') + renderer = get_renderer("/menu.mako") lookup = renderer.lookup # overwrite first entry in lookup's directory list @@ -742,7 +765,7 @@ def set_app_theme(request, theme, session=None): # persist current theme in db settings with app.short_session(session=session) as s: - app.save_setting(s, 'wuttaweb.theme', theme) + app.save_setting(s, "wuttaweb.theme", theme) # and cache in live app settings - request.registry.settings['wuttaweb.theme'] = theme + request.registry.settings["wuttaweb.theme"] = theme diff --git a/src/wuttaweb/views/__init__.py b/src/wuttaweb/views/__init__.py index 845ff49..6b59940 100644 --- a/src/wuttaweb/views/__init__.py +++ b/src/wuttaweb/views/__init__.py @@ -35,4 +35,4 @@ from .master import MasterView def includeme(config): - config.include('wuttaweb.views.essential') + config.include("wuttaweb.views.essential") diff --git a/src/wuttaweb/views/auth.py b/src/wuttaweb/views/auth.py index e70c09a..957ffeb 100644 --- a/src/wuttaweb/views/auth.py +++ b/src/wuttaweb/views/auth.py @@ -54,28 +54,32 @@ class AuthView(View): # nb. redirect to /setup if no users exist user = session.query(model.User).first() if not user: - return self.redirect(self.request.route_url('setup')) + return self.redirect(self.request.route_url("setup")) referrer = self.request.get_referrer() # redirect if already logged in if self.request.user: - self.request.session.flash(f"{self.request.user} is already logged in", 'error') + self.request.session.flash( + f"{self.request.user} is already logged in", "error" + ) return self.redirect(referrer) - form = self.make_form(schema=self.login_make_schema(), - align_buttons_right=True, - show_button_cancel=False, - show_button_reset=True, - button_label_submit="Login", - button_icon_submit='user') + form = self.make_form( + schema=self.login_make_schema(), + align_buttons_right=True, + show_button_cancel=False, + show_button_reset=True, + button_label_submit="Login", + button_icon_submit="user", + ) # validate basic form data (sanity check) data = form.validate() if data: # truly validate user credentials - user = auth.authenticate_user(session, data['username'], data['password']) + user = auth.authenticate_user(session, data["username"], data["password"]) if user: # okay now they're truly logged in @@ -83,11 +87,11 @@ class AuthView(View): return self.redirect(referrer, headers=headers) else: - self.request.session.flash("Invalid user credentials", 'error') + self.request.session.flash("Invalid user credentials", "error") return { - 'index_title': self.app.get_title(), - 'form': form, + "index_title": self.app.get_title(), + "form": form, # TODO # 'referrer': referrer, } @@ -99,19 +103,29 @@ class AuthView(View): # specify the ref attribute. this is needed for autofocus and # keydown behavior for login form. - schema.add(colander.SchemaNode( - colander.String(), - name='username', - widget=widgets.TextInputWidget(attributes={ - 'ref': 'username', - }))) + schema.add( + colander.SchemaNode( + colander.String(), + name="username", + widget=widgets.TextInputWidget( + attributes={ + "ref": "username", + } + ), + ) + ) - schema.add(colander.SchemaNode( - colander.String(), - name='password', - widget=widgets.PasswordWidget(attributes={ - 'ref': 'password', - }))) + schema.add( + colander.SchemaNode( + colander.String(), + name="password", + widget=widgets.PasswordWidget( + attributes={ + "ref": "password", + } + ), + ) + ) return schema @@ -138,7 +152,7 @@ class AuthView(View): # otherwise redirect to referrer, with 'login' page as fallback # TODO: should call request.get_referrer() # referrer = self.request.get_referrer(default=self.request.route_url('login')) - referrer = self.request.route_url('login') + referrer = self.request.route_url("login") return self.redirect(referrer, headers=headers) def change_password(self): @@ -155,46 +169,55 @@ class AuthView(View): * template: ``/auth/change_password.mako`` """ if not self.request.user: - return self.redirect(self.request.route_url('home')) + return self.redirect(self.request.route_url("home")) if self.request.user.prevent_edit: raise self.forbidden() - form = self.make_form(schema=self.change_password_make_schema(), - show_button_cancel=False, - show_button_reset=True) + form = self.make_form( + schema=self.change_password_make_schema(), + show_button_cancel=False, + show_button_reset=True, + ) data = form.validate() if data: auth = self.app.get_auth_handler() - auth.set_user_password(self.request.user, data['new_password']) + auth.set_user_password(self.request.user, data["new_password"]) self.request.session.flash("Your password has been changed.") # TODO: should use request.get_referrer() instead - referrer = self.request.route_url('home') + referrer = self.request.route_url("home") return self.redirect(referrer) - return {'index_title': str(self.request.user), - 'form': form} + return {"index_title": str(self.request.user), "form": form} def change_password_make_schema(self): """ """ schema = colander.Schema() - schema.add(colander.SchemaNode( - colander.String(), - name='current_password', - widget=widgets.PasswordWidget(), - validator=self.change_password_validate_current_password)) + schema.add( + colander.SchemaNode( + colander.String(), + name="current_password", + widget=widgets.PasswordWidget(), + validator=self.change_password_validate_current_password, + ) + ) # nb. must use different widget for Vue 3 + Oruga - widget = (widgets.WuttaCheckedPasswordWidget() - if self.request.use_oruga - else widgets.CheckedPasswordWidget()) - schema.add(colander.SchemaNode( - colander.String(), - name='new_password', - widget=widget, - validator=self.change_password_validate_new_password)) + widget = ( + widgets.WuttaCheckedPasswordWidget() + if self.request.use_oruga + else widgets.CheckedPasswordWidget() + ) + schema.add( + colander.SchemaNode( + colander.String(), + name="new_password", + widget=widget, + validator=self.change_password_validate_new_password, + ) + ) return schema @@ -220,14 +243,16 @@ class AuthView(View): See also :meth:`stop_root()`. """ - if self.request.method != 'POST': + if self.request.method != "POST": raise self.forbidden() if not self.request.is_admin: raise self.forbidden() - self.request.session['is_root'] = True - self.request.session.flash("You have been elevated to 'root' and now have full system access") + self.request.session["is_root"] = True + self.request.session.flash( + "You have been elevated to 'root' and now have full system access" + ) url = self.request.get_referrer() return self.redirect(url) @@ -240,13 +265,13 @@ class AuthView(View): See also :meth:`become_root()`. """ - if self.request.method != 'POST': + if self.request.method != "POST": raise self.forbidden() if not self.request.is_admin: raise self.forbidden() - self.request.session['is_root'] = False + self.request.session["is_root"] = False self.request.session.flash("Your normal system access has been restored") url = self.request.get_referrer() @@ -260,39 +285,37 @@ class AuthView(View): def _auth_defaults(cls, config): # login - config.add_route('login', '/login') - config.add_view(cls, attr='login', - route_name='login', - renderer='/auth/login.mako') + config.add_route("login", "/login") + config.add_view( + cls, attr="login", route_name="login", renderer="/auth/login.mako" + ) # logout - config.add_route('logout', '/logout') - config.add_view(cls, attr='logout', - route_name='logout') + config.add_route("logout", "/logout") + config.add_view(cls, attr="logout", route_name="logout") # change password - config.add_route('change_password', '/change-password') - config.add_view(cls, attr='change_password', - route_name='change_password', - renderer='/auth/change_password.mako') + config.add_route("change_password", "/change-password") + config.add_view( + cls, + attr="change_password", + route_name="change_password", + renderer="/auth/change_password.mako", + ) # become root - config.add_route('become_root', '/root/yes', - request_method='POST') - config.add_view(cls, attr='become_root', - route_name='become_root') + config.add_route("become_root", "/root/yes", request_method="POST") + config.add_view(cls, attr="become_root", route_name="become_root") # stop root - config.add_route('stop_root', '/root/no', - request_method='POST') - config.add_view(cls, attr='stop_root', - route_name='stop_root') + config.add_route("stop_root", "/root/no", request_method="POST") + config.add_view(cls, attr="stop_root", route_name="stop_root") def defaults(config, **kwargs): base = globals() - AuthView = kwargs.get('AuthView', base['AuthView']) + AuthView = kwargs.get("AuthView", base["AuthView"]) AuthView.defaults(config) diff --git a/src/wuttaweb/views/base.py b/src/wuttaweb/views/base.py index c5bb0dc..dd00877 100644 --- a/src/wuttaweb/views/base.py +++ b/src/wuttaweb/views/base.py @@ -169,4 +169,4 @@ class View: :returns: A :term:`response` with JSON content type. """ - return render_to_response('json', context, request=self.request) + return render_to_response("json", context, request=self.request) diff --git a/src/wuttaweb/views/batch.py b/src/wuttaweb/views/batch.py index 7ce3199..ab70044 100644 --- a/src/wuttaweb/views/batch.py +++ b/src/wuttaweb/views/batch.py @@ -52,18 +52,18 @@ class BatchMasterView(MasterView): """ labels = { - 'id': "Batch ID", - 'status_code': "Status", + "id": "Batch ID", + "status_code": "Status", } - sort_defaults = ('id', 'desc') + sort_defaults = ("id", "desc") has_rows = True rows_title = "Batch Rows" - rows_sort_defaults = 'sequence' + rows_sort_defaults = "sequence" row_labels = { - 'status_code': "Status", + "status_code": "Status", } def __init__(self, request, context=None): @@ -89,7 +89,7 @@ class BatchMasterView(MasterView): batches. """ templates = super().get_fallback_templates(template) - templates.insert(0, f'/batch/{template}.mako') + templates.insert(0, f"/batch/{template}.mako") return templates def render_to_response(self, template, context): @@ -105,16 +105,19 @@ class BatchMasterView(MasterView): * ``why_not_execute`` - text of reason (if any) not to execute batch * ``execution_described`` - HTML (rendered from markdown) describing batch execution """ - if template == 'view': - batch = context['instance'] - context['batch'] = batch - context['batch_handler'] = self.batch_handler - context['why_not_execute'] = self.batch_handler.why_not_execute(batch) + if template == "view": + batch = context["instance"] + context["batch"] = batch + context["batch_handler"] = self.batch_handler + context["why_not_execute"] = self.batch_handler.why_not_execute(batch) - description = (self.batch_handler.describe_execution(batch) - or "Handler does not say! Your guess is as good as mine.") - context['execution_described'] = markdown.markdown( - description, extensions=['fenced_code', 'codehilite']) + description = ( + self.batch_handler.describe_execution(batch) + or "Handler does not say! Your guess is as good as mine." + ) + context["execution_described"] = markdown.markdown( + description, extensions=["fenced_code", "codehilite"] + ) return super().render_to_response(template, context) @@ -125,24 +128,27 @@ class BatchMasterView(MasterView): # created_by CreatedBy = orm.aliased(model.User) - g.set_joiner('created_by', - lambda q: q.join(CreatedBy, - CreatedBy.uuid == self.model_class.created_by_uuid)) - g.set_sorter('created_by', CreatedBy.username) + g.set_joiner( + "created_by", + lambda q: q.join( + CreatedBy, CreatedBy.uuid == self.model_class.created_by_uuid + ), + ) + g.set_sorter("created_by", CreatedBy.username) # g.set_filter('created_by', CreatedBy.username, label="Created By Username") # id - g.set_renderer('id', self.render_batch_id) - g.set_link('id') + g.set_renderer("id", self.render_batch_id) + g.set_link("id") # description - g.set_link('description') + g.set_link("description") def render_batch_id(self, batch, key, value): """ """ if value: batch_id = int(value) - return f'{batch_id:08d}' + return f"{batch_id:08d}" def get_instance_title(self, batch): """ """ @@ -157,55 +163,55 @@ class BatchMasterView(MasterView): # id if self.creating: - f.remove('id') + f.remove("id") else: - f.set_readonly('id') - f.set_widget('id', BatchIdWidget()) + f.set_readonly("id") + f.set_widget("id", BatchIdWidget()) # notes - f.set_widget('notes', 'notes') + f.set_widget("notes", "notes") # rows - f.remove('rows') + f.remove("rows") if self.creating: - f.remove('row_count') + f.remove("row_count") else: - f.set_readonly('row_count') + f.set_readonly("row_count") # status - f.remove('status_text') + f.remove("status_text") if self.creating: - f.remove('status_code') + f.remove("status_code") else: - f.set_readonly('status_code') + f.set_readonly("status_code") # created if self.creating: - f.remove('created') + f.remove("created") else: - f.set_readonly('created') + f.set_readonly("created") # created_by - f.remove('created_by_uuid') + f.remove("created_by_uuid") if self.creating: - f.remove('created_by') + f.remove("created_by") else: - f.set_node('created_by', UserRef(self.request)) - f.set_readonly('created_by') + f.set_node("created_by", UserRef(self.request)) + f.set_readonly("created_by") # executed if self.creating or not batch.executed: - f.remove('executed') + f.remove("executed") else: - f.set_readonly('executed') + f.set_readonly("executed") # executed_by - f.remove('executed_by_uuid') + f.remove("executed_by_uuid") if self.creating or not batch.executed: - f.remove('executed_by') + f.remove("executed_by") else: - f.set_node('executed_by', UserRef(self.request)) - f.set_readonly('executed_by') + f.set_node("executed_by", UserRef(self.request)) + f.set_readonly("executed_by") def objectify(self, form, **kwargs): """ @@ -226,12 +232,16 @@ class BatchMasterView(MasterView): batch = schema.objectify(form.validated, context=form.model_instance) # then we collect attributes from the new batch - kw = dict([(key, getattr(batch, key)) - for key in form.validated - if hasattr(batch, key)]) + kw = dict( + [ + (key, getattr(batch, key)) + for key in form.validated + if hasattr(batch, key) + ] + ) # and set attribute for user creating the batch - kw['created_by'] = self.request.user + kw["created_by"] = self.request.user # plus caller can override anything kw.update(kwargs) @@ -252,15 +262,19 @@ class BatchMasterView(MasterView): """ # just view batch if should not populate 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)) # setup thread to populate batch route_prefix = self.get_route_prefix() - key = f'{route_prefix}.populate' - progress = self.make_progress(key, success_url=self.get_action_url('view', batch)) - thread = threading.Thread(target=self.populate_thread, - args=(batch.uuid,), - kwargs=dict(progress=progress)) + key = f"{route_prefix}.populate" + progress = self.make_progress( + key, success_url=self.get_action_url("view", batch) + ) + thread = threading.Thread( + target=self.populate_thread, + args=(batch.uuid,), + kwargs=dict(progress=progress), + ) # start thread and show progress page thread.start() @@ -316,9 +330,12 @@ class BatchMasterView(MasterView): except Exception as error: session.rollback() - log.warning("failed to populate %s: %s", - self.get_model_title(), batch, - exc_info=True) + log.warning( + "failed to populate %s: %s", + self.get_model_title(), + batch, + exc_info=True, + ) if progress: progress.handle_error(error) @@ -351,9 +368,9 @@ class BatchMasterView(MasterView): self.batch_handler.do_execute(batch, self.request.user) except Exception as error: log.warning("failed to execute batch: %s", batch, exc_info=True) - self.request.session.flash(f"Execution failed!: {error}", 'error') + self.request.session.flash(f"Execution failed!: {error}", "error") - return self.redirect(self.get_action_url('view', batch)) + return self.redirect(self.get_action_url("view", batch)) ############################## # row methods @@ -362,7 +379,7 @@ class BatchMasterView(MasterView): @classmethod def get_row_model_class(cls): """ """ - if hasattr(cls, 'row_model_class'): + if hasattr(cls, "row_model_class"): return cls.row_model_class Batch = cls.get_model_class() @@ -375,17 +392,16 @@ class BatchMasterView(MasterView): data. """ BatchRow = self.get_row_model_class() - query = self.Session.query(BatchRow)\ - .filter(BatchRow.batch == batch) + query = self.Session.query(BatchRow).filter(BatchRow.batch == batch) return query def configure_row_grid(self, g): """ """ super().configure_row_grid(g) - g.set_label('sequence', "Seq.", column_only=True) + g.set_label("sequence", "Seq.", column_only=True) - g.set_renderer('status_code', self.render_row_status) + g.set_renderer("status_code", self.render_row_status) def render_row_status(self, row, key, value): """ """ @@ -409,12 +425,17 @@ class BatchMasterView(MasterView): 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}") + 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}" + ) diff --git a/src/wuttaweb/views/common.py b/src/wuttaweb/views/common.py index 27ca77f..7e5554b 100644 --- a/src/wuttaweb/views/common.py +++ b/src/wuttaweb/views/common.py @@ -58,15 +58,15 @@ class CommonView(View): # nb. redirect to /setup if no users exist user = session.query(model.User).first() if not user: - return self.redirect(self.request.route_url('setup')) + return self.redirect(self.request.route_url("setup")) # maybe auto-redirect anons to login if not self.request.user: - if self.config.get_bool('wuttaweb.home_redirect_to_login'): - return self.redirect(self.request.route_url('login')) + if self.config.get_bool("wuttaweb.home_redirect_to_login"): + return self.redirect(self.request.route_url("login")) return { - 'index_title': self.app.get_title(), + "index_title": self.app.get_title(), } def forbidden_view(self): @@ -75,7 +75,7 @@ class CommonView(View): Template: ``/forbidden.mako`` """ - return {'index_title': self.app.get_title()} + return {"index_title": self.app.get_title()} def notfound_view(self): """ @@ -83,7 +83,7 @@ class CommonView(View): Template: ``/notfound.mako`` """ - return {'index_title': self.app.get_title()} + return {"index_title": self.app.get_title()} def feedback(self): """ """ @@ -96,46 +96,45 @@ class CommonView(View): if not form.validate(): # TODO: native Form class should better expose error(s) dform = form.get_deform() - return {'error': str(dform.error)} + return {"error": str(dform.error)} # build email template context context = dict(form.validated) - if context['user_uuid']: - context['user'] = session.get(model.User, context['user_uuid']) - context['user_url'] = self.request.route_url('users.view', uuid=context['user_uuid']) - context['client_ip'] = self.request.client_addr + if context["user_uuid"]: + context["user"] = session.get(model.User, context["user_uuid"]) + context["user_url"] = self.request.route_url( + "users.view", uuid=context["user_uuid"] + ) + context["client_ip"] = self.request.client_addr # send email try: self.feedback_send(context) except Exception as error: log.warning("failed to send feedback email", exc_info=True) - return {'error': str(error) or error.__class__.__name__} + return {"error": str(error) or error.__class__.__name__} - return {'ok': True} + return {"ok": True} def feedback_make_schema(self): """ """ schema = colander.Schema() - schema.add(colander.SchemaNode(colander.String(), - name='referrer')) + schema.add(colander.SchemaNode(colander.String(), name="referrer")) - schema.add(colander.SchemaNode(colander.String(), - name='user_uuid', - missing=None)) + schema.add( + colander.SchemaNode(colander.String(), name="user_uuid", missing=None) + ) - schema.add(colander.SchemaNode(colander.String(), - name='user_name')) + schema.add(colander.SchemaNode(colander.String(), name="user_name")) - schema.add(colander.SchemaNode(colander.String(), - name='message')) + schema.add(colander.SchemaNode(colander.String(), name="message")) return schema def feedback_send(self, context): """ """ - self.app.send_email('feedback', context) + self.app.send_email("feedback", context) def setup(self, session=None): """ @@ -162,87 +161,99 @@ class CommonView(View): # nb. this view only available until first user is created user = session.query(model.User).first() if user: - return self.redirect(self.request.route_url('home')) + return self.redirect(self.request.route_url("home")) - form = self.make_form(fields=['username', 'password', 'first_name', 'last_name'], - show_button_cancel=False, - show_button_reset=True) - form.set_widget('password', widgets.CheckedPasswordWidget()) - form.set_required('first_name', False) - form.set_required('last_name', False) + form = self.make_form( + fields=["username", "password", "first_name", "last_name"], + show_button_cancel=False, + show_button_reset=True, + ) + form.set_widget("password", widgets.CheckedPasswordWidget()) + form.set_required("first_name", False) + form.set_required("last_name", False) if form.validate(): auth = self.app.get_auth_handler() data = form.validated # make user - user = auth.make_user(session=session, username=data['username']) - auth.set_user_password(user, data['password']) + user = auth.make_user(session=session, username=data["username"]) + auth.set_user_password(user, data["password"]) # assign admin role admin = auth.get_role_administrator(session) user.roles.append(admin) - admin.notes = ("users in this role may \"become root\".\n\n" - "it's recommended not to grant other perms to this role.") + admin.notes = ( + 'users in this role may "become root".\n\n' + "it's recommended not to grant other perms to this role." + ) # initialize built-in roles authed = auth.get_role_authenticated(session) - authed.notes = ("this role represents any user who *is* logged in.\n\n" - "you may grant any perms you like to it.") + authed.notes = ( + "this role represents any user who *is* logged in.\n\n" + "you may grant any perms you like to it." + ) anon = auth.get_role_anonymous(session) - anon.notes = ("this role represents any user who is *not* logged in.\n\n" - "you may grant any perms you like to it.") + anon.notes = ( + "this role represents any user who is *not* logged in.\n\n" + "you may grant any perms you like to it." + ) # also make "Site Admin" role site_admin_perms = [ - 'appinfo.list', - 'appinfo.configure', - 'people.list', - 'people.create', - 'people.view', - 'people.edit', - 'people.delete', - 'roles.list', - 'roles.create', - 'roles.view', - 'roles.edit', - 'roles.edit_builtin', - 'roles.delete', - 'settings.list', - 'settings.create', - 'settings.view', - 'settings.edit', - 'settings.delete', - 'settings.delete_bulk', - 'upgrades.list', - 'upgrades.create', - 'upgrades.view', - 'upgrades.edit', - 'upgrades.delete', - 'upgrades.execute', - 'upgrades.download', - 'upgrades.configure', - 'users.list', - 'users.create', - 'users.view', - 'users.edit', - 'users.delete', + "appinfo.list", + "appinfo.configure", + "people.list", + "people.create", + "people.view", + "people.edit", + "people.delete", + "roles.list", + "roles.create", + "roles.view", + "roles.edit", + "roles.edit_builtin", + "roles.delete", + "settings.list", + "settings.create", + "settings.view", + "settings.edit", + "settings.delete", + "settings.delete_bulk", + "upgrades.list", + "upgrades.create", + "upgrades.view", + "upgrades.edit", + "upgrades.delete", + "upgrades.execute", + "upgrades.download", + "upgrades.configure", + "users.list", + "users.create", + "users.view", + "users.edit", + "users.delete", ] admin2 = model.Role(name="Site Admin") - admin2.notes = ("this is the \"daily driver\" admin role.\n\n" - "you may grant any perms you like to it.") + admin2.notes = ( + 'this is the "daily driver" admin role.\n\n' + "you may grant any perms you like to it." + ) session.add(admin2) user.roles.append(admin2) for perm in site_admin_perms: auth.grant_permission(admin2, perm) # maybe make person - if data['first_name'] or data['last_name']: - first = data['first_name'] - last = data['last_name'] - person = model.Person(first_name=first, - last_name=last, - full_name=(f"{first} {last}").strip()) + if data["first_name"] or data["last_name"]: + first = data["first_name"] + last = data["last_name"] + person = model.Person( + first_name=first, + last_name=last, + full_name=(f"{first} {last}").strip(), + ) session.add(person) user.person = person @@ -250,11 +261,11 @@ class CommonView(View): # send user to /login self.request.session.flash("Account created! Please login below.") - return self.redirect(self.request.route_url('login')) + return self.redirect(self.request.route_url("login")) return { - 'index_title': self.app.get_title(), - 'form': form, + "index_title": self.app.get_title(), + "form": form, } def setup_enhance_admin_user(self, user): @@ -273,14 +284,14 @@ class CommonView(View): This view will set the global app theme, then redirect back to the referring page. """ - theme = self.request.params.get('theme') + theme = self.request.params.get("theme") if theme: try: set_app_theme(self.request, theme, session=Session()) except Exception as error: error = self.app.render_error(error) - self.request.session.flash(f"Failed to set theme: {error}", 'error') - referrer = self.request.params.get('referrer') or self.request.get_referrer() + self.request.session.flash(f"Failed to set theme: {error}", "error") + referrer = self.request.params.get("referrer") or self.request.get_referrer() return self.redirect(referrer) @classmethod @@ -290,51 +301,52 @@ class CommonView(View): @classmethod def _defaults(cls, config): - config.add_wutta_permission_group('common', "(common)", overwrite=False) + config.add_wutta_permission_group("common", "(common)", overwrite=False) # home page - config.add_route('home', '/') - config.add_view(cls, attr='home', - route_name='home', - renderer='/home.mako') + config.add_route("home", "/") + config.add_view(cls, attr="home", route_name="home", renderer="/home.mako") # forbidden - config.add_forbidden_view(cls, attr='forbidden_view', - renderer='/forbidden.mako') + config.add_forbidden_view( + cls, attr="forbidden_view", renderer="/forbidden.mako" + ) # notfound # nb. also, auto-correct URLs which require trailing slash - config.add_notfound_view(cls, attr='notfound_view', - append_slash=True, - renderer='/notfound.mako') + config.add_notfound_view( + cls, attr="notfound_view", append_slash=True, renderer="/notfound.mako" + ) # feedback - config.add_route('feedback', '/feedback', - request_method='POST') - config.add_view(cls, attr='feedback', - route_name='feedback', - permission='common.feedback', - renderer='json') - config.add_wutta_permission('common', 'common.feedback', - "Send user feedback about the app") + config.add_route("feedback", "/feedback", request_method="POST") + config.add_view( + cls, + attr="feedback", + route_name="feedback", + permission="common.feedback", + renderer="json", + ) + config.add_wutta_permission( + "common", "common.feedback", "Send user feedback about the app" + ) # setup - config.add_route('setup', '/setup') - config.add_view(cls, attr='setup', - route_name='setup', - renderer='/setup.mako') + config.add_route("setup", "/setup") + config.add_view(cls, attr="setup", route_name="setup", renderer="/setup.mako") # change theme - config.add_route('change_theme', '/change-theme', request_method='POST') - config.add_view(cls, attr='change_theme', route_name='change_theme') - config.add_wutta_permission('common', 'common.change_theme', - "Change global theme") + config.add_route("change_theme", "/change-theme", request_method="POST") + config.add_view(cls, attr="change_theme", route_name="change_theme") + config.add_wutta_permission( + "common", "common.change_theme", "Change global theme" + ) def defaults(config, **kwargs): base = globals() - CommonView = kwargs.get('CommonView', base['CommonView']) + CommonView = kwargs.get("CommonView", base["CommonView"]) CommonView.defaults(config) diff --git a/src/wuttaweb/views/email.py b/src/wuttaweb/views/email.py index 587f7b8..700e88d 100644 --- a/src/wuttaweb/views/email.py +++ b/src/wuttaweb/views/email.py @@ -34,10 +34,11 @@ class EmailSettingView(MasterView): """ Master view for :term:`email settings `. """ - model_name = 'email_setting' - model_key = 'key' + + model_name = "email_setting" + model_key = "key" model_title = "Email Setting" - url_prefix = '/email/settings' + url_prefix = "/email/settings" filterable = False sortable = True sort_on_backend = False @@ -46,31 +47,31 @@ class EmailSettingView(MasterView): deletable = False labels = { - 'key': "Email Key", - 'replyto': "Reply-To", + "key": "Email Key", + "replyto": "Reply-To", } grid_columns = [ - 'key', - 'subject', - 'to', - 'enabled', + "key", + "subject", + "to", + "enabled", ] # TODO: why does this not work? - sort_defaults = 'key' + sort_defaults = "key" form_fields = [ - 'key', - 'description', - 'subject', - 'sender', - 'replyto', - 'to', - 'cc', - 'bcc', - 'notes', - 'enabled', + "key", + "description", + "subject", + "sender", + "replyto", + "to", + "cc", + "bcc", + "notes", + "enabled", ] def __init__(self, request, context=None): @@ -92,16 +93,18 @@ class EmailSettingView(MasterView): """ """ key = setting.__name__ return { - 'key': key, - 'description': setting.__doc__, - 'subject': self.email_handler.get_auto_subject(key, rendered=False, setting=setting), - 'sender': self.email_handler.get_auto_sender(key), - 'replyto': self.email_handler.get_auto_replyto(key) or colander.null, - 'to': self.email_handler.get_auto_to(key), - 'cc': self.email_handler.get_auto_cc(key), - 'bcc': self.email_handler.get_auto_bcc(key), - 'notes': self.email_handler.get_notes(key) or colander.null, - 'enabled': self.email_handler.is_enabled(key), + "key": key, + "description": setting.__doc__, + "subject": self.email_handler.get_auto_subject( + key, rendered=False, setting=setting + ), + "sender": self.email_handler.get_auto_sender(key), + "replyto": self.email_handler.get_auto_replyto(key) or colander.null, + "to": self.email_handler.get_auto_to(key), + "cc": self.email_handler.get_auto_cc(key), + "bcc": self.email_handler.get_auto_bcc(key), + "notes": self.email_handler.get_notes(key) or colander.null, + "enabled": self.email_handler.is_enabled(key), } def configure_grid(self, g): @@ -109,15 +112,15 @@ class EmailSettingView(MasterView): super().configure_grid(g) # key - g.set_searchable('key') - g.set_link('key') + g.set_searchable("key") + g.set_link("key") # subject - g.set_searchable('subject') - g.set_link('subject') + g.set_searchable("subject") + g.set_link("subject") # to - g.set_renderer('to', self.render_to_short) + g.set_renderer("to", self.render_to_short) def render_to_short(self, setting, field, value): """ """ @@ -126,14 +129,14 @@ class EmailSettingView(MasterView): return if len(recips) < 3: - return ', '.join(recips) + return ", ".join(recips) - recips = ', '.join(recips[:2]) + recips = ", ".join(recips[:2]) return f"{recips}, ..." def get_instance(self): """ """ - key = self.request.matchdict['key'] + key = self.request.matchdict["key"] setting = self.email_handler.get_email_setting(key, instance=False) if setting: return self.normalize_setting(setting) @@ -142,99 +145,105 @@ class EmailSettingView(MasterView): def get_instance_title(self, setting): """ """ - return setting['subject'] + return setting["subject"] def configure_form(self, f): """ """ super().configure_form(f) # description - f.set_readonly('description') + f.set_readonly("description") # replyto - f.set_required('replyto', False) + f.set_required("replyto", False) # to - f.set_node('to', EmailRecipients()) + f.set_node("to", EmailRecipients()) # cc - f.set_node('cc', EmailRecipients()) + f.set_node("cc", EmailRecipients()) # bcc - f.set_node('bcc', EmailRecipients()) + f.set_node("bcc", EmailRecipients()) # notes - f.set_widget('notes', 'notes') - f.set_required('notes', False) + f.set_widget("notes", "notes") + f.set_required("notes", False) # enabled - f.set_node('enabled', colander.Boolean()) + f.set_node("enabled", colander.Boolean()) def persist(self, setting): """ """ session = self.Session() - key = self.request.matchdict['key'] + key = self.request.matchdict["key"] def save(name, value): - self.app.save_setting(session, f'{self.config.appname}.email.{key}.{name}', value) + self.app.save_setting( + session, f"{self.config.appname}.email.{key}.{name}", value + ) def delete(name): - self.app.delete_setting(session, f'{self.config.appname}.email.{key}.{name}') + self.app.delete_setting( + session, f"{self.config.appname}.email.{key}.{name}" + ) # subject - if setting['subject']: - save('subject', setting['subject']) + if setting["subject"]: + save("subject", setting["subject"]) else: - delete('subject') + delete("subject") # sender - if setting['sender']: - save('sender', setting['sender']) + if setting["sender"]: + save("sender", setting["sender"]) else: - delete('sender') + delete("sender") # replyto - if setting['replyto']: - save('replyto', setting['replyto']) + if setting["replyto"]: + save("replyto", setting["replyto"]) else: - delete('replyto') + delete("replyto") # to - if setting['to']: - save('to', setting['to']) + if setting["to"]: + save("to", setting["to"]) else: - delete('to') + delete("to") # cc - if setting['cc']: - save('cc', setting['cc']) + if setting["cc"]: + save("cc", setting["cc"]) else: - delete('cc') + delete("cc") # bcc - if setting['bcc']: - save('bcc', setting['bcc']) + if setting["bcc"]: + save("bcc", setting["bcc"]) else: - delete('bcc') + delete("bcc") # notes - if setting['notes']: - save('notes', setting['notes']) + if setting["notes"]: + save("notes", setting["notes"]) else: - delete('notes') + delete("notes") # enabled - save('enabled', 'true' if setting['enabled'] else 'false') + save("enabled", "true" if setting["enabled"] else "false") def render_to_response(self, template, context): """ """ if self.viewing: - setting = context['instance'] - context['setting'] = setting - context['has_html_template'] = self.email_handler.get_auto_body_template( - setting['key'], 'html') - context['has_txt_template'] = self.email_handler.get_auto_body_template( - setting['key'], 'txt') + setting = context["instance"] + context["setting"] = setting + context["has_html_template"] = self.email_handler.get_auto_body_template( + setting["key"], "html" + ) + context["has_txt_template"] = self.email_handler.get_auto_body_template( + setting["key"], "txt" + ) return super().render_to_response(template, context) @@ -245,16 +254,16 @@ class EmailSettingView(MasterView): This will render the email template according to the "mode" requested - i.e. HTML or TXT. """ - key = self.request.matchdict['key'] + key = self.request.matchdict["key"] setting = self.email_handler.get_email_setting(key) context = setting.sample_data() - mode = self.request.params.get('mode', 'html') + mode = self.request.params.get("mode", "html") - if mode == 'txt': + if mode == "txt": body = self.email_handler.get_auto_txt_body(key, context) - self.request.response.content_type = 'text/plain' + self.request.response.content_type = "text/plain" - else: # html + else: # html body = self.email_handler.get_auto_html_body(key, context) self.request.response.text = body @@ -275,22 +284,24 @@ class EmailSettingView(MasterView): instance_url_prefix = cls.get_instance_url_prefix() # fix permission group - config.add_wutta_permission_group(permission_prefix, - model_title_plural, - overwrite=False) + config.add_wutta_permission_group( + permission_prefix, model_title_plural, overwrite=False + ) # preview - config.add_route(f'{route_prefix}.preview', - f'{instance_url_prefix}/preview') - config.add_view(cls, attr='preview', - route_name=f'{route_prefix}.preview', - permission=f'{permission_prefix}.view') + config.add_route(f"{route_prefix}.preview", f"{instance_url_prefix}/preview") + config.add_view( + cls, + attr="preview", + route_name=f"{route_prefix}.preview", + permission=f"{permission_prefix}.view", + ) def defaults(config, **kwargs): base = globals() - EmailSettingView = kwargs.get('EmailSettingView', base['EmailSettingView']) + EmailSettingView = kwargs.get("EmailSettingView", base["EmailSettingView"]) EmailSettingView.defaults(config) diff --git a/src/wuttaweb/views/essential.py b/src/wuttaweb/views/essential.py index 20b6881..a35b07d 100644 --- a/src/wuttaweb/views/essential.py +++ b/src/wuttaweb/views/essential.py @@ -44,15 +44,15 @@ That will in turn include the following modules: def defaults(config, **kwargs): mod = lambda spec: kwargs.get(spec, spec) - config.include(mod('wuttaweb.views.common')) - config.include(mod('wuttaweb.views.auth')) - config.include(mod('wuttaweb.views.email')) - config.include(mod('wuttaweb.views.settings')) - config.include(mod('wuttaweb.views.progress')) - config.include(mod('wuttaweb.views.people')) - config.include(mod('wuttaweb.views.roles')) - config.include(mod('wuttaweb.views.users')) - config.include(mod('wuttaweb.views.upgrades')) + config.include(mod("wuttaweb.views.common")) + config.include(mod("wuttaweb.views.auth")) + config.include(mod("wuttaweb.views.email")) + config.include(mod("wuttaweb.views.settings")) + config.include(mod("wuttaweb.views.progress")) + config.include(mod("wuttaweb.views.people")) + config.include(mod("wuttaweb.views.roles")) + config.include(mod("wuttaweb.views.users")) + config.include(mod("wuttaweb.views.upgrades")) def includeme(config): diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index 5903fe4..6c6fa86 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -468,35 +468,34 @@ class MasterView(View): self.listing = True context = { - 'index_url': None, # nb. avoid title link since this *is* the index + "index_url": None, # nb. avoid title link since this *is* the index } if self.has_grid: grid = self.make_model_grid() # handle "full" vs. "partial" differently - if self.request.GET.get('partial'): + if self.request.GET.get("partial"): # so-called 'partial' requests get just data, no html context = grid.get_vue_context() if grid.paginated and grid.paginate_on_backend: - context['pager_stats'] = grid.get_vue_pager_stats() + context["pager_stats"] = grid.get_vue_pager_stats() return self.json_response(context) - else: # full, not partial + else: # full, not partial # nb. when user asks to reset view, it is via the query # string. if so we then redirect to discard that. - if self.request.GET.get('reset-view'): + if self.request.GET.get("reset-view"): # nb. we want to preserve url hash if applicable - kw = {'_query': None, - '_anchor': self.request.GET.get('hash')} + kw = {"_query": None, "_anchor": self.request.GET.get("hash")} return self.redirect(self.request.current_route_url(**kw)) - context['grid'] = grid + context["grid"] = grid - return self.render_to_response('index', context) + return self.render_to_response("index", context) ############################## # create methods @@ -533,9 +532,9 @@ class MasterView(View): return self.redirect_after_create(obj) context = { - 'form': form, + "form": form, } - return self.render_to_response('create', context) + return self.render_to_response("create", context) def create_save_form(self, form): """ @@ -564,7 +563,7 @@ class MasterView(View): It is called automatically by :meth:`create()`. """ - return self.redirect(self.get_action_url('view', obj)) + return self.redirect(self.get_action_url("view", obj)) ############################## # view methods @@ -595,8 +594,8 @@ class MasterView(View): obj = self.get_instance() form = self.make_model_form(obj, readonly=True) context = { - 'instance': obj, - 'form': form, + "instance": obj, + "form": form, } if self.has_rows: @@ -607,25 +606,24 @@ class MasterView(View): # but if user did request a "reset" then we want to # redirect so the query string gets cleared out - if self.request.GET.get('reset-view'): + if self.request.GET.get("reset-view"): # nb. we want to preserve url hash if applicable - kw = {'_query': None, - '_anchor': self.request.GET.get('hash')} + kw = {"_query": None, "_anchor": self.request.GET.get("hash")} return self.redirect(self.request.current_route_url(**kw)) # so-called 'partial' requests get just the grid data - if self.request.params.get('partial'): + if self.request.params.get("partial"): context = grid.get_vue_context() if grid.paginated and grid.paginate_on_backend: - context['pager_stats'] = grid.get_vue_pager_stats() + context["pager_stats"] = grid.get_vue_pager_stats() return self.json_response(context) - context['rows_grid'] = grid + context["rows_grid"] = grid - context['xref_buttons'] = self.get_xref_buttons(obj) + context["xref_buttons"] = self.get_xref_buttons(obj) - return self.render_to_response('view', context) + return self.render_to_response("view", context) ############################## # edit methods @@ -657,18 +655,19 @@ class MasterView(View): self.editing = True instance = self.get_instance() - form = self.make_model_form(instance, - cancel_url_fallback=self.get_action_url('view', instance)) + form = self.make_model_form( + instance, cancel_url_fallback=self.get_action_url("view", instance) + ) if form.validate(): self.edit_save_form(form) - return self.redirect(self.get_action_url('view', instance)) + return self.redirect(self.get_action_url("view", instance)) context = { - 'instance': instance, - 'form': form, + "instance": instance, + "form": form, } - return self.render_to_response('edit', context) + return self.render_to_response("edit", context) def edit_save_form(self, form): """ @@ -720,14 +719,16 @@ class MasterView(View): instance = self.get_instance() if not self.is_deletable(instance): - return self.redirect(self.get_action_url('view', instance)) + return self.redirect(self.get_action_url("view", instance)) # nb. this form proper is not readonly.. - form = self.make_model_form(instance, - cancel_url_fallback=self.get_action_url('view', instance), - button_label_submit="DELETE Forever", - button_icon_submit='trash', - button_type_submit='is-danger') + form = self.make_model_form( + instance, + cancel_url_fallback=self.get_action_url("view", instance), + button_label_submit="DELETE Forever", + button_icon_submit="trash", + button_type_submit="is-danger", + ) # ..but *all* fields are readonly form.readonly_fields = set(form.fields) @@ -737,10 +738,10 @@ class MasterView(View): return self.redirect(self.get_index_url()) context = { - 'instance': instance, - 'form': form, + "instance": instance, + "form": form, } - return self.render_to_response('delete', context) + return self.render_to_response("delete", context) def delete_save_form(self, form): """ @@ -804,10 +805,13 @@ class MasterView(View): # start thread for delete; show progress page route_prefix = self.get_route_prefix() - key = f'{route_prefix}.delete_bulk' + key = f"{route_prefix}.delete_bulk" progress = self.make_progress(key, success_url=self.get_index_url()) - thread = threading.Thread(target=self.delete_bulk_thread, - args=(data,), kwargs={'progress': progress}) + thread = threading.Thread( + target=self.delete_bulk_thread, + args=(data,), + kwargs={"progress": progress}, + ) thread.start() return self.render_progress(progress) @@ -824,9 +828,12 @@ class MasterView(View): except Exception as error: session.rollback() - log.warning("failed to delete %s results for %s", - len(records), model_title_plural, - exc_info=True) + log.warning( + "failed to delete %s results for %s", + len(records), + model_title_plural, + exc_info=True, + ) if progress: progress.handle_error(error) @@ -856,30 +863,35 @@ class MasterView(View): if self.is_deletable(obj): self.delete_instance(obj) - self.app.progress_loop(delete, data, progress, - message=f"Deleting {model_title_plural}") + self.app.progress_loop( + delete, data, progress, message=f"Deleting {model_title_plural}" + ) def delete_bulk_make_button(self): """ """ route_prefix = self.get_route_prefix() label = HTML.literal( - '{{ deleteResultsSubmitting ? "Working, please wait..." : "Delete Results" }}') - button = self.make_button(label, - variant='is-danger', - icon_left='trash', - **{'@click': 'deleteResultsSubmit()', - ':disabled': 'deleteResultsDisabled'}) + '{{ deleteResultsSubmitting ? "Working, please wait..." : "Delete Results" }}' + ) + button = self.make_button( + label, + variant="is-danger", + icon_left="trash", + **{"@click": "deleteResultsSubmit()", ":disabled": "deleteResultsDisabled"}, + ) - form = HTML.tag('form', - method='post', - action=self.request.route_url(f'{route_prefix}.delete_bulk'), - ref='deleteResultsForm', - class_='control', - c=[ - render_csrf_token(self.request), - button, - ]) + form = HTML.tag( + "form", + method="post", + action=self.request.route_url(f"{route_prefix}.delete_bulk"), + ref="deleteResultsForm", + class_="control", + c=[ + render_csrf_token(self.request), + button, + ], + ) return form ############################## @@ -901,7 +913,7 @@ class MasterView(View): * :meth:`autocomplete_data()` * :meth:`autocomplete_normalize()` """ - term = self.request.GET.get('term', '') + term = self.request.GET.get("term", "") if not term: return [] @@ -909,7 +921,7 @@ class MasterView(View): if not data: return [] - max_results = 100 # TODO + max_results = 100 # TODO results = [] for obj in data[:max_results]: @@ -956,8 +968,8 @@ class MasterView(View): above. """ return { - 'value': obj.uuid, - 'label': str(obj), + "value": obj.uuid, + "label": str(obj), } ############################## @@ -992,7 +1004,7 @@ class MasterView(View): * :meth:`download_path()` """ obj = self.get_instance() - filename = self.request.GET.get('filename', None) + filename = self.request.GET.get("filename", None) path = self.download_path(obj, filename) if not path or not os.path.exists(path): @@ -1061,19 +1073,27 @@ class MasterView(View): obj = self.get_instance() # make the progress tracker - progress = self.make_progress(f'{route_prefix}.execute', - success_msg=f"{model_title} was executed.", - success_url=self.get_action_url('view', obj)) + progress = self.make_progress( + f"{route_prefix}.execute", + success_msg=f"{model_title} was executed.", + success_url=self.get_action_url("view", obj), + ) # start thread for execute; show progress page key = self.request.matchdict - thread = threading.Thread(target=self.execute_thread, - args=(key, self.request.user.uuid), - kwargs={'progress': progress}) + thread = threading.Thread( + target=self.execute_thread, + args=(key, self.request.user.uuid), + kwargs={"progress": progress}, + ) thread.start() - return self.render_progress(progress, context={ - 'instance': obj, - }, template=self.execute_progress_template) + return self.render_progress( + progress, + context={ + "instance": obj, + }, + template=self.execute_progress_template, + ) def execute_instance(self, obj, user, progress=None): """ @@ -1166,13 +1186,14 @@ class MasterView(View): config_title = self.get_config_title() # was form submitted? - if self.request.method == 'POST': + if self.request.method == "POST": # maybe just remove settings - if self.request.POST.get('remove_settings'): + if self.request.POST.get("remove_settings"): self.configure_remove_settings(session=session) - self.request.session.flash(f"All settings for {config_title} have been removed.", - 'warning') + self.request.session.flash( + f"All settings for {config_title} have been removed.", "warning" + ) # reload configure page return self.redirect(self.request.current_route_url()) @@ -1189,11 +1210,11 @@ class MasterView(View): # render configure page context = self.configure_get_context() - return self.render_to_response('configure', context) + return self.render_to_response("configure", context) def configure_get_context( - self, - simple_settings=None, + self, + simple_settings=None, ): """ Returns the full context dict, for rendering the @@ -1223,20 +1244,22 @@ class MasterView(View): for simple in simple_settings: # name - name = simple['name'] + name = simple["name"] # value - if 'value' in simple: - value = simple['value'] - elif simple.get('type') is bool: - value = self.config.get_bool(name, default=simple.get('default', False)) + if "value" in simple: + value = simple["value"] + elif simple.get("type") is bool: + value = self.config.get_bool( + name, default=simple.get("default", False) + ) else: - value = self.config.get(name, default=simple.get('default')) + value = self.config.get(name, default=simple.get("default")) normalized[name] = value # add to template context - context['simple_settings'] = normalized + context["simple_settings"] = normalized return context @@ -1281,9 +1304,9 @@ class MasterView(View): """ def configure_gather_settings( - self, - data, - simple_settings=None, + self, + data, + simple_settings=None, ): """ Collect the full set of "normalized" settings from user @@ -1332,37 +1355,36 @@ class MasterView(View): # we got some, so "normalize" each definition to name/value for simple in simple_settings: - name = simple['name'] + name = simple["name"] if name in data: value = data[name] - elif simple.get('type') is bool: + elif simple.get("type") is bool: # nb. bool false will be *missing* from data value = False else: - value = simple.get('default') + value = simple.get("default") - if simple.get('type') is bool: + if simple.get("type") is bool: value = str(bool(value)).lower() - elif simple.get('type') is int: - value = str(int(value or '0')) + elif simple.get("type") is int: + value = str(int(value or "0")) elif value is None: - value = '' + value = "" else: value = str(value) # only want to save this setting if we received a # value, or if empty values are okay to save - if value or simple.get('save_if_empty'): - settings.append({'name': name, - 'value': value}) + if value or simple.get("save_if_empty"): + settings.append({"name": name, "value": value}) return settings def configure_remove_settings( - self, - simple_settings=None, - session=None, + self, + simple_settings=None, + session=None, ): """ Remove all "known" settings from the DB; this is called by @@ -1384,8 +1406,7 @@ class MasterView(View): if simple_settings is None: simple_settings = self.configure_get_simple_settings() if simple_settings: - names.extend([simple['name'] - for simple in simple_settings]) + names.extend([simple["name"] for simple in simple_settings]) if names: # nb. must avoid self.Session here in case that does not @@ -1409,8 +1430,9 @@ class MasterView(View): # to our primary app DB session = session or self.Session() for setting in settings: - self.app.save_setting(session, setting['name'], setting['value'], - force_create=True) + self.app.save_setting( + session, setting["name"], setting["value"], force_create=True + ) ############################## # grid rendering methods @@ -1486,7 +1508,7 @@ class MasterView(View): if value is None: return - return value.strftime(fmt or '%Y-%m-%d %I:%M:%S %p') + return value.strftime(fmt or "%Y-%m-%d %I:%M:%S %p") def grid_render_enum(self, record, key, value, enum=None): """ @@ -1538,7 +1560,7 @@ class MasterView(View): if len(value) < maxlen: return value - return HTML.tag('span', title=value, c=f"{value[:maxlen]}...") + return HTML.tag("span", title=value, c=f"{value[:maxlen]}...") ############################## # support methods @@ -1576,7 +1598,7 @@ class MasterView(View): different prefix). """ permission_prefix = self.get_permission_prefix() - return self.request.has_perm(f'{permission_prefix}.{name}') + return self.request.has_perm(f"{permission_prefix}.{name}") def has_any_perm(self, *names): """ @@ -1592,12 +1614,12 @@ class MasterView(View): return False def make_button( - self, - label, - variant=None, - primary=False, - url=None, - **kwargs, + self, + label, + variant=None, + primary=False, + url=None, + **kwargs, ): """ Make and return a HTML ```` literal. @@ -1646,27 +1668,26 @@ class MasterView(View): """ btn_kw = kwargs - btn_kw.setdefault('c', label) - btn_kw.setdefault('icon_pack', 'fas') + btn_kw.setdefault("c", label) + btn_kw.setdefault("icon_pack", "fas") - if 'type' not in btn_kw: + if "type" not in btn_kw: if variant: - btn_kw['type'] = variant + btn_kw["type"] = variant elif primary: - btn_kw['type'] = 'is-primary' + btn_kw["type"] = "is-primary" if url: - btn_kw['href'] = url + btn_kw["href"] = url - button = HTML.tag('b-button', **btn_kw) + button = HTML.tag("b-button", **btn_kw) if url: # nb. unfortunately HTML.tag() calls its first arg 'tag' # and so we can't pass a kwarg with that name...so instead # we patch that into place manually button = str(button) - button = button.replace(' 0: @@ -304,8 +321,8 @@ class UpgradeView(MasterView): f.seek(offset) chunk = f.read(size) # data['stdout'] = chunk.decode('utf8').replace('\n', '
') - data['stdout'] = chunk.replace('\n', '
') - session['stdout.offset'] = offset + size + data["stdout"] = chunk.replace("\n", "
") + session["stdout.offset"] = offset + size session.save() return data @@ -313,16 +330,13 @@ class UpgradeView(MasterView): def configure_get_simple_settings(self): """ """ - script = self.config.get(f'{self.app.appname}.upgrades.command') + script = self.config.get(f"{self.app.appname}.upgrades.command") if not script: pass return [ - # basics - {'name': f'{self.app.appname}.upgrades.command', - 'default': script}, - + {"name": f"{self.app.appname}.upgrades.command", "default": script}, ] @classmethod @@ -330,7 +344,7 @@ class UpgradeView(MasterView): """ """ # nb. Upgrade may come from custom model - wutta_config = config.registry.settings['wutta_config'] + wutta_config = config.registry.settings["wutta_config"] app = wutta_config.get_app() cls.model_class = app.model.Upgrade @@ -344,18 +358,23 @@ class UpgradeView(MasterView): instance_url_prefix = cls.get_instance_url_prefix() # execution progress - config.add_route(f'{route_prefix}.execute_progress', - f'{instance_url_prefix}/execute/progress') - config.add_view(cls, attr='execute_progress', - route_name=f'{route_prefix}.execute_progress', - permission=f'{permission_prefix}.execute', - renderer='json') + config.add_route( + f"{route_prefix}.execute_progress", + f"{instance_url_prefix}/execute/progress", + ) + config.add_view( + cls, + attr="execute_progress", + route_name=f"{route_prefix}.execute_progress", + permission=f"{permission_prefix}.execute", + renderer="json", + ) def defaults(config, **kwargs): base = globals() - UpgradeView = kwargs.get('UpgradeView', base['UpgradeView']) + UpgradeView = kwargs.get("UpgradeView", base["UpgradeView"]) UpgradeView.defaults(config) diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py index aa5841e..fd2a09f 100644 --- a/src/wuttaweb/views/users.py +++ b/src/wuttaweb/views/users.py @@ -46,31 +46,32 @@ class UserView(MasterView): * ``/users/XXX/edit`` * ``/users/XXX/delete`` """ + model_class = User labels = { - 'api_tokens': "API Tokens", + "api_tokens": "API Tokens", } grid_columns = [ - 'username', - 'person', - 'active', + "username", + "person", + "active", ] filter_defaults = { - 'username': {'active': True}, - 'active': {'active': True, 'verb': 'is_true'}, + "username": {"active": True}, + "active": {"active": True, "verb": "is_true"}, } - sort_defaults = 'username' + sort_defaults = "username" form_fields = [ - 'username', - 'person', - 'active', - 'prevent_edit', - 'roles', - 'api_tokens', + "username", + "person", + "active", + "prevent_edit", + "roles", + "api_tokens", ] def get_query(self, session=None): @@ -89,23 +90,20 @@ class UserView(MasterView): model = self.app.model # never show these - g.remove('person_uuid', - 'role_refs', - 'password') + g.remove("person_uuid", "role_refs", "password") # username - g.set_link('username') + g.set_link("username") # person - g.set_link('person') - g.set_sorter('person', model.Person.full_name) - g.set_filter('person', model.Person.full_name, - label="Person Full Name") + g.set_link("person") + g.set_sorter("person", model.Person.full_name) + g.set_filter("person", model.Person.full_name, label="Person Full Name") def grid_row_class(self, user, data, i): """ """ if not user.active: - return 'has-background-warning' + return "has-background-warning" def is_editable(self, user): """ """ @@ -122,56 +120,55 @@ class UserView(MasterView): user = f.model_instance # username - f.set_validator('username', self.unique_username) + f.set_validator("username", self.unique_username) # person if self.creating or self.editing: - f.fields.insert_after('person', 'first_name') - f.set_required('first_name', False) - f.fields.insert_after('first_name', 'last_name') - f.set_required('last_name', False) - f.remove('person') + f.fields.insert_after("person", "first_name") + f.set_required("first_name", False) + f.fields.insert_after("first_name", "last_name") + f.set_required("last_name", False) + f.remove("person") if self.editing: person = user.person if person: - f.set_default('first_name', person.first_name) - f.set_default('last_name', person.last_name) + f.set_default("first_name", person.first_name) + f.set_default("last_name", person.last_name) else: - f.set_node('person', PersonRef(self.request)) + f.set_node("person", PersonRef(self.request)) # password # nb. we must avoid 'password' as field name since # ColanderAlchemy wants to handle the raw/hashed value - f.remove('password') + f.remove("password") # nb. no need for password field if readonly if self.creating or self.editing: # nb. use 'set_password' as field name - f.append('set_password') - f.set_required('set_password', False) - f.set_widget('set_password', widgets.CheckedPasswordWidget()) + f.append("set_password") + f.set_required("set_password", False) + f.set_widget("set_password", widgets.CheckedPasswordWidget()) # roles - f.append('roles') - f.set_node('roles', RoleRefs(self.request)) + f.append("roles") + f.set_node("roles", RoleRefs(self.request)) if not self.creating: - f.set_default('roles', [role.uuid.hex for role in user.roles]) + f.set_default("roles", [role.uuid.hex for role in user.roles]) # api_tokens - if self.viewing and self.has_perm('manage_api_tokens'): - f.set_grid('api_tokens', self.make_api_tokens_grid(user)) + if self.viewing and self.has_perm("manage_api_tokens"): + f.set_grid("api_tokens", self.make_api_tokens_grid(user)) else: - f.remove('api_tokens') + f.remove("api_tokens") def unique_username(self, node, value): """ """ model = self.app.model session = self.Session() - query = session.query(model.User)\ - .filter(model.User.username == value) + query = session.query(model.User).filter(model.User.username == value) if self.editing: - uuid = self.request.matchdict['uuid'] + uuid = self.request.matchdict["uuid"] query = query.filter(model.User.uuid != uuid) if query.count(): @@ -187,29 +184,34 @@ class UserView(MasterView): user = super().objectify(form) # maybe update person name - if 'first_name' in form or 'last_name' in form: - first_name = data.get('first_name') - last_name = data.get('last_name') + if "first_name" in form or "last_name" in form: + first_name = data.get("first_name") + last_name = data.get("last_name") if self.creating and (first_name or last_name): - user.person = auth.make_person(first_name=first_name, last_name=last_name) + user.person = auth.make_person( + first_name=first_name, last_name=last_name + ) elif self.editing: if first_name or last_name: if user.person: person = user.person - if 'first_name' in form: + if "first_name" in form: person.first_name = first_name - if 'last_name' in form: + if "last_name" in form: person.last_name = last_name - person.full_name = self.app.make_full_name(person.first_name, - person.last_name) + person.full_name = self.app.make_full_name( + person.first_name, person.last_name + ) else: - user.person = auth.make_person(first_name=first_name, last_name=last_name) + user.person = auth.make_person( + first_name=first_name, last_name=last_name + ) elif user.person: user.person = None # maybe set user password - if 'set_password' in form and data.get('set_password'): - auth.set_user_password(user, data['set_password']) + if "set_password" in form and data.get("set_password"): + auth.set_user_password(user, data["set_password"]) # update roles for user # TODO @@ -224,7 +226,7 @@ class UserView(MasterView): # if not self.has_perm('edit_roles'): # return data = form.validated - if 'roles' not in data: + if "roles" not in data: return model = self.app.model @@ -232,7 +234,7 @@ class UserView(MasterView): auth = self.app.get_auth_handler() old_roles = set([role.uuid for role in user.roles]) - new_roles = data['roles'] + new_roles = data["roles"] admin = auth.get_role_administrator(session) ignored = { @@ -274,33 +276,46 @@ class UserView(MasterView): model = self.app.model route_prefix = self.get_route_prefix() - grid = self.make_grid(key=f'{route_prefix}.view.api_tokens', - data=[self.normalize_api_token(t) for t in user.api_tokens], - columns=[ - 'description', - 'created', - ], - sortable=True, - sort_on_backend=False, - sort_defaults=[('created', 'desc')]) + grid = self.make_grid( + key=f"{route_prefix}.view.api_tokens", + data=[self.normalize_api_token(t) for t in user.api_tokens], + columns=[ + "description", + "created", + ], + sortable=True, + sort_on_backend=False, + sort_defaults=[("created", "desc")], + ) - if self.has_perm('manage_api_tokens'): + if self.has_perm("manage_api_tokens"): # create token - button = self.make_button("New", primary=True, icon_left='plus', **{'@click': "$emit('new-token')"}) - grid.add_tool(button, key='create') + button = self.make_button( + "New", + primary=True, + icon_left="plus", + **{"@click": "$emit('new-token')"}, + ) + grid.add_tool(button, key="create") # delete token - grid.add_action('delete', url='#', icon='trash', link_class='has-text-danger', click_handler="$emit('delete-token', props.row)") + grid.add_action( + "delete", + url="#", + icon="trash", + link_class="has-text-danger", + click_handler="$emit('delete-token', props.row)", + ) return grid def normalize_api_token(self, token): """ """ return { - 'uuid': token.uuid.hex, - 'description': token.description, - 'created': self.app.render_datetime(token.created), + "uuid": token.uuid.hex, + "description": token.description, + "created": self.app.render_datetime(token.created), } def add_api_token(self): @@ -316,13 +331,13 @@ class UserView(MasterView): user = self.get_instance() data = self.request.json_body - token = auth.add_api_token(user, data['description']) + token = auth.add_api_token(user, data["description"]) session.flush() session.refresh(token) result = self.normalize_api_token(token) - result['token_string'] = token.token_string - result['_action_url_delete'] = '#' + result["token_string"] = token.token_string + result["_action_url_delete"] = "#" return result def delete_api_token(self): @@ -339,12 +354,12 @@ class UserView(MasterView): user = self.get_instance() data = self.request.json_body - token = session.get(model.UserAPIToken, data['uuid']) + token = session.get(model.UserAPIToken, data["uuid"]) if not token: - return {'error': "API token not found"} + return {"error": "API token not found"} if token.user is not user: - return {'error': "API token not found"} + return {"error": "API token not found"} auth.delete_api_token(token) return {} @@ -354,7 +369,7 @@ class UserView(MasterView): """ """ # nb. User may come from custom model - wutta_config = config.registry.settings['wutta_config'] + wutta_config = config.registry.settings["wutta_config"] app = wutta_config.get_app() cls.model_class = app.model.User @@ -372,29 +387,41 @@ class UserView(MasterView): model_title = cls.get_model_title() # manage API tokens - config.add_wutta_permission(permission_prefix, - f'{permission_prefix}.manage_api_tokens', - f"Manage API tokens for any {model_title}") - config.add_route(f'{route_prefix}.add_api_token', - f'{instance_url_prefix}/add-api-token', - request_method='POST') - config.add_view(cls, attr='add_api_token', - route_name=f'{route_prefix}.add_api_token', - permission=f'{permission_prefix}.manage_api_tokens', - renderer='json') - config.add_route(f'{route_prefix}.delete_api_token', - f'{instance_url_prefix}/delete-api-token', - request_method='POST') - config.add_view(cls, attr='delete_api_token', - route_name=f'{route_prefix}.delete_api_token', - permission=f'{permission_prefix}.manage_api_tokens', - renderer='json') + config.add_wutta_permission( + permission_prefix, + f"{permission_prefix}.manage_api_tokens", + f"Manage API tokens for any {model_title}", + ) + config.add_route( + f"{route_prefix}.add_api_token", + f"{instance_url_prefix}/add-api-token", + request_method="POST", + ) + config.add_view( + cls, + attr="add_api_token", + route_name=f"{route_prefix}.add_api_token", + permission=f"{permission_prefix}.manage_api_tokens", + renderer="json", + ) + config.add_route( + f"{route_prefix}.delete_api_token", + f"{instance_url_prefix}/delete-api-token", + request_method="POST", + ) + config.add_view( + cls, + attr="delete_api_token", + route_name=f"{route_prefix}.delete_api_token", + permission=f"{permission_prefix}.manage_api_tokens", + renderer="json", + ) def defaults(config, **kwargs): base = globals() - UserView = kwargs.get('UserView', base['UserView']) + UserView = kwargs.get("UserView", base["UserView"]) UserView.defaults(config) diff --git a/tasks.py b/tasks.py index 4635a82..0189843 100644 --- a/tasks.py +++ b/tasks.py @@ -15,14 +15,14 @@ def release(c, skip_tests=False): Release a new version of WuttJamaican """ if not skip_tests: - c.run('pytest') + c.run("pytest") # rebuild pkg - if os.path.exists('dist'): - shutil.rmtree('dist') - if os.path.exists('WuttJamaican.egg-info'): - shutil.rmtree('WuttJamaican.egg-info') - c.run('python -m build --sdist') + if os.path.exists("dist"): + shutil.rmtree("dist") + if os.path.exists("WuttJamaican.egg-info"): + shutil.rmtree("WuttJamaican.egg-info") + c.run("python -m build --sdist") # upload - c.run('twine upload dist/*') + c.run("twine upload dist/*") diff --git a/tests/cli/test_webapp.py b/tests/cli/test_webapp.py index 2688912..7f473a5 100644 --- a/tests/cli/test_webapp.py +++ b/tests/cli/test_webapp.py @@ -10,8 +10,8 @@ from wuttaweb.cli import webapp as mod class TestWebapp(ConfigTestCase): def make_context(self, **kwargs): - params = {'auto_reload': False} - params.update(kwargs.get('params', {})) + params = {"auto_reload": False} + params.update(kwargs.get("params", {})) ctx = MagicMock(params=params) ctx.parent.wutta_config = self.config return ctx @@ -19,7 +19,7 @@ class TestWebapp(ConfigTestCase): def test_missing_config_file(self): # nb. our default config has no files, so can test w/ that ctx = self.make_context() - with patch.object(mod, 'sys') as sys: + with patch.object(mod, "sys") as sys: sys.exit.side_effect = RuntimeError self.assertRaises(RuntimeError, mod.webapp, ctx) sys.stderr.write.assert_called_once_with("no config files found!\n") @@ -28,14 +28,17 @@ class TestWebapp(ConfigTestCase): def test_invalid_runner(self): # make new config from file, with bad setting - path = self.write_file('my.conf', """ + path = self.write_file( + "my.conf", + """ [wutta.web] app.runner = bogus -""") +""", + ) self.config = self.make_config(files=[path]) ctx = self.make_context() - with patch.object(mod, 'sys') as sys: + with patch.object(mod, "sys") as sys: sys.exit.side_effect = RuntimeError self.assertRaises(RuntimeError, mod.webapp, ctx) sys.stderr.write.assert_called_once_with("unknown web app runner: bogus\n") @@ -43,64 +46,76 @@ app.runner = bogus def test_pserve(self): - path = self.write_file('my.conf', """ + path = self.write_file( + "my.conf", + """ [wutta.web] app.runner = pserve -""") +""", + ) self.config = self.make_config(files=[path]) # normal - with patch.object(mod, 'pserve') as pserve: + with patch.object(mod, "pserve") as pserve: ctx = self.make_context() mod.webapp(ctx) - pserve.main.assert_called_once_with(argv=['pserve', f'file+ini:{path}']) + pserve.main.assert_called_once_with(argv=["pserve", f"file+ini:{path}"]) # with reload - with patch.object(mod, 'pserve') as pserve: - ctx = self.make_context(params={'auto_reload': True}) + with patch.object(mod, "pserve") as pserve: + ctx = self.make_context(params={"auto_reload": True}) mod.webapp(ctx) - pserve.main.assert_called_once_with(argv=['pserve', f'file+ini:{path}', '--reload']) + pserve.main.assert_called_once_with( + argv=["pserve", f"file+ini:{path}", "--reload"] + ) def test_uvicorn(self): - path = self.write_file('my.conf', """ + path = self.write_file( + "my.conf", + """ [wutta.web] app.runner = uvicorn app.spec = wuttaweb.app:make_wsgi_app -""") +""", + ) self.config = self.make_config(files=[path]) orig_import = __import__ uvicorn = MagicMock() def mock_import(name, *args, **kwargs): - if name == 'uvicorn': + if name == "uvicorn": return uvicorn return orig_import(name, *args, **kwargs) # normal - with patch('builtins.__import__', side_effect=mock_import): + with patch("builtins.__import__", side_effect=mock_import): ctx = self.make_context() mod.webapp(ctx) - uvicorn.run.assert_called_once_with('wuttaweb.app:make_wsgi_app', - host='127.0.0.1', - port=8000, - reload=False, - reload_dirs=None, - factory=False, - interface='auto', - root_path='') + uvicorn.run.assert_called_once_with( + "wuttaweb.app:make_wsgi_app", + host="127.0.0.1", + port=8000, + reload=False, + reload_dirs=None, + factory=False, + interface="auto", + root_path="", + ) # with reload uvicorn.run.reset_mock() - with patch('builtins.__import__', side_effect=mock_import): - ctx = self.make_context(params={'auto_reload': True}) + with patch("builtins.__import__", side_effect=mock_import): + ctx = self.make_context(params={"auto_reload": True}) mod.webapp(ctx) - uvicorn.run.assert_called_once_with('wuttaweb.app:make_wsgi_app', - host='127.0.0.1', - port=8000, - reload=True, - reload_dirs=None, - factory=False, - interface='auto', - root_path='') + uvicorn.run.assert_called_once_with( + "wuttaweb.app:make_wsgi_app", + host="127.0.0.1", + port=8000, + reload=True, + reload_dirs=None, + factory=False, + interface="auto", + root_path="", + ) diff --git a/tests/db/test_continuum.py b/tests/db/test_continuum.py index 0503fd1..b564181 100644 --- a/tests/db/test_continuum.py +++ b/tests/db/test_continuum.py @@ -11,7 +11,7 @@ from wuttaweb.testing import WebTestCase class TestWuttaWebContinuumPlugin(WebTestCase): def setUp(self): - if not hasattr(mod, 'WuttaWebContinuumPlugin'): + if not hasattr(mod, "WuttaWebContinuumPlugin"): pytest.skip("test not relevant without sqlalchemy-continuum") self.setup_web() @@ -21,17 +21,17 @@ class TestWuttaWebContinuumPlugin(WebTestCase): def test_get_remote_addr(self): plugin = self.make_plugin() - with patch.object(mod, 'get_current_request', return_value=None): + with patch.object(mod, "get_current_request", return_value=None): self.assertIsNone(plugin.get_remote_addr(None, self.session)) - self.request.client_addr = '127.0.0.1' - self.assertEqual(plugin.get_remote_addr(None, self.session), '127.0.0.1') + self.request.client_addr = "127.0.0.1" + self.assertEqual(plugin.get_remote_addr(None, self.session), "127.0.0.1") def test_get_user_id(self): plugin = self.make_plugin() - with patch.object(mod, 'get_current_request', return_value=None): + with patch.object(mod, "get_current_request", return_value=None): self.assertIsNone(plugin.get_user_id(None, self.session)) - self.request.user = MagicMock(uuid='some-random-uuid') - self.assertEqual(plugin.get_user_id(None, self.session), 'some-random-uuid') + self.request.user = MagicMock(uuid="some-random-uuid") + self.assertEqual(plugin.get_user_id(None, self.session), "some-random-uuid") diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py index 7b14088..eaa2286 100644 --- a/tests/forms/test_base.py +++ b/tests/forms/test_base.py @@ -18,17 +18,22 @@ from wuttaweb.grids import Grid class TestForm(TestCase): def setUp(self): - self.config = WuttaConfig(defaults={ - 'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler', - }) + self.config = WuttaConfig( + defaults={ + "wutta.web.menus.handler_spec": "tests.util:NullMenuHandler", + } + ) self.app = self.config.get_app() self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False) - self.pyramid_config = testing.setUp(request=self.request, settings={ - 'wutta_config': self.config, - 'mako.directories': ['wuttaweb:templates'], - 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform', - }) + self.pyramid_config = testing.setUp( + request=self.request, + settings={ + "wutta_config": self.config, + "mako.directories": ["wuttaweb:templates"], + "pyramid_deform.template_search_path": "wuttaweb:templates/deform", + }, + ) event = MagicMock(request=self.request) subscribers.new_request(event) @@ -40,12 +45,12 @@ class TestForm(TestCase): return base.Form(self.request, **kwargs) def make_schema(self): - schema = colander.Schema(children=[ - colander.SchemaNode(colander.String(), - name='foo'), - colander.SchemaNode(colander.String(), - name='bar'), - ]) + schema = colander.Schema( + children=[ + colander.SchemaNode(colander.String(), name="foo"), + colander.SchemaNode(colander.String(), name="bar"), + ] + ) return schema def test_init_with_none(self): @@ -53,126 +58,126 @@ class TestForm(TestCase): self.assertEqual(form.fields, []) def test_init_with_fields(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertEqual(form.fields, ['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) + self.assertEqual(form.fields, ["foo", "bar"]) def test_init_with_schema(self): schema = self.make_schema() form = self.make_form(schema=schema) - self.assertEqual(form.fields, ['foo', 'bar']) + self.assertEqual(form.fields, ["foo", "bar"]) def test_vue_tagname(self): form = self.make_form() - self.assertEqual(form.vue_tagname, 'wutta-form') + self.assertEqual(form.vue_tagname, "wutta-form") def test_vue_component(self): form = self.make_form() - self.assertEqual(form.vue_component, 'WuttaForm') + self.assertEqual(form.vue_component, "WuttaForm") def test_contains(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertIn('foo', form) - self.assertNotIn('baz', form) + form = self.make_form(fields=["foo", "bar"]) + self.assertIn("foo", form) + self.assertNotIn("baz", form) def test_iter(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) fields = list(iter(form)) - self.assertEqual(fields, ['foo', 'bar']) + self.assertEqual(fields, ["foo", "bar"]) fields = [] for field in form: fields.append(field) - self.assertEqual(fields, ['foo', 'bar']) + self.assertEqual(fields, ["foo", "bar"]) def test_set_fields(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertEqual(form.fields, ['foo', 'bar']) - form.set_fields(['baz']) - self.assertEqual(form.fields, ['baz']) + form = self.make_form(fields=["foo", "bar"]) + self.assertEqual(form.fields, ["foo", "bar"]) + form.set_fields(["baz"]) + self.assertEqual(form.fields, ["baz"]) def test_append(self): - form = self.make_form(fields=['one', 'two']) - self.assertEqual(form.fields, ['one', 'two']) - form.append('one', 'two', 'three') - self.assertEqual(form.fields, ['one', 'two', 'three']) + form = self.make_form(fields=["one", "two"]) + self.assertEqual(form.fields, ["one", "two"]) + form.append("one", "two", "three") + self.assertEqual(form.fields, ["one", "two", "three"]) def test_remove(self): - form = self.make_form(fields=['one', 'two', 'three', 'four']) - self.assertEqual(form.fields, ['one', 'two', 'three', 'four']) - form.remove('two', 'three') - self.assertEqual(form.fields, ['one', 'four']) + form = self.make_form(fields=["one", "two", "three", "four"]) + self.assertEqual(form.fields, ["one", "two", "three", "four"]) + form.remove("two", "three") + self.assertEqual(form.fields, ["one", "four"]) def test_set_node(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.nodes, {}) # complete node - node = colander.SchemaNode(colander.Bool(), name='foo') - form.set_node('foo', node) - self.assertIs(form.nodes['foo'], node) + node = colander.SchemaNode(colander.Bool(), name="foo") + form.set_node("foo", node) + self.assertIs(form.nodes["foo"], node) # type only typ = colander.Bool() - form.set_node('foo', typ) - node = form.nodes['foo'] + form.set_node("foo", typ) + node = form.nodes["foo"] self.assertIsInstance(node, colander.SchemaNode) self.assertIsInstance(node.typ, colander.Bool) - self.assertEqual(node.name, 'foo') + self.assertEqual(node.name, "foo") # schema is updated if already present schema = form.get_schema() self.assertIsNotNone(schema) typ = colander.Date() - form.set_node('foo', typ) - node = form.nodes['foo'] + form.set_node("foo", typ) + node = form.nodes["foo"] self.assertIsInstance(node, colander.SchemaNode) self.assertIsInstance(node.typ, colander.Date) - self.assertEqual(node.name, 'foo') + self.assertEqual(node.name, "foo") def test_set_widget(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.widgets, {}) # basic widget = widgets.SelectWidget() - form.set_widget('foo', widget) - self.assertIs(form.widgets['foo'], widget) + form.set_widget("foo", widget) + self.assertIs(form.widgets["foo"], widget) # schema is updated if already present schema = form.get_schema() self.assertIsNotNone(schema) - self.assertIs(schema['foo'].widget, widget) + self.assertIs(schema["foo"].widget, widget) new_widget = widgets.TextInputWidget() - form.set_widget('foo', new_widget) - self.assertIs(form.widgets['foo'], new_widget) - self.assertIs(schema['foo'].widget, new_widget) + form.set_widget("foo", new_widget) + self.assertIs(form.widgets["foo"], new_widget) + self.assertIs(schema["foo"].widget, new_widget) # can also just specify widget pseudo-type (invalid) - self.assertNotIn('bar', form.widgets) - self.assertRaises(ValueError, form.set_widget, 'bar', 'ldjfadjfadj') + self.assertNotIn("bar", form.widgets) + self.assertRaises(ValueError, form.set_widget, "bar", "ldjfadjfadj") # can also just specify widget pseudo-type (valid) - self.assertNotIn('bar', form.widgets) - form.set_widget('bar', 'notes') - self.assertIsInstance(form.widgets['bar'], widgets.NotesWidget) + self.assertNotIn("bar", form.widgets) + form.set_widget("bar", "notes") + self.assertIsInstance(form.widgets["bar"], widgets.NotesWidget) def test_make_widget(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) # notes - widget = form.make_widget('notes') + widget = form.make_widget("notes") self.assertIsInstance(widget, widgets.NotesWidget) # invalid - widget = form.make_widget('fdajvdafjjf') + widget = form.make_widget("fdajvdafjjf") self.assertIsNone(widget) def test_set_default_widgets(self): model = self.app.model # no defaults for "plain" schema - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.widgets, {}) # no defaults for "plain" mapped class @@ -184,52 +189,56 @@ class TestForm(TestCase): # widget set for datetime mapped field form = self.make_form(model_class=model.Upgrade) - self.assertIn('created', form.widgets) - self.assertIsNot(form.widgets['created'], MyWidget) - self.assertNotIsInstance(form.widgets['created'], MyWidget) + self.assertIn("created", form.widgets) + self.assertIsNot(form.widgets["created"], MyWidget) + self.assertNotIsInstance(form.widgets["created"], MyWidget) # widget *not* set for datetime, if override present - form = self.make_form(model_class=model.Upgrade, - widgets={'created': MyWidget()}) - self.assertIn('created', form.widgets) - self.assertIsInstance(form.widgets['created'], MyWidget) + form = self.make_form( + model_class=model.Upgrade, widgets={"created": MyWidget()} + ) + self.assertIn("created", form.widgets) + self.assertIsInstance(form.widgets["created"], MyWidget) # mock up a table with all relevant column types class Whatever(model.Base): - __tablename__ = 'whatever' + __tablename__ = "whatever" id = sa.Column(sa.Integer(), primary_key=True) date = sa.Column(sa.Date()) date_time = sa.Column(sa.DateTime()) # widget set for all known types form = self.make_form(model_class=Whatever) - self.assertIsInstance(form.widgets['date'], widgets.WuttaDateWidget) - self.assertIsInstance(form.widgets['date_time'], widgets.WuttaDateTimeWidget) + self.assertIsInstance(form.widgets["date"], widgets.WuttaDateWidget) + self.assertIsInstance(form.widgets["date_time"], widgets.WuttaDateTimeWidget) def test_set_grid(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertNotIn('foo', form.widgets) - self.assertNotIn('foogrid', form.grid_vue_context) + form = self.make_form(fields=["foo", "bar"]) + self.assertNotIn("foo", form.widgets) + self.assertNotIn("foogrid", form.grid_vue_context) - grid = Grid(self.request, key='foogrid', - columns=['a', 'b'], - data=[{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]) + grid = Grid( + self.request, + key="foogrid", + columns=["a", "b"], + data=[{"a": 1, "b": 2}, {"a": 3, "b": 4}], + ) - form.set_grid('foo', grid) - self.assertIn('foo', form.widgets) - self.assertIsInstance(form.widgets['foo'], widgets.GridWidget) - self.assertIn('foogrid', form.grid_vue_context) + form.set_grid("foo", grid) + self.assertIn("foo", form.widgets) + self.assertIsInstance(form.widgets["foo"], widgets.GridWidget) + self.assertIn("foogrid", form.grid_vue_context) def test_set_validator(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.validators, {}) def validate1(node, value): pass # basic - form.set_validator('foo', validate1) - self.assertIs(form.validators['foo'], validate1) + form.set_validator("foo", validate1) + self.assertIs(form.validators["foo"], validate1) def validate2(node, value): pass @@ -237,18 +246,18 @@ class TestForm(TestCase): # schema is updated if already present schema = form.get_schema() self.assertIsNotNone(schema) - self.assertIs(schema['foo'].validator, validate1) - form.set_validator('foo', validate2) - self.assertIs(form.validators['foo'], validate2) - self.assertIs(schema['foo'].validator, validate2) + self.assertIs(schema["foo"].validator, validate1) + form.set_validator("foo", validate2) + self.assertIs(form.validators["foo"], validate2) + self.assertIs(schema["foo"].validator, validate2) def test_set_default(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.defaults, {}) # basic - form.set_default('foo', 42) - self.assertEqual(form.defaults['foo'], 42) + form.set_default("foo", 42) + self.assertEqual(form.defaults["foo"], 42) def test_get_schema(self): model = self.app.model @@ -262,10 +271,10 @@ class TestForm(TestCase): self.assertIs(form.get_schema(), schema) # schema is auto-generated if fields provided - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) schema = form.get_schema() self.assertEqual(len(schema.children), 2) - self.assertEqual(schema['foo'].name, 'foo') + self.assertEqual(schema["foo"].name, "foo") # but auto-generating without fields is not supported form = self.make_form() @@ -276,58 +285,62 @@ class TestForm(TestCase): form = self.make_form(model_class=model.Setting) schema = form.get_schema() self.assertEqual(len(schema.children), 2) - self.assertIn('name', schema) - self.assertIn('value', schema) + self.assertIn("name", schema) + self.assertIn("value", schema) # but node overrides are honored when auto-generating form = self.make_form(model_class=model.Setting) - value_node = colander.SchemaNode(colander.Bool(), name='value') - form.set_node('value', value_node) + value_node = colander.SchemaNode(colander.Bool(), name="value") + form.set_node("value", value_node) schema = form.get_schema() - self.assertIs(schema['value'], value_node) + self.assertIs(schema["value"], value_node) # schema is auto-generated if model_instance provided - form = self.make_form(model_instance=model.Setting(name='uhoh')) - self.assertEqual(form.fields, ['name', 'value']) + form = self.make_form(model_instance=model.Setting(name="uhoh")) + self.assertEqual(form.fields, ["name", "value"]) self.assertIsNone(form.schema) # nb. force method to get new fields del form.fields schema = form.get_schema() self.assertEqual(len(schema.children), 2) - self.assertIn('name', schema) - self.assertIn('value', schema) + self.assertIn("name", schema) + self.assertIn("value", schema) # ColanderAlchemy schema still has *all* requested fields - form = self.make_form(model_instance=model.Setting(name='uhoh'), - fields=['name', 'value', 'foo', 'bar']) - self.assertEqual(form.fields, ['name', 'value', 'foo', 'bar']) + form = self.make_form( + model_instance=model.Setting(name="uhoh"), + fields=["name", "value", "foo", "bar"], + ) + self.assertEqual(form.fields, ["name", "value", "foo", "bar"]) self.assertIsNone(form.schema) schema = form.get_schema() self.assertEqual(len(schema.children), 4) - self.assertIn('name', schema) - self.assertIn('value', schema) - self.assertIn('foo', schema) - self.assertIn('bar', schema) + self.assertIn("name", schema) + self.assertIn("value", schema) + self.assertIn("foo", schema) + self.assertIn("bar", schema) # schema nodes are required by default - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) schema = form.get_schema() - self.assertIs(schema['foo'].missing, colander.required) - self.assertIs(schema['bar'].missing, colander.required) + self.assertIs(schema["foo"].missing, colander.required) + self.assertIs(schema["bar"].missing, colander.required) # but fields can be marked *not* required - form = self.make_form(fields=['foo', 'bar']) - form.set_required('bar', False) + form = self.make_form(fields=["foo", "bar"]) + form.set_required("bar", False) schema = form.get_schema() - self.assertIs(schema['foo'].missing, colander.required) - self.assertIs(schema['bar'].missing, colander.null) + self.assertIs(schema["foo"].missing, colander.required) + self.assertIs(schema["bar"].missing, colander.null) # validator overrides are honored - def validate(node, value): pass + def validate(node, value): + pass + form = self.make_form(model_class=model.Setting) - form.set_validator('name', validate) + form.set_validator("name", validate) schema = form.get_schema() - self.assertIs(schema['name'].validator, validate) + self.assertIs(schema["name"].validator, validate) # validator can be set for whole form form = self.make_form(model_class=model.Setting) @@ -340,9 +353,9 @@ class TestForm(TestCase): # default value overrides are honored form = self.make_form(model_class=model.Setting) - form.set_default('name', 'foo') + form.set_default("name", "foo") schema = form.get_schema() - self.assertEqual(schema['name'].default, 'foo') + self.assertEqual(schema["name"].default, "foo") def test_get_deform(self): model = self.app.model @@ -350,139 +363,142 @@ class TestForm(TestCase): # basic form = self.make_form(schema=schema) - self.assertFalse(hasattr(form, 'deform_form')) + self.assertFalse(hasattr(form, "deform_form")) dform = form.get_deform() self.assertIsInstance(dform, deform.Form) self.assertIs(form.deform_form, dform) # with model instance as dict - myobj = {'foo': 'one', 'bar': 'two'} + myobj = {"foo": "one", "bar": "two"} form = self.make_form(schema=schema, model_instance=myobj) dform = form.get_deform() self.assertEqual(dform.cstruct, myobj) # with sqlalchemy model instance - myobj = model.Setting(name='foo', value='bar') + myobj = model.Setting(name="foo", value="bar") form = self.make_form(model_instance=myobj) dform = form.get_deform() - self.assertEqual(dform.cstruct, {'name': 'foo', 'value': 'bar'}) + self.assertEqual(dform.cstruct, {"name": "foo", "value": "bar"}) # sqlalchemy instance with null value - myobj = model.Setting(name='foo', value=None) + myobj = model.Setting(name="foo", value=None) form = self.make_form(model_instance=myobj) dform = form.get_deform() - self.assertEqual(dform.cstruct, {'name': 'foo', 'value': colander.null}) + self.assertEqual(dform.cstruct, {"name": "foo", "value": colander.null}) def test_get_cancel_url(self): # is referrer by default form = self.make_form() - self.request.get_referrer = MagicMock(return_value='/cancel-default') - self.assertEqual(form.get_cancel_url(), '/cancel-default') + self.request.get_referrer = MagicMock(return_value="/cancel-default") + self.assertEqual(form.get_cancel_url(), "/cancel-default") del self.request.get_referrer # or can be static URL - form = self.make_form(cancel_url='/cancel-static') - self.assertEqual(form.get_cancel_url(), '/cancel-static') + form = self.make_form(cancel_url="/cancel-static") + self.assertEqual(form.get_cancel_url(), "/cancel-static") # or can be fallback URL (nb. 'NOPE' indicates no referrer) - form = self.make_form(cancel_url_fallback='/cancel-fallback') - self.request.get_referrer = MagicMock(return_value='NOPE') - self.assertEqual(form.get_cancel_url(), '/cancel-fallback') + form = self.make_form(cancel_url_fallback="/cancel-fallback") + self.request.get_referrer = MagicMock(return_value="NOPE") + self.assertEqual(form.get_cancel_url(), "/cancel-fallback") del self.request.get_referrer # or can be referrer fallback, i.e. home page form = self.make_form() + def get_referrer(default=None): - if default == 'NOPE': - return 'NOPE' - return '/home-page' + if default == "NOPE": + return "NOPE" + return "/home-page" + self.request.get_referrer = get_referrer - self.assertEqual(form.get_cancel_url(), '/home-page') + self.assertEqual(form.get_cancel_url(), "/home-page") del self.request.get_referrer def test_get_label(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertEqual(form.get_label('foo'), "Foo") - form.set_label('foo', "Baz") - self.assertEqual(form.get_label('foo'), "Baz") + form = self.make_form(fields=["foo", "bar"]) + self.assertEqual(form.get_label("foo"), "Foo") + form.set_label("foo", "Baz") + self.assertEqual(form.get_label("foo"), "Baz") def test_set_label(self): - form = self.make_form(fields=['foo', 'bar']) - self.assertEqual(form.get_label('foo'), "Foo") - form.set_label('foo', "Baz") - self.assertEqual(form.get_label('foo'), "Baz") + form = self.make_form(fields=["foo", "bar"]) + self.assertEqual(form.get_label("foo"), "Foo") + form.set_label("foo", "Baz") + self.assertEqual(form.get_label("foo"), "Baz") # schema should be updated when setting label schema = self.make_schema() form = self.make_form(schema=schema) - form.set_label('foo', "Woohoo") - self.assertEqual(form.get_label('foo'), "Woohoo") - self.assertEqual(schema['foo'].title, "Woohoo") + form.set_label("foo", "Woohoo") + self.assertEqual(form.get_label("foo"), "Woohoo") + self.assertEqual(schema["foo"].title, "Woohoo") def test_readonly_fields(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.readonly_fields, set()) - self.assertFalse(form.is_readonly('foo')) + self.assertFalse(form.is_readonly("foo")) - form.set_readonly('foo') - self.assertEqual(form.readonly_fields, {'foo'}) - self.assertTrue(form.is_readonly('foo')) - self.assertFalse(form.is_readonly('bar')) + form.set_readonly("foo") + self.assertEqual(form.readonly_fields, {"foo"}) + self.assertTrue(form.is_readonly("foo")) + self.assertFalse(form.is_readonly("bar")) - form.set_readonly('bar') - self.assertEqual(form.readonly_fields, {'foo', 'bar'}) - self.assertTrue(form.is_readonly('foo')) - self.assertTrue(form.is_readonly('bar')) + form.set_readonly("bar") + self.assertEqual(form.readonly_fields, {"foo", "bar"}) + self.assertTrue(form.is_readonly("foo")) + self.assertTrue(form.is_readonly("bar")) - form.set_readonly('foo', False) - self.assertEqual(form.readonly_fields, {'bar'}) - self.assertFalse(form.is_readonly('foo')) - self.assertTrue(form.is_readonly('bar')) + form.set_readonly("foo", False) + self.assertEqual(form.readonly_fields, {"bar"}) + self.assertFalse(form.is_readonly("foo")) + self.assertTrue(form.is_readonly("bar")) def test_required_fields(self): - form = self.make_form(fields=['foo', 'bar']) + form = self.make_form(fields=["foo", "bar"]) self.assertEqual(form.required_fields, {}) - self.assertIsNone(form.is_required('foo')) + self.assertIsNone(form.is_required("foo")) - form.set_required('foo') - self.assertEqual(form.required_fields, {'foo': True}) - self.assertTrue(form.is_required('foo')) - self.assertIsNone(form.is_required('bar')) + form.set_required("foo") + self.assertEqual(form.required_fields, {"foo": True}) + self.assertTrue(form.is_required("foo")) + self.assertIsNone(form.is_required("bar")) - form.set_required('bar') - self.assertEqual(form.required_fields, {'foo': True, 'bar': True}) - self.assertTrue(form.is_required('foo')) - self.assertTrue(form.is_required('bar')) + form.set_required("bar") + self.assertEqual(form.required_fields, {"foo": True, "bar": True}) + self.assertTrue(form.is_required("foo")) + self.assertTrue(form.is_required("bar")) - form.set_required('foo', False) - self.assertEqual(form.required_fields, {'foo': False, 'bar': True}) - self.assertFalse(form.is_required('foo')) - self.assertTrue(form.is_required('bar')) + form.set_required("foo", False) + self.assertEqual(form.required_fields, {"foo": False, "bar": True}) + self.assertFalse(form.is_required("foo")) + self.assertTrue(form.is_required("bar")) def test_render_vue_tag(self): schema = self.make_schema() form = self.make_form(schema=schema) html = form.render_vue_tag() - self.assertEqual(html, '') + self.assertEqual(html, "") def test_render_vue_template(self): - self.pyramid_config.include('pyramid_mako') - self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render', - 'pyramid.events.BeforeRender') + self.pyramid_config.include("pyramid_mako") + self.pyramid_config.add_subscriber( + "wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender" + ) # form button is disabled on @submit by default schema = self.make_schema() - form = self.make_form(schema=schema, cancel_url='/') + form = self.make_form(schema=schema, cancel_url="/") html = form.render_vue_template() self.assertIn('