general: add support for prefilling blocks (#45264)

This commit is contained in:
Frédéric Péters 2020-10-06 19:53:07 +02:00
parent 9e837f2ffe
commit 3a78582845
3 changed files with 272 additions and 98 deletions

View File

@ -8841,6 +8841,151 @@ def test_block_geoloc_prefill(pub, blocks_feature):
assert resp.html.find('div', {'data-geolocation': 'road'})
def test_block_string_prefill(pub, blocks_feature):
create_user(pub)
FormDef.wipe()
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='Test', type='string',
prefill={'type': 'string', 'value': '{{ form_var_foo }} World'}),
]
block.store()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.PageField(id='0', label='1st page', type='page'),
fields.StringField(id='1', label='string', varname='foo'),
fields.PageField(id='2', label='2nd page', type='page'),
fields.BlockField(id='3', label='test', type='block:foobar'),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get(formdef.get_url())
resp.form['f1'] = 'Hello'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Hello World'
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('submit') # -> end page
resp = resp.follow()
formdata = formdef.data_class().select()[0]
assert formdata.data['3']['data'][0]['123'] == 'Hello World'
# check unmodified prefilled field
app = get_app(pub)
resp = app.get(formdef.get_url())
resp.form['f1'] = 'Hello'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Hello World'
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('previous') # -> 2nd page
resp = resp.form.submit('previous') # -> 1st page
resp.form['f1'] = 'Test'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Test World'
# check modified prefilled field
app = get_app(pub)
resp = app.get(formdef.get_url())
resp.form['f1'] = 'Hello'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Hello World'
resp.form['f3$element0$f123'] = 'Foobar'
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('previous') # -> 2nd page
resp = resp.form.submit('previous') # -> 1st page
resp.form['f1'] = 'Test'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Foobar'
def test_block_locked_prefill(pub, blocks_feature):
create_user(pub)
FormDef.wipe()
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='Test', type='string',
prefill={'type': 'string', 'value': '{{ form_var_foo }} World', 'locked': True}),
]
block.store()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.PageField(id='0', label='1st page', type='page'),
fields.StringField(id='1', label='string', varname='foo'),
fields.PageField(id='2', label='2nd page', type='page'),
fields.BlockField(id='3', label='test', type='block:foobar'),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get(formdef.get_url())
resp.form['f1'] = 'Hello'
resp = resp.form.submit('submit') # -> 2nd page
assert resp.form['f3$element0$f123'].value == 'Hello World'
assert 'readonly' in resp.form['f3$element0$f123'].attrs
resp.form['f3$element0$f123'].value = 'Hello' # try changing the value
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('submit') # -> end page
resp = resp.follow()
formdata = formdef.data_class().select()[0]
assert formdata.data['3']['data'][0]['123'] == 'Hello World' # value got reverted
def test_workflow_form_block_prefill(pub):
create_user(pub)
FormDef.wipe()
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='Test', type='string',
prefill={'type': 'user', 'value': 'email'}),
]
block.store()
wf = Workflow(name='status')
st1 = wf.add_status('Status1', 'st1')
display_form = FormWorkflowStatusItem()
display_form.id = '_x'
display_form.by = ['_submitter']
display_form.varname = 'xxx'
display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
display_form.formdef.fields.append(
fields.BlockField(id='3', label='test', type='block:foobar')
)
st1.items.append(display_form)
display_form.parent = st1
wf.store()
formdef = create_formdef()
formdef.workflow_id = wf.id
formdef.fields = []
formdef.store()
formdef.data_class().wipe()
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
resp = resp.form.submit('submit') # -> validation
resp = resp.form.submit('submit').follow()
assert 'The form has been recorded' in resp
assert resp.form['f3$element0$f123'].value == 'foo@localhost'
def test_block_title_and_comment(pub, blocks_feature):
create_user(pub)
FormDef.wipe()

View File

@ -298,6 +298,104 @@ class FormPage(Directory, FormTemplateMixin):
'current_page_no': current_position,
})
@classmethod
def iter_with_block_fields(cls, form, fields):
for field in fields:
field_key = '%s' % field.id
widget = form.get_widget('f%s' % field_key) if form else None
yield field, field_key, widget, None
if field.key == 'block':
# we only ever prefill the first item
subwidget = widget.widgets[0] if widget else None
for subfield in field.block.fields:
subfield_key = '%s$%s' % (field.id, subfield.id)
subfield_widget = subwidget.get_widget('f%s' % subfield.id) if subwidget else None
yield subfield, subfield_key, subfield_widget, field
@classmethod
def apply_field_prefills(cls, data, form, displayed_fields):
req = get_request()
had_prefill = False
for field, field_key, widget, block in cls.iter_with_block_fields(form, displayed_fields):
v = None
prefilled = False
locked = False
if field.prefill:
prefill_user = get_request().user
if get_request().is_in_backoffice():
prefill_user = get_publisher().substitutions.get_context_variables(
).get('form_user')
v, locked = field.get_prefill_value(user=prefill_user)
# always set additional attributes as they will be used for
# "live prefill", regardless of existing data.
widget.prefill_attributes = field.get_prefill_attributes()
should_prefill = bool(field.prefill)
has_current_value = False
if block:
try:
current_value = data[block.id]['data'][0][field.id]
has_current_value = True
except (IndexError, KeyError, ValueError):
pass
else:
try:
current_value = data[field_key]
has_current_value = True
except KeyError:
pass
if has_current_value:
# existing value, update it with the new computed value
# if it's the same that was previously computed.
prefill_value = v
v = current_value
if data.get('prefilling_data', {}).get(field_key) == current_value:
# replace value with new value computed for prefill
v = prefill_value
else:
should_prefill = False
if should_prefill:
if get_request().is_in_backoffice() and (
field.prefill and field.prefill.get('type') == 'geoloc'):
# turn off prefilling from geolocation attributes if
# the form is filled from the backoffice
v = None
if v:
prefilled = True
widget.prefilled = True
if not prefilled and widget:
widget.clear_error()
widget._parsed = False
if v is not None:
# store computed value, it will be used to compare with
# submitted value if page is visited again.
if should_prefill:
if 'prefilling_data' not in data:
data['prefilling_data'] = {}
data['prefilling_data'][field_key] = v
if not isinstance(v, str) and field.convert_value_to_str:
v = field.convert_value_to_str(v)
widget.set_value(v)
widget.transfer_form_value(req)
if field.type == 'item' and v and widget.value != v:
# mark field as invalid if the value was not accepted
# (this is required by quixote>=3 as the value would
# not be evaluated in the initial GET request of the
# page).
widget.set_error(get_selection_error_text())
if locked:
widget.readonly = 'readonly'
widget.attrs['readonly'] = 'readonly'
had_prefill = True
return had_prefill
def page(self, page, page_change=True, page_error_messages=None, submit_button=None):
displayed_fields = []
@ -360,70 +458,8 @@ class FormPage(Directory, FormTemplateMixin):
# visited, we restore values; otherwise we set req.form as empty.
req = get_request()
req.environ['REQUEST_METHOD'] = 'GET'
for field in displayed_fields:
k = field.id
v = None
prefilled = False
locked = False
if field.prefill:
prefill_user = get_request().user
if get_request().is_in_backoffice():
prefill_user = get_publisher().substitutions.get_context_variables(
).get('form_user')
v, locked = field.get_prefill_value(user=prefill_user)
# always set additional attributes as they will be used for
# "live prefill", regardless of existing data.
form.get_widget('f%s' % k).prefill_attributes = field.get_prefill_attributes()
should_prefill = bool(field.prefill)
if k in data:
# existing value, update it with the new computed value
# if it's the same that was previously computed.
prefill_value = v
v = data[k]
if data.get('prefilling_data', {}).get(k) == data[k]:
# replace value with new value computed for prefill
v = prefill_value
else:
should_prefill = False
if should_prefill:
if get_request().is_in_backoffice() and (
field.prefill and field.prefill.get('type') == 'geoloc'):
# turn off prefilling from geolocation attributes if
# the form is filled from the backoffice
v = None
if v:
prefilled = True
form.get_widget('f%s' % k).prefilled = True
if not prefilled and form.get_widget('f%s' % k):
form.get_widget('f%s' % k).clear_error()
if v is not None:
# store computed value, it will be used to compare with
# submitted value if page is visited again.
if should_prefill:
if 'prefilling_data' not in data:
data['prefilling_data'] = {}
data['prefilling_data'][k] = v
if not isinstance(v, str) and field.convert_value_to_str:
v = field.convert_value_to_str(v)
form.get_widget('f%s' % k).set_value(v)
form.get_widget('f%s' % k).transfer_form_value(req)
if field.type == 'item' and v and form.get_widget('f%s' % k).value != v:
# mark field as invalid if the value was not accepted
# (this is required by quixote>=3 as the value would
# not be evaluated in the initial GET request of the
# page).
form.get_widget('f%s' % k).set_error(get_selection_error_text())
if locked:
form.get_widget('f%s' % k).readonly = 'readonly'
form.get_widget('f%s' % k).attrs['readonly'] = 'readonly'
had_prefill = True
had_prefill = self.apply_field_prefills(data, form, displayed_fields)
if had_prefill:
# include prefilled data
@ -438,16 +474,16 @@ class FormPage(Directory, FormTemplateMixin):
else:
# not a page change, reset_locked_data() will have been called
# earlier, we use that to set appropriate fields as readonly.
for field in displayed_fields:
if get_request().form.get('__locked_f%s' % field.id):
form.get_widget('f%s' % field.id).readonly = 'readonly'
form.get_widget('f%s' % field.id).attrs['readonly'] = 'readonly'
for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
if get_request().form.get('__locked_f%s' % field_key):
widget.readonly = 'readonly'
widget.attrs['readonly'] = 'readonly'
for field in displayed_fields:
for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
if field.prefill:
# always set additional attributes as they will be used for
# "live prefill", regardless of existing data.
form.get_widget('f%s' % field.id).prefill_attributes = field.get_prefill_attributes()
widget.prefill_attributes = field.get_prefill_attributes()
self.formdef.set_live_condition_sources(form, displayed_fields)
@ -455,8 +491,7 @@ class FormPage(Directory, FormTemplateMixin):
# pass over prefilled fields that are used as live source of item
# fields
fields_to_update = set()
for field in displayed_fields:
widget = form.get_widget('f%s' % field.id)
for field, field_key, widget, block in self.iter_with_block_fields(form, displayed_fields):
if getattr(widget, 'prefilled', False) and getattr(widget, 'live_condition_source', False):
fields_to_update.update(widget.live_condition_fields)
elif field in fields_to_update and field.type == 'item':
@ -857,7 +892,7 @@ class FormPage(Directory, FormTemplateMixin):
transient_formdata, force_mode='lazy'):
# reset locked data with newly submitted values, this allows
# for templates referencing fields from the sampe page.
self.reset_locked_data()
self.reset_locked_data(form)
data = self.formdef.get_data(form)
form_data.update(data)
@ -969,7 +1004,7 @@ class FormPage(Directory, FormTemplateMixin):
else:
return self.page(self.pages[page_no])
self.reset_locked_data()
self.reset_locked_data(form)
if step == 1:
form.add_submit('previous')
magictoken = form.get_widget('magictoken').parse()
@ -1020,15 +1055,18 @@ class FormPage(Directory, FormTemplateMixin):
return self.submitted(form, existing_formdata)
def reset_locked_data(self):
def reset_locked_data(self, form):
# reset locked fields, making sure the user cannot alter them.
prefill_user = get_request().user
if get_request().is_in_backoffice():
prefill_user = get_publisher().substitutions.get_context_variables().get('form_user')
for field in self.formdef.fields:
for field, field_key, widget, block in self.iter_with_block_fields(form, self.formdef.fields):
if not field.prefill:
continue
if not 'f%s' % field.id in get_request().form:
post_key = 'f%s' % field_key
if block:
post_key = 'f%s$element0$f%s' % (block.id, field.id)
if post_key not in get_request().form:
continue
v, locked = field.get_prefill_value(user=prefill_user)
if locked:
@ -1036,10 +1074,19 @@ class FormPage(Directory, FormTemplateMixin):
# convert structured data to strings as if they were
# submitted by the browser.
v = field.convert_value_to_str(v)
get_request().form['f%s' % field.id] = v
get_request().form[post_key] = v
if widget:
widget.set_value(v)
if block:
# child widget value was changed, mark parent widgets
# as unparsed
block_widget = form.get_widget('f%s' % block.id)
block_widget._parsed = False
block_widget.widgets[0]._parsed = False
# keep track of locked field, this will be used when
# redisplaying the same page in case of errors.
get_request().form['__locked_f%s' % field.id] = True
get_request().form['__locked_f%s' % field_key] = True
def previous_page(self, page_no, magictoken):
session = get_session()

View File

@ -26,6 +26,7 @@ from wcs.admin.fields import FieldDefPage, FieldsDirectory
from wcs.formdata import get_dict_with_varnames
from wcs.forms.common import FileDirectory
from wcs.forms.root import FormPage
def lookup_wf_form_file(self, filename):
@ -177,7 +178,6 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
formdata.feed_session()
req = get_request()
self.formdef.set_live_condition_sources(form, self.formdef.fields)
if form.is_submitted():
@ -188,25 +188,7 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
if displayed_fields is not None:
fields = displayed_fields
for field in fields:
if ('f%s' % field.id) in req.form:
continue
if not field.prefill or field.prefill.get('type') == 'none':
continue
# FIXME: this code duplicates code from wcs/forms/root.py :/
prefill_user = get_request().user
if get_request().is_in_backoffice():
prefill_user = get_publisher().substitutions.get_context_variables(
).get('form_user')
v, verified = field.get_prefill_value(user=prefill_user)
if get_request().is_in_backoffice() and (
field.prefill and field.prefill.get('type') == 'geoloc'):
# turn off prefilling from geolocation attributes if
# the form is filled from the backoffice
v = None
if v:
form.get_widget('f%s' % field.id).set_value(v)
req.form['f%s' % field.id] = v
FormPage.apply_field_prefills({}, form, fields)
def evaluate_live_form(self, form, formdata, user):
workflow_data = {}