From 374e1dd0d6375a397ec49caebe21d76ebad6460a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 9 Jul 2012 13:56:08 -0500 Subject: [PATCH] add autocomplete field renderer --- edbob/pyramid/forms/formalchemy.py | 60 +++++++++++++++++++ .../templates/forms/field_autocomplete.mako | 26 ++++++++ edbob/pyramid/views/__init__.py | 55 +++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 edbob/pyramid/templates/forms/field_autocomplete.mako diff --git a/edbob/pyramid/forms/formalchemy.py b/edbob/pyramid/forms/formalchemy.py index 3addfdc..220cc61 100644 --- a/edbob/pyramid/forms/formalchemy.py +++ b/edbob/pyramid/forms/formalchemy.py @@ -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) diff --git a/edbob/pyramid/templates/forms/field_autocomplete.mako b/edbob/pyramid/templates/forms/field_autocomplete.mako new file mode 100644 index 0000000..d65673a --- /dev/null +++ b/edbob/pyramid/templates/forms/field_autocomplete.mako @@ -0,0 +1,26 @@ +
+ ${hidden(fieldname, id=fieldname, value=fieldvalue)} + ${text(fieldname+'-textbox', id=fieldname+'-textbox', value=display, + class_='autocomplete-textbox', style='display: none;' if fieldvalue else '')} + +
+ diff --git a/edbob/pyramid/views/__init__.py b/edbob/pyramid/views/__init__.py index 236a454..debf617 100644 --- a/edbob/pyramid/views/__init__.py +++ b/edbob/pyramid/views/__init__.py @@ -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): """