From f080dd1fec19b0a898993ae22fbb60c40b26966c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Sun, 28 May 2017 20:03:14 +0200 Subject: [PATCH] workflows: add possibility to mark and jump to marked status (#16524) This keeps a stack of marked status and allow to pop back to previous ones. --- tests/test_backoffice_pages.py | 110 ++++++++++++++++++++++++++++++++++++ wcs/admin/workflows.py | 3 +- wcs/backoffice/management.py | 11 +++- wcs/formdata.py | 27 +++++++-- wcs/qommon/static/css/dc2/admin.css | 4 ++ wcs/wf/jump.py | 4 +- wcs/workflows.py | 39 ++++++++++--- 7 files changed, 183 insertions(+), 15 deletions(-) diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py index fd13e892..927fe0e1 100644 --- a/tests/test_backoffice_pages.py +++ b/tests/test_backoffice_pages.py @@ -7,6 +7,7 @@ import shutil import StringIO import time import hashlib +import random import pytest from webtest import Upload @@ -3105,6 +3106,115 @@ def test_inspect_page(pub): .parents('li').children('div.value span') .text() == '\'\\xed\\xa0\\x00\'') +def test_workflow_jump_previous(pub): + user = create_user(pub) + create_environment(pub) + + wf = Workflow(name='jump around') + # North + # / \ + # West <----> East + # | | + # | autojump + # | | + # \ / + # South + + st1 = wf.add_status('North') + st1.id = 'north' + st2 = wf.add_status('West') + st2.id = 'west' + st3 = wf.add_status('East') + st3.id = 'east' + st4 = wf.add_status('Autojump') + st4.id = 'autojump' + st5 = wf.add_status('South') + st5.id = 'south' + + button_by_id = {} + + def add_jump(label, src, dst_id): + jump = ChoiceWorkflowStatusItem() + jump.id = str(random.random()) + jump.label = label + jump.by = ['logged-users'] + jump.status = dst_id + src.items.append(jump) + jump.parent = src + if dst_id != '_previous': + jump.set_marker_on_status = True + button_by_id[label] = 'button%s' % jump.id + return jump + + add_jump('Go West', st1, st2.id) + add_jump('Go East', st1, st3.id) + add_jump('Go South', st2, st5.id) + add_jump('Go Autojump', st3, st4.id) + add_jump('Go Back', st5, '_previous') + + add_jump('Jump West', st3, st2.id) + add_jump('Jump East', st2, st3.id) + + jump = JumpWorkflowStatusItem() + jump.id = '_auto-jump' + jump.status = st5.id + st4.items.append(jump) + jump.parent = st4 + + wf.store() + + formdef = FormDef.get_by_urlname('form-title') + formdef.data_class().wipe() + formdef.workflow = wf + formdef.store() + + formdata = formdef.data_class()() + formdata.data = {} + formdata.just_created() + formdata.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/management/form-title/%s/' % formdata.id) + + # jump around using buttons + resp = resp.form.submit(button_by_id['Go West']).follow() # push (north) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st2.id + resp = resp.form.submit(button_by_id['Go South']).follow() # push (north, west) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st5.id + resp = resp.form.submit(button_by_id['Go Back']).follow() # pop (north) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st2.id + resp = resp.form.submit(button_by_id['Go South']).follow() # push (north, west) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st5.id + resp = resp.form.submit(button_by_id['Go Back']).follow() # pop (north) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st2.id + resp = resp.form.submit(button_by_id['Jump East']).follow() # push (north, west) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st3.id + resp = resp.form.submit(button_by_id['Go Autojump']).follow() # push (north, west, east) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st5.id + + # check markers are displayed in /inspect page + user.is_admin = True + user.store() + resp2 = app.get('/backoffice/management/form-title/%s/inspect' % formdata.id) + assert 'Markers Stack' in resp2.body + assert 'East' in resp2.body + assert 'West' in resp2.body + assert 'North' in resp2.body + assert resp2.body.find('East') < resp2.body.find('West') + assert resp2.body.find('West') < resp2.body.find('North') + + resp = resp.form.submit(button_by_id['Go Back']).follow() # pop (north, west) + assert formdef.data_class().get(formdata.id).status == 'wf-%s' % st3.id + + # and do a last jump using the API + formdata = formdef.data_class().get(formdata.id) + formdata.jump_status('_previous') # pop (north) + assert formdata.status == 'wf-%s' % st2.id + + formdata = formdef.data_class().get(formdata.id) + formdata.jump_status('_previous') # pop () + assert formdata.status == 'wf-%s' % st1.id + def test_backoffice_fields(pub): user = create_user(pub) create_environment(pub) diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py index 71961e1c..daa76895 100644 --- a/wcs/admin/workflows.py +++ b/wcs/admin/workflows.py @@ -153,7 +153,8 @@ def graphviz(workflow, url_prefix='', select=None, svg=True, for status in workflow.possible_status: i = status.id for item in status.items: - next_status_ids = [x.id for x in item.get_target_status() if x.id != status.id] + next_status_ids = [x.id for x in item.get_target_status() + if x.id and x.id != status.id] if not next_status_ids: next_status_ids = [status.id] done = {} diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py index 55d39d1d..468c3627 100644 --- a/wcs/backoffice/management.py +++ b/wcs/backoffice/management.py @@ -2085,9 +2085,18 @@ class FormBackOfficeStatusPage(FormStatusPage): if not isinstance(v, basestring): r += htmltext(' (%r)') % type(v) r += htmltext('') + + if '_markers_stack' in (self.filled.workflow_data or {}): + r += htmltext('
  • %s

  • ') % _('Markers Stack') + for marker in reversed(self.filled.workflow_data['_markers_stack']): + status = self.filled.get_status(marker['status_id']) + if status: + r += htmltext('
  • %s
  • ') % status.name + else: + r += htmltext('
  • %s
  • ') % _('Unknown') + r += htmltext('') r += htmltext('') - return r.getvalue() diff --git a/wcs/formdata.py b/wcs/formdata.py index ff089bde..146f7e2d 100644 --- a/wcs/formdata.py +++ b/wcs/formdata.py @@ -404,9 +404,10 @@ class FormData(StorableObject): return None if not self.formdef: return None + if status.startswith('wf-'): + status = status[3:] try: - status_id = status.split('-')[1] - wf_status = [x for x in self.formdef.workflow.possible_status if x.id == status_id][0] + wf_status = [x for x in self.formdef.workflow.possible_status if x.id == status][0] except IndexError: return None return wf_status @@ -453,7 +454,21 @@ class FormData(StorableObject): return None return wf_status.handle_form(form, self, user) + def get_previous_marked_status(self): + if not self.workflow_data or not '_markers_stack' in self.workflow_data: + return None + try: + marker_data = self.workflow_data['_markers_stack'].pop() + status_id = marker_data['status_id'] + except IndexError: + return None + return self.formdef.workflow.get_status(status_id) + def jump_status(self, status_id): + if status_id == '_previous': + previous_status = self.get_previous_marked_status() + assert previous_status, 'failed to compute previous status' + status_id = previous_status.id evo = Evolution() evo.time = time.localtime() evo.status = 'wf-%s' % status_id @@ -632,9 +647,13 @@ class FormData(StorableObject): if self.workflow_data: d.update(self.workflow_data) - # pass over uploaded files and attach an extra attribute with the - # url to the file. + # pass over workflow data to: + # - attach an extra url attribute to uploaded files + # - remove "private" attributes for k, v in self.workflow_data.items(): + if k[0] == '_': + del d[k] + continue if isinstance(v, Upload): try: formvar, fieldvar = re.match('(.*)_var_(.*)_raw$', k).groups() diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 85079f6d..879d1542 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -1525,6 +1525,10 @@ ul.form-inspector li code { font-size: 100%; } +ul.form-inspector li span.status { + padding: 0 1ex; +} + ul.form-inspector li div.value { display: block; padding: 0 0 0.5ex 1em; diff --git a/wcs/wf/jump.py b/wcs/wf/jump.py index af4dcf49..c2c92b50 100644 --- a/wcs/wf/jump.py +++ b/wcs/wf/jump.py @@ -145,7 +145,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem): return _('Change Status Automatically (to %s)') % wf_status[0].name def get_parameters(self): - return ('status', 'condition', 'trigger', 'by', 'timeout') + return ('status', 'set_marker_on_status', 'condition', 'trigger', 'by', 'timeout') def add_parameters_widgets(self, form, parameters, prefix='', formdef=None): WorkflowStatusJumpItem.add_parameters_widgets(self, form, parameters, prefix, formdef) @@ -190,7 +190,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem): return if self.must_jump(formdata): - wf_status = self.get_target_status() + wf_status = self.get_target_status(formdata) if wf_status: formdata.status = 'wf-%s' % wf_status[0].id diff --git a/wcs/workflows.py b/wcs/workflows.py index 26f6b231..82232144 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -1633,11 +1633,21 @@ class WorkflowStatusItem(XmlSerialisable): def get_substitution_variables(self, formdata): return {} - def get_target_status(self): + def get_target_status(self, formdata=None): """Returns a list of status this item can lead to.""" if not getattr(self, 'status', None): return [] + if self.status == '_previous': + if formdata is None: + # must be in a formdata to compute destination, just give a + # fake status for presentation purpose + return [WorkflowStatus(_('Previously Marked Status'))] + previous_status = formdata.get_previous_marked_status() + if previous_status: + return [previous_status] + return [] + try: return [x for x in self.parent.parent.possible_status if x.id == self.status] except IndexError: @@ -1684,14 +1694,21 @@ class WorkflowStatusItem(XmlSerialisable): class WorkflowStatusJumpItem(WorkflowStatusItem): status = None endpoint = False + set_marker_on_status = False def add_parameters_widgets(self, form, parameters, prefix='', formdef=None): if 'status' in parameters: form.add(SingleSelectWidget, '%sstatus' % prefix, title = _('Status'), value = self.status, - options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status]) + options = [(None, '---')] + + [(x.id, x.name) for x in self.parent.parent.possible_status] + + [('_previous', _('Previously Marked Status'))]) + if 'set_marker_on_status' in parameters: + form.add(CheckboxWidget, '%sset_marker_on_status' % prefix, + title=_('Set marker on status'), + value=self.set_marker_on_status) def get_parameters(self): - return ('status',) + return ('status', 'set_marker_on_status') def get_role_translation(formdata, role_name): @@ -1891,9 +1908,16 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem): def submit_form(self, form, formdata, user, evo): if form.get_submit() == 'button%s' % self.id: - wf_status = self.get_target_status() + wf_status = self.get_target_status(formdata) if wf_status: evo.status = 'wf-%s' % wf_status[0].id + if self.set_marker_on_status: + if formdata.workflow_data and '_markers_stack' in formdata.workflow_data: + markers_stack = formdata.workflow_data.get('_markers_stack') + else: + markers_stack = [] + markers_stack.append({'status_id': formdata.status[3:]}) + formdata.update_workflow_data({'_markers_stack': markers_stack}) form.clear_errors() return True # get out of processing loop @@ -1918,7 +1942,8 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem): value=self.backoffice_info_text) def get_parameters(self): - return ('by', 'status', 'label', 'backoffice_info_text', 'require_confirmation') + return ('by', 'status', 'label', 'backoffice_info_text', + 'require_confirmation', 'set_marker_on_status') register_item_class(ChoiceWorkflowStatusItem) @@ -1939,12 +1964,12 @@ class JumpOnSubmitWorkflowStatusItem(WorkflowStatusJumpItem): def submit_form(self, form, formdata, user, evo): if form.is_submitted() and not form.has_errors(): - wf_status = self.get_target_status() + wf_status = self.get_target_status(formdata) if wf_status: evo.status = 'wf-%s' % wf_status[0].id def get_parameters(self): - return ('status',) + return ('status', 'set_marker_on_status') register_item_class(JumpOnSubmitWorkflowStatusItem) -- 2.11.0