diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py index e16494a8c..71aa46eab 100644 --- a/tests/backoffice_pages/test_all.py +++ b/tests/backoffice_pages/test_all.py @@ -2373,8 +2373,8 @@ def test_backoffice_wscall_attachment(http_requests, pub): assert resp.text == '' formdata = formdef.data_class().get(number31.id) - assert formdata.evolution[-1].parts[0].orig_filename == 'xxx.xml' - assert formdata.evolution[-1].parts[0].content_type == 'text/xml' + assert formdata.evolution[-1].parts[1].orig_filename == 'xxx.xml' + assert formdata.evolution[-1].parts[1].content_type == 'text/xml' assert formdata.get_substitution_variables()['attachments'].xxx.filename == 'xxx.xml' resp = app.get(formdata.get_substitution_variables()['attachments'].xxx.url) resp = resp.follow() @@ -6104,3 +6104,90 @@ def test_status_visibility(pub): wf.store() resp = app.get(formdata.get_backoffice_url()) assert resp.pyquery('.status').text() == 'st1 st2 st3' + + +def test_workflow_track_jumps(pub): + create_user(pub) + create_environment(pub) + + wf = Workflow(name='blah') + st1 = wf.add_status('One') + st1.id = 'one' + st2 = wf.add_status('Two') + st2.id = 'two' + + st3 = wf.add_status('Three') + st3.id = 'three' + st4 = wf.add_status('Four') + st4.id = 'four' + st5 = wf.add_status('Five') + st5.id = 'five' + + commentable = st1.add_action('commentable', id='_commentable') + commentable.by = ['_submitter', '_receiver'] + commentable.button_label = 'CLICK ME!' + + jump = st1.add_action('jumponsubmit', id='_jump') + jump.status = st2.id + jump.identifier = 'to_two' + + to_three = st2.add_action('choice', id='_tothree') + to_three.label = 'ToThree' + to_three.by = ['_receiver'] + to_three.status = 'three' + to_three.identifier = 'to_three' + + to_four = st3.add_action('choice', id='_tofour') + to_four.label = 'ToFour' + to_four.by = ['_receiver'] + to_four.status = 'four' + to_four.identifier = 'to_four' + + to_five = st4.add_action('jump', id='_jump') + to_five.status = 'five' + to_five.identifier = 'to_five' + + 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) + resp.form['comment'] = 'HELLO WORLD' + resp = resp.form.submit('button_commentable') + resp = resp.follow() + + formdata.refresh_from_storage() + assert formdata.status == 'wf-two' + substitution_variables = formdata.get_substitution_variables() + assert substitution_variables['form_jumps'] == ['to_two'] + assert substitution_variables['form_latest_jump'] == 'to_two' + + resp = resp.form.submit('button_tothree') + resp = resp.follow() + formdata.refresh_from_storage() + assert formdata.status == 'wf-three' + substitution_variables = formdata.get_substitution_variables() + assert substitution_variables['form_jumps'] == ['to_two', 'to_three'] + assert substitution_variables['form_latest_jump'] == 'to_three' + + resp = resp.form.submit('button_tofour') + resp = resp.follow() + formdata.refresh_from_storage() + assert formdata.status == 'wf-five' + substitution_variables = formdata.get_substitution_variables() + assert substitution_variables['form_jumps'] == [ + 'to_two', + 'to_three', + 'to_four', + 'to_five', + ] + assert substitution_variables['form_latest_jump'] == 'to_five' diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py index 8b07a424e..72df3f98b 100644 --- a/tests/form_pages/test_all.py +++ b/tests/form_pages/test_all.py @@ -5438,7 +5438,7 @@ def test_rich_commentable_action(pub): assert '

hello world

' in resp.text formdata = formdef.data_class().select()[0] - assert formdata.evolution[-1].parts[-1].comment == '

hello world

' + assert formdata.evolution[-1].parts[-2].comment == '

hello world

' # check link resp.form['comment'] = '

hello link.

' @@ -5446,7 +5446,7 @@ def test_rich_commentable_action(pub): assert '

hello link.

' in resp.text formdata = formdef.data_class().select()[0] assert ( - formdata.evolution[-1].parts[-1].comment + formdata.evolution[-1].parts[-2].comment == '

hello link.

' ) @@ -5455,7 +5455,7 @@ def test_rich_commentable_action(pub): resp = resp.form.submit('button_x1').follow() assert '

hello evil

' in resp.text formdata = formdef.data_class().select()[0] - assert formdata.evolution[-1].parts[-1].comment == '

hello evil

' + assert formdata.evolution[-1].parts[-2].comment == '

hello evil

' resp.form['comment'] = '

' # left empty resp = resp.form.submit('button_x1') @@ -5486,7 +5486,7 @@ def test_rich_commentable_action(pub): resp.form['comment'] = '

hello
world

' resp = resp.form.submit('button_x1').follow() formdata = formdef.data_class().select()[0] - assert formdata.evolution[-1].parts[-1].comment == '

hello
world

' + assert formdata.evolution[-1].parts[-2].comment == '

hello
world

' pub.substitutions.feed(formdata) context = pub.substitutions.get_context_variables(mode='lazy') tmpl = Template('{{form_comment}}') diff --git a/tests/test_ctl.py b/tests/test_ctl.py index 242272008..23188a6fe 100644 --- a/tests/test_ctl.py +++ b/tests/test_ctl.py @@ -168,12 +168,14 @@ def test_trigger_jumps(pub): formdata.id = 1 formdata.data = {'0': 'Alice', '1': 'alice@example.net'} formdata.status = 'wf-%s' % st1.id + formdata.just_created() formdata.store() id1 = formdata.id formdata = formdef.data_class()() formdata.id = 2 formdata.data = {'0': 'Bob', '1': 'bob@example.net'} formdata.status = 'wf-%s' % st1.id + formdata.just_created() formdata.store() id2 = formdata.id select_and_jump_formdata(formdef, trigger, rows) diff --git a/wcs/formdata.py b/wcs/formdata.py index 8d9aa6c4b..9112f7ba9 100644 --- a/wcs/formdata.py +++ b/wcs/formdata.py @@ -790,7 +790,7 @@ class FormData(StorableObject): return None def jump_status(self, status_id, user_id=None): - from wcs.workflows import ContentSnapshotPart + from wcs.workflows import ContentSnapshotPart, JumpEvolutionPart if status_id == '_previous': previous_status = self.pop_previous_marked_status() @@ -811,7 +811,11 @@ class FormData(StorableObject): self.status == status and self.evolution[-1].status == status and not self.evolution[-1].comment - and not [x for x in self.evolution[-1].parts or [] if not isinstance(x, ContentSnapshotPart)] + and not [ + x + for x in self.evolution[-1].parts or [] + if not isinstance(x, (ContentSnapshotPart, JumpEvolutionPart)) + ] ): # if status do not change and last evolution is empty, # just update last jump time on last evolution, do not add one diff --git a/wcs/variables.py b/wcs/variables.py index 0ef54211c..fec29f088 100644 --- a/wcs/variables.py +++ b/wcs/variables.py @@ -1011,6 +1011,29 @@ class LazyFormData(LazyFormDef): data = self._formdata.workflow_data or {} return {k: data[k] for k in data if not k.startswith('_')} + @property + def jumps(self): + from wcs.workflows import JumpEvolutionPart + + jump_parts = [] + for part in self._formdata.iter_evolution_parts(): + if not isinstance(part, JumpEvolutionPart): + continue + jump_parts.append(part.identifier or 'undefined') + return jump_parts + + @property + def latest_jump(self): + from wcs.workflows import JumpEvolutionPart + + if self._formdata.evolution: + for evolution in reversed(self._formdata.evolution or []): + if evolution.parts: + for part in reversed(evolution.parts): + if isinstance(part, JumpEvolutionPart): + return part.identifier or 'undefined' + return '' + def export_to_json(self, include_files=True): # this gets used to generate an email attachment :/ return self._formdata.export_to_json(include_files=include_files) diff --git a/wcs/wf/choice.py b/wcs/wf/choice.py index bfbba2867..7c0ce076e 100644 --- a/wcs/wf/choice.py +++ b/wcs/wf/choice.py @@ -21,7 +21,6 @@ from wcs.qommon.form import ( CheckboxWidget, ComputedExpressionWidget, SingleSelectWidget, - VarnameWidget, WidgetList, WysiwygTextWidget, ) @@ -35,7 +34,6 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem): ok_in_global_action = True label = None - identifier = None by = [] backoffice_info_text = None require_confirmation = False @@ -116,6 +114,7 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem): formdata.record_workflow_event('button', action_item_id=self.id) evo.status = 'wf-%s' % wf_status[0].id self.handle_markers_stack(formdata) + self.add_jump_part(formdata, evo) form.clear_errors() return True # get out of processing loop @@ -156,14 +155,6 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem): title=_('Information Text for Backoffice'), value=self.backoffice_info_text, ) - if 'identifier' in parameters: - form.add( - VarnameWidget, - '%sidentifier' % prefix, - title=_('Identifier'), - value=self.identifier, - advanced=True, - ) if 'ignore_form_errors' in parameters: form.add( CheckboxWidget, diff --git a/wcs/wf/jump.py b/wcs/wf/jump.py index 09c55f2aa..0f6806cb2 100644 --- a/wcs/wf/jump.py +++ b/wcs/wf/jump.py @@ -42,6 +42,7 @@ JUMP_TIMEOUT_INTERVAL = max((60 // int(os.environ.get('WCS_JUMP_TIMEOUT_CHECKS', def jump_and_perform(formdata, action, workflow_data=None): action.handle_markers_stack(formdata) + action.add_jump_part(formdata) if workflow_data: formdata.update_workflow_data(workflow_data) formdata.store() @@ -186,7 +187,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem): def get_parameters(self): if hasattr(self, 'parent') and isinstance(self.parent, WorkflowGlobalAction): return ('status', 'condition', 'set_marker_on_status') - return ('status', 'condition', 'trigger', 'by', 'timeout', 'set_marker_on_status') + return ('status', 'condition', 'trigger', 'by', 'timeout', 'set_marker_on_status', 'identifier') def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs): super().add_parameters_widgets(form, parameters, prefix, formdef, **kwargs) @@ -260,7 +261,9 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem): wf_status = self.get_target_status(formdata) if wf_status: self.handle_markers_stack(formdata) + self.add_jump_part(formdata) formdata.status = 'wf-%s' % wf_status[0].id + formdata.store() def check_condition(self, formdata, *args, trigger=None, **kwargs): result = super().check_condition(formdata, *args, **kwargs) diff --git a/wcs/wf/jump_on_submit.py b/wcs/wf/jump_on_submit.py index e7ca9173e..29b92b561 100644 --- a/wcs/wf/jump_on_submit.py +++ b/wcs/wf/jump_on_submit.py @@ -45,9 +45,10 @@ class JumpOnSubmitWorkflowStatusItem(WorkflowStatusJumpItem): if wf_status: evo.status = 'wf-%s' % wf_status[0].id self.handle_markers_stack(formdata) + self.add_jump_part(formdata, evo) def get_parameters(self): - return ('status', 'set_marker_on_status', 'condition') + return ('status', 'set_marker_on_status', 'condition', 'identifier') register_item_class(JumpOnSubmitWorkflowStatusItem) diff --git a/wcs/workflows.py b/wcs/workflows.py index 42b7c4f85..ab632dae8 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -55,6 +55,7 @@ from .qommon.form import ( SingleSelectWidget, SingleSelectWidgetWithOther, StringWidget, + VarnameWidget, WidgetList, WidgetListOfRoles, ) @@ -3230,11 +3231,17 @@ class WorkflowStatusItem(XmlSerialisable): return '<%s>' % ' '.join(parts) +class JumpEvolutionPart(EvolutionPart): + def __init__(self, identifier): + self.identifier = identifier + + class WorkflowStatusJumpItem(WorkflowStatusItem): status = None endpoint = False set_marker_on_status = False category = 'status-change' + identifier = None def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs): super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs) @@ -3256,9 +3263,23 @@ class WorkflowStatusJumpItem(WorkflowStatusItem): advanced=True, ) + if 'identifier' in parameters: + form.add( + VarnameWidget, + '%sidentifier' % prefix, + title=_('Identifier'), + value=self.identifier, + advanced=True, + ) + def get_parameters(self): return ('status', 'set_marker_on_status', 'condition') + def add_jump_part(self, formdata, evo=None): + if evo is None: + evo = formdata.evolution[-1] + evo.add_part(JumpEvolutionPart(self.identifier)) + class NoLongerAvailableAction(WorkflowStatusItem): pass # marker class, loadable from pickle files but removed in migrate()