diff --git a/tailbone/views/core.py b/tailbone/views/core.py index 59db1d3a..9efdb90f 100644 --- a/tailbone/views/core.py +++ b/tailbone/views/core.py @@ -29,6 +29,7 @@ from __future__ import unicode_literals, absolute_import import os from rattail.db import model +from rattail.util import progress_loop from pyramid import httpexceptions from pyramid.renderers import render_to_response @@ -86,6 +87,9 @@ class View(object): """ return httpexceptions.HTTPFound(location=url, **kwargs) + def progress_loop(self, func, items, factory, *args, **kwargs): + return progress_loop(func, items, factory, *args, **kwargs) + def render_progress(self, kwargs): """ Render the progress page, with given kwargs as context. diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 8ff3ab9a..0ba720fb 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -32,9 +32,11 @@ from sqlalchemy import orm import sqlalchemy_continuum as continuum +from rattail.db import Session as RattailSession from rattail.db.continuum import model_transaction_query from rattail.util import prettify from rattail.time import localtime +from rattail.threads import Thread import formalchemy as fa from pyramid import httpexceptions @@ -43,6 +45,7 @@ from webhelpers2.html import HTML, tags from tailbone import forms, grids from tailbone.views import View +from tailbone.progress import SessionProgress class MasterView(View): @@ -702,20 +705,60 @@ class MasterView(View): Delete all records matching the current grid query """ if self.request.method == 'POST': - query = self.get_effective_query(sortable=False) - count = query.count() - self.bulk_delete_objects(query) - self.request.session.flash("Deleted {:,d} {}".format(count, self.get_model_title_plural())) + key = '{}.bulk_delete'.format(self.model_class.__tablename__) + objects = self.get_effective_data() + progress = SessionProgress(self.request, key) + thread = Thread(target=self.bulk_delete_thread, args=(objects, progress)) + thread.start() + return self.render_progress({ + 'key': key, + 'cancel_url': self.get_index_url(), + 'cancel_msg': "Bulk deletion was canceled", + }) else: self.request.session.flash("Sorry, you must POST to do a bulk delete operation") return self.redirect(self.get_index_url()) - def bulk_delete_objects(self, query): - # TODO: sometimes the first makes sense, and would be preferred for - # efficiency's sake. might even need to add progress to latter? - # query.delete(synchronize_session=False) - for obj in query: - self.Session.delete(obj) + def bulk_delete_objects(self, session, objects, progress=None): + + def delete(obj, i): + session.delete(obj) + + self.progress_loop(delete, objects, progress, + message="Deleting objects") + + def get_bulk_delete_session(self): + return RattailSession() + + def bulk_delete_thread(self, objects, progress): + """ + Thread target for bulk-deleting current results, with progress. + """ + session = self.get_bulk_delete_session() + objects = objects.with_session(session).all() + try: + self.bulk_delete_objects(session, objects, progress=progress) + + # If anything goes wrong, rollback and log the error etc. + except Exception as error: + session.rollback() + log.exception("execution failed for batch results") + session.close() + if progress: + progress.session.load() + progress.session['error'] = True + progress.session['error_msg'] = "Bulk deletion failed: {}: {}".format(type(error).__name__, error) + progress.session.save() + + # If no error, check result flag (false means user canceled). + else: + session.commit() + session.close() + if progress: + progress.session.load() + progress.session['complete'] = True + progress.session['success_url'] = self.get_index_url() + progress.session.save() def get_merge_fields(self): if hasattr(self, 'merge_fields'): diff --git a/tailbone/views/tempmon/clients.py b/tailbone/views/tempmon/clients.py index 7f275565..2144cf14 100644 --- a/tailbone/views/tempmon/clients.py +++ b/tailbone/views/tempmon/clients.py @@ -114,6 +114,19 @@ class TempmonClientView(MasterView): del fs.probes del fs.online + def delete_instance(self, client): + # bulk-delete all readings first + readings = self.Session.query(tempmon.Reading)\ + .filter(tempmon.Reading.client == client) + readings.delete(synchronize_session=False) + self.Session.flush() + self.Session.refresh(client) + + # Flush immediately to force any pending integrity errors etc.; that + # way we don't set flash message until we know we have success. + self.Session.delete(client) + self.Session.flush() + def restartable_client(self, client): return True diff --git a/tailbone/views/tempmon/core.py b/tailbone/views/tempmon/core.py index 6b605206..545eb3cb 100644 --- a/tailbone/views/tempmon/core.py +++ b/tailbone/views/tempmon/core.py @@ -26,6 +26,8 @@ Common stuff for tempmon views from __future__ import unicode_literals, absolute_import +from rattail_tempmon.db import Session as RawTempmonSession + from formalchemy.fields import SelectFieldRenderer from webhelpers2.html import tags @@ -39,6 +41,9 @@ class MasterView(views.MasterView2): """ Session = TempmonSession + def get_bulk_delete_session(self): + return RawTempmonSession() + class ClientFieldRenderer(SelectFieldRenderer): diff --git a/tailbone/views/tempmon/probes.py b/tailbone/views/tempmon/probes.py index 75d6540c..4add1ca2 100644 --- a/tailbone/views/tempmon/probes.py +++ b/tailbone/views/tempmon/probes.py @@ -109,6 +109,19 @@ class TempmonProbeView(MasterView): if self.creating or self.editing: del fs.status + def delete_instance(self, probe): + # bulk-delete all readings first + readings = self.Session.query(tempmon.Reading)\ + .filter(tempmon.Reading.probe == probe) + readings.delete(synchronize_session=False) + self.Session.flush() + self.Session.refresh(probe) + + # Flush immediately to force any pending integrity errors etc.; that + # way we don't set flash message until we know we have success. + self.Session.delete(probe) + self.Session.flush() + def includeme(config): TempmonProbeView.defaults(config)