Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deform2: AutocompleteInputWidget - more powerful configuration for typeahead.js #185

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deform/templates/autocomplete_input.pt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
class string: form-control ${css_class};
style style"
id="${oid}"/>
<script tal:condition="field.widget.values" type="text/javascript">
<script tal:condition="options" type="text/javascript">
deform.addCallback(
'${field.oid}',
function (oid) {
$('#' + oid).typeahead(${options});
$('#' + oid).typeahead(${structure: options});
}
);
</script>
Expand Down
63 changes: 63 additions & 0 deletions deform/tests/test_widget.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import warnings

from deform.compat import (
text_type,
Expand Down Expand Up @@ -222,6 +223,25 @@ def test_deserialize_with_nondefault_thousands_separator(self):
result = widget.deserialize(field, pstruct)
self.assertEqual(result, '1000000.00')

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
Expand Down Expand Up @@ -288,6 +308,49 @@ def test_serialize_iterable(self):
"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()
Expand Down
94 changes: 83 additions & 11 deletions deform/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import csv
import json
import random
from warnings import warn

from colander import (
Invalid,
Expand All @@ -17,6 +18,7 @@
string_types,
StringIO,
string,
text_type,
url_quote,
uppercase,
)
Expand Down Expand Up @@ -369,11 +371,37 @@ def deserialize(self, field, pstruct):
return null
return pstruct

class AutocompleteInputWidget(Widget):
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 ``<input type="text"/>`` widget which provides
autocompletion via a list of values using bootstrap's typeahead plugin
http://twitter.github.com/bootstrap/javascript.html#typeahead.
autocompletion via a list of values using twitters's typeahead plugin
https://github.com/twitter/typeahead.js

**Attributes/Arguments**

Expand Down Expand Up @@ -423,6 +451,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
readonly_template = 'readonly/textinput'
Expand All @@ -432,6 +484,7 @@ class AutocompleteInputWidget(Widget):
style = None
template = 'autocomplete_input'
values = None
datasets = None
requirements = (('typeahead', None), ('deform', None))

def __init__(self, **kw):
Expand All @@ -445,18 +498,37 @@ def serialize(self, field, cstruct, **kw):
)
if cstruct in (null, None):
cstruct = ''
self.values = self.values or []
readonly = kw.get('readonly', self.readonly)

options = {}
if isinstance(self.values, string_types):
options['remote'] = '%s?term=%%QUERY' % self.values
options = {
'minLength': kw.pop('min_length', self.min_length),
'limit': kw.pop('items', self.items),
}

datasets = kw.pop('datasets', self.datasets)
if datasets is None:
if isinstance(self.values, string_types):
options['remote'] = '%s?term=%%QUERY' % self.values
datasets = options
elif self.values:
options['local'] = self.values
datasets = options
else:
options['local'] = self.values
if self.values:
warn('AutocompleteWidget ignores the *values* parameter '
'when *datasets* is also specified.')

options['minLength'] = kw.pop('min_length', self.min_length)
options['limit'] = kw.pop('items', self.items)
kw['options'] = json.dumps(options)
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 = 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)
Expand Down