From aa188443b23cc97c32f5caaacfe69f8da82c7299 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 26 Sep 2013 16:16:17 -0700 Subject: [PATCH 1/2] Add ``datasets`` parameter to AutocompleteInputWidget. This parameter provides an alternative, more powerful (though hairy) means of configuring typeahead.js, providing access to the complete feature set of the plugin. --- deform/templates/autocomplete_input.pt | 4 +- deform/tests/test_widget.py | 63 +++++++++++++++++++ deform/widget.py | 85 ++++++++++++++++++++++++-- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/deform/templates/autocomplete_input.pt b/deform/templates/autocomplete_input.pt index 88c60b45..a9a8774a 100644 --- a/deform/templates/autocomplete_input.pt +++ b/deform/templates/autocomplete_input.pt @@ -11,11 +11,11 @@ style style; attributes|field.widget.attributes|{};" id="${oid}"/> - diff --git a/deform/tests/test_widget.py b/deform/tests/test_widget.py index f41c4a74..9981ea18 100644 --- a/deform/tests/test_widget.py +++ b/deform/tests/test_widget.py @@ -1,6 +1,7 @@ """Widget tests.""" # Standard Library import unittest +import warnings # Pyramid import colander @@ -243,6 +244,25 @@ def test_deserialize_bad_type(self): self.assertRaises(colander.Invalid, widget.deserialize, field, {}) +class Test_serialize_js_config(unittest.TestCase): + def _makeLiteral(self, js): + from deform.widget import literal_js + return literal_js(js) + + def _callIt(self, obj): + from deform.widget import serialize_js_config + return serialize_js_config(obj) + + def test_serialize_literal(self): + literal = self._makeLiteral('func()') + serialized = self._callIt(literal) + self.assertEqual(serialized, 'func()') + + def test_serialize_literal_in_object(self): + literal = self._makeLiteral('func()') + serialized = self._callIt({'x': literal}) + self.assertEqual(serialized, '{"x": func()}') + class TestAutocompleteInputWidget(unittest.TestCase): def _makeOne(self, **kw): from deform.widget import AutocompleteInputWidget @@ -316,6 +336,49 @@ def test_serialize_iterable(self): {"local": [1, 2, 3, 4], "minLength": 1, "limit": 8}, ) + def test_serialize_dataset(self): + import json + widget = self._makeOne() + dataset = {'local': [5,6,7,8], + 'minLength': 2, + } + widget.datasets = dataset + renderer = DummyRenderer() + schema = DummySchema() + field = DummyField(schema, renderer=renderer) + cstruct = 'abc' + widget.serialize(field, cstruct) + self.assertEqual(renderer.template, widget.template) + self.assertEqual(renderer.kw['field'], field) + self.assertEqual(renderer.kw['cstruct'], cstruct) + self.assertEqual(json.loads(renderer.kw['options']), + [{"local": [5,6,7,8], + "minLength": 2, + "limit": 8}]) + + def test_serialize_warns_if_datasets_and_values(self): + widget = self._makeOne() + widget.datasets = [{'local': [42]}] + widget.values = [9] + renderer = DummyRenderer() + schema = DummySchema() + field = DummyField(schema, renderer=renderer) + cstruct = 'abc' + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + widget.serialize(field, cstruct) + self.assertEqual(len(w), 1) + self.assertTrue(' ignores ' in str(w[0].message)) + + def test_serialize_datasets_type_error(self): + widget = self._makeOne() + widget.datasets = 2 + renderer = DummyRenderer() + schema = DummySchema() + field = DummyField(schema, renderer=renderer) + cstruct = 'abc' + self.assertRaises(TypeError, widget.serialize, field, cstruct) + def test_serialize_not_null_readonly(self): widget = self._makeOne() renderer = DummyRenderer() diff --git a/deform/widget.py b/deform/widget.py index 182d8dbf..fb2835ca 100644 --- a/deform/widget.py +++ b/deform/widget.py @@ -4,6 +4,7 @@ import csv import json import random +from warnings import warn # Pyramid from colander import Invalid @@ -400,6 +401,35 @@ def deserialize(self, field, pstruct): return pstruct +class literal_js(text_type): + """ A marker class which can be used to include literal javascript within + the configurations for javascript functions. + + """ + def __json__(self): + return self + + +class JSConfigSerializer(json.JSONEncoder): + """ A hack-up of the stock python JSON encoder to support the inclusion + of literal javascript in the JSON output. + + """ + def _iterencode(self, o, markers): + if hasattr(o, '__json__') and callable(o.__json__): + return (chunk for chunk in (o.__json__(),)) + else: + return super(JSConfigSerializer, self)._iterencode(o, markers) + + def encode(self, o): + # bypass "extremely simple cases and benchmarks" optimization + chunks = list(self.iterencode(o)) + return ''.join(chunks) + + +serialize_js_config = JSConfigSerializer().encode + + class AutocompleteInputWidget(Widget): """ Renders an ```` widget which provides @@ -444,6 +474,30 @@ class AutocompleteInputWidget(Widget): The max number of items to display in the dropdown. Defaults to ``8``. + datasets + This is an alternative way to configure the typeahead + javascript plugin. Use of this parameter provides access to + the complete set of functionality provided by ``typeahead.js``. + + If set to other than ``None`` (the default), a JSONified + version of this value is used to configure the typeahead + plugin. (In this case any value specified for the ``values`` + parameter is ignored. The ``min_length`` and ``items`` + parameters will be used to fill in ``minLength`` and ``limit`` + in ``datasets`` if they are not already there.) + + Literal javascript can be included in the configuration using + the :class:`literal_js` marker. For example, assuming your + data provider yields datums that have an ``html`` attribute + containing HTML markup you want displayed for the choice:: + + datasets = { + 'remote': 'https://example.com/search.json?q=%QUERY', + 'template': + literal_js('function (datum) { return datum.html; }'), + } + + might do the trick. """ min_length = 1 @@ -452,6 +506,7 @@ class AutocompleteInputWidget(Widget): items = 8 template = "autocomplete_input" values = None + datasets = None requirements = (("typeahead", None), ("deform", None)) def serialize(self, field, cstruct, **kw): @@ -466,14 +521,32 @@ def serialize(self, field, cstruct, **kw): readonly = kw.get("readonly", self.readonly) options = {} - if isinstance(self.values, string_types): - options["remote"] = "%s?term=%%QUERY" % self.values - else: - options["local"] = self.values - options["minLength"] = kw.pop("min_length", self.min_length) options["limit"] = kw.pop("items", self.items) - kw["options"] = json.dumps(options) + + datasets = kw.pop('datasets', self.datasets) + if datasets is None: + if isinstance(self.values, string_types): + options['remote'] = '%s?term=%%QUERY' % self.values + elif self.values: + options['local'] = self.values + datasets = options + else: + if self.values: + warn('AutocompleteWidget ignores the *values* parameter ' + 'when *datasets* is also specified.') + + if isinstance(datasets, dict): + datasets = [datasets] + elif not isinstance(datasets, (list, tuple)): + raise TypeError( + 'Expected list, dict, or tuple, not %r' % datasets) + + def set_defaults(dataset): + return dict(options, **dataset) + datasets = list(map(set_defaults, datasets)) + + kw['options'] = serialize_js_config(datasets) if datasets else None tmpl_values = self.get_template_values(field, cstruct, kw) template = readonly and self.readonly_template or self.template return field.renderer(template, **tmpl_values) From 49d0cab7b517892947e953dfc44488f57b70d636 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Fri, 27 Sep 2013 10:04:28 -0700 Subject: [PATCH 2/2] docstring fix: different typeahead --- deform/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deform/widget.py b/deform/widget.py index fb2835ca..4b06a2c8 100644 --- a/deform/widget.py +++ b/deform/widget.py @@ -433,8 +433,8 @@ def encode(self, o): class AutocompleteInputWidget(Widget): """ Renders an ```` widget which provides - autocompletion via a list of values using bootstrap's typeahead plugin - https://github.com/twitter/typeahead.js/ + autocompletion via a list of values using twitters's typeahead plugin + https://github.com/twitter/typeahead.js **Attributes/Arguments**