Add progress support for bulk deletion

plus bulk-delete all tempmon readings when deleting client or probe
This commit is contained in:
Lance Edgar 2017-08-04 16:11:45 -05:00
parent d8be651e95
commit 3205d61ba6
5 changed files with 88 additions and 10 deletions

View file

@ -29,6 +29,7 @@ from __future__ import unicode_literals, absolute_import
import os import os
from rattail.db import model from rattail.db import model
from rattail.util import progress_loop
from pyramid import httpexceptions from pyramid import httpexceptions
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
@ -86,6 +87,9 @@ class View(object):
""" """
return httpexceptions.HTTPFound(location=url, **kwargs) 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): def render_progress(self, kwargs):
""" """
Render the progress page, with given kwargs as context. Render the progress page, with given kwargs as context.

View file

@ -32,9 +32,11 @@ from sqlalchemy import orm
import sqlalchemy_continuum as continuum import sqlalchemy_continuum as continuum
from rattail.db import Session as RattailSession
from rattail.db.continuum import model_transaction_query from rattail.db.continuum import model_transaction_query
from rattail.util import prettify from rattail.util import prettify
from rattail.time import localtime from rattail.time import localtime
from rattail.threads import Thread
import formalchemy as fa import formalchemy as fa
from pyramid import httpexceptions from pyramid import httpexceptions
@ -43,6 +45,7 @@ from webhelpers2.html import HTML, tags
from tailbone import forms, grids from tailbone import forms, grids
from tailbone.views import View from tailbone.views import View
from tailbone.progress import SessionProgress
class MasterView(View): class MasterView(View):
@ -702,20 +705,60 @@ class MasterView(View):
Delete all records matching the current grid query Delete all records matching the current grid query
""" """
if self.request.method == 'POST': if self.request.method == 'POST':
query = self.get_effective_query(sortable=False) key = '{}.bulk_delete'.format(self.model_class.__tablename__)
count = query.count() objects = self.get_effective_data()
self.bulk_delete_objects(query) progress = SessionProgress(self.request, key)
self.request.session.flash("Deleted {:,d} {}".format(count, self.get_model_title_plural())) 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: else:
self.request.session.flash("Sorry, you must POST to do a bulk delete operation") self.request.session.flash("Sorry, you must POST to do a bulk delete operation")
return self.redirect(self.get_index_url()) return self.redirect(self.get_index_url())
def bulk_delete_objects(self, query): def bulk_delete_objects(self, session, objects, progress=None):
# TODO: sometimes the first makes sense, and would be preferred for
# efficiency's sake. might even need to add progress to latter? def delete(obj, i):
# query.delete(synchronize_session=False) session.delete(obj)
for obj in query:
self.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): def get_merge_fields(self):
if hasattr(self, 'merge_fields'): if hasattr(self, 'merge_fields'):

View file

@ -114,6 +114,19 @@ class TempmonClientView(MasterView):
del fs.probes del fs.probes
del fs.online 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): def restartable_client(self, client):
return True return True

View file

@ -26,6 +26,8 @@ Common stuff for tempmon views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from rattail_tempmon.db import Session as RawTempmonSession
from formalchemy.fields import SelectFieldRenderer from formalchemy.fields import SelectFieldRenderer
from webhelpers2.html import tags from webhelpers2.html import tags
@ -39,6 +41,9 @@ class MasterView(views.MasterView2):
""" """
Session = TempmonSession Session = TempmonSession
def get_bulk_delete_session(self):
return RawTempmonSession()
class ClientFieldRenderer(SelectFieldRenderer): class ClientFieldRenderer(SelectFieldRenderer):

View file

@ -109,6 +109,19 @@ class TempmonProbeView(MasterView):
if self.creating or self.editing: if self.creating or self.editing:
del fs.status 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): def includeme(config):
TempmonProbeView.defaults(config) TempmonProbeView.defaults(config)