Skip to content

Commit db3c038

Browse files
EnxDevdpgaspar
andauthored
feat(filters): add "FilterIn" and "FilterNotIn" operators with unit tests (#2354)
Co-authored-by: Daniel Vaz Gaspar <[email protected]>
1 parent 6682f34 commit db3c038

File tree

4 files changed

+161
-5
lines changed

4 files changed

+161
-5
lines changed

flask_appbuilder/models/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def get_filters_values_tojson(self) -> List[Tuple[str, str, Any]]:
295295

296296
def apply_all(self, query):
297297
for flt, values in zip(self.filters, self.values):
298-
if isinstance(values, list):
298+
if isinstance(values, list) and flt.arg_name not in {"in", "not_in"}:
299299
for value in values:
300300
query = flt.apply(query, value)
301301
else:

flask_appbuilder/models/generic/filters.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ class GenericFilterConverter(BaseFilterConverter):
107107
FilterStartsWith,
108108
],
109109
),
110-
("is_integer", [FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller]),
110+
(
111+
"is_integer",
112+
[
113+
FilterEqual,
114+
FilterNotEqual,
115+
FilterGreater,
116+
FilterSmaller,
117+
],
118+
),
111119
("is_date", [FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller]),
112120
)

flask_appbuilder/models/sqla/filters.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"FilterEqualFunction",
2626
"FilterGreater",
2727
"FilterNotEndsWith",
28+
"FilterIn",
29+
"FilterNotIn",
2830
"FilterRelationManyToManyEqual",
2931
"FilterRelationOneToManyEqual",
3032
"FilterRelationOneToManyNotEqual",
@@ -183,6 +185,30 @@ def apply(self, query, value):
183185
return query.filter(field < value)
184186

185187

188+
class FilterIn(BaseFilter):
189+
name = lazy_gettext("In")
190+
arg_name = "in"
191+
192+
def apply(self, query, value):
193+
query, field = get_field_setup_query(query, self.model, self.column_name)
194+
typed_values = [
195+
set_value_to_type(self.datamodel, self.column_name, v) for v in value
196+
]
197+
return query.filter(field.in_(typed_values))
198+
199+
200+
class FilterNotIn(BaseFilter):
201+
name = lazy_gettext("Not In")
202+
arg_name = "not_in"
203+
204+
def apply(self, query, value):
205+
query, field = get_field_setup_query(query, self.model, self.column_name)
206+
typed_values = [
207+
set_value_to_type(self.datamodel, self.column_name, v) for v in value
208+
]
209+
return query.filter(~field.in_(typed_values))
210+
211+
186212
class FilterRelationOneToManyEqual(FilterRelation):
187213
name = lazy_gettext("Relation")
188214
arg_name = "rel_o_m"
@@ -319,6 +345,8 @@ class SQLAFilterConverter(BaseFilterConverter):
319345
FilterNotEndsWith,
320346
FilterNotContains,
321347
FilterNotEqual,
348+
FilterIn,
349+
FilterNotIn,
322350
],
323351
),
324352
(
@@ -345,11 +373,43 @@ class SQLAFilterConverter(BaseFilterConverter):
345373
FilterNotEndsWith,
346374
FilterNotContains,
347375
FilterNotEqual,
376+
FilterIn,
377+
FilterNotIn,
378+
],
379+
),
380+
(
381+
"is_integer",
382+
[
383+
FilterEqual,
384+
FilterGreater,
385+
FilterSmaller,
386+
FilterNotEqual,
387+
FilterIn,
388+
FilterNotIn,
389+
],
390+
),
391+
(
392+
"is_float",
393+
[
394+
FilterEqual,
395+
FilterGreater,
396+
FilterSmaller,
397+
FilterNotEqual,
398+
FilterIn,
399+
FilterNotIn,
400+
],
401+
),
402+
(
403+
"is_numeric",
404+
[
405+
FilterEqual,
406+
FilterGreater,
407+
FilterSmaller,
408+
FilterNotEqual,
409+
FilterIn,
410+
FilterNotIn,
348411
],
349412
),
350-
("is_integer", [FilterEqual, FilterGreater, FilterSmaller, FilterNotEqual]),
351-
("is_float", [FilterEqual, FilterGreater, FilterSmaller, FilterNotEqual]),
352-
("is_numeric", [FilterEqual, FilterGreater, FilterSmaller, FilterNotEqual]),
353413
("is_date", [FilterEqual, FilterGreater, FilterSmaller, FilterNotEqual]),
354414
("is_boolean", [FilterEqual, FilterNotEqual]),
355415
("is_datetime", [FilterEqual, FilterGreater, FilterSmaller, FilterNotEqual]),

tests/test_api.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,88 @@ def test_get_list_filters(self):
15931593
self.assertEqual(data[API_RESULT_RES_KEY][0], expected_result)
15941594
self.assertEqual(rv.status_code, 200)
15951595

1596+
def test_get_list_filter_in_operator(self):
1597+
"""
1598+
REST API: Test 'in' filter on field_integer
1599+
"""
1600+
client = self.app.test_client()
1601+
token = self.login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1602+
1603+
with model1_data(self.appbuilder.session, 5) as models:
1604+
field_integers = [models[0].field_integer, models[1].field_integer]
1605+
expected_results = [
1606+
{
1607+
"field_date": model.field_date.isoformat()
1608+
if model.field_date
1609+
else None,
1610+
"field_float": float(model.field_float),
1611+
"field_integer": model.field_integer,
1612+
"field_string": model.field_string,
1613+
}
1614+
for model in models[:2]
1615+
]
1616+
1617+
arguments = {
1618+
API_FILTERS_RIS_KEY: [
1619+
{"col": "field_integer", "opr": "in", "value": field_integers}
1620+
],
1621+
"order_column": "field_integer",
1622+
"order_direction": "asc",
1623+
}
1624+
1625+
uri = f"api/v1/model1api/?q={prison.dumps(arguments)}"
1626+
rv = self.auth_client_get(client, token, uri)
1627+
data = json.loads(rv.data.decode("utf-8"))
1628+
1629+
self.assertEqual(rv.status_code, 200)
1630+
actual_results = data[API_RESULT_RES_KEY]
1631+
self.assertEqual(len(actual_results), len(expected_results))
1632+
for result in expected_results:
1633+
self.assertIn(result, actual_results)
1634+
1635+
def test_get_list_filter_not_in_operator(self):
1636+
"""
1637+
REST API: Test 'not in' filter on field_integer
1638+
"""
1639+
client = self.app.test_client()
1640+
token = self.login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1641+
1642+
with model1_data(self.appbuilder.session, 5) as models:
1643+
excluded_field_integers = [models[0].field_integer, models[1].field_integer]
1644+
expected_results = [
1645+
{
1646+
"field_date": model.field_date.isoformat()
1647+
if model.field_date
1648+
else None,
1649+
"field_float": float(model.field_float),
1650+
"field_integer": model.field_integer,
1651+
"field_string": model.field_string,
1652+
}
1653+
for model in models[2:]
1654+
]
1655+
1656+
arguments = {
1657+
API_FILTERS_RIS_KEY: [
1658+
{
1659+
"col": "field_integer",
1660+
"opr": "not_in",
1661+
"value": excluded_field_integers,
1662+
}
1663+
],
1664+
"order_column": "field_integer",
1665+
"order_direction": "asc",
1666+
}
1667+
1668+
uri = f"api/v1/model1api/?q={prison.dumps(arguments)}"
1669+
rv = self.auth_client_get(client, token, uri)
1670+
data = json.loads(rv.data.decode("utf-8"))
1671+
1672+
self.assertEqual(rv.status_code, 200)
1673+
actual_results = data[API_RESULT_RES_KEY]
1674+
self.assertEqual(len(actual_results), len(expected_results))
1675+
for result in expected_results:
1676+
self.assertIn(result, actual_results)
1677+
15961678
def test_get_list_invalid_filters(self):
15971679
"""
15981680
REST Api: Test get list filter params
@@ -1993,12 +2075,16 @@ def test_info_filters(self):
19932075
{"name": "Greater than", "operator": "gt"},
19942076
{"name": "Smaller than", "operator": "lt"},
19952077
{"name": "Not Equal to", "operator": "neq"},
2078+
{"name": "In", "operator": "in"},
2079+
{"name": "Not In", "operator": "not_in"},
19962080
],
19972081
"field_integer": [
19982082
{"name": "Equal to", "operator": "eq"},
19992083
{"name": "Greater than", "operator": "gt"},
20002084
{"name": "Smaller than", "operator": "lt"},
20012085
{"name": "Not Equal to", "operator": "neq"},
2086+
{"name": "In", "operator": "in"},
2087+
{"name": "Not In", "operator": "not_in"},
20022088
],
20032089
"field_string": [
20042090
{"name": "Starts with", "operator": "sw"},
@@ -2009,6 +2095,8 @@ def test_info_filters(self):
20092095
{"name": "Not Ends with", "operator": "new"},
20102096
{"name": "Not Contains", "operator": "nct"},
20112097
{"name": "Not Equal to", "operator": "neq"},
2098+
{"name": "In", "operator": "in"},
2099+
{"name": "Not In", "operator": "not_in"},
20122100
],
20132101
}
20142102
self.assertEqual(data["filters"], expected_filters)

0 commit comments

Comments
 (0)