From 871dd35a3a3f22ac3e3d8bb5c5fc28fd0e998ce7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 7 Dec 2021 17:45:21 -0600 Subject: [PATCH] Add basic views to expose Problem Reports, and run them not very sophisticated yet but heck better than we had yesterday --- tailbone/templates/reports/problems/view.mako | 76 ++++++++++ tailbone/views/batch/core.py | 12 +- tailbone/views/master.py | 64 +++++--- tailbone/views/reports.py | 139 +++++++++++++++++- 4 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 tailbone/templates/reports/problems/view.mako diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako new file mode 100644 index 00000000..cbd2a942 --- /dev/null +++ b/tailbone/templates/reports/problems/view.mako @@ -0,0 +1,76 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + % if master.has_perm('execute'): + + + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 90614079..c0a3a1a3 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -850,7 +850,7 @@ class BatchMasterView(MasterView): # launch thread to invoke handler action 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) thread.start() @@ -859,7 +859,7 @@ class BatchMasterView(MasterView): # launch thread to populate batch; that will update session progress directly 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() return self.render_progress(progress, { @@ -894,7 +894,7 @@ class BatchMasterView(MasterView): log.debug("launching command in subprocess: %s", 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, 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 the batch. """ + batch_uuid = key[0] + # figure out the (sub)command args we'll be passing subargs = [ '--batch-type', @@ -1216,7 +1218,7 @@ class BatchMasterView(MasterView): def execute_error_message(self, 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. """ @@ -1224,7 +1226,7 @@ class BatchMasterView(MasterView): # session here; can't use tailbone because it has web request # transaction binding etc. 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) try: result = self.handler.do_execute(batch, user=user, progress=progress, **kwargs) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index cdd958a0..73562e8d 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -1688,36 +1688,43 @@ class MasterView(View): """ obj = self.get_instance() 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} - thread = Thread(target=self.execute_thread, args=(obj.uuid, self.request.user.uuid), kwargs=kwargs) - thread.start() + kwargs = {'progress': progress} + 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() - return self.render_progress(progress, { - 'instance': obj, - 'initial_msg': self.execute_progress_initial_msg, - 'cancel_url': self.get_action_url('view', obj), - 'cancel_msg': "{} execution was canceled".format(model_title), - }, 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)) + return self.render_progress(progress, { + 'instance': obj, + 'initial_msg': self.execute_progress_initial_msg, + 'cancel_url': self.get_action_url('view', obj), + 'cancel_msg': "{} execution was canceled".format(model_title), + }, template=self.execute_progress_template) def make_execute_progress(self, obj): key = '{}.execute'.format(self.get_grid_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. """ 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) 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. except Exception as error: @@ -1733,13 +1740,21 @@ class MasterView(View): # If no error, check result flag (false means user canceled). else: 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) session.close() if progress: progress.session.load() progress.session['complete'] = True progress.session['success_url'] = success_url + if success_msg: + progress.session['success_msg'] = success_msg progress.session.save() 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 model class by default, e.g. 'products'. """ + if hasattr(cls, 'route_prefix'): + return cls.route_prefix model_name = cls.get_normalized_model_name() - return getattr(cls, 'route_prefix', '{0}s'.format(model_name)) + return '{}s'.format(model_name) @classmethod def get_url_prefix(cls): @@ -2377,7 +2394,10 @@ class MasterView(View): mapper = orm.object_mapper(row) except orm.exc.UnmappedInstanceError: 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: return {self.model_key: getattr(row, self.model_key)} else: @@ -4311,7 +4331,9 @@ class MasterView(View): if cls.executable: config.add_tailbone_permission(permission_prefix, '{}.execute'.format(permission_prefix), "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), permission='{}.execute'.format(permission_prefix)) diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index 30a9c6b6..4c8fe6e9 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -43,12 +43,12 @@ import colander from deform import widget as dfwidget from mako.template import Template from pyramid.response import Response -from webhelpers2.html import HTML +from webhelpers2.html import HTML, tags from tailbone import forms from tailbone.db import Session 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})$') @@ -511,6 +511,140 @@ class NewReport(colander.Schema): 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): config.add_route('reports.ordering', '/reports/ordering') config.add_route('reports.inventory', '/reports/inventory') @@ -531,3 +665,4 @@ def includeme(config): # note that GenerateReport must come first, per route matching GenerateReport.defaults(config) ReportOutputView.defaults(config) + ProblemReportView.defaults(config)