diff --git a/docs/api/wuttaweb/index.rst b/docs/api/wuttaweb/index.rst
index 739935b..7299034 100644
--- a/docs/api/wuttaweb/index.rst
+++ b/docs/api/wuttaweb/index.rst
@@ -20,6 +20,7 @@
handler
helpers
menus
+ progress
static
subscribers
util
@@ -30,6 +31,7 @@
views.essential
views.master
views.people
+ views.progress
views.roles
views.settings
views.upgrades
diff --git a/docs/api/wuttaweb/progress.rst b/docs/api/wuttaweb/progress.rst
new file mode 100644
index 0000000..498d641
--- /dev/null
+++ b/docs/api/wuttaweb/progress.rst
@@ -0,0 +1,6 @@
+
+``wuttaweb.progress``
+=====================
+
+.. automodule:: wuttaweb.progress
+ :members:
diff --git a/docs/api/wuttaweb/views.people.rst b/docs/api/wuttaweb/views.people.rst
index 89c6883..2dc919b 100644
--- a/docs/api/wuttaweb/views.people.rst
+++ b/docs/api/wuttaweb/views.people.rst
@@ -1,6 +1,6 @@
``wuttaweb.views.people``
-===========================
+=========================
.. automodule:: wuttaweb.views.people
:members:
diff --git a/docs/api/wuttaweb/views.progress.rst b/docs/api/wuttaweb/views.progress.rst
new file mode 100644
index 0000000..34e2661
--- /dev/null
+++ b/docs/api/wuttaweb/views.progress.rst
@@ -0,0 +1,6 @@
+
+``wuttaweb.views.progress``
+===========================
+
+.. automodule:: wuttaweb.views.progress
+ :members:
diff --git a/src/wuttaweb/progress.py b/src/wuttaweb/progress.py
new file mode 100644
index 0000000..759c2da
--- /dev/null
+++ b/src/wuttaweb/progress.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# wuttaweb -- Web App for Wutta Framework
+# Copyright © 2024 Lance Edgar
+#
+# This file is part of Wutta Framework.
+#
+# Wutta Framework is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# Wutta Framework is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Wutta Framework. If not, see .
+#
+################################################################################
+"""
+Progress Indicators
+"""
+
+from wuttjamaican.progress import ProgressBase
+
+from beaker.session import Session as BeakerSession
+
+
+def get_basic_session(request, **kwargs):
+ """
+ Create/get a "basic" Beaker session object.
+ """
+ kwargs['use_cookies'] = False
+ return BeakerSession(request, **kwargs)
+
+
+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}'
+ return get_basic_session(request, **kwargs)
+
+
+class SessionProgress(ProgressBase):
+ """
+ Progress indicator which uses Beaker session storage to track
+ current status.
+
+ This is a subclass of
+ :class:`wuttjamaican:wuttjamaican.progress.ProgressBase`.
+
+ A view callable can create one of these, and then pass it into
+ :meth:`~wuttjamaican.app.AppHandler.progress_loop()` or similar.
+
+ As the loop updates progress along the way, this indicator will
+ update the Beaker session to match.
+
+ Separately then, the client side can send requests for the
+ :func:`~wuttaweb.views.progress.progress()` view, to fetch current
+ status out of the Beaker session.
+
+ :param request: Current :term:`request` object.
+
+ :param key: Unique key for this progress indicator. Used to
+ distinguish progress indicators in the Beaker session.
+
+ Note that in addition to
+ :meth:`~wuttjamaican:wuttjamaican.progress.ProgressBase.update()`
+ and
+ :meth:`~wuttjamaican:wuttjamaican.progress.ProgressBase.finish()`
+ this progres class has some extra attributes and methods:
+
+ .. attribute:: success_msg
+
+ Optional message to display to the user (via session flash)
+ when the operation completes successfully.
+
+ .. attribute:: success_url
+
+ URL to which user should be redirected, once the operation
+ completes.
+
+ .. attribute:: error_url
+
+ URL to which user should be redirected, if the operation
+ encounters an error. If not specified, will fall back to
+ :attr:`success_url`.
+ """
+
+ def __init__(self, request, key, success_msg=None, success_url=None, error_url=None):
+ self.key = key
+ self.success_msg = success_msg
+ self.success_url = success_url
+ self.error_url = error_url or self.success_url
+ self.session = get_progress_session(request, key)
+ self.clear()
+
+ 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.save()
+ return self
+
+ def clear(self):
+ """ """
+ self.session.clear()
+ self.session['complete'] = False
+ self.session['error'] = False
+ self.session.save()
+
+ def update(self, value):
+ """ """
+ self.session.load()
+ self.session['value'] = value
+ self.session.save()
+
+ def handle_error(self, error, error_url=None):
+ """
+ This should be called by the view code, within a try/catch
+ block upon error.
+
+ The session storage will be updated to reflect details of the
+ error. Next time client requests the progress status it will
+ learn of the error and redirect the user.
+
+ :param error: :class:`python:Exception` instance.
+
+ :param error_url: Optional redirect URL; if not specified
+ :attr:`error_url` is used.
+ """
+ self.session.load()
+ self.session['error'] = True
+ self.session['error_msg'] = str(error)
+ self.session['error_url'] = error_url or self.error_url
+ self.session.save()
+
+ def handle_success(self, success_msg=None, success_url=None):
+ """
+ This should be called by the view code, when the long-running
+ operation completes.
+
+ The session storage will be updated to reflect the completed
+ status. Next time client requests the progress status it will
+ discover it has completed, and redirect the user.
+
+ :param success_msg: Optional message to display to the user
+ (via session flash) when the operation completes
+ successfully. If not specified :attr:`success_msg` (or
+ nothing) is used
+
+ :param success_url: Optional redirect URL; if not specified
+ :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.save()
diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako
index a85bc2d..3b3f115 100644
--- a/src/wuttaweb/templates/base.mako
+++ b/src/wuttaweb/templates/base.mako
@@ -3,13 +3,7 @@
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
-
-
- ${base_meta.global_title()} » ${capture(self.title)|n}
- ${base_meta.favicon()}
- ${self.header_core()}
- ${self.head_tags()}
-
+ ${self.html_head()}
@@ -30,7 +24,20 @@
-## nb. this becomes part of the page
tag within
+<%def name="html_head()">
+
+
+ ${self.head_title()}
+ ${base_meta.favicon()}
+ ${self.header_core()}
+ ${self.head_tags()}
+
+%def>
+
+## nb. this is the full within html
+<%def name="head_title()">${base_meta.global_title()} » ${self.title()}%def>
+
+## nb. this becomes part of head_title() above
## it also is used as default value for content_title() below
<%def name="title()">%def>
@@ -39,9 +46,9 @@
<%def name="content_title()">${self.title()}%def>
<%def name="header_core()">
- ${self.core_javascript()}
+ ${self.base_javascript()}
${self.extra_javascript()}
- ${self.core_styles()}
+ ${self.base_styles()}
${self.extra_styles()}
%def>
@@ -49,6 +56,10 @@
${self.vuejs()}
${self.buefy()}
${self.fontawesome()}
+%def>
+
+<%def name="base_javascript()">
+ ${self.core_javascript()}
${self.hamburger_menu_js()}
%def>
@@ -99,7 +110,6 @@
<%def name="core_styles()">
${self.buefy_styles()}
- ${self.base_styles()}
%def>
<%def name="buefy_styles()">
@@ -107,6 +117,7 @@
%def>
<%def name="base_styles()">
+ ${self.core_styles()}