Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generic filter types #1212

Merged
merged 1 commit into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading