workflows: delay cleaning of live workflow form data (#64792)

This commit is contained in:
Frédéric Péters 2022-05-03 16:03:12 +02:00
parent 420e68bd29
commit e8668e89b6
4 changed files with 170 additions and 6 deletions

View File

@ -7978,6 +7978,166 @@ def test_frontoffice_workflow_form_with_dynamic_comment(pub):
assert live_resp.json['result']['blah_2']['content'] == '<p>value is test</p>'
def test_frontoffice_workflow_form_with_dynamic_list(pub):
user = create_user(pub)
wf = Workflow('dynamic list in workflow')
status = wf.add_status('st1')
status2 = wf.add_status('st2')
display_form = status.add_action('form', id='_display_form')
display_form.by = ['_submitter']
display_form.varname = 'blah'
display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
display_form.formdef.fields = [
fields.ItemField(id='1', label='Test', varname='foo', type='item', items=['10', '20']),
fields.ItemField(
id='2',
label='Test2',
type='item',
varname='item2',
data_source={'type': 'json', 'value': 'http://example.org/{{form_workflow_form_blah_var_foo}}'},
),
]
jump1 = status.add_action('choice', id='_jump')
jump1.label = 'Jump'
jump1.by = ['_submitter']
jump1.status = status2.id
wf.store()
formdef = create_formdef()
formdef.workflow_id = wf.id
formdef.store()
formdef.data_class().wipe()
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.store()
app = login(get_app(pub), username='foo', password='foo')
resp = app.get(formdata.get_url(backoffice=False))
with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
def side_effect(url, *args):
print('url:', url)
if url == 'http://example.org/10':
data = [
{'id': '1', 'text': 'hello', 'extra': 'foo'},
{'id': '2', 'text': 'world', 'extra': 'bar'},
]
elif url == 'http://example.org/20':
data = [
{'id': '11', 'text': 'hello2', 'extra': 'foo'},
{'id': '21', 'text': 'world2', 'extra': 'bar'},
]
else:
data = []
return io.StringIO(json.dumps({'data': data}))
urlopen.side_effect = side_effect
live_url = resp.html.find('form').attrs['data-live-url']
live_resp = app.post(live_url + '?modified_field_id=init', params=resp.form.submit_fields())
assert [x['id'] for x in live_resp.json['result']['blah_2']['items']] == ['1', '2']
resp.form['fblah_1'] = '20'
live_resp = app.post(live_url + '?modified_field_id=blah_1', params=resp.form.submit_fields())
assert [x['id'] for x in live_resp.json['result']['blah_2']['items']] == ['11', '21']
resp.form['fblah_2'].force_value('11')
resp = resp.form.submit('submit').follow()
assert 'Technical error, please try again' not in resp.text
formdata.refresh_from_storage()
pub.substitutions.feed(formdata)
context = pub.substitutions.get_context_variables(mode='lazy')
assert context['form_workflow_form_blah_var_item2'] == 'hello2'
assert context['form_workflow_data_blah_var_item2'] == 'hello2'
@pytest.mark.parametrize('button_position', ['before', 'after'])
def test_frontoffice_workflow_form_and_other_button(pub, button_position):
user = create_user(pub)
wf = Workflow('form and other button')
status = wf.add_status('st1')
status2 = wf.add_status('st2')
display_form = status.add_action('form', id='_display_form')
display_form.by = ['_submitter']
display_form.varname = 'blah'
display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
display_form.formdef.fields = [
fields.StringField(id='1', label='Test', varname='foo', type='string', required=True),
fields.StringField(id='2', label='Test2', varname='foo2', type='string', required=True),
]
jump1 = status.add_action('choice', id='_jump', prepend=bool(button_position == 'before'))
jump1.label = 'Jump'
jump1.by = ['_submitter']
jump1.status = status2.id
wf.store()
formdef = create_formdef()
formdef.workflow_id = wf.id
formdef.store()
formdef.data_class().wipe()
app = login(get_app(pub), username='foo', password='foo')
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.store()
resp = app.get(formdata.get_url(backoffice=False))
resp.form['fblah_1'] = 'a'
resp.form['fblah_2'] = 'b'
resp = resp.form.submit('submit')
formdata.refresh_from_storage()
pub.substitutions.reset()
pub.substitutions.feed(formdata)
context = pub.substitutions.get_context_variables(mode='lazy')
assert 'form_workflow_form_blah_var_foo' in context
assert context['form_workflow_form_blah_var_foo'] == 'a'
assert context['form_workflow_data_blah_var_foo'] == 'a'
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.store()
resp = app.get(formdata.get_url(backoffice=False))
resp.form['fblah_1'] = 'a'
resp.form['fblah_2'] = 'b'
resp = resp.form.submit('button_jump')
formdata.refresh_from_storage()
pub.substitutions.reset()
pub.substitutions.feed(formdata)
context = pub.substitutions.get_context_variables(mode='lazy')
# check workflow form data is not saved (good)
assert 'form_workflow_form_blah_var_foo' not in context
# but legacy behaviout it leaks into workflow_data :/
assert context['form_workflow_data_blah_var_foo'] == 'a'
# check it also happens with invalid/partial form
jump1.ignore_form_errors = True
wf.store()
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.store()
resp = app.get(formdata.get_url(backoffice=False))
resp.form['fblah_1'] = 'a'
resp.form['fblah_2'] = ''
resp = resp.form.submit('button_jump')
formdata.refresh_from_storage()
pub.substitutions.reset()
pub.substitutions.feed(formdata)
context = pub.substitutions.get_context_variables(mode='lazy')
# check workflow form data is not saved (good)
assert 'form_workflow_form_blah_var_foo' not in context
# but legacy behaviout it leaks into workflow_data :/
assert context['form_workflow_data_blah_var_foo'] == 'a'
def test_frontoffice_workflow_form_with_impossible_condition(pub):
user = create_user(pub)
wf = Workflow.get_default_workflow()

View File

@ -343,6 +343,13 @@ class FormData(StorableObject):
if changed:
self.store()
def clean_live_evolution_items(self):
for evolution in reversed(self.evolution or []):
if getattr(evolution, 'parts', None):
# cleanup evolution, remove parts that have only been added for
# the live evaluation
evolution.parts = [x for x in evolution.parts or [] if not getattr(x, 'live', False)]
@invalidate_substitution_cache
def store(self, *args, **kwargs):
# make sure the class set under the formdef name in the sys.modules
@ -359,6 +366,7 @@ class FormData(StorableObject):
has_id = self.id is not None
if has_id:
self.set_auto_fields()
self.clean_live_evolution_items()
super().store(*args, **kwargs)
if not has_id: # got it now
if self.set_auto_fields():

View File

@ -368,12 +368,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
and 'f%s' % field.id in form._names
):
del form._names['f%s' % field.id]
if self.filled.evolution:
# cleanup evolution, remove parts that have only been added for
# the live evaluation
self.filled.evolution[-1].parts = [
x for x in self.filled.evolution[-1].parts or [] if not getattr(x, 'live', False)
]
if form:
form.attrs['data-live-url'] = self.filled.get_url() + 'live'
return form

View File

@ -2571,6 +2571,7 @@ class SqlDataMixin(SqlMixin):
self.id = cur.fetchone()[0]
self._set_auto_fields(cur)
self.clean_live_evolution_items()
if self._evolution:
for evo in self._evolution: