From ef6eaefef50c8d9555f538aebe4453e75e553600 Mon Sep 17 00:00:00 2001 From: Thomas NOEL Date: Thu, 7 Jun 2018 12:16:20 +0200 Subject: [PATCH] can notify exceptions on formdata only in UI (#24327) --- tests/test_form_pages.py | 11 +++++- wcs/admin/logged_errors.py | 17 +++++++++ wcs/logged_errors.py | 78 +++++++++++++++++++++++++++++++------- wcs/publisher.py | 2 +- wcs/workflows.py | 12 +++--- 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py index cda29638..ca0193c3 100644 --- a/tests/test_form_pages.py +++ b/tests/test_form_pages.py @@ -1694,7 +1694,9 @@ def test_form_multi_page_post_edit(pub): assert 'barXYZ' in resp.body # unchanged value is still there assert formdef.data_class().get(data_id).status == 'wf-%s' % st2.id - # jump to a nonexistent status == do not jump + # jump to a nonexistent status == do not jump, but add a LoggedError + LoggedError.wipe() + assert LoggedError.count() == 0 editable.status = 'deleted_status_id' workflow.store() # go back to st1 @@ -1710,6 +1712,13 @@ def test_form_multi_page_post_edit(pub): resp = resp.forms[0].submit('submit') resp = resp.follow() assert formdef.data_class().get(data_id).status == 'wf-%s' % st1.id # stay on st1 + assert LoggedError.count() == 1 + logged_error = LoggedError.select()[0] + assert logged_error.formdata_id == str(formdata.id) + assert logged_error.formdef_id == formdef.id + assert logged_error.workflow_id == workflow.id + assert logged_error.status_id == st1.id + assert logged_error.status_item_id == editable.id def test_form_count_dispatching(pub): user = create_user(pub) diff --git a/wcs/admin/logged_errors.py b/wcs/admin/logged_errors.py index c2373925..019f009f 100644 --- a/wcs/admin/logged_errors.py +++ b/wcs/admin/logged_errors.py @@ -56,6 +56,19 @@ class LoggedErrorDirectory(Directory): if workflow: r += htmltext('
  • %s %s
  • ') % ( _('Workflow: '), workflow.id, workflow.name) + status = self.error.get_status() + if status: + r += htmltext('') if formdef: formdata = self.error.get_formdata() @@ -68,6 +81,10 @@ class LoggedErrorDirectory(Directory): r += htmltext('') r += htmltext('') + + if not self.error.traceback: + return r.getvalue() + parts = (N_('Exception'), N_('Stack trace (most recent call first)'), N_('Form'), N_('Cookies'), N_('Environment')) current_part = None diff --git a/wcs/logged_errors.py b/wcs/logged_errors.py index 39e568fb..50f932b2 100644 --- a/wcs/logged_errors.py +++ b/wcs/logged_errors.py @@ -31,6 +31,8 @@ class LoggedError(XmlStorableObject): formdata_id = None formdef_id = None workflow_id = None + status_id = None + status_item_id = None traceback = None occurences_count = 0 first_occurence_timestamp = None @@ -41,32 +43,43 @@ class LoggedError(XmlStorableObject): XML_NODES = [ ('summary', 'str'), ('traceback', 'str'), ('formdata_id', 'str'), ('formdef_id', 'str'), ('workflow_id', 'str'), + ('status_id', 'str'), ('status_item_id', 'str'), ('occurences_count', 'int'), ('first_occurence_timestamp', 'datetime'), ('latest_occurence_timestamp', 'datetime'), ('acked', 'bool')] @classmethod - def record(cls, error_summary, plain_error_msg, publisher): + def record(cls, error_summary, plain_error_msg=None, formdata=None, formdata_id=None, + formdef=None, workflow=None, status=None, status_item=None): error = cls() error.summary = error_summary error.traceback = plain_error_msg - try: - context = publisher.substitutions.get_context_variables() - except: - return - formdef_urlname = context.get('form_slug') - if not formdef_urlname: + if formdata: + error.formdata_id = str(formdata.id) + error.formdef_id = formdata.formdef.id + error.workflow_id = formdata.formdef.workflow.id + else: + error.formdata_id = str(formdata_id) + if formdef: + error.formdef_id = formdef.id + error.workflow_id = formdef.workflow.id + + if not error.formdef_id: # cannot attach error to formdef, don't record in journal, it will # still be sent by email to administrators. return - error.formdata_id = context.get('form_number_raw') - if formdef_urlname: - formdef = FormDef.get_by_urlname(formdef_urlname) - error.formdef_id = formdef.id - error.workflow_id = formdef.workflow_id + if not error.workflow_id: + error.workflow_id = workflow.id + + if status_item: + error.status_item_id = status_item.id + if status_item.parent: + error.status_id = status_item.parent.id + if status: + error.status_id = status.id error.first_occurence_timestamp = datetime.datetime.now() error.id = '%s-%s' % ( @@ -80,9 +93,26 @@ class LoggedError(XmlStorableObject): error.latest_occurence_timestamp = datetime.datetime.now() error.store() + @classmethod + def record_exception(cls, error_summary, plain_error_msg, publisher): + try: + context = publisher.substitutions.get_context_variables() + except Exception as e: + return + formdata_id = context.get('form_number_raw') + formdef_urlname = context.get('form_slug') + if not formdef_urlname: + # cannot attach error to formdef, don't record in journal, it will + # still be sent by email to administrators. + return + formdef = FormDef.get_by_urlname(formdef_urlname) + cls.record(error_summary, plain_error_msg, formdata_id=formdata_id, + formdef=formdef, workflow=formdef.workflow) + @property def tech_id(self): - return ('%s-%s-%s' % (self.formdef_id, self.workflow_id, simplify(self.summary)))[:200] + return ('%s-%s-%s-%s-%s' % (self.formdef_id, self.workflow_id, self.status_id, + self.status_item_id, simplify(self.summary)))[:200] def get_formdef(self): return FormDef.get(self.formdef_id, ignore_errors=True) @@ -91,4 +121,26 @@ class LoggedError(XmlStorableObject): return Workflow.get(self.workflow_id, ignore_errors=True) def get_formdata(self): + if not self.formdata_id: + return None return self.get_formdef().data_class().get(self.formdata_id, ignore_errors=True) + + def get_status(self): + if not self.status_id: + return None + workflow = self.get_workflow() + if not workflow: + return None + for status in workflow.possible_status: + if status.id == self.status_id: + return status + return None + + def get_status_item(self): + status = self.get_status() + if not status or not status.items: + return None + for status_item in status.items: + if status_item.id == self.status_item_id: + return status_item + return None diff --git a/wcs/publisher.py b/wcs/publisher.py index 2a1e49c6..a30c221b 100644 --- a/wcs/publisher.py +++ b/wcs/publisher.py @@ -287,7 +287,7 @@ class WcsPublisher(StubWcsPublisher): super(WcsPublisher, self).log_internal_error(error_summary, plain_error_msg, record=record) if record: - LoggedError.record(error_summary, plain_error_msg, publisher=self) + LoggedError.record_exception(error_summary, plain_error_msg, publisher=self) def apply_global_action_timeouts(self): from wcs.workflows import Workflow, WorkflowGlobalActionTimeoutTrigger diff --git a/wcs/workflows.py b/wcs/workflows.py index bc064e5f..0c65506f 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -1725,12 +1725,12 @@ class WorkflowStatusItem(XmlSerialisable): return [] targets = [x for x in self.parent.parent.possible_status if x.id == self.status] - if not targets: - get_publisher().get_app_logger().error( - 'reference to invalid status in workflow %r, status %r, item %r' % ( - self.parent.parent.name, - self.parent.name, - self.description)) + if not targets and formdata: # do not log in presentation context: formdata is needed + from wcs.logged_errors import LoggedError + message = _('reference to invalid status %s in status %s, action %s') % ( + self.status, self.parent.name, _(self.description)) + LoggedError.record(message, formdata=formdata, status_item=self) + return targets def get_jump_label(self): -- 2.17.1