From a97897280703385dc0e0204f7d376dd404499533 Mon Sep 17 00:00:00 2001 From: Emmanuel Robert Ssebaggala Date: Sun, 5 Aug 2018 08:02:07 +0300 Subject: [PATCH 1/3] Add per column regex filtering --- .idea/vcs.xml | 6 ++++++ datatables/__init__.py | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/datatables/__init__.py b/datatables/__init__.py index 05e56c8..ab36f88 100644 --- a/datatables/__init__.py +++ b/datatables/__init__.py @@ -136,7 +136,8 @@ def yadcf_multi_select(expr, value): 'yadcf_multi_select': yadcf_multi_select, 'yadcf_range_number': yadcf_range_number, 'yadcf_range_number_slider': yadcf_range_number, - 'yadcf_range_date': yadcf_range_date + 'yadcf_range_date': yadcf_range_date, + 'regex': lambda expr, value, op: expr.op(op)(value) } @@ -335,7 +336,10 @@ def _set_column_filter_expressions(self): 'columns[{:d}][search][value]'.format(i), '') if value: search_func = search_methods[self.columns[i].search_method] - filter_expr = search_func(self.columns[i].sqla_expr, value) + if self.columns[i].search_method == 'regex': + filter_expr = search_func(self.columns[i].sqla_expr, value, self._get_regex_operator()) + else: + filter_expr = search_func(self.columns[i].sqla_expr, value) self.filter_expressions.append(filter_expr) def _set_global_filter_expression(self): @@ -398,7 +402,7 @@ def _get_regex_operator(self): if isinstance( self.query.session.bind.dialect, postgresql.dialect): - return '~' + return '~*' elif isinstance( self.query.session.bind.dialect, mysql.dialect): From 8bdbb6fc049210e7ea9f6ac533e56be0b71a9c48 Mon Sep 17 00:00:00 2001 From: Emmanuel Robert Ssebaggala Date: Mon, 13 Aug 2018 02:16:42 +0300 Subject: [PATCH 2/3] Add unit test for column regex filter --- tests/models.py | 2 +- tests/test_searching.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/models.py b/tests/models.py index b2944ad..d458329 100644 --- a/tests/models.py +++ b/tests/models.py @@ -17,7 +17,7 @@ class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) - name = Column(String, unique=True) + name = Column(String) created_at = Column(DateTime, default=datetime.datetime.utcnow) birthday = Column(Date) address = relationship('Address', uselist=False, backref=backref('user')) diff --git a/tests/test_searching.py b/tests/test_searching.py index 6a1d490..7c3c74d 100644 --- a/tests/test_searching.py +++ b/tests/test_searching.py @@ -25,6 +25,18 @@ def test_method_none(self): search_value='abc') assert res['recordsFiltered'] == '50' + def test_method_regex(self): + res = self.get_result( + column=User.name, + search_method='regex', + search_value='.*') + + if 'error' in res: + # unfortunately sqlite doesn't support regexp out of the box' + assert 'no such function: REGEXP' in res['error'] + else: + assert res['recordsFiltered'] == '50' + def test_method_numeric(self): res = self.get_result( column=User.id, From 65bd440c59d4b24d0533ead1b664d1a99a2174ff Mon Sep 17 00:00:00 2001 From: Emmanuel Robert Ssebaggala Date: Fri, 17 Aug 2018 14:54:30 +0300 Subject: [PATCH 3/3] Fix per column regex filter PR --- .idea/vcs.xml | 6 ------ datatables/__init__.py | 1 - tests/__init__.py | 12 +++++++++++- tests/models.py | 2 +- tests/test_fields.py | 14 +++++--------- tests/test_searching.py | 6 +----- 6 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/datatables/__init__.py b/datatables/__init__.py index ab36f88..0e6e7f8 100644 --- a/datatables/__init__.py +++ b/datatables/__init__.py @@ -352,7 +352,6 @@ def _set_global_filter_expression(self): self.params.get('search[regex]') == 'true'): op = self._get_regex_operator() val = clean_regex(global_search) - def filter_for(col): return col.sqla_expr.op(op)(val) else: diff --git a/tests/__init__.py b/tests/__init__.py index 6c6ce38..0d5f8b0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,8 +2,9 @@ import faker import unittest import itertools +import re -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from datetime import datetime, timedelta @@ -19,6 +20,9 @@ def setUpClass(cls): """Set up fake database session before all tests.""" cls.engine = create_engine( 'sqlite:///db.sqlite', echo=False) # echo=True for debug + + event.listen(cls.engine, 'begin', BaseTest.add_regex_on_connect) + Base.metadata.create_all(cls.engine) Session = sessionmaker(bind=cls.engine) cls.session = Session() @@ -27,6 +31,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): """Tear down database session after all tests.""" + event.remove(cls.engine, 'begin', BaseTest.add_regex_on_connect) Base.metadata.drop_all(cls.engine) @classmethod @@ -74,3 +79,8 @@ def create_dt_params( params['order[%s][%s]' % (i, key)] = str(value) return params + + @classmethod + def add_regex_on_connect(cls, dbapi_con): + """Add REGEX functionality for sqlite""" + dbapi_con.connection.create_function("REGEXP", 2, lambda expr, item : re.search(expr, str(item)) is not None) \ No newline at end of file diff --git a/tests/models.py b/tests/models.py index d458329..f236479 100644 --- a/tests/models.py +++ b/tests/models.py @@ -48,7 +48,7 @@ class Address(Base): __tablename__ = 'addresses' id = Column(Integer, primary_key=True) - description = Column(String, unique=True) + description = Column(String) user_id = Column(Integer, ForeignKey('users.id')) def __unicode__(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index bf1d330..0f303d0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -45,7 +45,7 @@ def test_fields_search_filters(self): params['columns[0][search][value]'] = '=4' params['columns[1][search][value]'] = user.name params['columns[2][search][value]'] = '>1965-02-02' - params['columns[2][search][value]'] = '<=99' + params['columns[3][search][value]'] = '<=99' rowTable = DataTables(params, query, columns) res = rowTable.output_result() @@ -141,14 +141,10 @@ def test_fields_global_search_filtering_with_regex(self): params, query, columns, allow_regex_searches=True) res = rowTable.output_result() - if 'error' in res: - # unfortunately sqlite doesn't support regexp out of the box' - assert 'no such function: REGEXP' in res['error'] - else: - assert len(res['data']) == 1 - assert res['recordsTotal'] == '1' - assert res['recordsFiltered'] == '1' - assert res['data'][0]['1'] == 'Feeeeear Of' + assert len(res['data']) == 1 + assert res['recordsTotal'] == '52' + assert res['recordsFiltered'] == '1' + assert res['data'][0]['1'] == 'Feeeeear Of' class FieldsTest4(BaseTest): diff --git a/tests/test_searching.py b/tests/test_searching.py index 7c3c74d..64c99b4 100644 --- a/tests/test_searching.py +++ b/tests/test_searching.py @@ -31,11 +31,7 @@ def test_method_regex(self): search_method='regex', search_value='.*') - if 'error' in res: - # unfortunately sqlite doesn't support regexp out of the box' - assert 'no such function: REGEXP' in res['error'] - else: - assert res['recordsFiltered'] == '50' + assert res['recordsFiltered'] == '50' def test_method_numeric(self): res = self.get_result(