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')