From d0d568b3a55f8e3ec8e699c4b2239b4b99871f97 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 15 Apr 2024 12:44:46 -0500 Subject: [PATCH] Escape underscore char for "contains" query filter since underscore has special meaning for LIKE clause --- tailbone/grids/filters.py | 50 ++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/tailbone/grids/filters.py b/tailbone/grids/filters.py index f70670b6..3b198614 100644 --- a/tailbone/grids/filters.py +++ b/tailbone/grids/filters.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -484,9 +484,13 @@ class AlchemyStringFilter(AlchemyGridFilter): """ if value is None or value == '': return query - return query.filter(sa.and_( - *[self.column.ilike(self.encode_value('%{}%'.format(v))) - for v in value.split()])) + + criteria = [] + for val in value.split(): + val = val.replace('_', r'\_') + val = self.encode_value(f'%{val}%') + criteria.append(self.column.ilike(val)) + return query.filter(sa.and_(*criteria)) def filter_does_not_contain(self, query, value): """ @@ -495,14 +499,17 @@ class AlchemyStringFilter(AlchemyGridFilter): if value is None or value == '': return query + criteria = [] + for val in value.split(): + val = val.replace('_', r'\_') + val = self.encode_value(f'%{val}%') + criteria.append(~self.column.ilike(val)) + # When saying something is 'not like' something else, we must also # include things which are nothing at all, in our result set. return query.filter(sa.or_( self.column == None, - sa.and_( - *[~self.column.ilike(self.encode_value('%{}%'.format(v))) - for v in value.split()]), - )) + sa.and_(*criteria))) def filter_contains_any_of(self, query, value): """ @@ -531,9 +538,12 @@ class AlchemyStringFilter(AlchemyGridFilter): conditions = [] for value in values: - conditions.append(sa.and_( - *[self.column.ilike(self.encode_value('%{}%'.format(v))) - for v in value.split()])) + criteria = [] + for val in value.split(): + val = val.replace('_', r'\_') + val = self.encode_value(f'%{val}%') + criteria.append(self.column.ilike(val)) + conditions.append(sa.and_(*criteria)) return query.filter(sa.or_(*conditions)) @@ -588,8 +598,13 @@ class AlchemyByteStringFilter(AlchemyStringFilter): """ if value is None or value == '': return query - return query.filter(sa.and_( - *[self.column.ilike(b'%{}%'.format(v)) for v in value.split()])) + + criteria = [] + for val in value.split(): + val = val.replace('_', r'\_') + val = b'%{}%'.format(val) + criteria.append(self.column.ilike(val)) + return query.filters(sa.and_(*criteria)) def filter_does_not_contain(self, query, value): """ @@ -598,13 +613,16 @@ class AlchemyByteStringFilter(AlchemyStringFilter): if value is None or value == '': return query + for val in value.split(): + val = val.replace('_', '\_') + val = b'%{}%'.format(val) + criteria.append(~self.column.ilike(val)) + # When saying something is 'not like' something else, we must also # include things which are nothing at all, in our result set. return query.filter(sa.or_( self.column == None, - sa.and_( - *[~self.column.ilike(b'%{}%'.format(v)) for v in value.split()]), - )) + sa.and_(*criteria))) class AlchemyNumericFilter(AlchemyGridFilter):