Skip to content

Commit 2357727

Browse files
committed
Merge pull request #41 from louking/regex-support-issue-#40
Allow regex from columns, to support yadcf multi_select
2 parents 37a85c9 + 2739809 commit 2357727

File tree

1 file changed

+78
-5
lines changed

1 file changed

+78
-5
lines changed

Diff for: datatables/__init__.py

+78-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
'nullslast': nullslast
2222
}
2323

24+
REGEX_OP = {
25+
'mysql': 'regexp',
26+
'postgresql': '~',
27+
}
28+
2429
ColumnTuple = namedtuple(
2530
'ColumnDT',
2631
['column_name', 'mData', 'search_like', 'filter', 'searchable',
@@ -38,6 +43,46 @@ def get_attr(sqla_object, attribute):
3843
return output
3944

4045

46+
def clean_regex(regex):
47+
'''
48+
escape any regex special characters other than alternation |
49+
50+
:param regex: regex from datatables interface
51+
:type regex: str
52+
:rtype: str with regex to use with database
53+
'''
54+
# copy for return
55+
ret_regex = regex
56+
57+
# these characters are escaped (all except alternation | and escape \)
58+
# see http://www.regular-expressions.info/refquick.html
59+
escape_chars = '[^$.?*+(){}'
60+
61+
# remove any escape chars
62+
ret_regex = ret_regex.replace('\\','')
63+
64+
# escape any characters which are used by regex
65+
# could probably concoct something incomprehensible using re.sub() but
66+
# prefer to write clear code with this loop
67+
# note expectation that no characters have already been escaped
68+
for c in escape_chars:
69+
ret_regex = ret_regex.replace(c,'\\'+c)
70+
71+
# remove any double alternations until these don't exist any more
72+
while True:
73+
old_regex = ret_regex
74+
ret_regex = ret_regex.replace('||', '|')
75+
if old_regex == ret_regex: break
76+
77+
# if last char is alternation | remove it because this
78+
# will cause operational error
79+
# this can happen as user is typing in global search box
80+
while len(ret_regex) >= 1 and ret_regex[-1] == '|':
81+
ret_regex = ret_regex[:-1]
82+
83+
# and back to the caller
84+
return ret_regex
85+
4186
class InvalidParameter(Exception):
4287

4388
"""Class defining an invalid parameter exception."""
@@ -108,14 +153,15 @@ class DataTables:
108153
:returns: a DataTables object
109154
"""
110155

111-
def __init__(self, request, sqla_object, query, columns):
156+
def __init__(self, request, sqla_object, query, columns, dialect=None):
112157
"""Initialize object and run the query."""
113158
self.request_values, self.legacy = DataTables.prepare_arguments(
114159
request)
115160
self.sqla_object = sqla_object
116161
self.query = query
117162
self.columns = columns
118163
self.results = None
164+
self.dialect = dialect
119165

120166
# total in the table after filtering
121167
self.cardinality_filtered = 0
@@ -219,13 +265,18 @@ def filtering(self):
219265
box is used.
220266
"""
221267
if self.legacy:
268+
# see http://legacy.datatables.net/usage/server-side
222269
searchValue = self.request_values.get('sSearch')
270+
searchRegex = self.request_values.get('bRegex')
223271
searchableColumn = 'bSearchable_%s'
224272
searchableColumnValue = 'sSearch_%s'
273+
searchableColumnRegex = 'bRegex_%s'
225274
else:
226275
searchValue = self.request_values.get('search[value]')
276+
searchRegex = self.request_values.get('search[regex]')
227277
searchableColumn = 'columns[%s][searchable]'
228278
searchableColumnValue = 'columns[%s][search][value]'
279+
searchableColumnRegex = 'columns[%s][search][regex]'
229280

230281
condition = None
231282

@@ -264,13 +315,26 @@ def search(idx, col):
264315
if searchValue:
265316
conditions = []
266317

318+
# only need to call this once
319+
regex = clean_regex(searchValue)
320+
321+
# loop through columns looking for global search value
267322
for idx, col in enumerate(self.columns):
268323
if self.request_values.get(searchableColumn % idx) in (
269324
True, 'true') and col.searchable:
270325
sqla_obj, column_name = search(idx, col)
271-
conditions.append(cast(
272-
get_attr(sqla_obj, column_name), String)
273-
.ilike('%%%s%%' % searchValue))
326+
# regex takes precedence
327+
if (searchRegex in ( True, 'true')
328+
and self.dialect in REGEX_OP
329+
and len(regex) >= 1):
330+
conditions.append(cast(
331+
get_attr(sqla_obj, column_name), String)
332+
.op(REGEX_OP[self.dialect])(regex))
333+
# use like
334+
else:
335+
conditions.append(cast(
336+
get_attr(sqla_obj, column_name), String)
337+
.ilike('%%%s%%' % searchValue))
274338
condition = or_(*conditions)
275339
conditions = []
276340
for idx, col in enumerate(self.columns):
@@ -280,7 +344,16 @@ def search(idx, col):
280344
if search_value2:
281345
sqla_obj, column_name = search(idx, col)
282346

283-
if col.search_like:
347+
# regex takes precedence over search_like
348+
regex = clean_regex(search_value2)
349+
if (self.request_values.get(searchableColumnRegex % idx)
350+
in ( True, 'true') and
351+
self.dialect in REGEX_OP and
352+
len(regex) >= 1):
353+
conditions.append(cast(
354+
get_attr(sqla_obj, column_name), String)
355+
.op(REGEX_OP[self.dialect])(regex))
356+
elif col.search_like:
284357
conditions.append(cast(
285358
get_attr(sqla_obj, column_name), String)
286359
.ilike('%%%s%%' % search_value2))

0 commit comments

Comments
 (0)