forms: add option for locking prefilled fields (#39167)
This commit is contained in:
parent
b4e3b767c7
commit
0bd57943eb
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue