forms: add support for live list contents (#27173)
This commit is contained in:
parent
9e2226a713
commit
14c51926a2
|
@ -5456,3 +5456,56 @@ def test_field_live_condition_multipages(pub):
|
|||
assert 'name="f2"' in resp.body
|
||||
assert 'name="f4"' in resp.body
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
def test_field_live_select_content(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='Bar2', size='40',
|
||||
required=True, varname='bar2'),
|
||||
fields.ItemField(type='item', id='3', label='Foo',
|
||||
data_source={
|
||||
'type': 'json',
|
||||
'value': '{% if form_var_bar2 %}http://remote.example.net/json-list?plop={{form_var_bar2}}{% endif %}'
|
||||
}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/foo/')
|
||||
assert 'f1' in resp.form.fields
|
||||
assert 'f2' in resp.form.fields
|
||||
assert resp.html.find('div', {'data-field-id': '2'}).attrs['data-live-source'] == 'true'
|
||||
assert resp.html.find('div', {'data-field-id': '3'}).find('select')
|
||||
resp.form['f1'] = 'hello'
|
||||
live_resp = app.post('/foo/live', params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['1']['visible']
|
||||
assert live_resp.json['result']['2']['visible']
|
||||
assert live_resp.json['result']['3']['visible']
|
||||
assert not 'items' in live_resp.json['result']['3']
|
||||
resp.form['f2'] = 'plop'
|
||||
live_resp = app.post('/foo/live?modified_field_id=2', params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['1']['visible']
|
||||
assert live_resp.json['result']['2']['visible']
|
||||
assert live_resp.json['result']['3']['visible']
|
||||
assert 'items' in live_resp.json['result']['3']
|
||||
resp.form['f3'].options = []
|
||||
for item in live_resp.json['result']['3']['items']:
|
||||
# simulate javascript filling the <select>
|
||||
resp.form['f3'].options.append((item['id'], False, item['text']))
|
||||
resp.form['f3'] = 'a'
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'Check values then click submit.' in resp.body
|
||||
assert 'name="f1"' in resp.body
|
||||
assert 'name="f2"' in resp.body
|
||||
assert 'name="f3"' in resp.body
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.follow()
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data['1'] == 'hello'
|
||||
assert formdata.data['2'] == 'plop'
|
||||
assert formdata.data['3'] == 'a'
|
||||
assert formdata.data['3_display'] == 'b'
|
||||
|
|
|
@ -327,6 +327,7 @@ class HttpRequestsMocking(object):
|
|||
'http://remote.example.net/404-json': (404, '{"err": 1}', None),
|
||||
'http://remote.example.net/500': (500, 'internal server error', None),
|
||||
'http://remote.example.net/json': (200, '{"foo": "bar"}', None),
|
||||
'http://remote.example.net/json-list': (200, '{"data": [{"id": "a", "text": "b"}]}', None),
|
||||
'http://remote.example.net/json-err0': (200, '{"data": "foo", "err": 0}', None),
|
||||
'http://remote.example.net/json-err1': (200, '{"data": "", "err": 1}', None),
|
||||
'http://remote.example.net/json-errstr': (200, '{"data": "", "err": "bug"}', None),
|
||||
|
|
|
@ -95,8 +95,8 @@ class DataSourceSelectionWidget(CompositeWidget):
|
|||
return r.getvalue()
|
||||
|
||||
|
||||
def get_items(data_source, include_disabled=False):
|
||||
structured_items = get_structured_items(data_source)
|
||||
def get_items(data_source, include_disabled=False, mode=None):
|
||||
structured_items = get_structured_items(data_source, mode=mode)
|
||||
tupled_items = []
|
||||
for item in structured_items:
|
||||
if item.get('disabled') and not include_disabled:
|
||||
|
@ -108,7 +108,7 @@ def get_items(data_source, include_disabled=False):
|
|||
return tupled_items
|
||||
|
||||
|
||||
def get_structured_items(data_source):
|
||||
def get_structured_items(data_source, mode=None):
|
||||
cache_duration = 0
|
||||
if data_source.get('type') not in ('json', 'jsonp', 'formula'):
|
||||
# named data source
|
||||
|
@ -126,7 +126,7 @@ def get_structured_items(data_source):
|
|||
# - three elements, (id, text, key)
|
||||
# - two elements, (id, text)
|
||||
# - a single element, (id,)
|
||||
variables = get_publisher().substitutions.get_context_variables()
|
||||
variables = get_publisher().substitutions.get_context_variables(mode=mode)
|
||||
global_eval_dict = get_publisher().get_global_eval_dict()
|
||||
global_eval_dict.update(data_source_functions)
|
||||
try:
|
||||
|
@ -162,7 +162,7 @@ def get_structured_items(data_source):
|
|||
return []
|
||||
url = url.strip()
|
||||
if Template.is_template_string(url):
|
||||
vars = get_publisher().substitutions.get_context_variables()
|
||||
vars = get_publisher().substitutions.get_context_variables(mode=mode)
|
||||
url = get_variadic_url(url, vars)
|
||||
|
||||
request = get_request()
|
||||
|
|
|
@ -1169,9 +1169,9 @@ class ItemField(WidgetField):
|
|||
self.items = []
|
||||
WidgetField.__init__(self, **kwargs)
|
||||
|
||||
def get_options(self):
|
||||
def get_options(self, mode=None):
|
||||
if self.data_source:
|
||||
return [x[:3] for x in data_sources.get_items(self.data_source)]
|
||||
return [x[:3] for x in data_sources.get_items(self.data_source, mode=mode)]
|
||||
if self.items:
|
||||
return [(x, x) for x in self.items]
|
||||
return []
|
||||
|
|
|
@ -512,7 +512,7 @@ class FormDef(StorableObject):
|
|||
def get_display_id_format(self):
|
||||
return '[formdef_id]-[form_number_raw]'
|
||||
|
||||
def create_form(self, page=None, displayed_fields=None):
|
||||
def create_form(self, page=None, displayed_fields=None, transient_formdata=None):
|
||||
form = Form(enctype="multipart/form-data", use_tokens=False)
|
||||
if self.appearance_keywords:
|
||||
form.attrs['class'] = 'quixote %s' % self.appearance_keywords
|
||||
|
@ -521,10 +521,18 @@ class FormDef(StorableObject):
|
|||
form.ERROR_NOTICE = _('There were errors processing the form and '
|
||||
'you cannot go to the next page. Do '
|
||||
'check below that you filled all fields correctly.')
|
||||
self.add_fields_to_form(form, page=page, displayed_fields=displayed_fields)
|
||||
self.add_fields_to_form(form,
|
||||
page=page,
|
||||
displayed_fields=displayed_fields,
|
||||
transient_formdata=transient_formdata)
|
||||
return form
|
||||
|
||||
def add_fields_to_form(self, form, page=None, displayed_fields=None, form_data=None):
|
||||
def add_fields_to_form(self,
|
||||
form,
|
||||
page=None,
|
||||
displayed_fields=None,
|
||||
form_data=None, # a dictionary, to fill fields
|
||||
transient_formdata=None): # a FormData
|
||||
current_page = 0
|
||||
on_page = (page is None)
|
||||
for field in self.fields:
|
||||
|
@ -549,6 +557,10 @@ class FormDef(StorableObject):
|
|||
widget = field.add_to_form(form, value)
|
||||
widget.is_hidden = not(visible)
|
||||
widget.field = field
|
||||
if transient_formdata and not widget.is_hidden:
|
||||
transient_formdata.data.update(self.get_field_data(field, widget))
|
||||
widget._parsed = False
|
||||
widget.error = None
|
||||
|
||||
def get_page(self, page_no):
|
||||
return [x for x in self.fields if x.type == 'page'][page_no]
|
||||
|
@ -611,29 +623,33 @@ class FormDef(StorableObject):
|
|||
|
||||
return form
|
||||
|
||||
def get_field_data(self, field, widget):
|
||||
d = {}
|
||||
d[field.id] = widget.parse()
|
||||
if d.get(field.id) and field.convert_value_from_str:
|
||||
d[field.id] = field.convert_value_from_str(d[field.id])
|
||||
if d.get(field.id) and field.store_display_value:
|
||||
display_value = field.store_display_value(d, field.id)
|
||||
if display_value:
|
||||
d['%s_display' % field.id] = display_value
|
||||
elif d.has_key('%s_display' % field.id):
|
||||
del d['%s_display' % field.id]
|
||||
if d.get(field.id) and field.store_structured_value:
|
||||
structured_value = field.store_structured_value(d, field.id)
|
||||
if structured_value:
|
||||
d['%s_structured' % field.id] = structured_value
|
||||
elif '%s_structured' % field.id in d:
|
||||
del d['%s_structured' % field.id]
|
||||
if getattr(widget, 'cleanup', None):
|
||||
widget.cleanup()
|
||||
return d
|
||||
|
||||
def get_data(self, form):
|
||||
d = {}
|
||||
for field in self.fields:
|
||||
widget = form.get_widget('f%s' % field.id)
|
||||
if widget:
|
||||
d[field.id] = widget.parse()
|
||||
if d.get(field.id) and field.convert_value_from_str:
|
||||
d[field.id] = field.convert_value_from_str(d[field.id])
|
||||
if d.get(field.id) and field.store_display_value:
|
||||
display_value = field.store_display_value(d, field.id)
|
||||
if display_value:
|
||||
d['%s_display' % field.id] = display_value
|
||||
elif d.has_key('%s_display' % field.id):
|
||||
del d['%s_display' % field.id]
|
||||
if d.get(field.id) and field.store_structured_value:
|
||||
structured_value = field.store_structured_value(d, field.id)
|
||||
if structured_value:
|
||||
d['%s_structured' % field.id] = structured_value
|
||||
elif '%s_structured' % field.id in d:
|
||||
del d['%s_structured' % field.id]
|
||||
if widget and widget.cleanup:
|
||||
widget.cleanup()
|
||||
|
||||
d.update(self.get_field_data(field, widget))
|
||||
return d
|
||||
|
||||
def export_to_json(self, include_id=False, indent=None):
|
||||
|
|
|
@ -45,6 +45,7 @@ from qommon.form import *
|
|||
from qommon.logger import BotFilter
|
||||
from qommon import emails
|
||||
|
||||
from wcs import data_sources
|
||||
from wcs.categories import Category
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.formdata import FormData
|
||||
|
@ -376,6 +377,16 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
if not varname in live_condition_fields:
|
||||
live_condition_fields[varname] = []
|
||||
live_condition_fields[varname].append(field)
|
||||
if field.key == 'item' and field.data_source:
|
||||
real_data_source = data_sources.get_real(field.data_source)
|
||||
if real_data_source.get('type') != 'json':
|
||||
continue
|
||||
varnames = re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)',
|
||||
real_data_source.get('value'))
|
||||
for varname in varnames:
|
||||
if not varname in live_condition_fields:
|
||||
live_condition_fields[varname] = []
|
||||
live_condition_fields[varname].append(field)
|
||||
|
||||
for field in displayed_fields:
|
||||
if field.varname in live_condition_fields:
|
||||
|
@ -705,7 +716,12 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
self.feed_current_data(magictoken)
|
||||
|
||||
submitted_fields = []
|
||||
form = self.create_form(page=page, displayed_fields=submitted_fields)
|
||||
transient_formdata = self.get_transient_formdata()
|
||||
with get_publisher().substitutions.temporary_feed(
|
||||
transient_formdata, force_mode='lazy'):
|
||||
form = self.create_form(page=page,
|
||||
displayed_fields=submitted_fields,
|
||||
transient_formdata=transient_formdata)
|
||||
form.add_submit('previous')
|
||||
if self.formdef.enable_tracking_codes:
|
||||
form.add_submit('removedraft')
|
||||
|
@ -1013,9 +1029,6 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
if not session:
|
||||
return result_error('missing session')
|
||||
|
||||
formdata = self.get_transient_formdata()
|
||||
get_publisher().substitutions.feed(formdata)
|
||||
|
||||
page_id = get_request().form.get('page_id')
|
||||
if page_id:
|
||||
for field in self.formdef.fields:
|
||||
|
@ -1025,14 +1038,36 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
else:
|
||||
page = None
|
||||
|
||||
formdata = self.get_transient_formdata()
|
||||
get_publisher().substitutions.feed(formdata)
|
||||
displayed_fields = []
|
||||
form = self.create_form(page=page, displayed_fields=displayed_fields)
|
||||
with get_publisher().substitutions.temporary_feed(formdata, force_mode='lazy'):
|
||||
form = self.create_form(
|
||||
page=page,
|
||||
displayed_fields=displayed_fields,
|
||||
transient_formdata=formdata)
|
||||
formdata.data.update(self.formdef.get_data(form))
|
||||
|
||||
result = {}
|
||||
for field in displayed_fields:
|
||||
result[field.id] = {'visible': field.is_visible(formdata.data, self.formdef)}
|
||||
|
||||
modified_field_varname = None
|
||||
for field in displayed_fields:
|
||||
if field.id == get_request().form.get('modified_field_id'):
|
||||
modified_field_varname = field.varname
|
||||
|
||||
for field in displayed_fields:
|
||||
if field.key == 'item' and field.data_source:
|
||||
real_data_source = data_sources.get_real(field.data_source)
|
||||
if real_data_source.get('type') != 'json':
|
||||
continue
|
||||
varnames = re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)',
|
||||
real_data_source.get('value'))
|
||||
if modified_field_varname in varnames:
|
||||
result[field.id]['items'] = [
|
||||
{'id': x[2], 'text': x[1]} for x in field.get_options(mode='lazy')]
|
||||
|
||||
return json.dumps({'result': result})
|
||||
|
||||
def submitted(self, form, existing_formdata = None):
|
||||
|
|
|
@ -91,6 +91,19 @@ $(function() {
|
|||
} else {
|
||||
$widget.hide();
|
||||
}
|
||||
if (value.items) {
|
||||
// replace <select> contents
|
||||
var $select = $widget.find('select');
|
||||
var current_value = $select.val();
|
||||
$select.empty();
|
||||
for (var i=0; i<value.items.length; i++) {
|
||||
var $option = $('<option></option>', {value: value.items[i].id, text: value.items[i].text});
|
||||
if (value.items[i].id == current_value) {
|
||||
$option.attr('selected', 'selected');
|
||||
}
|
||||
$option.appendTo($select);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,9 +32,10 @@ def invalidate_substitution_cache(func):
|
|||
class Substitutions(object):
|
||||
substitutions_dict = {}
|
||||
dynamic_sources = []
|
||||
|
||||
sources = None
|
||||
|
||||
_forced_mode = None
|
||||
|
||||
def __init__(self):
|
||||
self.sources = []
|
||||
|
||||
|
@ -77,7 +78,7 @@ class Substitutions(object):
|
|||
self.invalidate_cache()
|
||||
|
||||
@contextmanager
|
||||
def temporary_feed(self, source):
|
||||
def temporary_feed(self, source, force_mode=None):
|
||||
if source is None or source in self.sources:
|
||||
yield
|
||||
return
|
||||
|
@ -85,7 +86,9 @@ class Substitutions(object):
|
|||
orig_sources, self.sources = self.sources, self.sources[:]
|
||||
self.sources.append(source)
|
||||
self.invalidate_cache()
|
||||
old_mode, self._forced_mode = self._forced_mode, force_mode
|
||||
yield
|
||||
self._forced_mode = old_mode
|
||||
self.sources = orig_sources
|
||||
self.invalidate_cache()
|
||||
|
||||
|
@ -95,6 +98,8 @@ class Substitutions(object):
|
|||
delattr(self, '_cache_context_variables%r' % value)
|
||||
|
||||
def get_context_variables(self, mode=None):
|
||||
if self._forced_mode:
|
||||
mode = self._forced_mode
|
||||
lazy = mode in get_publisher().get_lazy_variables_modes() if mode else False
|
||||
d = getattr(self, '_cache_context_variables%r' % lazy, None)
|
||||
if d is not None:
|
||||
|
|
Loading…
Reference in New Issue