workflows : add an identifier to automatic and on submit jumps (#74723) #161
|
@ -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'
|
||||||
|
|
|
@ -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}}')
|
||||||
|
|
|
@ -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
|
||||||
|
|||||||
|
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
ecazenave
commented
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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
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.