workflows : add an identifier to automatic and on submit jumps (#74723) #161

Open
ecazenave wants to merge 2 commits from wip/74723-last-jump into main
9 changed files with 152 additions and 20 deletions

View File

@ -2373,8 +2373,8 @@ def test_backoffice_wscall_attachment(http_requests, pub):
assert resp.text == '<?xml version="1.0"><foo/>' assert resp.text == '<?xml version="1.0"><foo/>'
formdata = formdef.data_class().get(number31.id) formdata = formdef.data_class().get(number31.id)
assert formdata.evolution[-1].parts[0].orig_filename == 'xxx.xml' assert formdata.evolution[-1].parts[1].orig_filename == 'xxx.xml'
assert formdata.evolution[-1].parts[0].content_type == 'text/xml' assert formdata.evolution[-1].parts[1].content_type == 'text/xml'
assert formdata.get_substitution_variables()['attachments'].xxx.filename == 'xxx.xml' assert formdata.get_substitution_variables()['attachments'].xxx.filename == 'xxx.xml'
resp = app.get(formdata.get_substitution_variables()['attachments'].xxx.url) resp = app.get(formdata.get_substitution_variables()['attachments'].xxx.url)
resp = resp.follow() resp = resp.follow()
@ -6104,3 +6104,90 @@ def test_status_visibility(pub):
wf.store() wf.store()
resp = app.get(formdata.get_backoffice_url()) resp = app.get(formdata.get_backoffice_url())
assert resp.pyquery('.status').text() == 'st1 st2 st3' 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'

View File

@ -5438,7 +5438,7 @@ def test_rich_commentable_action(pub):
assert '<p>hello <i>world</i></p>' in resp.text assert '<p>hello <i>world</i></p>' in resp.text
formdata = formdef.data_class().select()[0] formdata = formdef.data_class().select()[0]
assert formdata.evolution[-1].parts[-1].comment == '<p>hello <i>world</i></p>' assert formdata.evolution[-1].parts[-2].comment == '<p>hello <i>world</i></p>'
# check link # check link
resp.form['comment'] = '<p>hello <a href="http://localhost/">link</a>.</p>' resp.form['comment'] = '<p>hello <a href="http://localhost/">link</a>.</p>'
@ -5446,7 +5446,7 @@ def test_rich_commentable_action(pub):
assert '<p>hello <a href="http://localhost/" rel="nofollow">link</a>.</p>' in resp.text assert '<p>hello <a href="http://localhost/" rel="nofollow">link</a>.</p>' in resp.text
formdata = formdef.data_class().select()[0] formdata = formdef.data_class().select()[0]
assert ( assert (
formdata.evolution[-1].parts[-1].comment formdata.evolution[-1].parts[-2].comment
== '<p>hello <a href="http://localhost/" rel="nofollow">link</a>.</p>' == '<p>hello <a href="http://localhost/" rel="nofollow">link</a>.</p>'
) )
@ -5455,7 +5455,7 @@ def test_rich_commentable_action(pub):
resp = resp.form.submit('button_x1').follow() resp = resp.form.submit('button_x1').follow()
assert '<p>hello evil</p>' in resp.text assert '<p>hello evil</p>' in resp.text
formdata = formdef.data_class().select()[0] formdata = formdef.data_class().select()[0]
assert formdata.evolution[-1].parts[-1].comment == '<p>hello evil</p>' assert formdata.evolution[-1].parts[-2].comment == '<p>hello evil</p>'
resp.form['comment'] = '<p></p>' # left empty resp.form['comment'] = '<p></p>' # left empty
resp = resp.form.submit('button_x1') resp = resp.form.submit('button_x1')
@ -5486,7 +5486,7 @@ def test_rich_commentable_action(pub):
resp.form['comment'] = '<p>hello<br>world</p>' resp.form['comment'] = '<p>hello<br>world</p>'
resp = resp.form.submit('button_x1').follow() resp = resp.form.submit('button_x1').follow()
formdata = formdef.data_class().select()[0] formdata = formdef.data_class().select()[0]
assert formdata.evolution[-1].parts[-1].comment == '<p>hello<br>world</p>' assert formdata.evolution[-1].parts[-2].comment == '<p>hello<br>world</p>'
pub.substitutions.feed(formdata) pub.substitutions.feed(formdata)
context = pub.substitutions.get_context_variables(mode='lazy') context = pub.substitutions.get_context_variables(mode='lazy')
tmpl = Template('{{form_comment}}') tmpl = Template('{{form_comment}}')

View File

@ -168,12 +168,14 @@ def test_trigger_jumps(pub):
formdata.id = 1 formdata.id = 1
formdata.data = {'0': 'Alice', '1': 'alice@example.net'} formdata.data = {'0': 'Alice', '1': 'alice@example.net'}
formdata.status = 'wf-%s' % st1.id formdata.status = 'wf-%s' % st1.id

Sinon le tests crash sur add_jump_part qui ne trouve pas d'évolution.

L'absence d'evolution me parait être une situation artificielle du test, d'où l'ajout du just_created.

Sinon le tests crash sur add_jump_part qui ne trouve pas d'évolution. L'absence d'evolution me parait être une situation artificielle du test, d'où l'ajout du just_created.
formdata.just_created()
formdata.store() formdata.store()
id1 = formdata.id id1 = formdata.id
formdata = formdef.data_class()() formdata = formdef.data_class()()
formdata.id = 2 formdata.id = 2
formdata.data = {'0': 'Bob', '1': 'bob@example.net'} formdata.data = {'0': 'Bob', '1': 'bob@example.net'}
formdata.status = 'wf-%s' % st1.id formdata.status = 'wf-%s' % st1.id
formdata.just_created()
formdata.store() formdata.store()
id2 = formdata.id id2 = formdata.id
select_and_jump_formdata(formdef, trigger, rows) select_and_jump_formdata(formdef, trigger, rows)

View File

@ -790,7 +790,7 @@ class FormData(StorableObject):
return None return None
def jump_status(self, status_id, user_id=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': if status_id == '_previous':
previous_status = self.pop_previous_marked_status() previous_status = self.pop_previous_marked_status()
@ -811,7 +811,11 @@ class FormData(StorableObject):
self.status == status self.status == status
and self.evolution[-1].status == status and self.evolution[-1].status == status
and not self.evolution[-1].comment 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, # if status do not change and last evolution is empty,
# just update last jump time on last evolution, do not add one # just update last jump time on last evolution, do not add one

View File

@ -1011,6 +1011,29 @@ class LazyFormData(LazyFormDef):
data = self._formdata.workflow_data or {} data = self._formdata.workflow_data or {}
return {k: data[k] for k in data if not k.startswith('_')} 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): def export_to_json(self, include_files=True):
# this gets used to generate an email attachment :/ # this gets used to generate an email attachment :/
return self._formdata.export_to_json(include_files=include_files) return self._formdata.export_to_json(include_files=include_files)

View File

@ -21,7 +21,6 @@ from wcs.qommon.form import (
CheckboxWidget, CheckboxWidget,
ComputedExpressionWidget, ComputedExpressionWidget,
SingleSelectWidget, SingleSelectWidget,
VarnameWidget,
WidgetList, WidgetList,
WysiwygTextWidget, WysiwygTextWidget,
) )
@ -35,7 +34,6 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
ok_in_global_action = True ok_in_global_action = True
label = None label = None
identifier = None
by = [] by = []
backoffice_info_text = None backoffice_info_text = None
require_confirmation = False require_confirmation = False
@ -116,6 +114,7 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
formdata.record_workflow_event('button', action_item_id=self.id) formdata.record_workflow_event('button', action_item_id=self.id)
evo.status = 'wf-%s' % wf_status[0].id evo.status = 'wf-%s' % wf_status[0].id
self.handle_markers_stack(formdata) self.handle_markers_stack(formdata)
self.add_jump_part(formdata, evo)

On passe l'evolution qui vient d'être créée sinon pas de persistance de la JumpEvolutionPart (voir plus bas).

On passe l'evolution qui vient d'être créée sinon pas de persistance de la JumpEvolutionPart (voir plus bas).
form.clear_errors() form.clear_errors()
return True # get out of processing loop return True # get out of processing loop
@ -156,14 +155,6 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
title=_('Information Text for Backoffice'), title=_('Information Text for Backoffice'),
value=self.backoffice_info_text, 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: if 'ignore_form_errors' in parameters:
form.add( form.add(
CheckboxWidget, CheckboxWidget,

View File

@ -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): def jump_and_perform(formdata, action, workflow_data=None):
action.handle_markers_stack(formdata) action.handle_markers_stack(formdata)
action.add_jump_part(formdata)
if workflow_data: if workflow_data:
formdata.update_workflow_data(workflow_data) formdata.update_workflow_data(workflow_data)
formdata.store() formdata.store()
@ -186,7 +187,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
def get_parameters(self): def get_parameters(self):
if hasattr(self, 'parent') and isinstance(self.parent, WorkflowGlobalAction): if hasattr(self, 'parent') and isinstance(self.parent, WorkflowGlobalAction):
return ('status', 'condition', 'set_marker_on_status') 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): def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
super().add_parameters_widgets(form, parameters, prefix, formdef, **kwargs) super().add_parameters_widgets(form, parameters, prefix, formdef, **kwargs)
@ -260,7 +261,9 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
wf_status = self.get_target_status(formdata) wf_status = self.get_target_status(formdata)
if wf_status: if wf_status:
self.handle_markers_stack(formdata) self.handle_markers_stack(formdata)
self.add_jump_part(formdata)
formdata.status = 'wf-%s' % wf_status[0].id formdata.status = 'wf-%s' % wf_status[0].id
formdata.store()
def check_condition(self, formdata, *args, trigger=None, **kwargs): def check_condition(self, formdata, *args, trigger=None, **kwargs):
result = super().check_condition(formdata, *args, **kwargs) result = super().check_condition(formdata, *args, **kwargs)

View File

@ -45,9 +45,10 @@ class JumpOnSubmitWorkflowStatusItem(WorkflowStatusJumpItem):
if wf_status: if wf_status:
evo.status = 'wf-%s' % wf_status[0].id evo.status = 'wf-%s' % wf_status[0].id
self.handle_markers_stack(formdata) self.handle_markers_stack(formdata)
self.add_jump_part(formdata, evo)
def get_parameters(self): def get_parameters(self):
return ('status', 'set_marker_on_status', 'condition') return ('status', 'set_marker_on_status', 'condition', 'identifier')
register_item_class(JumpOnSubmitWorkflowStatusItem) register_item_class(JumpOnSubmitWorkflowStatusItem)

View File

@ -55,6 +55,7 @@ from .qommon.form import (
SingleSelectWidget, SingleSelectWidget,
SingleSelectWidgetWithOther, SingleSelectWidgetWithOther,
StringWidget, StringWidget,
VarnameWidget,
WidgetList, WidgetList,
WidgetListOfRoles, WidgetListOfRoles,
) )
@ -3230,11 +3231,17 @@ class WorkflowStatusItem(XmlSerialisable):
return '<%s>' % ' '.join(parts) return '<%s>' % ' '.join(parts)
class JumpEvolutionPart(EvolutionPart):
def __init__(self, identifier):
self.identifier = identifier
class WorkflowStatusJumpItem(WorkflowStatusItem): class WorkflowStatusJumpItem(WorkflowStatusItem):
status = None status = None
endpoint = False endpoint = False
set_marker_on_status = False set_marker_on_status = False
category = 'status-change' category = 'status-change'
identifier = None
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs): def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs) super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
@ -3256,9 +3263,23 @@ class WorkflowStatusJumpItem(WorkflowStatusItem):
advanced=True, advanced=True,
) )
if 'identifier' in parameters:
form.add(
VarnameWidget,
'%sidentifier' % prefix,
title=_('Identifier'),
value=self.identifier,
advanced=True,
)
def get_parameters(self): def get_parameters(self):
return ('status', 'set_marker_on_status', 'condition') 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): class NoLongerAvailableAction(WorkflowStatusItem):
pass # marker class, loadable from pickle files but removed in migrate() pass # marker class, loadable from pickle files but removed in migrate()