Add basic views to expose Problem Reports, and run them

not very sophisticated yet but heck better than we had yesterday
This commit is contained in:
Lance Edgar 2021-12-07 17:45:21 -06:00
parent f687078bbf
commit 871dd35a3a
4 changed files with 263 additions and 28 deletions

View file

@ -0,0 +1,76 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="object_helpers()">
${parent.object_helpers()}
% if master.has_perm('execute'):
<nav class="panel">
<p class="panel-heading">Tools</p>
<div class="panel-block buttons">
<b-button type="is-primary"
@click="runReportShowDialog = true"
icon-pack="fas"
icon-left="arrow-circle-right">
Run this Report
</b-button>
</div>
</nav>
<b-modal has-modal-card
:active.sync="runReportShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Run Problem Report</p>
</header>
<section class="modal-card-body">
<p class="block">
You can run this problem report right now if you like.
</p>
<p class="block">
Keep in mind the following may receive email, should the
report find any problems.
</p>
<ul>
% for recip in instance['email_recipients']:
<li>${recip}</li>
% endfor
</ul>
</section>
<footer class="modal-card-foot">
<b-button @click="runReportShowDialog = false">
Cancel
</b-button>
${h.form(master.get_action_url('execute', instance))}
${h.csrf_token(request)}
<b-button type="is-primary"
native-type="submit"
@click="runReportSubmitting = true"
:disabled="runReportSubmitting"
icon-pack="fas"
icon-left="arrow-circle-right">
{{ runReportSubmitting ? "Working, please wait..." : "Run Problem Report" }}
</b-button>
${h.end_form()}
</footer>
</div>
</b-modal>
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.runReportShowDialog = false
ThisPageData.runReportSubmitting = false
</script>
</%def>
${parent.body()}

View file

@ -850,7 +850,7 @@ class BatchMasterView(MasterView):
# launch thread to invoke handler action # launch thread to invoke handler action
thread = Thread(target=self.action_subprocess_thread, thread = Thread(target=self.action_subprocess_thread,
args=(batch.uuid, port, username, batch_action, progress), args=((batch.uuid,), port, username, batch_action, progress),
kwargs=kwargs) kwargs=kwargs)
thread.start() thread.start()
@ -859,7 +859,7 @@ class BatchMasterView(MasterView):
# launch thread to populate batch; that will update session progress directly # launch thread to populate batch; that will update session progress directly
target = getattr(self, '{}_thread'.format(batch_action)) target = getattr(self, '{}_thread'.format(batch_action))
thread = Thread(target=target, args=(batch.uuid, user_uuid, progress), kwargs=kwargs) thread = Thread(target=target, args=((batch.uuid,), user_uuid, progress), kwargs=kwargs)
thread.start() thread.start()
return self.render_progress(progress, { return self.render_progress(progress, {
@ -894,7 +894,7 @@ class BatchMasterView(MasterView):
log.debug("launching command in subprocess: %s", cmd) log.debug("launching command in subprocess: %s", cmd)
subprocess.check_call(cmd) subprocess.check_call(cmd)
def action_subprocess_thread(self, batch_uuid, port, username, handler_action, progress, **kwargs): def action_subprocess_thread(self, key, port, username, handler_action, progress, **kwargs):
""" """
This method is sort of an alternative thread target for batch actions, This method is sort of an alternative thread target for batch actions,
to be used in the event versioning is enabled for the main process but to be used in the event versioning is enabled for the main process but
@ -902,6 +902,8 @@ class BatchMasterView(MasterView):
launch a separate process with versioning disabled in order to act on launch a separate process with versioning disabled in order to act on
the batch. the batch.
""" """
batch_uuid = key[0]
# figure out the (sub)command args we'll be passing # figure out the (sub)command args we'll be passing
subargs = [ subargs = [
'--batch-type', '--batch-type',
@ -1216,7 +1218,7 @@ class BatchMasterView(MasterView):
def execute_error_message(self, error): def execute_error_message(self, error):
return "Batch execution failed: {}".format(simple_error(error)) return "Batch execution failed: {}".format(simple_error(error))
def execute_thread(self, batch_uuid, user_uuid, progress, **kwargs): def execute_thread(self, key, user_uuid, progress, **kwargs):
""" """
Thread target for executing a batch with progress indicator. Thread target for executing a batch with progress indicator.
""" """
@ -1224,7 +1226,7 @@ class BatchMasterView(MasterView):
# session here; can't use tailbone because it has web request # session here; can't use tailbone because it has web request
# transaction binding etc. # transaction binding etc.
session = RattailSession() session = RattailSession()
batch = session.query(self.model_class).get(batch_uuid) batch = self.get_instance_for_key(key, session)
user = session.query(model.User).get(user_uuid) user = session.query(model.User).get(user_uuid)
try: try:
result = self.handler.do_execute(batch, user=user, progress=progress, **kwargs) result = self.handler.do_execute(batch, user=user, progress=progress, **kwargs)

View file

@ -1688,36 +1688,43 @@ class MasterView(View):
""" """
obj = self.get_instance() obj = self.get_instance()
model_title = self.get_model_title() model_title = self.get_model_title()
if self.request.method == 'POST': progress = self.make_execute_progress(obj)
progress = self.make_execute_progress(obj) kwargs = {'progress': progress}
kwargs = {'progress': progress} key = [self.request.matchdict[k]
thread = Thread(target=self.execute_thread, args=(obj.uuid, self.request.user.uuid), kwargs=kwargs) for k in self.get_model_key(as_tuple=True)]
thread.start() thread = Thread(target=self.execute_thread, args=(key, self.request.user.uuid), kwargs=kwargs)
thread.start()
return self.render_progress(progress, { return self.render_progress(progress, {
'instance': obj, 'instance': obj,
'initial_msg': self.execute_progress_initial_msg, 'initial_msg': self.execute_progress_initial_msg,
'cancel_url': self.get_action_url('view', obj), 'cancel_url': self.get_action_url('view', obj),
'cancel_msg': "{} execution was canceled".format(model_title), 'cancel_msg': "{} execution was canceled".format(model_title),
}, template=self.execute_progress_template) }, template=self.execute_progress_template)
self.request.session.flash("Sorry, you must POST to execute a {}.".format(model_title), 'error')
return self.redirect(self.get_action_url('view', obj))
def make_execute_progress(self, obj): def make_execute_progress(self, obj):
key = '{}.execute'.format(self.get_grid_key()) key = '{}.execute'.format(self.get_grid_key())
return self.make_progress(key) return self.make_progress(key)
def execute_thread(self, uuid, user_uuid, progress=None, **kwargs): def get_instance_for_key(self, key, session):
model_key = self.get_model_key(as_tuple=True)
if len(model_key) == 1 and model_key[0] == 'uuid':
uuid = key[0]
return session.query(self.model_class).get(uuid)
raise NotImplementedError
def execute_thread(self, key, user_uuid, progress=None, **kwargs):
""" """
Thread target for executing an object. Thread target for executing an object.
""" """
session = RattailSession() session = RattailSession()
obj = session.query(self.model_class).get(uuid) obj = self.get_instance_for_key(key, session)
user = session.query(model.User).get(user_uuid) user = session.query(model.User).get(user_uuid)
try: try:
self.execute_instance(obj, user, progress=progress, **kwargs) success_msg = self.execute_instance(obj, user,
progress=progress,
**kwargs)
# If anything goes wrong, rollback and log the error etc. # If anything goes wrong, rollback and log the error etc.
except Exception as error: except Exception as error:
@ -1733,13 +1740,21 @@ class MasterView(View):
# If no error, check result flag (false means user canceled). # If no error, check result flag (false means user canceled).
else: else:
session.commit() session.commit()
session.refresh(obj) try:
needs_refresh = obj in session
except:
pass
else:
if needs_refresh:
session.refresh(obj)
success_url = self.get_execute_success_url(obj) success_url = self.get_execute_success_url(obj)
session.close() session.close()
if progress: if progress:
progress.session.load() progress.session.load()
progress.session['complete'] = True progress.session['complete'] = True
progress.session['success_url'] = success_url progress.session['success_url'] = success_url
if success_msg:
progress.session['success_msg'] = success_msg
progress.session.save() progress.session.save()
def execute_error_message(self, error): def execute_error_message(self, error):
@ -1991,8 +2006,10 @@ class MasterView(View):
the master view class. This is the plural, lower-cased name of the the master view class. This is the plural, lower-cased name of the
model class by default, e.g. 'products'. model class by default, e.g. 'products'.
""" """
if hasattr(cls, 'route_prefix'):
return cls.route_prefix
model_name = cls.get_normalized_model_name() model_name = cls.get_normalized_model_name()
return getattr(cls, 'route_prefix', '{0}s'.format(model_name)) return '{}s'.format(model_name)
@classmethod @classmethod
def get_url_prefix(cls): def get_url_prefix(cls):
@ -2377,7 +2394,10 @@ class MasterView(View):
mapper = orm.object_mapper(row) mapper = orm.object_mapper(row)
except orm.exc.UnmappedInstanceError: except orm.exc.UnmappedInstanceError:
try: try:
return {self.model_key: row[self.model_key]} if isinstance(self.model_key, six.string_types):
return {self.model_key: row[self.model_key]}
return dict([(key, row[key])
for key in self.model_key])
except TypeError: except TypeError:
return {self.model_key: getattr(row, self.model_key)} return {self.model_key: getattr(row, self.model_key)}
else: else:
@ -4311,7 +4331,9 @@ class MasterView(View):
if cls.executable: if cls.executable:
config.add_tailbone_permission(permission_prefix, '{}.execute'.format(permission_prefix), config.add_tailbone_permission(permission_prefix, '{}.execute'.format(permission_prefix),
"Execute {}".format(model_title)) "Execute {}".format(model_title))
config.add_route('{}.execute'.format(route_prefix), '{}/execute'.format(instance_url_prefix)) config.add_route('{}.execute'.format(route_prefix),
'{}/execute'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='execute', route_name='{}.execute'.format(route_prefix), config.add_view(cls, attr='execute', route_name='{}.execute'.format(route_prefix),
permission='{}.execute'.format(permission_prefix)) permission='{}.execute'.format(permission_prefix))

View file

@ -43,12 +43,12 @@ import colander
from deform import widget as dfwidget from deform import widget as dfwidget
from mako.template import Template from mako.template import Template
from pyramid.response import Response from pyramid.response import Response
from webhelpers2.html import HTML from webhelpers2.html import HTML, tags
from tailbone import forms from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.views import View from tailbone.views import View
from tailbone.views.exports import ExportMasterView from tailbone.views.exports import ExportMasterView, MasterView
plu_upc_pattern = re.compile(r'^000000000(\d{5})$') plu_upc_pattern = re.compile(r'^000000000(\d{5})$')
@ -511,6 +511,140 @@ class NewReport(colander.Schema):
validator=valid_report_type) validator=valid_report_type)
class ProblemReportView(MasterView):
"""
Master view for problem reports
"""
model_title = "Problem Report"
model_key = ('system_key', 'problem_key')
route_prefix = 'problem_reports'
url_prefix = '/reports/problems'
creatable = False
editable = False
deletable = False
filterable = False
pageable = False
executable = True
labels = {
'system_key': "System",
}
grid_columns = [
'system_key',
# 'problem_key',
'problem_title',
'email_recipients',
]
form_fields = [
'system_key',
'problem_title',
'email_key',
'email_recipients',
]
def __init__(self, request):
super(ProblemReportView, self).__init__(request)
app = self.get_rattail_app()
self.handler = app.get_problem_report_handler()
def normalize(self, report, keep_report=True):
data = {
'system_key': report.system_key,
'problem_key': report.problem_key,
'problem_title': report.problem_title,
'email_key': self.handler.get_email_key(report),
}
app = self.get_rattail_app()
handler = app.get_mail_handler()
email = handler.get_email(data['email_key'])
data['email_recipients'] = email.get_recips('all')
if keep_report:
data['_report'] = report
return data
def get_data(self, session=None):
data = []
reports = self.handler.get_all_problem_reports()
organized = self.handler.organize_problem_reports(reports)
for system_key, reports in six.iteritems(organized):
for report in six.itervalues(reports):
data.append(self.normalize(report))
return data
def configure_grid(self, g):
super(ProblemReportView, self).configure_grid(g)
g.set_renderer('email_recipients', self.render_email_recipients)
g.set_link('problem_key')
g.set_link('problem_title')
def get_instance(self):
system_key = self.request.matchdict['system_key']
problem_key = self.request.matchdict['problem_key']
return self.get_instance_for_key((system_key, problem_key),
None)
def get_instance_for_key(self, key, session):
report = self.handler.get_problem_report(*key)
if report:
return self.normalize(report)
raise self.notfound()
def get_instance_title(self, report_info):
return report_info['problem_title']
def make_form_schema(self):
return ProblemReportSchema()
def configure_form(self, f):
super(ProblemReportView, self).configure_form(f)
f.set_renderer('email_key', self.render_email_key)
f.set_renderer('email_recipients', self.render_email_recipients)
def render_email_key(self, report_info, field):
email_key = report_info[field]
if not email_key:
return
if self.request.has_perm('emailprofiles.view'):
text = email_key
url = self.request.route_url('emailprofiles.view', key=email_key)
return tags.link_to(text, url)
return email_key
def render_email_recipients(self, report_info, field):
recips = report_info['email_recipients']
return ', '.join(recips)
def execute_instance(self, report_info, user, progress=None, **kwargs):
report = report_info['_report']
problems = self.handler.run_problem_report(report)
return "Report found {} problems".format(len(problems))
class ProblemReportSchema(colander.MappingSchema):
system_key = colander.SchemaNode(colander.String())
problem_key = colander.SchemaNode(colander.String())
problem_title = colander.SchemaNode(colander.String())
email_key = colander.SchemaNode(colander.String())
def add_routes(config): def add_routes(config):
config.add_route('reports.ordering', '/reports/ordering') config.add_route('reports.ordering', '/reports/ordering')
config.add_route('reports.inventory', '/reports/inventory') config.add_route('reports.inventory', '/reports/inventory')
@ -531,3 +665,4 @@ def includeme(config):
# note that GenerateReport must come first, per route matching # note that GenerateReport must come first, per route matching
GenerateReport.defaults(config) GenerateReport.defaults(config)
ReportOutputView.defaults(config) ReportOutputView.defaults(config)
ProblemReportView.defaults(config)