21
21
'nullslast' : nullslast
22
22
}
23
23
24
+ REGEX_OP = {
25
+ 'mysql' : 'regexp' ,
26
+ 'postgresql' : '~' ,
27
+ }
28
+
24
29
ColumnTuple = namedtuple (
25
30
'ColumnDT' ,
26
31
['column_name' , 'mData' , 'search_like' , 'filter' , 'searchable' ,
@@ -38,6 +43,46 @@ def get_attr(sqla_object, attribute):
38
43
return output
39
44
40
45
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
+
41
86
class InvalidParameter (Exception ):
42
87
43
88
"""Class defining an invalid parameter exception."""
@@ -108,14 +153,15 @@ class DataTables:
108
153
:returns: a DataTables object
109
154
"""
110
155
111
- def __init__ (self , request , sqla_object , query , columns ):
156
+ def __init__ (self , request , sqla_object , query , columns , dialect = None ):
112
157
"""Initialize object and run the query."""
113
158
self .request_values , self .legacy = DataTables .prepare_arguments (
114
159
request )
115
160
self .sqla_object = sqla_object
116
161
self .query = query
117
162
self .columns = columns
118
163
self .results = None
164
+ self .dialect = dialect
119
165
120
166
# total in the table after filtering
121
167
self .cardinality_filtered = 0
@@ -219,13 +265,18 @@ def filtering(self):
219
265
box is used.
220
266
"""
221
267
if self .legacy :
268
+ # see http://legacy.datatables.net/usage/server-side
222
269
searchValue = self .request_values .get ('sSearch' )
270
+ searchRegex = self .request_values .get ('bRegex' )
223
271
searchableColumn = 'bSearchable_%s'
224
272
searchableColumnValue = 'sSearch_%s'
273
+ searchableColumnRegex = 'bRegex_%s'
225
274
else :
226
275
searchValue = self .request_values .get ('search[value]' )
276
+ searchRegex = self .request_values .get ('search[regex]' )
227
277
searchableColumn = 'columns[%s][searchable]'
228
278
searchableColumnValue = 'columns[%s][search][value]'
279
+ searchableColumnRegex = 'columns[%s][search][regex]'
229
280
230
281
condition = None
231
282
@@ -264,13 +315,26 @@ def search(idx, col):
264
315
if searchValue :
265
316
conditions = []
266
317
318
+ # only need to call this once
319
+ regex = clean_regex (searchValue )
320
+
321
+ # loop through columns looking for global search value
267
322
for idx , col in enumerate (self .columns ):
268
323
if self .request_values .get (searchableColumn % idx ) in (
269
324
True , 'true' ) and col .searchable :
270
325
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 ))
274
338
condition = or_ (* conditions )
275
339
conditions = []
276
340
for idx , col in enumerate (self .columns ):
@@ -280,7 +344,16 @@ def search(idx, col):
280
344
if search_value2 :
281
345
sqla_obj , column_name = search (idx , col )
282
346
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 :
284
357
conditions .append (cast (
285
358
get_attr (sqla_obj , column_name ), String )
286
359
.ilike ('%%%s%%' % search_value2 ))
0 commit comments