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:
parent
f687078bbf
commit
871dd35a3a
76
tailbone/templates/reports/problems/view.mako
Normal file
76
tailbone/templates/reports/problems/view.mako
Normal 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()}
|
|
@ -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)
|
||||||
|
|
|
@ -1688,11 +1688,12 @@ 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}
|
||||||
thread = Thread(target=self.execute_thread, args=(obj.uuid, self.request.user.uuid), kwargs=kwargs)
|
key = [self.request.matchdict[k]
|
||||||
|
for k in self.get_model_key(as_tuple=True)]
|
||||||
|
thread = Thread(target=self.execute_thread, args=(key, self.request.user.uuid), kwargs=kwargs)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return self.render_progress(progress, {
|
return self.render_progress(progress, {
|
||||||
|
@ -1702,22 +1703,28 @@ class MasterView(View):
|
||||||
'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,6 +1740,12 @@ 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()
|
||||||
|
try:
|
||||||
|
needs_refresh = obj in session
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if needs_refresh:
|
||||||
session.refresh(obj)
|
session.refresh(obj)
|
||||||
success_url = self.get_execute_success_url(obj)
|
success_url = self.get_execute_success_url(obj)
|
||||||
session.close()
|
session.close()
|
||||||
|
@ -1740,6 +1753,8 @@ class MasterView(View):
|
||||||
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:
|
||||||
|
if isinstance(self.model_key, six.string_types):
|
||||||
return {self.model_key: row[self.model_key]}
|
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))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue