feat: add basic progress page/indicator support
so far "delete results" (for Raw Settings) is the only use case. user cancel is not yet supported
This commit is contained in:
parent
6fa8b0aeaa
commit
1a8900c9f4
13 changed files with 746 additions and 40 deletions
165
src/wuttaweb/progress.py
Normal file
165
src/wuttaweb/progress.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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()
|
|
@ -3,13 +3,7 @@
|
|||
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>${base_meta.global_title()} » ${capture(self.title)|n}</title>
|
||||
${base_meta.favicon()}
|
||||
${self.header_core()}
|
||||
${self.head_tags()}
|
||||
</head>
|
||||
${self.html_head()}
|
||||
<body>
|
||||
<div id="app" style="height: 100%;">
|
||||
<whole-page />
|
||||
|
@ -30,7 +24,20 @@
|
|||
</body>
|
||||
</html>
|
||||
|
||||
## nb. this becomes part of the page <title> tag within <head>
|
||||
<%def name="html_head()">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>${self.head_title()}</title>
|
||||
${base_meta.favicon()}
|
||||
${self.header_core()}
|
||||
${self.head_tags()}
|
||||
</head>
|
||||
</%def>
|
||||
|
||||
## nb. this is the full <title> within html <head>
|
||||
<%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()}
|
||||
<style>
|
||||
|
||||
##############################
|
||||
|
@ -194,16 +205,7 @@
|
|||
|
||||
<%def name="head_tags()"></%def>
|
||||
|
||||
<%def name="render_vue_template_whole_page()">
|
||||
<script type="text/x-template" id="whole-page-template">
|
||||
|
||||
## nb. the whole-page contains 3 elements:
|
||||
## 1) header-wrapper
|
||||
## 2) content-wrapper
|
||||
## 3) footer
|
||||
<div id="whole-page"
|
||||
style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
|
||||
<%def name="whole_page_content()">
|
||||
## nb. the header-wrapper contains 2 elements:
|
||||
## 1) header proper (menu + index title area)
|
||||
## 2) page/content title area
|
||||
|
@ -327,7 +329,18 @@
|
|||
${base_meta.footer()}
|
||||
</div>
|
||||
</footer>
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_template_whole_page()">
|
||||
<script type="text/x-template" id="whole-page-template">
|
||||
|
||||
## nb. the whole-page normally contains 3 elements:
|
||||
## 1) header-wrapper
|
||||
## 2) content-wrapper
|
||||
## 3) footer
|
||||
<div id="whole-page"
|
||||
style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
${self.whole_page_content()}
|
||||
</div>
|
||||
</script>
|
||||
</%def>
|
||||
|
@ -418,7 +431,7 @@
|
|||
|
||||
mounted() {
|
||||
for (let hook of this.mountedHooks) {
|
||||
hook(this)
|
||||
hook.call(this)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
122
src/wuttaweb/templates/progress.mako
Normal file
122
src/wuttaweb/templates/progress.mako
Normal file
|
@ -0,0 +1,122 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="head_title()">${initial_msg or "Working"}...</%def>
|
||||
|
||||
<%def name="base_javascript()">
|
||||
${self.core_javascript()}
|
||||
</%def>
|
||||
|
||||
<%def name="base_styles()">
|
||||
${self.core_styles()}
|
||||
</%def>
|
||||
|
||||
<%def name="whole_page_content()">
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
|
||||
<div style="display: flex;">
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<div>
|
||||
|
||||
<p class="block">
|
||||
{{ progressMessage }} ... {{ totalDisplay }}
|
||||
</p>
|
||||
|
||||
<div class="level">
|
||||
|
||||
<div class="level-item">
|
||||
<b-progress size="is-large"
|
||||
style="width: 400px;"
|
||||
:max="progressMax"
|
||||
:value="progressValue"
|
||||
show-value
|
||||
format="percent"
|
||||
precision="0">
|
||||
</b-progress>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
</div>
|
||||
|
||||
${self.after_progress()}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</%def>
|
||||
|
||||
<%def name="after_progress()"></%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
<script>
|
||||
|
||||
WholePageData.progressURL = '${url('progress', key=progress.key, _query={'sessiontype': progress.session.type})}'
|
||||
WholePageData.progressMessage = "${(initial_msg or "Working").replace('"', '\\"')} (please wait)"
|
||||
WholePageData.progressMax = null
|
||||
WholePageData.progressMaxDisplay = null
|
||||
WholePageData.progressValue = null
|
||||
WholePageData.stillInProgress = true
|
||||
|
||||
WholePage.computed.totalDisplay = function() {
|
||||
|
||||
if (!this.stillInProgress) {
|
||||
return "done!"
|
||||
}
|
||||
|
||||
if (this.progressMaxDisplay) {
|
||||
return `(${'$'}{this.progressMaxDisplay} total)`
|
||||
}
|
||||
}
|
||||
|
||||
WholePageData.mountedHooks.push(function() {
|
||||
|
||||
// fetch first progress data, one second from now
|
||||
setTimeout(() => {
|
||||
this.updateProgress()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
WholePage.methods.updateProgress = function() {
|
||||
|
||||
this.$http.get(this.progressURL).then(response => {
|
||||
|
||||
if (response.data.error) {
|
||||
// errors stop the show; redirect
|
||||
location.href = response.data.error_url
|
||||
|
||||
} else {
|
||||
|
||||
if (response.data.complete || response.data.maximum) {
|
||||
this.progressMessage = response.data.message
|
||||
this.progressMaxDisplay = response.data.maximum_display
|
||||
|
||||
if (response.data.complete) {
|
||||
this.progressValue = this.progressMax
|
||||
this.stillInProgress = false
|
||||
|
||||
location.href = response.data.success_url
|
||||
|
||||
} else {
|
||||
this.progressValue = response.data.value
|
||||
this.progressMax = response.data.maximum
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stillInProgress) {
|
||||
|
||||
// fetch progress data again, in one second from now
|
||||
setTimeout(() => {
|
||||
this.updateProgress()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
|
@ -32,6 +32,7 @@ That will in turn include the following modules:
|
|||
* :mod:`wuttaweb.views.auth`
|
||||
* :mod:`wuttaweb.views.common`
|
||||
* :mod:`wuttaweb.views.settings`
|
||||
* :mod:`wuttaweb.views.progress`
|
||||
* :mod:`wuttaweb.views.people`
|
||||
* :mod:`wuttaweb.views.roles`
|
||||
* :mod:`wuttaweb.views.users`
|
||||
|
@ -45,6 +46,7 @@ def defaults(config, **kwargs):
|
|||
config.include(mod('wuttaweb.views.auth'))
|
||||
config.include(mod('wuttaweb.views.common'))
|
||||
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'))
|
||||
|
|
|
@ -25,6 +25,7 @@ Base Logic for Master Views
|
|||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
@ -35,6 +36,7 @@ from webhelpers2.html import HTML
|
|||
from wuttaweb.views import View
|
||||
from wuttaweb.util import get_form_data, get_model_fields, render_csrf_token
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.progress import SessionProgress
|
||||
from wuttjamaican.util import get_class_hierarchy
|
||||
|
||||
|
||||
|
@ -295,6 +297,19 @@ class MasterView(View):
|
|||
deleting" - i.e. it should have a :meth:`delete_bulk()` view.
|
||||
Default value is ``False``.
|
||||
|
||||
See also :attr:`deletable_bulk_quick`.
|
||||
|
||||
.. attribute:: deletable_bulk_quick
|
||||
|
||||
Boolean indicating whether the view model supports "quick" bulk
|
||||
deleting, i.e. the operation is reliably quick enough that it
|
||||
should happen *synchronously* with no progress indicator.
|
||||
|
||||
Default is ``False`` in which case a progress indicator is
|
||||
shown while the bulk deletion is performed.
|
||||
|
||||
Only relevant if :attr:`deletable_bulk` is true.
|
||||
|
||||
.. attribute:: form_fields
|
||||
|
||||
List of fields for the model form.
|
||||
|
@ -333,6 +348,7 @@ class MasterView(View):
|
|||
editable = True
|
||||
deletable = True
|
||||
deletable_bulk = False
|
||||
deletable_bulk_quick = False
|
||||
has_autocomplete = False
|
||||
configurable = False
|
||||
|
||||
|
@ -634,7 +650,7 @@ class MasterView(View):
|
|||
session = self.app.get_session(obj)
|
||||
session.delete(obj)
|
||||
|
||||
def delete_bulk(self, session=None):
|
||||
def delete_bulk(self):
|
||||
"""
|
||||
View to delete all records in the current :meth:`index()` grid
|
||||
data set, i.e. those matching current query.
|
||||
|
@ -652,35 +668,79 @@ class MasterView(View):
|
|||
one of the related methods which are called (in)directly by
|
||||
this one:
|
||||
|
||||
* :meth:`delete_bulk_data()`
|
||||
* :meth:`delete_bulk_action()`
|
||||
"""
|
||||
|
||||
# get current data set from grid
|
||||
# nb. this must *not* be paginated, we need it all
|
||||
grid = self.make_model_grid(paginated=False)
|
||||
data = grid.get_visible_data()
|
||||
|
||||
# delete it all and go back to listing
|
||||
self.delete_bulk_data(data, session=session)
|
||||
return self.redirect(self.get_index_url())
|
||||
if self.deletable_bulk_quick:
|
||||
|
||||
def delete_bulk_data(self, data, session=None):
|
||||
# delete it all and go back to listing
|
||||
self.delete_bulk_action(data)
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
else:
|
||||
|
||||
# start thread for delete; show progress page
|
||||
route_prefix = self.get_route_prefix()
|
||||
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.start()
|
||||
return self.render_progress(progress)
|
||||
|
||||
def delete_bulk_thread(self, query, success_url=None, progress=None):
|
||||
""" """
|
||||
model_title_plural = self.get_model_title_plural()
|
||||
|
||||
# nb. use new session, separate from web transaction
|
||||
session = self.app.make_session()
|
||||
records = query.with_session(session).all()
|
||||
|
||||
try:
|
||||
self.delete_bulk_action(records, progress=progress)
|
||||
|
||||
except Exception as error:
|
||||
session.rollback()
|
||||
log.warning("failed to delete %s results for %s",
|
||||
len(records), model_title_plural,
|
||||
exc_info=True)
|
||||
if progress:
|
||||
progress.handle_error(error, "Bulk deletion failed")
|
||||
|
||||
else:
|
||||
session.commit()
|
||||
if progress:
|
||||
progress.handle_success()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete_bulk_action(self, data, progress=None):
|
||||
"""
|
||||
This method performs the actual bulk deletion, for the given
|
||||
data set.
|
||||
data set. This is called via :meth:`delete_bulk()`.
|
||||
|
||||
Default logic will call :meth:`is_deletable()` for every data
|
||||
record, and if that returns true then it calls
|
||||
:meth:`delete_instance()`.
|
||||
:meth:`delete_instance()`. A progress indicator will be
|
||||
updated if one is provided.
|
||||
|
||||
As of now there is no progress indicator or async; caller must
|
||||
simply wait until delete is finished.
|
||||
Subclass should override if needed.
|
||||
"""
|
||||
session = session or self.Session()
|
||||
model_title_plural = self.get_model_title_plural()
|
||||
|
||||
for obj in data:
|
||||
def delete(obj, i):
|
||||
if self.is_deletable(obj):
|
||||
self.delete_instance(obj)
|
||||
|
||||
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()
|
||||
|
@ -1308,6 +1368,39 @@ class MasterView(View):
|
|||
|
||||
return HTML.tag('b-button', **btn_kw)
|
||||
|
||||
def make_progress(self, key, **kwargs):
|
||||
"""
|
||||
Create and return a
|
||||
:class:`~wuttaweb.progress.SessionProgress` instance, with the
|
||||
given key.
|
||||
|
||||
This is normally done just before calling
|
||||
:meth:`render_progress()`.
|
||||
"""
|
||||
return SessionProgress(self.request, key, **kwargs)
|
||||
|
||||
def render_progress(self, progress, context=None, template=None):
|
||||
"""
|
||||
Render the progress page, with given template/context.
|
||||
|
||||
When a view method needs to start a long-running operation, it
|
||||
first starts a thread to do the work, and then it renders the
|
||||
"progress" page. As the operation continues the progress page
|
||||
is updated. When the operation completes (or fails) the user
|
||||
is redirected to the final destination.
|
||||
|
||||
TODO: should document more about how to do this..
|
||||
|
||||
:param progress: Progress indicator instance as returned by
|
||||
:meth:`make_progress()`.
|
||||
|
||||
:returns: A :term:`response` with rendered progress page.
|
||||
"""
|
||||
template = template or '/progress.mako'
|
||||
context = context or {}
|
||||
context['progress'] = progress
|
||||
return render_to_response(template, context, request=self.request)
|
||||
|
||||
def render_to_response(self, template, context):
|
||||
"""
|
||||
Locate and render an appropriate template, with the given
|
||||
|
|
75
src/wuttaweb/views/progress.py
Normal file
75
src/wuttaweb/views/progress.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
# -*- 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Progress Views
|
||||
"""
|
||||
|
||||
from wuttaweb.progress import get_progress_session
|
||||
|
||||
|
||||
def progress(request):
|
||||
"""
|
||||
View which returns JSON with current progress status.
|
||||
|
||||
The URL is like ``/progress/XXX`` where ``XXX`` is the "key" to a
|
||||
particular progress indicator, tied to a long-running operation.
|
||||
|
||||
This key is used to lookup the progress status within the Beaker
|
||||
session storage. See also
|
||||
:class:`~wuttaweb.progress.SessionProgress`.
|
||||
"""
|
||||
key = request.matchdict['key']
|
||||
session = get_progress_session(request, key)
|
||||
|
||||
# session has 'complete' flag set when operation is over
|
||||
if session.get('complete'):
|
||||
|
||||
# set a flash msg for user if one is defined. this is the
|
||||
# time to do it since user is about to get redirected.
|
||||
msg = session.get('success_msg')
|
||||
if msg:
|
||||
request.session.flash(msg)
|
||||
|
||||
elif session.get('error'): # uh-oh
|
||||
|
||||
# set an error flash msg for user. this is the time to do it
|
||||
# since user is about to get redirected.
|
||||
msg = session.get('error_msg', "An unspecified error occurred.")
|
||||
request.session.flash(msg, 'error')
|
||||
|
||||
# nb. we return the session as-is; since it is dict-like (and only
|
||||
# contains relevant progress data) it can be used directly for the
|
||||
# JSON response context
|
||||
return session
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
progress = kwargs.get('progress', base['progress'])
|
||||
config.add_route('progress', '/progress/{key}')
|
||||
config.add_view(progress, route_name='progress', renderer='json')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
Loading…
Add table
Add a link
Reference in a new issue