Source code for camelot.view.search

#  ============================================================================
#
#  Copyright (C) 2007-2016 Conceptive Engineering bvba.
#  www.conceptive.be / info@conceptive.be
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#      * Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#      * Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#      * Neither the name of Conceptive Engineering nor the
#        names of its contributors may be used to endorse or promote products
#        derived from this software without specific prior written permission.
#  
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#  DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
#  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
#  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#  ============================================================================

"""
Helper functions to search through a collection of entities
"""
import datetime
import decimal
import logging

LOGGER = logging.getLogger('camelot.view.search')

import six

from camelot.types import virtual_address
from sqlalchemy import sql

import camelot.types

[docs]def create_entity_search_query_decorator( admin, text ): """create a query decorator to search through a collection of entities :param admin: the admin interface of the entity :param text: the text to search for :return: a function that can be applied to a query to make the query filter only the objects related to the requested text or None if no such decorator could be build """ from camelot.view import utils if len(text.strip()): # arguments for the where clause args = [] # join conditions : list of join entities joins = [] def append_column( c, text, args ): """add column c to the where clause using a clause that is relevant for that type of column""" arg = None try: python_type = c.type.python_type except NotImplementedError: return # @todo : this should use the from_string field attribute, without # looking at the sql code if issubclass(c.type.__class__, camelot.types.Color): pass elif issubclass(c.type.__class__, camelot.types.File): pass elif issubclass(c.type.__class__, camelot.types.Enumeration): pass elif issubclass(c.type.__class__, camelot.types.Code): codes = [u'%%%s%%'%s for s in text.split(c.type.separator)] codes = codes + ['%']*(len(c.type.parts) - len(codes)) arg = c.like( codes ) elif issubclass(python_type, virtual_address): arg = c.like(virtual_address('%', '%'+text+'%')) elif issubclass(c.type.__class__, camelot.types.Image): pass elif issubclass(python_type, bool): try: arg = (c==utils.bool_from_string(text)) except ( Exception, utils.ParsingError ): pass elif issubclass(python_type, int): try: arg = (c==utils.int_from_string(text)) except ( Exception, utils.ParsingError ): pass elif issubclass(python_type, datetime.date): try: arg = (c==utils.date_from_string(text)) except ( Exception, utils.ParsingError ): pass elif issubclass(python_type, datetime.timedelta): try: days = utils.int_from_string(text) arg = (c==datetime.timedelta(days=days)) except ( Exception, utils.ParsingError ): pass elif issubclass(python_type, (float, decimal.Decimal)): try: float_value = utils.float_from_string(text) precision = c.type.precision if isinstance(precision, (tuple)): precision = precision[1] delta = 0.1**( precision or 0 ) arg = sql.and_(c>=float_value-delta, c<=float_value+delta) except ( Exception, utils.ParsingError ): pass elif issubclass(python_type, six.string_types): arg = sql.operators.ilike_op(c, '%'+text+'%') if arg is not None: arg = sql.and_(c != None, arg) args.append(arg) for t in text.split(' '): subexp = [] for column_name in admin.get_search_fields(t): path = column_name.split('.') target = admin.entity related_admin = admin for path_segment in path: # use the field attributes for the introspection, as these # have detected hybrid properties fa = related_admin.get_descriptor_field_attributes(path_segment) instrumented_attribute = getattr(target, path_segment) if fa.get('target', False): joins.append(instrumented_attribute) target = fa['target'] related_admin = related_admin.get_related_admin(target) else: append_column(instrumented_attribute, t, subexp) args.append(subexp) def create_query_decorator(joins, args): """Bind the join and args to a query decorator function""" def query_decorator(query): """The actual query decorator, call this function with a query as its first argument and it will return a query with a where clause for searching the resultset of the original query""" for join in joins: query = query.outerjoin(join) subqueries = (sql.or_(*arg) for arg in args) query = query.filter(sql.and_(*subqueries)) return query return query_decorator return create_query_decorator(joins, args)