add autocomplete field renderer

This commit is contained in:
Lance Edgar 2012-07-09 13:56:08 -05:00
parent 288035b294
commit 374e1dd0d6
3 changed files with 141 additions and 0 deletions

View file

@ -29,13 +29,16 @@
from __future__ import absolute_import
import datetime
import new
from pyramid.renderers import render
from webhelpers import paginate
from webhelpers.html import tags
from webhelpers.html.builder import format_attrs
from webhelpers.html.tags import literal
import formalchemy
from formalchemy import fields
from formalchemy.validators import accepts_none
import edbob
@ -46,6 +49,7 @@ from edbob.pyramid import Session
__all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField',
'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer',
'AutocompleteFieldRenderer',
'make_fieldset', 'required', 'pretty_datetime']
@ -365,3 +369,59 @@ class PrettyDateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
def render_readonly(self, **kwargs):
return pretty_datetime(self.raw_value)
def AutocompleteFieldRenderer(service_url, display, width='300px', callback=None, **kwargs):
"""
Returns a field renderer class which leverages jQuery autocomplete to
provide a more user-friendly experience. This is typically used in place
of a ``SelectFieldRenderer`` when the data set is deemed too large for that
renderer.
``service_url`` is required and will ultimately be passed to the
``jQuery.autocomplete()`` function via the ``serviceUrl`` data parameter.
``display`` must be either a callable which accepts an object key as its
only positional argument, or else a tuple of the form ``(Class, 'attr')``.
``width`` is optional but is also passed to the jQuery function.
If ``callback`` is specified, it should be the name of a JavaScript
function within the containing page's scope. This is used in place of
event handlers for autocomplete fields.
"""
kwargs['service_url'] = service_url
kwargs['width'] = width
kwargs['callback'] = callback
Renderer = new.classobj('AutocompleteFieldRenderer', (_AutocompleteFieldRenderer,), kwargs)
if callable(display):
Renderer.display = classmethod(display)
else:
Renderer.display = display
return Renderer
class _AutocompleteFieldRenderer(fields.FieldRenderer):
"""
Implementation for :class:`AutocompleteFieldRenderer` class.
"""
def _display(self, value):
if callable(self.display):
return self.display(value)
if not value:
return ''
obj = Session.query(self.display[0]).get(value)
if not obj:
return ''
return getattr(obj, self.display[1])
def render(self, **kwargs):
autocompleter_name = 'autocompleter_%s' % self.name.replace('-', '_')
return formalchemy.config.engine('field_autocomplete', fieldname=self.name,
fieldvalue=self.value, display=self._display(self.value),
autocompleter_name=autocompleter_name,
service_url=self.service_url, width=self.width,
callback=self.callback, hidden=tags.hidden, text=tags.text,
**kwargs)

View file

@ -0,0 +1,26 @@
<div id="${fieldname}-container" class="autocomplete-container">
${hidden(fieldname, id=fieldname, value=fieldvalue)}
${text(fieldname+'-textbox', id=fieldname+'-textbox', value=display,
class_='autocomplete-textbox', style='display: none;' if fieldvalue else '')}
<div id="${fieldname}-display" class="autocomplete-display"${'' if fieldvalue else ' style="display: none;"'|n}>
<span>${display}</span>
<button type="button" id="${fieldname}-change" class="autocomplete-change">Change</button>
</div>
</div>
<script language="javascript" type="text/javascript">
$(function() {
var ${autocompleter_name} = $('#${fieldname}-textbox').autocomplete({
serviceUrl: '${service_url}',
width: '${width}',
onSelect: function(value, data) {
$('#${fieldname}').val(data);
$('#${fieldname}-display span').text(value);
$('#${fieldname}-textbox').hide();
$('#${fieldname}-display').show();
% if callback:
${callback}(value, data);
% endif
},
});
});
</script>

View file

@ -27,11 +27,66 @@
"""
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render_to_response
from pyramid.security import authenticated_userid
from webhelpers.html import literal
from webhelpers.html.tags import link_to
from edbob.pyramid import Session
from edbob.pyramid.forms.formalchemy import AutocompleteFieldRenderer
from edbob.util import requires_impl
class Autocomplete(object):
def __init__(self, request):
self.request = request
@property
@requires_impl(is_property=True)
def mapped_class(self):
raise NotImplementedError
@property
@requires_impl(is_property=True)
def fieldname(self):
raise NotImplementedError
@property
@requires_impl(is_property=True)
def route_name(self):
raise NotImplementedError
def __call__(self):
query = self.request.params['query']
q = Session.query(self.mapped_class)
q = q.filter(getattr(self.mapped_class, self.fieldname).ilike('%%%s%%' % query))
objs = q.order_by(getattr(self.mapped_class, self.fieldname)).all()
data = dict(
query=query,
suggestions=[getattr(x, self.fieldname) for x in objs],
data=[x.uuid for x in objs],
)
response = render_to_response('json', data, request=self.request)
response.headers['Content-Type'] = 'application/json'
return response
@classmethod
def add_route(cls, config, url):
# def add_routes(cls, config, route_prefix, url_prefix, template_prefix=None, permission_prefix=None):
"""
Add 'autocomplete' route to the config object.
"""
config.add_route(cls.route_name, url)
config.add_view(cls, route_name=cls.route_name, http_cache=0)
@classmethod
def renderer(cls, request):
return AutocompleteFieldRenderer(request.route_url(cls.route_name),
(cls.mapped_class, cls.fieldname))
def forbidden(request):
"""