diff --git a/tailbone/templates/autocomplete.mako b/tailbone/templates/autocomplete.mako index e9548794..cf76ee7c 100644 --- a/tailbone/templates/autocomplete.mako +++ b/tailbone/templates/autocomplete.mako @@ -1,4 +1,6 @@ -<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', selected=None, cleared=None)"> +## -*- coding: utf-8 -*- +## TODO: This function signature is getting out of hand... +<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width=u'300px', select=None, selected=None, cleared=None, options={})">
${h.hidden(field_name, id=field_name, value=field_value)} ${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display, @@ -13,19 +15,26 @@ $('#${field_name}-textbox').autocomplete({ source: '${service_url}', autoFocus: true, + % for key, value in options.items(): + ${key}: ${value}, + % endfor focus: function(event, ui) { return false; }, - select: function(event, ui) { - $('#${field_name}').val(ui.item.value); - $('#${field_name}-display span:first').text(ui.item.label); - $('#${field_name}-textbox').hide(); - $('#${field_name}-display').show(); - % if selected: - ${selected}(ui.item.value, ui.item.label); - % endif - return false; - } + % if select: + select: ${select} + % else: + select: function(event, ui) { + $('#${field_name}').val(ui.item.value); + $('#${field_name}-display span:first').text(ui.item.label); + $('#${field_name}-textbox').hide(); + $('#${field_name}-display').show(); + % if selected: + ${selected}(ui.item.value, ui.item.label); + % endif + return false; + } + % endif }); $('#${field_name}-change').click(function() { $('#${field_name}').val(''); diff --git a/tailbone/views/__init__.py b/tailbone/views/__init__.py index adc82ac6..6a8ae08b 100644 --- a/tailbone/views/__init__.py +++ b/tailbone/views/__init__.py @@ -28,7 +28,7 @@ Pyramid Views from .core import * from .grids import * from .crud import * -from .autocomplete import * +from tailbone.views.autocomplete import AutocompleteView def home(request): diff --git a/tailbone/views/autocomplete.py b/tailbone/views/autocomplete.py index 2865c5ca..26fb47db 100644 --- a/tailbone/views/autocomplete.py +++ b/tailbone/views/autocomplete.py @@ -1,9 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2012 Lance Edgar +# Copyright © 2010-2014 Lance Edgar # # This file is part of Rattail. # @@ -26,14 +25,20 @@ Autocomplete View """ -from .core import View -from ..db import Session - - -__all__ = ['AutocompleteView'] +from tailbone.views.core import View +from tailbone.db import Session class AutocompleteView(View): + """ + Base class for generic autocomplete views. + """ + + def prepare_term(self, term): + """ + If necessary, massage the incoming search term for use with the query. + """ + return term def filter_query(self, q): return q @@ -51,11 +56,21 @@ class AutocompleteView(View): def display(self, instance): return getattr(instance, self.fieldname) + def value(self, instance): + """ + Determine the data value for a query result instance. + """ + return instance.uuid + def __call__(self): - term = self.request.params.get('term') + """ + View implementation. + """ + term = self.request.params.get(u'term') or u'' + term = term.strip() if term: - term = term.strip() + term = self.prepare_term(term) if not term: return [] results = self.query(term).all() - return [{'label': self.display(x), 'value': x.uuid} for x in results] + return [{u'label': self.display(x), u'value': self.value(x)} for x in results] diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index 0a41ade6..bdedcad9 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -25,12 +25,14 @@ Customer Views """ -from sqlalchemy import and_ +import re -from . import SearchableAlchemyGridView, CrudView -from ..forms import EnumFieldRenderer +from sqlalchemy import func, and_ +from sqlalchemy.orm import joinedload -from ..db import Session +from tailbone.views import SearchableAlchemyGridView, CrudView, AutocompleteView +from tailbone.forms import EnumFieldRenderer +from tailbone.db import Session from rattail import enum from rattail.db import model @@ -133,12 +135,67 @@ class CustomerCrud(CrudView): return fs +class CustomerNameAutocomplete(AutocompleteView): + """ + Autocomplete view which operates on customer name. + """ + mapped_class = model.Customer + fieldname = u'name' + + +class CustomerPhoneAutocomplete(AutocompleteView): + """ + Autocomplete view which operates on customer phone number. + + .. note:: + As currently implemented, this view will only work with a PostgreSQL + database. It normalizes the user's search term and the database values + to numeric digits only (i.e. removes special characters from each) in + order to be able to perform smarter matching. However normalizing the + database value currently uses the PG SQL ``regexp_replace()`` function. + """ + invalid_pattern = re.compile(ur'\D') + + def prepare_term(self, term): + return self.invalid_pattern.sub(u'', term) + + def query(self, term): + return Session.query(model.CustomerPhoneNumber)\ + .filter(func.regexp_replace(model.CustomerPhoneNumber.number, ur'\D', u'', u'g').like(u'%{0}%'.format(term)))\ + .order_by(model.CustomerPhoneNumber.number)\ + .options(joinedload(model.CustomerPhoneNumber.customer)) + + def display(self, phone): + return u"{0} {1}".format(phone.number, phone.customer) + + def value(self, phone): + return phone.customer.uuid + + +def customer_info(request): + """ + View which returns simple dictionary of info for a particular customer. + """ + uuid = request.params.get(u'uuid') + customer = Session.query(model.Customer).get(uuid) if uuid else None + if not customer: + return {} + return { + u'uuid': customer.uuid, + u'name': customer.name, + u'phone_number': customer.phone.number if customer.phone else u'', + } + + def add_routes(config): - config.add_route('customers', '/customers') - config.add_route('customer.create', '/customers/new') - config.add_route('customer.read', '/customers/{uuid}') - config.add_route('customer.update', '/customers/{uuid}/edit') - config.add_route('customer.delete', '/customers/{uuid}/delete') + config.add_route(u'customers', u'/customers') + config.add_route(u'customer.create', u'/customers/new') + config.add_route(u'customer.info', u'/customers/info') + config.add_route(u'customers.autocomplete', u'/customers/autocomplete') + config.add_route(u'customers.autocomplete.phone', u'/customers/autocomplete/phone') + config.add_route(u'customer.read', u'/customers/{uuid}') + config.add_route(u'customer.update', u'/customers/{uuid}/edit') + config.add_route(u'customer.delete', u'/customers/{uuid}/delete') def includeme(config): @@ -147,6 +204,7 @@ def includeme(config): config.add_view(CustomersGrid, route_name='customers', renderer='/customers/index.mako', permission='customers.list') + config.add_view(CustomerCrud, attr='create', route_name='customer.create', renderer='/customers/crud.mako', permission='customers.create') @@ -158,3 +216,14 @@ def includeme(config): permission='customers.update') config.add_view(CustomerCrud, attr='delete', route_name='customer.delete', permission='customers.delete') + + config.add_view(CustomerNameAutocomplete, route_name=u'customers.autocomplete', + renderer=u'json', + permission=u'customers.list') + config.add_view(CustomerPhoneAutocomplete, route_name=u'customers.autocomplete.phone', + renderer=u'json', + permission=u'customers.list') + + config.add_view(customer_info, route_name=u'customer.info', + renderer=u'json', + permission=u'customers.read')