Add customer phone autocomplete and customer "info" AJAX view.

This autocomplete view is a little different than the typical ones used
prior, and required some refactoring of the base autocomplete view as well
as the autocomplete template.
This commit is contained in:
Lance Edgar 2014-07-13 12:43:58 -07:00
parent bfd1b034ee
commit f9d22f59f2
4 changed files with 125 additions and 32 deletions

View file

@ -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={})">
<div id="${field_name}-container" class="autocomplete-container"> <div id="${field_name}-container" class="autocomplete-container">
${h.hidden(field_name, id=field_name, value=field_value)} ${h.hidden(field_name, id=field_name, value=field_value)}
${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display, ${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display,
@ -13,19 +15,26 @@
$('#${field_name}-textbox').autocomplete({ $('#${field_name}-textbox').autocomplete({
source: '${service_url}', source: '${service_url}',
autoFocus: true, autoFocus: true,
% for key, value in options.items():
${key}: ${value},
% endfor
focus: function(event, ui) { focus: function(event, ui) {
return false; return false;
}, },
select: function(event, ui) { % if select:
$('#${field_name}').val(ui.item.value); select: ${select}
$('#${field_name}-display span:first').text(ui.item.label); % else:
$('#${field_name}-textbox').hide(); select: function(event, ui) {
$('#${field_name}-display').show(); $('#${field_name}').val(ui.item.value);
% if selected: $('#${field_name}-display span:first').text(ui.item.label);
${selected}(ui.item.value, ui.item.label); $('#${field_name}-textbox').hide();
% endif $('#${field_name}-display').show();
return false; % if selected:
} ${selected}(ui.item.value, ui.item.label);
% endif
return false;
}
% endif
}); });
$('#${field_name}-change').click(function() { $('#${field_name}-change').click(function() {
$('#${field_name}').val(''); $('#${field_name}').val('');

View file

@ -28,7 +28,7 @@ Pyramid Views
from .core import * from .core import *
from .grids import * from .grids import *
from .crud import * from .crud import *
from .autocomplete import * from tailbone.views.autocomplete import AutocompleteView
def home(request): def home(request):

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python # -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar # Copyright © 2010-2014 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -26,14 +25,20 @@
Autocomplete View Autocomplete View
""" """
from .core import View from tailbone.views.core import View
from ..db import Session from tailbone.db import Session
__all__ = ['AutocompleteView']
class AutocompleteView(View): 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): def filter_query(self, q):
return q return q
@ -51,11 +56,21 @@ class AutocompleteView(View):
def display(self, instance): def display(self, instance):
return getattr(instance, self.fieldname) return getattr(instance, self.fieldname)
def value(self, instance):
"""
Determine the data value for a query result instance.
"""
return instance.uuid
def __call__(self): 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: if term:
term = term.strip() term = self.prepare_term(term)
if not term: if not term:
return [] return []
results = self.query(term).all() 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]

View file

@ -25,12 +25,14 @@
Customer Views Customer Views
""" """
from sqlalchemy import and_ import re
from . import SearchableAlchemyGridView, CrudView from sqlalchemy import func, and_
from ..forms import EnumFieldRenderer 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 import enum
from rattail.db import model from rattail.db import model
@ -133,12 +135,67 @@ class CustomerCrud(CrudView):
return fs 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): def add_routes(config):
config.add_route('customers', '/customers') config.add_route(u'customers', u'/customers')
config.add_route('customer.create', '/customers/new') config.add_route(u'customer.create', u'/customers/new')
config.add_route('customer.read', '/customers/{uuid}') config.add_route(u'customer.info', u'/customers/info')
config.add_route('customer.update', '/customers/{uuid}/edit') config.add_route(u'customers.autocomplete', u'/customers/autocomplete')
config.add_route('customer.delete', '/customers/{uuid}/delete') 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): def includeme(config):
@ -147,6 +204,7 @@ def includeme(config):
config.add_view(CustomersGrid, route_name='customers', config.add_view(CustomersGrid, route_name='customers',
renderer='/customers/index.mako', renderer='/customers/index.mako',
permission='customers.list') permission='customers.list')
config.add_view(CustomerCrud, attr='create', route_name='customer.create', config.add_view(CustomerCrud, attr='create', route_name='customer.create',
renderer='/customers/crud.mako', renderer='/customers/crud.mako',
permission='customers.create') permission='customers.create')
@ -158,3 +216,14 @@ def includeme(config):
permission='customers.update') permission='customers.update')
config.add_view(CustomerCrud, attr='delete', route_name='customer.delete', config.add_view(CustomerCrud, attr='delete', route_name='customer.delete',
permission='customers.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')