Skip to content

Commit

Permalink
Allow generic filter types
Browse files Browse the repository at this point in the history
  • Loading branch information
eugenenelou committed Jul 20, 2024
1 parent 711d4b9 commit 1bc113d
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 9 deletions.
11 changes: 9 additions & 2 deletions docs/docs/guides/input/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,21 @@ The `name` field will be converted into `Q(name=...)` expression.
When your database lookups are more complicated than that, you can explicitly specify them in the field definition using a `"q"` kwarg:
```python hl_lines="2"
class BookFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q='name__icontains')
name: Optional[str] = Field(None, q='name__icontains')
```
You can even specify multiple lookup keyword argument names as a list:
```python hl_lines="2 3 4"
class BookFilterSchema(FilterSchema):
search: Optional[str] = Field(None, q=['name__icontains',
'author__name__icontains',
'publisher__name__icontains'])
'publisher__name__icontains'])
```
And to make generic fields, you can make the field name implicit by skipping it:
```python hl_lines="2"
IContainsField = Annotated[Optional[str], Field(None, q='__icontains')]

class BookFilterSchema(FilterSchema):
name: IContainsField
```
By default, field-level expressions are combined using `"OR"` connector, so with the above setup, a query parameter `?search=foobar` will search for books that have "foobar" in either of their name, author or publisher.

Expand Down
8 changes: 7 additions & 1 deletion ninja/filter_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,20 @@ def _resolve_field_expression(
if not q_expression:
return Q(**{field_name: field_value})
elif isinstance(q_expression, str):
if q_expression.startswith("__"):
q_expression = f"{field_name}{q_expression}"
return Q(**{q_expression: field_value})
elif isinstance(q_expression, list):
expression_connector = field_extra.get( # type: ignore
"expression_connector", DEFAULT_FIELD_LEVEL_EXPRESSION_CONNECTOR
)
q = Q()
for q_expression_part in q_expression:
q_expression_part = str(q_expression_part)
if q_expression_part.startswith("__"):
q_expression_part = f"{field_name}{q_expression_part}"
q = q._combine( # type: ignore
Q(**{q_expression_part: field_value}), # type: ignore
Q(**{q_expression_part: field_value}),
expression_connector,
)
return q
Expand All @@ -87,6 +92,7 @@ def _resolve_field_expression(
f" {field_name}: {field.annotation} = Field(..., q='<here>')\n"
f"or\n"
f" {field_name}: {field.annotation} = Field(..., q=['lookup1', 'lookup2', ...])\n"
f"You can omit the field name and make it implicit by starting the lookup directly by '__'."
f"Alternatively, you can implement {self.__class__.__name__}.filter_{field_name} that must return a Q expression for that field"
)

Expand Down
22 changes: 16 additions & 6 deletions tests/test_filter_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ class DummyFilterSchema(FilterSchema):
assert q == Q()


def test_q_expressions2():
@pytest.mark.parametrize("implicit_field_name", [False, True])
def test_q_expressions2(implicit_field_name):
if implicit_field_name:
q = "__icontains"
else:
q = "name__icontains"

class DummyFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q="name__icontains")
name: Optional[str] = Field(None, q=q)
tag: Optional[str] = Field(None, q="tag")

filter_instance = DummyFilterSchema(name="John", tag=None)
Expand All @@ -66,11 +72,15 @@ class DummyFilterSchema(FilterSchema):
assert q == Q(name__icontains="John") & Q(tag="active")


def test_q_is_a_list():
@pytest.mark.parametrize("implicit_field_name", [False, True])
def test_q_is_a_list(implicit_field_name):
if implicit_field_name:
q__name = "__icontains"
else:
q__name = "name__icontains"

class DummyFilterSchema(FilterSchema):
name: Optional[str] = Field(
None, q=["name__icontains", "user__username__icontains"]
)
name: Optional[str] = Field(None, q=[q__name, "user__username__icontains"])
tag: Optional[str] = Field(None, q="tag")

filter_instance = DummyFilterSchema(name="foo", tag="bar")
Expand Down

0 comments on commit 1bc113d

Please sign in to comment.