forms: add option for locking prefilled fields (#39167)

This commit is contained in:
Frédéric Péters 2020-01-22 10:28:45 +01:00
parent b4e3b767c7
commit 0bd57943eb
8 changed files with 197 additions and 58 deletions

View File

@ -1317,7 +1317,7 @@ def test_form_edit_field_advanced(pub):
assert resp.location == 'http://example.net/backoffice/forms/1/fields/#itemId_1'
resp = resp.follow()
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test'}
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test', 'locked': False}
# do the same with 'data sources' field
resp = resp.click('Edit', href='1/')
@ -1356,19 +1356,19 @@ def test_form_prefill_field(pub):
resp.form['prefill$type'] = 'String / Template'
resp.form['prefill$value_string'] = 'test'
resp = resp.form.submit('submit').follow()
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test'}
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': 'test', 'locked': False}
resp = app.get('/backoffice/forms/1/fields/1/')
resp.form['prefill$type'] = 'Python Expression'
resp.form['prefill$value_formula'] = 'True'
resp = resp.form.submit('submit').follow()
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'formula', 'value': 'True'}
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'formula', 'value': 'True', 'locked': False}
resp = app.get('/backoffice/forms/1/fields/1/')
resp.form['prefill$type'] = 'String / Template'
resp.form['prefill$value_string'] = '{{form_var_toto}}'
resp = resp.form.submit('submit').follow()
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': '{{form_var_toto}}'}
assert FormDef.get(formdef.id).fields[0].prefill == {'type': 'string', 'value': '{{form_var_toto}}', 'locked': False}
# check error handling
resp = app.get('/backoffice/forms/1/fields/1/')
@ -1762,7 +1762,7 @@ def test_form_edit_map_field(pub):
resp.form['prefill$value_geolocation'].value = 'Position'
resp = resp.form.submit('submit')
assert FormDef.get(formdef.id).fields[0].prefill == {
'type': 'geolocation', 'value': 'position'}
'type': 'geolocation', 'value': 'position', 'locked': False}
def test_form_edit_field_warnings(pub):

View File

@ -5240,31 +5240,55 @@ def test_form_page_profile_verified_prefill(pub):
user.verified_fields = ['email']
user.store()
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
assert resp.form['f0'].value == 'foo@localhost'
assert 'readonly' in resp.form['f0'].attrs
for prefill_settings in (
{'type': 'user', 'value': 'email'}, # verified profile
{'type': 'string', 'value': 'foo@localhost', 'locked': True}, # locked value
):
formdef.confirmation = True
formdef.fields[0].prefill = prefill_settings
formdef.store()
formdef.data_class().wipe()
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
assert resp.form['f0'].value == 'foo@localhost'
assert 'readonly' in resp.form['f0'].attrs
resp.form['f0'].value = 'Hello' # try changing the value
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.text
assert resp.form['f0'].value == 'foo@localhost' # it is reverted
resp.form['f0'].value = 'Hello' # try changing the value
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.text
assert resp.form['f0'].value == 'foo@localhost' # it is reverted
resp.form['f0'].value = 'Hello' # try again changing the value
resp = resp.form.submit('submit')
resp.form['f0'].value = 'Hello' # try again changing the value
resp = resp.form.submit('submit')
formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
assert len(formdatas) == 1
assert formdatas[0].data['0'] == 'foo@localhost'
formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
assert len(formdatas) == 1
assert formdatas[0].data['0'] == 'foo@localhost'
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
assert resp.form['f0'].value == 'foo@localhost'
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.text
resp.form['f0'].value = 'Hello' # try changing
resp = resp.form.submit('previous')
assert 'readonly' in resp.form['f0'].attrs
assert not 'Check values then click submit.' in resp.text
assert resp.form['f0'].value == 'foo@localhost'
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
assert resp.form['f0'].value == 'foo@localhost'
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.text
resp.form['f0'].value = 'Hello' # try changing
resp = resp.form.submit('previous')
assert 'readonly' in resp.form['f0'].attrs
assert not 'Check values then click submit.' in resp.text
assert resp.form['f0'].value == 'foo@localhost'
# try it without validation page
formdef.confirmation = False
formdef.store()
formdef.data_class().wipe()
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
assert resp.form['f0'].value == 'foo@localhost'
assert 'readonly' in resp.form['f0'].attrs
resp.form['f0'].value = 'Hello' # try changing the value
resp = resp.form.submit('submit')
formdatas = [x for x in formdef.data_class().select() if not x.is_draft()]
assert len(formdatas) == 1
assert formdatas[0].data['0'] == 'foo@localhost'
def test_form_page_profile_verified_date_prefill(pub):
@ -7684,3 +7708,47 @@ def test_js_libraries(pub):
assert 'jquery.js' not in resp.text
assert 'jquery.min.js' not in resp.text
assert 'qommon.forms.js' in resp.text
def test_field_live_locked_prefilled_field(pub, http_requests):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'Foo'
formdef.fields = [
fields.StringField(type='string', id='1', label='Bar', size='40',
required=True, varname='bar'),
fields.StringField(type='string', id='2', label='locked', size='40',
required=True,
prefill={'type': 'string', 'value': 'bla {{form_var_bar}} bla', 'locked': True}),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get('/foo/')
assert 'f1' in resp.form.fields
assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
resp.form['f1'] = 'hello'
live_resp = app.post('/foo/live', params=resp.form.submit_fields())
assert live_resp.json['result']['2']['content'] == 'bla hello bla'
resp.form['f1'] = 'toto'
live_resp = app.post('/foo/live?modified_field_id=1', params=resp.form.submit_fields())
assert live_resp.json['result']['2']['content'] == 'bla toto bla'
def test_field_live_locked_error_prefilled_field(pub, http_requests):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'Foo'
formdef.fields = [
fields.StringField(type='string', id='2', label='locked', size='40',
required=True,
prefill={'type': 'string', 'value': 'bla {% if foo %}{{ foo }}{% end %}', 'locked': True}),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get('/foo/')
assert 'readonly' in resp.form['f2'].attrs
assert not resp.form['f2'].attrs.get('value')

View File

@ -593,3 +593,32 @@ def test_digest_template():
formdef.digest_template = '{{form_number}}'
f2 = assert_xml_import_export_works(formdef)
assert f2.digest_template == formdef.digest_template
def test_field_prefill():
formdef = FormDef()
formdef.name = 'Foo'
formdef.fields = [
fields.StringField(type='string', id=1, label='Bar', size='40',
prefill={'type': 'string', 'value': 'plop'})
]
f2 = assert_xml_import_export_works(formdef)
assert len(f2.fields) == len(formdef.fields)
assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop'}
formdef.fields = [
fields.StringField(type='string', id=1, label='Bar', size='40',
prefill={'type': 'string', 'value': 'plop', 'locked': True})
]
f2 = assert_xml_import_export_works(formdef)
assert len(f2.fields) == len(formdef.fields)
assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop', 'locked': True}
formdef.fields = [
fields.StringField(type='string', id=1, label='Bar', size='40',
prefill={'type': 'string', 'value': 'plop', 'locked': False})
]
formdef_xml = formdef.export_to_xml()
f2 = FormDef.import_from_xml_tree(formdef_xml)
assert len(f2.fields) == len(formdef.fields)
assert f2.fields[0].prefill == {'type': 'string', 'value': 'plop'}

View File

@ -122,14 +122,26 @@ class PrefillSelectionWidget(CompositeWidget):
attrs={'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value': prefill_types.get('geolocation')})
self._parsed = False
# exclude geolocation from locked prefill as the data necessarily
# comes from the user device.
self.add(CheckboxWidget,
'locked',
value=value.get('locked'),
attrs={'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value-in': '|'.join(
[x[1] for x in options if x[0] not in ('none', 'geolocation')]),
'inline_title': _('Locked'),
}
)
self._parsed = False
def _parse(self, request):
values = {}
type_ = self.get('type')
if type_:
values['type'] = type_
values['locked'] = self.get('locked')
value = self.get('value_%s' % type_)
if value:
values['value'] = value
@ -310,6 +322,17 @@ class Field(object):
elif node.text:
self.condition = {'type': 'python', 'value': force_str(node.text).strip()}
def prefill_init_with_xml(self, node, charset, include_id=False):
self.prefill = {}
if node is not None and node.findall('type'):
self.prefill = {
'type': force_str(node.find('type').text),
}
if self.prefill['type'] and self.prefill['type'] != 'none':
self.prefill['value'] = force_str(node.find('value').text)
if xml_node_text(node.find('locked')) == 'True':
self.prefill['locked'] = True
def get_rst_view_value(self, value, indent=''):
return indent + self.get_view_value(value)
@ -327,30 +350,32 @@ class Field(object):
def get_prefill_value(self, user=None, force_string=True):
# returns a tuple with two items,
# 1. value[str], the value that will be used to prefill
# 2. verified[bool], a flag to know if this is a "verified" value
# (that will therefore be marked as readonly etc.)
# 2. locked[bool], a flag to know if this is a locked value
# (because it has been explicitely marked so or because it
# comes from verified identity data).
t = self.prefill.get('type')
explicit_lock = bool(self.prefill.get('locked'))
if t == 'string':
value = self.prefill.get('value')
if not Template.is_template_string(value):
return (value, False)
return (value, explicit_lock)
context = get_publisher().substitutions.get_context_variables()
try:
return (Template(value, autoescape=False, raises=True).render(context), False)
return (Template(value, autoescape=False, raises=True).render(context), explicit_lock)
except TemplateError:
return (None, False)
return ('', explicit_lock)
elif t == 'user' and user:
x = self.prefill.get('value')
if x == 'email':
return (user.email, 'email' in (user.verified_fields or []))
return (user.email, explicit_lock or 'email' in (user.verified_fields or []))
elif user.form_data:
userform = user.get_formdef()
for userfield in userform.fields:
if userfield.id == x:
return (user.form_data.get(x),
str(userfield.id) in (user.verified_fields or []))
explicit_lock or str(userfield.id) in (user.verified_fields or []))
elif t == 'formula':
formula = self.prefill.get('value')
@ -369,7 +394,7 @@ class Field(object):
# (items field are prefilled with list of strings, and
# will get the native python object)
ret = str(ret)
return (ret, False)
return (ret, explicit_lock)
except:
pass

View File

@ -687,6 +687,11 @@ class FormDef(StorableObject):
if not varname in live_condition_fields:
live_condition_fields[varname] = []
live_condition_fields[varname].append(field)
if field.prefill and field.prefill.get('locked') and field.prefill.get('type') == 'string':
for varname in field.get_referenced_varnames(formdef=self, value=field.prefill.get('value', '')):
if varname not in live_condition_fields:
live_condition_fields[varname] = []
live_condition_fields[varname].append(field)
if field.key == 'comment':
for varname in field.get_referenced_varnames(formdef=self, value=field.label):
if not varname in live_condition_fields:

View File

@ -714,6 +714,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
continue
if widget.field.key == 'comment':
result[widget.field.id]['content'] = widget.content
elif widget.field.prefill and widget.field.prefill.get('locked') and widget.field.prefill.get('type') == 'string':
value, locked = widget.field.get_prefill_value()
result[widget.field.id]['content'] = value
return json.dumps({'result': result})

View File

@ -352,14 +352,14 @@ class FormPage(Directory, FormTemplateMixin):
k = field.id
v = None
prefilled = False
verified = 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, verified = field.get_prefill_value(user=prefill_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.
@ -390,7 +390,7 @@ class FormPage(Directory, FormTemplateMixin):
# not be evaluated in the initial GET request of the
# page).
form.get_widget('f%s' % k).set_error(get_selection_error_text())
if verified:
if locked:
form.get_widget('f%s' % k).readonly = 'readonly'
form.get_widget('f%s' % k).attrs['readonly'] = 'readonly'
if field.key == 'map':
@ -733,24 +733,6 @@ class FormPage(Directory, FormTemplateMixin):
except (TypeError, ValueError):
step = 0
# reset verified 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')
if prefill_user:
for field in self.formdef.fields:
if not field.prefill:
continue
if not 'f%s' % field.id in get_request().form:
continue
v, verified = field.get_prefill_value(user=prefill_user)
if verified:
if not isinstance(v, six.string_types) and field.convert_value_to_str:
# 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
if step == 0:
try:
page_no = int(form.get_widget('page').parse())
@ -789,7 +771,11 @@ class FormPage(Directory, FormTemplateMixin):
form_data = session.get_by_magictoken(magictoken, {})
with get_publisher().substitutions.temporary_feed(
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()
data = self.formdef.get_data(form)
form_data.update(data)
if self.has_draft_support() and form.get_submit() == 'savedraft':
@ -892,6 +878,7 @@ class FormPage(Directory, FormTemplateMixin):
else:
return self.page(self.pages[page_no])
self.reset_locked_data()
if step == 1:
form.add_submit('previous')
magictoken = form.get_widget('magictoken').parse()
@ -942,6 +929,23 @@ class FormPage(Directory, FormTemplateMixin):
return self.submitted(form, existing_formdata)
def reset_locked_data(self):
# 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:
if not field.prefill:
continue
if not 'f%s' % field.id in get_request().form:
continue
v, locked = field.get_prefill_value(user=prefill_user)
if locked:
if not isinstance(v, six.string_types) and field.convert_value_to_str:
# 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
def previous_page(self, page_no, magictoken):
session = get_session()

View File

@ -114,9 +114,14 @@ $(function() {
}
}
if (value.content) {
// replace comment content
var $widget = $('[data-field-id="' + key + '"]');
$widget.html(value.content);
if ($widget.hasClass('comment-field')) {
// replace comment content
$widget.html(value.content);
} else {
// replace text input value
$widget.find('input, textarea').val(value.content);
}
}
if (value.source_url) {
// json change of URL