From 4b00979587c18c33365d22ac3f4368a009b11a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 31 Aug 2016 16:35:30 +0200 Subject: [PATCH] fields: make it possible to include disabled items in datasources (#12967) This only works with JSON data sources and the Item field type. --- tests/test_datasource.py | 50 ++++++++++++++++++++++---------- tests/test_form_pages.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ wcs/data_sources.py | 9 ++++-- wcs/fields.py | 10 +++++-- wcs/qommon/form.py | 62 ++++++++++++++++++++++++++-------------- 5 files changed, 165 insertions(+), 40 deletions(-) diff --git a/tests/test_datasource.py b/tests/test_datasource.py index 0e837a4..cc7b7d2 100644 --- a/tests/test_datasource.py +++ b/tests/test_datasource.py @@ -58,7 +58,9 @@ def test_item_field_python_datasource(): def test_python_datasource(): plain_list = [('1', 'foo'), ('2', 'bar')] datasource = {'type': 'formula', 'value': repr(plain_list)} - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': '1', 'text': 'foo'}), + ('2', 'bar', '2', {'id': '2', 'text': 'bar'})] assert data_sources.get_structured_items(datasource) == [ {'id': '1', 'text': 'foo'}, {'id': '2', 'text': 'bar'}] @@ -73,17 +75,23 @@ def test_python_datasource(): # three-item tuples plain_list = [('1', 'foo', 'a'), ('2', 'bar', 'b')] datasource = {'type': 'formula', 'value': repr(plain_list)} - assert data_sources.get_items(datasource) == [('1', 'foo', 'a'), ('2', 'bar', 'b')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', 'a', {'id': '1', 'key': 'a', 'text': 'foo'}), + ('2', 'bar', 'b', {'id': '2', 'key': 'b', 'text': 'bar'})] # single-item tuples plain_list = [('foo', ), ('bar', )] datasource = {'type': 'formula', 'value': repr(plain_list)} - assert data_sources.get_items(datasource) == [('foo', 'foo', 'foo'), ('bar', 'bar', 'bar')] + assert data_sources.get_items(datasource) == [ + ('foo', 'foo', 'foo', {'id': 'foo', 'text': 'foo'}), + ('bar', 'bar', 'bar', {'id': 'bar', 'text': 'bar'})] # list of strings plain_list = ['foo', 'bar'] datasource = {'type': 'formula', 'value': repr(plain_list)} - assert data_sources.get_items(datasource) == [('foo', 'foo', 'foo'), ('bar', 'bar', 'bar')] + assert data_sources.get_items(datasource) == [ + ('foo', 'foo', 'foo', {'id': 'foo', 'text': 'foo'}), + ('bar', 'bar', 'bar', {'id': 'bar', 'text': 'bar'})] def test_json_datasource(): datasource = {'type': 'json', 'value': ''} @@ -122,7 +130,9 @@ def test_json_datasource(): json_file = open(json_file_path, 'w') json.dump({'data': [{'id': '1', 'text': 'foo'}, {'id': '2', 'text': 'bar'}]}, json_file) json_file.close() - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': '1', 'text': 'foo'}), + ('2', 'bar', '2', {'id': '2', 'text': 'bar'})] assert data_sources.get_structured_items(datasource) == [ {'id': '1', 'text': 'foo'}, {'id': '2', 'text': 'bar'}] @@ -131,7 +141,9 @@ def test_json_datasource(): json.dump({'data': [{'id': '1', 'text': 'foo', 'more': 'xxx'}, {'id': '2', 'text': 'bar', 'more': 'yyy'}]}, json_file) json_file.close() - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'more': 'xxx'}), + ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'more': 'yyy'})] assert data_sources.get_structured_items(datasource) == [ {'id': '1', 'text': 'foo', 'more': 'xxx'}, {'id': '2', 'text': 'bar', 'more': 'yyy'}] @@ -142,13 +154,17 @@ def test_json_datasource(): return {'json_url': 'file://%s' % json_file_path} pub.substitutions.feed(JsonUrlPath()) datasource = {'type': 'json', 'value': '[json_url]'} - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'more': 'xxx'}), + ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'more': 'yyy'})] # a json file with integer as 'id' json_file = open(json_file_path, 'w') json.dump({'data': [{'id': 1, 'text': 'foo'}, {'id': 2, 'text': 'bar'}]}, json_file) json_file.close() - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': 1, 'text': 'foo'}), + ('2', 'bar', '2', {'id': 2, 'text': 'bar'})] assert data_sources.get_structured_items(datasource) == [ {'id': 1, 'text': 'foo'}, {'id': 2, 'text': 'bar'}] @@ -156,7 +172,9 @@ def test_json_datasource(): json_file = open(json_file_path, 'w') json.dump({'data': [{'id': '1', 'text': ''}, {'id': '2'}]}, json_file) json_file.close() - assert data_sources.get_items(datasource) == [('1', '', '1'), ('2', '2', '2')] + assert data_sources.get_items(datasource) == [ + ('1', '', '1', {'id': '1', 'text': ''}), + ('2', '2', '2', {'id': '2', 'text': '2'})] assert data_sources.get_structured_items(datasource) == [ {'id': '1', 'text': ''}, {'id': '2', 'text': '2'}] @@ -193,7 +211,9 @@ def test_register_data_source_function(): register_data_source_function(xxx) datasource = {'type': 'formula', 'value': 'xxx()'} - assert data_sources.get_items(datasource) == [('1', 'foo', '1'), ('2', 'bar', '2')] + assert data_sources.get_items(datasource) == [ + ('1', 'foo', '1', {'id': '1', 'text': 'foo'}), + ('2', 'bar', '2', {'id': '2', 'text': 'bar'})] assert data_sources.get_structured_items(datasource) == [ {'id': '1', 'text': 'foo'}, {'id': '2', 'text': 'bar'}] @@ -243,8 +263,8 @@ def test_data_source_unicode(): data_source2 = NamedDataSource.select()[0] assert data_source2.data_source == data_source.data_source assert data_sources.get_items({'type': 'foobar'}) == [ - ('uné',) * 3, - ('deux',) * 3, + ('uné', 'uné', 'uné', {'id': 'uné', 'text': 'uné'}), + ('deux', 'deux', 'deux', {'id': 'deux', 'text': 'deux'}), ] NamedDataSource.wipe() @@ -258,7 +278,7 @@ def test_data_source_unicode(): urllib2.urlopen.return_value.read.return_value = \ '{"data": [{"id": 0, "text": "zéro"}, {"id": 1, "text": "uné"}, {"id": 2, "text": "deux"}]}' assert data_sources.get_items({'type': 'foobar'}) == [ - ('0', 'zéro', '0'), - ('1', 'uné', '1'), - ('2', 'deux', '2'), + ('0', 'zéro', '0', {"id": 0, "text": "zéro"}), + ('1', 'uné', '1', {"id": 1, "text": "uné"}), + ('2', 'deux', '2', {"id": 2, "text": "deux"}), ] diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py index 16e0859..fde256e 100644 --- a/tests/test_form_pages.py +++ b/tests/test_form_pages.py @@ -3261,3 +3261,77 @@ def test_form_page_profile_verified_prefill(pub): assert 'readonly' in resp.form['f0'].attrs assert not 'Check values then click submit.' in resp.body assert resp.form['f0'].value == 'foo@localhost' + +def test_item_field_with_disabled_items(pub): + user = create_user(pub) + formdef = create_formdef() + formdef.data_class().wipe() + ds = {'type': 'json', 'value': 'http://remote.example.net/json'} + formdef.fields = [fields.ItemField(id='0', label='string', data_source=ds)] + formdef.store() + + with mock.patch('urllib2.urlopen') as urlopen: + data = {'data': [{'id': '1', 'text': 'hello'}, {'id': '2', 'text': 'world'}]} + urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data)) + resp = get_app(pub).get('/test/') + resp.form['f0'] = '1' + resp.form['f0'] = '2' + resp = resp.form.submit('submit') # -> validation page + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['0'] == '2' + assert formdef.data_class().select()[0].data['0_display'] == 'world' + + formdef.data_class().wipe() + + with mock.patch('urllib2.urlopen') as urlopen: + data = {'data': [{'id': '1', 'text': 'hello', 'disabled': True}, {'id': '2', 'text': 'world'}]} + urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data)) + resp = get_app(pub).get('/test/') + assert '' in resp.body + resp.form['f0'] = '1' + resp.form['f0'] = '2' + resp = resp.form.submit('submit') # -> validation page + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['0'] == '2' + assert formdef.data_class().select()[0].data['0_display'] == 'world' + + resp = get_app(pub).get('/test/') + assert '' in resp.body + resp.form['f0'] = '1' + resp = resp.form.submit('submit') # -> validation page + assert 'There were errors processing the form' in resp.body + + formdef.data_class().wipe() + formdef.fields = [fields.ItemField(id='0', label='string', data_source=ds, show_as_radio=True)] + formdef.store() + + with mock.patch('urllib2.urlopen') as urlopen: + data = {'data': [{'id': '1', 'text': 'hello'}, {'id': '2', 'text': 'world'}]} + urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data)) + resp = get_app(pub).get('/test/') + resp.form['f0'] = '1' + resp.form['f0'] = '2' + resp = resp.form.submit('submit') # -> validation page + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['0'] == '2' + assert formdef.data_class().select()[0].data['0_display'] == 'world' + + formdef.data_class().wipe() + + with mock.patch('urllib2.urlopen') as urlopen: + data = {'data': [{'id': '1', 'text': 'hello', 'disabled': True}, {'id': '2', 'text': 'world'}]} + urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data)) + resp = get_app(pub).get('/test/') + assert '' in resp.body + resp.form['f0'] = '1' + resp.form['f0'] = '2' + resp = resp.form.submit('submit') # -> validation page + resp = resp.form.submit('submit') # -> submit + assert formdef.data_class().select()[0].data['0'] == '2' + assert formdef.data_class().select()[0].data['0_display'] == 'world' + + resp = get_app(pub).get('/test/') + assert '' in resp.body + resp.form['f0'] = '1' + resp = resp.form.submit('submit') # -> validation page + assert 'There were errors processing the form' in resp.body diff --git a/wcs/data_sources.py b/wcs/data_sources.py index cf8811e..9991550 100644 --- a/wcs/data_sources.py +++ b/wcs/data_sources.py @@ -84,11 +84,16 @@ class DataSourceSelectionWidget(CompositeWidget): return r.getvalue() -def get_items(data_source): +def get_items(data_source, include_disabled=False): structured_items = get_structured_items(data_source) tupled_items = [] for item in structured_items: - tupled_items.append((str(item['id']), str(item['text']), str(item.get('key', item['id'])))) + if item.get('disabled') and not include_disabled: + continue + tupled_items.append((str(item['id']), + str(item['text']), + str(item.get('key', item['id'])), + item)) return tupled_items diff --git a/wcs/fields.py b/wcs/fields.py index f94f6cb..c36d0ba 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -1056,7 +1056,7 @@ class ItemField(WidgetField): def get_options(self): if self.data_source: - return data_sources.get_items(self.data_source) + return [x[:3] for x in data_sources.get_items(self.data_source)] if self.items: return self.items[:] return [] @@ -1066,6 +1066,12 @@ class ItemField(WidgetField): if real_data_source and real_data_source.get('type') == 'jsonp': kwargs['url'] = real_data_source.get('value') self.widget_class = JsonpSingleSelectWidget + elif self.items: + kwargs['options'] = self.items[:] + elif self.data_source: + items = data_sources.get_items(self.data_source, include_disabled=True) + kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')] + kwargs['options_with_disabled'] = items[:] else: kwargs['options'] = self.get_options() if not kwargs.get('options'): @@ -1239,7 +1245,7 @@ class ItemsField(WidgetField): if self.data_source: if self._cached_data_source: return self._cached_data_source - self._cached_data_source = data_sources.get_items(self.data_source) + self._cached_data_source = [x[:3] for x in data_sources.get_items(self.data_source)] return self._cached_data_source[:] elif self.items: return self.items[:] diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index b87b072..28e10a2 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -200,23 +200,33 @@ def submit_render_content(self): SubmitWidget.render_content = submit_render_content -def radiobuttons_render_content(self): - tags = [] - for object, description, key in self.options: - if self.is_selected(object): - checked = 'checked' - else: - checked = None - r = htmltag("input", xml_end=True, - type="radio", - name=self.name, - value=key, - checked=checked, - **self.attrs) - tags.append(htmltext('')) - return htmlescape(self.delim).join(tags) -RadiobuttonsWidget.render_content = radiobuttons_render_content +class RadiobuttonsWidget(quixote.form.RadiobuttonsWidget): + def __init__(self, name, value=None, **kwargs): + self.options_with_disabled = kwargs.pop('options_with_disabled', None) + super(RadiobuttonsWidget, self).__init__(name, value=value, **kwargs) + def render_content(self): + include_disabled = False + options = self.options[:] + if self.options_with_disabled: + options = self.options_with_disabled + include_disabled = True + + tags = [] + for option in options: + object, description, key = option[:3] + html_attrs = self.attrs.copy() + if self.is_selected(object): + html_attrs['checked'] = 'checked' + if self.options_with_disabled and option[-1].get('disabled'): + html_attrs['disabled'] = 'disabled' + r = htmltag("input", xml_end=True, + type="radio", + name=self.name, + value=key, + **html_attrs) + tags.append(htmltext('')) + return htmlescape(self.delim).join(tags) def checkbox_render_content(self): attrs = {'id': 'form_' + self.name} @@ -1523,6 +1533,9 @@ class CheckboxesTableWidget(TableWidget): class SingleSelectHintWidget(SingleSelectWidget): + def __init__(self, name, value=None, **kwargs): + self.options_with_disabled = kwargs.pop('options_with_disabled', None) + super(SingleSelectHintWidget, self).__init__(name, value=value, **kwargs) def separate_hint(self): return (self.hint and len(self.hint) > 80) @@ -1533,6 +1546,10 @@ class SingleSelectHintWidget(SingleSelectWidget): attrs.update(self.attrs) tags = [htmltag('select', name=self.name, **attrs)] options = self.options[:] + include_disabled = False + if self.options_with_disabled: + options = self.options_with_disabled + include_disabled = True if not self.separate_hint() and self.hint: r = htmltag('option', value='', selected=None) tags.append(r + htmlescape(self.hint) + htmltext('')) @@ -1540,14 +1557,17 @@ class SingleSelectHintWidget(SingleSelectWidget): # hint has been put as first element, skip the default empty # value. options = self.options[1:] - for object, description, key in options: + for option in options: + object, description, key = option[:3] + html_attrs = {} + html_attrs['value'] = key if self.is_selected(object): - selected = 'selected' - else: - selected = None + html_attrs['selected'] = 'selected' + if self.options_with_disabled and option[-1].get('disabled'): + html_attrs['disabled'] = 'disabled' if description is None: description = '' - r = htmltag('option', value=key, selected=selected) + r = htmltag('option', **html_attrs) tags.append(r + htmlescape(description) + htmltext('')) tags.append(htmltext('')) return htmltext('\n').join(tags) -- 2.9.3