Refactor autocomplete view logic to leverage new "autocompleters"
finally! this cleans up some view config and AFAIK there is no loss in functionality etc.
This commit is contained in:
parent
e0dff55ffa
commit
a7f4b2e6ef
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2021 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -29,9 +29,6 @@ from __future__ import unicode_literals, absolute_import
|
||||||
from .core import View
|
from .core import View
|
||||||
from .master import MasterView
|
from .master import MasterView
|
||||||
|
|
||||||
# TODO: deprecate / remove some of this
|
|
||||||
from .autocomplete import AutocompleteView
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Rattail -- Retail Software Framework
|
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Rattail.
|
|
||||||
#
|
|
||||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
|
||||||
# terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Autocomplete View
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def make_query(self, term):
|
|
||||||
"""
|
|
||||||
Make and return the "complete" query for the given search term.
|
|
||||||
"""
|
|
||||||
# we are querying one table (and column) primarily
|
|
||||||
query = Session.query(self.mapped_class)
|
|
||||||
column = getattr(self.mapped_class, self.fieldname)
|
|
||||||
|
|
||||||
# filter according to business logic, if applicable
|
|
||||||
query = self.filter_query(query)
|
|
||||||
|
|
||||||
# filter according to search term(s)
|
|
||||||
criteria = [column.ilike('%{}%'.format(word))
|
|
||||||
for word in term.split()]
|
|
||||||
query = query.filter(sa.and_(*criteria))
|
|
||||||
|
|
||||||
# sort results by something meaningful
|
|
||||||
query = query.order_by(column)
|
|
||||||
return query
|
|
||||||
|
|
||||||
def query(self, term):
|
|
||||||
return self.make_query(term)
|
|
||||||
|
|
||||||
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 get_data(self, term):
|
|
||||||
return self.query(term).all()
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
"""
|
|
||||||
View implementation.
|
|
||||||
"""
|
|
||||||
term = self.request.params.get(u'term') or u''
|
|
||||||
term = term.strip()
|
|
||||||
if term:
|
|
||||||
term = self.prepare_term(term)
|
|
||||||
if not term:
|
|
||||||
return []
|
|
||||||
results = self.get_data(term)
|
|
||||||
return [{'label': self.display(x), 'value': self.value(x)} for x in results]
|
|
|
@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
class BrandView(MasterView):
|
class BrandView(MasterView):
|
||||||
|
@ -38,6 +38,7 @@ class BrandView(MasterView):
|
||||||
model_class = model.Brand
|
model_class = model.Brand
|
||||||
has_versions = True
|
has_versions = True
|
||||||
bulk_deletable = True
|
bulk_deletable = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
mergeable = True
|
mergeable = True
|
||||||
merge_additive_fields = [
|
merge_additive_fields = [
|
||||||
|
@ -133,21 +134,6 @@ class BrandView(MasterView):
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
self.Session.delete(removing)
|
self.Session.delete(removing)
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
BrandsView = BrandView
|
|
||||||
|
|
||||||
|
|
||||||
class BrandsAutocomplete(AutocompleteView):
|
|
||||||
|
|
||||||
mapped_class = model.Brand
|
|
||||||
fieldname = 'name'
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('brands.autocomplete', '/brands/autocomplete')
|
|
||||||
config.add_view(BrandsAutocomplete, route_name='brands.autocomplete',
|
|
||||||
renderer='json', permission='brands.list')
|
|
||||||
|
|
||||||
BrandView.defaults(config)
|
BrandView.defaults(config)
|
||||||
|
|
|
@ -26,11 +26,8 @@ Customer Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from pyramid.httpexceptions import HTTPNotFound
|
from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
@ -38,7 +35,7 @@ from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import grids
|
from tailbone import grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
@ -52,6 +49,7 @@ class CustomerView(MasterView):
|
||||||
has_versions = True
|
has_versions = True
|
||||||
people_detachable = True
|
people_detachable = True
|
||||||
touchable = True
|
touchable = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
# whether to show "view full profile" helper for customer view
|
# whether to show "view full profile" helper for customer view
|
||||||
show_profiles_helper = True
|
show_profiles_helper = True
|
||||||
|
@ -451,9 +449,6 @@ class CustomerView(MasterView):
|
||||||
route_name='{}.detach_person'.format(route_prefix),
|
route_name='{}.detach_person'.format(route_prefix),
|
||||||
permission='{}.detach_person'.format(permission_prefix))
|
permission='{}.detach_person'.format(permission_prefix))
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
CustomersView = CustomerView
|
|
||||||
|
|
||||||
|
|
||||||
# # TODO: this is referenced by some custom apps, but should be moved??
|
# # TODO: this is referenced by some custom apps, but should be moved??
|
||||||
# def unique_id(value, field):
|
# def unique_id(value, field):
|
||||||
|
@ -473,43 +468,6 @@ def unique_id(node, value):
|
||||||
raise colander.Invalid(node, "Customer ID must be unique")
|
raise colander.Invalid(node, "Customer ID must be unique")
|
||||||
|
|
||||||
|
|
||||||
class CustomerNameAutocomplete(AutocompleteView):
|
|
||||||
"""
|
|
||||||
Autocomplete view which operates on customer name.
|
|
||||||
"""
|
|
||||||
mapped_class = model.Customer
|
|
||||||
fieldname = '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(r'\D')
|
|
||||||
|
|
||||||
def prepare_term(self, term):
|
|
||||||
return self.invalid_pattern.sub('', term)
|
|
||||||
|
|
||||||
def query(self, term):
|
|
||||||
return Session.query(model.CustomerPhoneNumber)\
|
|
||||||
.filter(sa.func.regexp_replace(model.CustomerPhoneNumber.number, r'\D', '', 'g').like('%{0}%'.format(term)))\
|
|
||||||
.order_by(model.CustomerPhoneNumber.number)\
|
|
||||||
.options(orm.joinedload(model.CustomerPhoneNumber.customer))
|
|
||||||
|
|
||||||
def display(self, phone):
|
|
||||||
return "{0} {1}".format(phone.number, phone.customer)
|
|
||||||
|
|
||||||
def value(self, phone):
|
|
||||||
return phone.customer.uuid
|
|
||||||
|
|
||||||
|
|
||||||
def customer_info(request):
|
def customer_info(request):
|
||||||
"""
|
"""
|
||||||
View which returns simple dictionary of info for a particular customer.
|
View which returns simple dictionary of info for a particular customer.
|
||||||
|
@ -527,14 +485,6 @@ def customer_info(request):
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('customers.autocomplete', '/customers/autocomplete')
|
|
||||||
config.add_view(CustomerNameAutocomplete, route_name='customers.autocomplete',
|
|
||||||
renderer='json', permission='customers.list')
|
|
||||||
config.add_route('customers.autocomplete.phone', '/customers/autocomplete/phone')
|
|
||||||
config.add_view(CustomerPhoneAutocomplete, route_name='customers.autocomplete.phone',
|
|
||||||
renderer='json', permission='customers.list')
|
|
||||||
|
|
||||||
# info
|
# info
|
||||||
config.add_route('customer.info', '/customers/info')
|
config.add_route('customer.info', '/customers/info')
|
||||||
config.add_view(customer_info, route_name='customer.info',
|
config.add_view(customer_info, route_name='customer.info',
|
||||||
|
|
|
@ -30,11 +30,10 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from deform import widget as dfwidget
|
|
||||||
from webhelpers2.html import HTML
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
from tailbone import grids
|
from tailbone import grids
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
class DepartmentView(MasterView):
|
class DepartmentView(MasterView):
|
||||||
|
@ -44,6 +43,7 @@ class DepartmentView(MasterView):
|
||||||
model_class = model.Department
|
model_class = model.Department
|
||||||
touchable = True
|
touchable = True
|
||||||
has_versions = True
|
has_versions = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'number',
|
'number',
|
||||||
|
@ -239,21 +239,6 @@ class DepartmentView(MasterView):
|
||||||
|
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
DepartmentsView = DepartmentView
|
|
||||||
|
|
||||||
|
|
||||||
class DepartmentsAutocomplete(AutocompleteView):
|
|
||||||
|
|
||||||
mapped_class = model.Department
|
|
||||||
fieldname = 'name'
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('departments.autocomplete', '/departments/autocomplete')
|
|
||||||
config.add_view(DepartmentsAutocomplete, route_name='departments.autocomplete',
|
|
||||||
renderer='json', permission='departments.list')
|
|
||||||
|
|
||||||
DepartmentView.defaults(config)
|
DepartmentView.defaults(config)
|
||||||
|
|
|
@ -36,8 +36,7 @@ from deform import widget as dfwidget
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from tailbone import grids
|
from tailbone import grids
|
||||||
from tailbone.db import Session
|
from tailbone.views import MasterView
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
|
||||||
|
|
||||||
|
|
||||||
class EmployeeView(MasterView):
|
class EmployeeView(MasterView):
|
||||||
|
@ -47,6 +46,7 @@ class EmployeeView(MasterView):
|
||||||
model_class = model.Employee
|
model_class = model.Employee
|
||||||
has_versions = True
|
has_versions = True
|
||||||
touchable = True
|
touchable = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'id': "ID",
|
'id': "ID",
|
||||||
|
@ -310,31 +310,6 @@ class EmployeeView(MasterView):
|
||||||
(model.EmployeeDepartment, 'employee_uuid'),
|
(model.EmployeeDepartment, 'employee_uuid'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
EmployeesView = EmployeeView
|
|
||||||
|
|
||||||
|
|
||||||
class EmployeesAutocomplete(AutocompleteView):
|
|
||||||
"""
|
|
||||||
Autocomplete view for the Employee model, but restricted to return only
|
|
||||||
results for current employees.
|
|
||||||
"""
|
|
||||||
mapped_class = model.Person
|
|
||||||
fieldname = 'display_name'
|
|
||||||
|
|
||||||
def filter_query(self, q):
|
|
||||||
return q.join(model.Employee)\
|
|
||||||
.filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)
|
|
||||||
|
|
||||||
def value(self, person):
|
|
||||||
return person.employee.uuid
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('employees.autocomplete', '/employees/autocomplete')
|
|
||||||
config.add_view(EmployeesAutocomplete, route_name='employees.autocomplete',
|
|
||||||
renderer='json', permission='employees.list')
|
|
||||||
|
|
||||||
EmployeeView.defaults(config)
|
EmployeeView.defaults(config)
|
||||||
|
|
|
@ -97,6 +97,7 @@ class MasterView(View):
|
||||||
delete_confirm = 'full'
|
delete_confirm = 'full'
|
||||||
bulk_deletable = False
|
bulk_deletable = False
|
||||||
set_deletable = False
|
set_deletable = False
|
||||||
|
supports_autocomplete = False
|
||||||
supports_set_enabled_toggle = False
|
supports_set_enabled_toggle = False
|
||||||
populatable = False
|
populatable = False
|
||||||
mergeable = False
|
mergeable = False
|
||||||
|
@ -3573,6 +3574,35 @@ class MasterView(View):
|
||||||
return self.after_delete_url
|
return self.after_delete_url
|
||||||
return self.get_index_url()
|
return self.get_index_url()
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# Autocomplete Stuff
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def autocomplete(self):
|
||||||
|
"""
|
||||||
|
View which accepts a single ``term`` param, and returns a list
|
||||||
|
of autocomplete results to match.
|
||||||
|
"""
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
key = self.get_autocompleter_key()
|
||||||
|
# url may include key, for more specific autocompleter
|
||||||
|
if 'key' in self.request.matchdict:
|
||||||
|
key = '{}.{}'.format(key, self.request.matchdict['key'])
|
||||||
|
autocompleter = app.get_autocompleter(key)
|
||||||
|
|
||||||
|
term = self.request.params.get('term', '')
|
||||||
|
return autocompleter.autocomplete(self.Session(), term)
|
||||||
|
|
||||||
|
def get_autocompleter_key(self):
|
||||||
|
"""
|
||||||
|
Must return the "key" to be used when locating the
|
||||||
|
Autocompleter object, for use with autocomplete view.
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'autocompleter_key'):
|
||||||
|
if self.autocompleter_key:
|
||||||
|
return self.autocompleter_key
|
||||||
|
return self.get_route_prefix()
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# Associated Rows Stuff
|
# Associated Rows Stuff
|
||||||
##############################
|
##############################
|
||||||
|
@ -3965,6 +3995,25 @@ class MasterView(View):
|
||||||
config.add_view(cls, attr='quickie', route_name='{}.quickie'.format(route_prefix),
|
config.add_view(cls, attr='quickie', route_name='{}.quickie'.format(route_prefix),
|
||||||
permission='{}.quickie'.format(permission_prefix))
|
permission='{}.quickie'.format(permission_prefix))
|
||||||
|
|
||||||
|
# autocomplete
|
||||||
|
if cls.supports_autocomplete:
|
||||||
|
|
||||||
|
# default
|
||||||
|
config.add_route('{}.autocomplete'.format(route_prefix),
|
||||||
|
'{}/autocomplete'.format(url_prefix))
|
||||||
|
config.add_view(cls, attr='autocomplete',
|
||||||
|
route_name='{}.autocomplete'.format(route_prefix),
|
||||||
|
renderer='json',
|
||||||
|
permission='{}.list'.format(permission_prefix))
|
||||||
|
|
||||||
|
# special
|
||||||
|
config.add_route('{}.autocomplete_special'.format(route_prefix),
|
||||||
|
'{}/autocomplete/{{key}}'.format(url_prefix))
|
||||||
|
config.add_view(cls, attr='autocomplete',
|
||||||
|
route_name='{}.autocomplete_special'.format(route_prefix),
|
||||||
|
renderer='json',
|
||||||
|
permission='{}.list'.format(permission_prefix))
|
||||||
|
|
||||||
# create
|
# create
|
||||||
if cls.creatable:
|
if cls.creatable:
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.create'.format(permission_prefix),
|
config.add_tailbone_permission(permission_prefix, '{}.create'.format(permission_prefix),
|
||||||
|
|
|
@ -41,7 +41,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import forms, grids
|
from tailbone import forms, grids
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
class PersonView(MasterView):
|
class PersonView(MasterView):
|
||||||
|
@ -56,6 +56,7 @@ class PersonView(MasterView):
|
||||||
bulk_deletable = True
|
bulk_deletable = True
|
||||||
is_contact = True
|
is_contact = True
|
||||||
manage_notes_from_profile_view = False
|
manage_notes_from_profile_view = False
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'default_phone': "Phone Number",
|
'default_phone': "Phone Number",
|
||||||
|
@ -854,25 +855,6 @@ class PersonView(MasterView):
|
||||||
config.add_view(cls, attr='request_merge', route_name='{}.request_merge'.format(route_prefix),
|
config.add_view(cls, attr='request_merge', route_name='{}.request_merge'.format(route_prefix),
|
||||||
permission='{}.request_merge'.format(permission_prefix))
|
permission='{}.request_merge'.format(permission_prefix))
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
PeopleView = PersonView
|
|
||||||
|
|
||||||
|
|
||||||
class PeopleAutocomplete(AutocompleteView):
|
|
||||||
|
|
||||||
mapped_class = model.Person
|
|
||||||
fieldname = 'display_name'
|
|
||||||
|
|
||||||
|
|
||||||
class PeopleEmployeesAutocomplete(PeopleAutocomplete):
|
|
||||||
"""
|
|
||||||
Autocomplete view for the Person model, but restricted to return only
|
|
||||||
results for people who are employees.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def filter_query(self, q):
|
|
||||||
return q.join(model.Employee)
|
|
||||||
|
|
||||||
|
|
||||||
class PersonNoteView(MasterView):
|
class PersonNoteView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
@ -1044,15 +1026,6 @@ class MergePeopleRequestView(MasterView):
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('people.autocomplete', '/people/autocomplete')
|
|
||||||
config.add_view(PeopleAutocomplete, route_name='people.autocomplete',
|
|
||||||
renderer='json', permission='people.list')
|
|
||||||
config.add_route('people.autocomplete.employees', '/people/autocomplete/employees')
|
|
||||||
config.add_view(PeopleEmployeesAutocomplete, route_name='people.autocomplete.employees',
|
|
||||||
renderer='json', permission='people.list')
|
|
||||||
|
|
||||||
PersonView.defaults(config)
|
PersonView.defaults(config)
|
||||||
PersonNoteView.defaults(config)
|
PersonNoteView.defaults(config)
|
||||||
MergePeopleRequestView.defaults(config)
|
MergePeopleRequestView.defaults(config)
|
||||||
|
|
|
@ -51,7 +51,7 @@ from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from tailbone import forms, grids
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
from tailbone.util import raw_datetime
|
from tailbone.util import raw_datetime
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ class ProductView(MasterView):
|
||||||
model_class = model.Product
|
model_class = model.Product
|
||||||
has_versions = True
|
has_versions = True
|
||||||
results_downloadable_xlsx = True
|
results_downloadable_xlsx = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'item_id': "Item ID",
|
'item_id': "Item ID",
|
||||||
|
@ -1886,31 +1887,6 @@ class ProductView(MasterView):
|
||||||
renderer='json',
|
renderer='json',
|
||||||
permission='{}.versions'.format(permission_prefix))
|
permission='{}.versions'.format(permission_prefix))
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
ProductsView = ProductView
|
|
||||||
|
|
||||||
|
|
||||||
class ProductsAutocomplete(AutocompleteView):
|
|
||||||
"""
|
|
||||||
Autocomplete view for products.
|
|
||||||
"""
|
|
||||||
mapped_class = model.Product
|
|
||||||
fieldname = 'description'
|
|
||||||
|
|
||||||
def query(self, term):
|
|
||||||
q = Session.query(model.Product).outerjoin(model.Brand)
|
|
||||||
q = q.filter(sa.or_(
|
|
||||||
model.Brand.name.ilike('%{}%'.format(term)),
|
|
||||||
model.Product.description.ilike('%{}%'.format(term))))
|
|
||||||
if not self.request.has_perm('products.view_deleted'):
|
|
||||||
q = q.filter(model.Product.deleted == False)
|
|
||||||
q = q.order_by(model.Brand.name, model.Product.description)
|
|
||||||
q = q.options(orm.joinedload(model.Product.brand))
|
|
||||||
return q
|
|
||||||
|
|
||||||
def display(self, product):
|
|
||||||
return product.full_description
|
|
||||||
|
|
||||||
|
|
||||||
def print_labels(request):
|
def print_labels(request):
|
||||||
profile = request.params.get('profile')
|
profile = request.params.get('profile')
|
||||||
|
@ -1942,10 +1918,6 @@ def print_labels(request):
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
config.add_route('products.autocomplete', '/products/autocomplete')
|
|
||||||
config.add_view(ProductsAutocomplete, route_name='products.autocomplete',
|
|
||||||
renderer='json', permission='products.list')
|
|
||||||
|
|
||||||
config.add_route('products.print_labels', '/products/labels')
|
config.add_route('products.print_labels', '/products/labels')
|
||||||
config.add_view(print_labels, route_name='products.print_labels',
|
config.add_view(print_labels, route_name='products.print_labels',
|
||||||
renderer='json', permission='products.print_labels')
|
renderer='json', permission='products.print_labels')
|
||||||
|
|
4
tailbone/views/vendors/__init__.py
vendored
4
tailbone/views/vendors/__init__.py
vendored
|
@ -26,9 +26,7 @@ Views pertaining to vendors
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import VendorView, VendorsAutocomplete
|
from .core import VendorView
|
||||||
# TODO: deprecate / remove this
|
|
||||||
from .core import VendorsView
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
18
tailbone/views/vendors/core.py
vendored
18
tailbone/views/vendors/core.py
vendored
|
@ -32,7 +32,7 @@ from rattail.db import model
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
class VendorView(MasterView):
|
class VendorView(MasterView):
|
||||||
|
@ -42,6 +42,7 @@ class VendorView(MasterView):
|
||||||
model_class = model.Vendor
|
model_class = model.Vendor
|
||||||
has_versions = True
|
has_versions = True
|
||||||
touchable = True
|
touchable = True
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'id': "ID",
|
'id': "ID",
|
||||||
|
@ -167,21 +168,6 @@ class VendorView(MasterView):
|
||||||
(model.VendorContact, 'vendor_uuid'),
|
(model.VendorContact, 'vendor_uuid'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
VendorsView = VendorView
|
|
||||||
|
|
||||||
|
|
||||||
class VendorsAutocomplete(AutocompleteView):
|
|
||||||
|
|
||||||
mapped_class = model.Vendor
|
|
||||||
fieldname = 'name'
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
# autocomplete
|
|
||||||
config.add_route('vendors.autocomplete', '/vendors/autocomplete')
|
|
||||||
config.add_view(VendorsAutocomplete, route_name='vendors.autocomplete',
|
|
||||||
renderer='json', permission='vendors.list')
|
|
||||||
|
|
||||||
VendorView.defaults(config)
|
VendorView.defaults(config)
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
|
|
||||||
from mock import Mock
|
|
||||||
from pyramid import testing
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from .. import TestCase, mock_query
|
|
||||||
from tailbone.views import autocomplete
|
|
||||||
|
|
||||||
|
|
||||||
class BareAutocompleteViewTests(TestCase):
|
|
||||||
|
|
||||||
def view(self, **kwargs):
|
|
||||||
request = testing.DummyRequest(**kwargs)
|
|
||||||
return autocomplete.AutocompleteView(request)
|
|
||||||
|
|
||||||
def test_attributes(self):
|
|
||||||
view = self.view()
|
|
||||||
self.assertRaises(AttributeError, getattr, view, 'mapped_class')
|
|
||||||
self.assertRaises(AttributeError, getattr, view, 'fieldname')
|
|
||||||
|
|
||||||
def test_filter_query(self):
|
|
||||||
view = self.view()
|
|
||||||
query = Mock()
|
|
||||||
filtered = view.filter_query(query)
|
|
||||||
self.assertTrue(filtered is query)
|
|
||||||
|
|
||||||
def test_make_query(self):
|
|
||||||
view = self.view()
|
|
||||||
# No mapped_class defined for view.
|
|
||||||
self.assertRaises(AttributeError, view.make_query, 'test')
|
|
||||||
|
|
||||||
def test_query(self):
|
|
||||||
view = self.view()
|
|
||||||
query = Mock()
|
|
||||||
view.make_query = Mock(return_value=query)
|
|
||||||
filtered = view.query('test')
|
|
||||||
self.assertTrue(filtered is query)
|
|
||||||
|
|
||||||
def test_display(self):
|
|
||||||
view = self.view()
|
|
||||||
instance = Mock()
|
|
||||||
# No fieldname defined for view.
|
|
||||||
self.assertRaises(AttributeError, view.display, instance)
|
|
||||||
|
|
||||||
def test_call(self):
|
|
||||||
# Empty or missing query term yields empty list.
|
|
||||||
view = self.view(params={})
|
|
||||||
self.assertEqual(view(), [])
|
|
||||||
view = self.view(params={'term': None})
|
|
||||||
self.assertEqual(view(), [])
|
|
||||||
view = self.view(params={'term': ''})
|
|
||||||
self.assertEqual(view(), [])
|
|
||||||
view = self.view(params={'term': '\t'})
|
|
||||||
self.assertEqual(view(), [])
|
|
||||||
# No mapped_class defined for view.
|
|
||||||
view = self.view(params={'term': 'bogus'})
|
|
||||||
self.assertRaises(AttributeError, view)
|
|
||||||
|
|
||||||
|
|
||||||
class SampleAutocompleteViewTests(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SampleAutocompleteViewTests, self).setUp()
|
|
||||||
self.Session_query = autocomplete.Session.query
|
|
||||||
self.query = mock_query()
|
|
||||||
autocomplete.Session.query = self.query
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super(SampleAutocompleteViewTests, self).tearDown()
|
|
||||||
autocomplete.Session.query = self.Session_query
|
|
||||||
|
|
||||||
def view(self, **kwargs):
|
|
||||||
request = testing.DummyRequest(**kwargs)
|
|
||||||
view = autocomplete.AutocompleteView(request)
|
|
||||||
view.mapped_class = Mock()
|
|
||||||
view.fieldname = 'thing'
|
|
||||||
return view
|
|
||||||
|
|
||||||
def test_make_query(self):
|
|
||||||
view = self.view()
|
|
||||||
whatever = sa.text('whatever')
|
|
||||||
view.mapped_class.thing.ilike.return_value = whatever
|
|
||||||
self.assertTrue(view.make_query('test') is self.query)
|
|
||||||
view.mapped_class.thing.ilike.assert_called_with('%test%')
|
|
||||||
self.query.filter.assert_called_with(whatever)
|
|
||||||
self.query.order_by.assert_called_with(view.mapped_class.thing)
|
|
Loading…
Reference in a new issue