From a web point of view, if you want to stop a user from doing stupid things (like submitting a form before reviewing it, sending a heinous e-mail late at night, or clicking a button that launches a nuclear missile), you might want to do one one of three things:
- Pop-up a confirmation dialog
- Go to a second page, with yet another submit button asking for confirmation, with a summary of the submitted data
- Just let it go, but offer a timed "UNDO" or "CANCEL" link, like Gmail implemented for e-mails
Of course, the best of all three is the latter. The difference here is major. It's the difference between pro-active annoyingness (pop-ups asking confirmation when you know very well you want to send the e-mail), or reactive flexibility. It also gives your user a great boost of confidence in your application, because the user can do stupid things and knows he'll be able to fix them in a timely manner.
Now here's how to do it in Pylons, using the just-released WebUndo lib. You can find it on PyPI, so just
pip install WebUndo from your project.
We're lucky Pylons runs as a single application with threads, since that's what we'll use to delay our operations and give the user a chance to cancel. PHP apps for instance, wouldn't give us that flexibility, since everything vanishes once the request is sent. Take a look at the source code to know how it's built.
Let's consider this piece of Pylons controller:
class ThController(BaseController): def index(self): return render('th.mako') def save(self): open('/tmp/dump.txt', 'w').write(request.params.get('stuff')) return "Everything saved"
th.mako contains a simple form with a text field named
stuff. In this case, once you submit the form in
th.mako, it's done: the file is written to disk, and there's no way you can cancel it.
Now let's have a look at this version:
class ThController(BaseController): def index(self): return render('th.mako') def save(self): req = request def do_save(): open('/tmp/dump.txt', 'w').write(req.params.get('stuff')) key = webundo.launch_cancelable_job(do_save, 5) return 'Saving, click to <a href="%s">cancel</a>' % h.url_for(controller='th', action='cancel', id=key) def cancel(self, id): if webundo.cancel_job(id): return "Thread cancelled successfully" else: return "Too late, thread finished already."
We added some pieces of webundo, the
save() action returns a confirmation message, and a link to cancel the operation. We also added a
def do_save() wrapper, a call to the
launch_cancelable_job function and the passing on of the
key. Notice also the
req = request, used to make sure the
request object gets closed in the
do_save() function. The fun part here, is that the operation will not be carried out until the 5 seconds (second parameter to
launch_cancelable_job) are elapsed. The last thing is obviously, the
cancel action, which will deal with the cancelation.
Using this feature, even if you close your browser window after calling the
save action, the
do_save() function will be executed normally, only delayed by 5 seconds.
For the Pylons addicts who read that code and say "hey, that
request object can't traverse threads, it's a StackedObjectProxy"! See the details and answer in my other blog post.
Of course, there are cases where you want a certain operation to be carried out directly (when calling
save() for example) but you want some special code to be executed if someone click an undo button for example. The WebUndo lib provides you with another pair of functions to do just that:
Take a look at the documentation for full details. Happy undoing!
UPDATED March 20th: typos following feedback