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 from __future__ import absolute_import
import datetime import datetime
import new
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers import paginate from webhelpers import paginate
from webhelpers.html import tags
from webhelpers.html.builder import format_attrs from webhelpers.html.builder import format_attrs
from webhelpers.html.tags import literal from webhelpers.html.tags import literal
import formalchemy import formalchemy
from formalchemy import fields
from formalchemy.validators import accepts_none from formalchemy.validators import accepts_none
import edbob import edbob
@ -46,6 +49,7 @@ from edbob.pyramid import Session
__all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField', __all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField',
'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer', 'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer',
'AutocompleteFieldRenderer',
'make_fieldset', 'required', 'pretty_datetime'] 'make_fieldset', 'required', 'pretty_datetime']
@ -365,3 +369,59 @@ class PrettyDateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
def render_readonly(self, **kwargs): def render_readonly(self, **kwargs):
return pretty_datetime(self.raw_value) 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.httpexceptions import HTTPFound
from pyramid.renderers import render_to_response
from pyramid.security import authenticated_userid from pyramid.security import authenticated_userid
from webhelpers.html import literal from webhelpers.html import literal
from webhelpers.html.tags import link_to 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): def forbidden(request):
""" """