-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(filters): add template meta filter
This commit implements a `JSONValueFilter` and adds a FilterSet to the template view. The value for this filter needs to be json encoded and has the following structure: ```python [ { "key": "some-key", "value": "some value", "lookup": "contains", # optional, defaults to "exact" }, ... ] ``` Multiple lookups are ANDed.
- Loading branch information
Showing
4 changed files
with
124 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import json | ||
|
||
from django.db.models.fields.json import KeyTextTransform | ||
from django_filters import Filter, FilterSet | ||
from django_filters.constants import EMPTY_VALUES | ||
from rest_framework.exceptions import ValidationError | ||
|
||
from . import models | ||
|
||
|
||
# TODO: refactor into reusable package later | ||
class JSONValueFilter(Filter): | ||
def filter(self, qs, value): | ||
if value in EMPTY_VALUES: | ||
return qs | ||
|
||
valid_lookups = self._valid_lookups(qs) | ||
|
||
try: | ||
value = json.loads(value) | ||
except json.decoder.JSONDecodeError: | ||
raise ValidationError("JSONValueFilter value needs to be json encoded.") | ||
|
||
if isinstance(value, dict): | ||
# be a bit more tolerant | ||
value = [value] | ||
|
||
for expr in value: | ||
if expr in EMPTY_VALUES: # pragma: no cover | ||
continue | ||
if not all(("key" in expr, "value" in expr)): | ||
raise ValidationError( | ||
'JSONValueFilter value needs to have a "key" and "value" and an ' | ||
'optional "lookup" key.' | ||
) | ||
|
||
lookup_expr = expr.get("lookup", self.lookup_expr) | ||
if lookup_expr not in valid_lookups: | ||
raise ValidationError( | ||
f'Lookup expression "{lookup_expr}" not allowed for field ' | ||
f'"{self.field_name}". Valid expressions: ' | ||
f'{", ".join(valid_lookups.keys())}' | ||
) | ||
# "contains" behaves differently on JSONFields as it does on TextFields. | ||
# That's why we annotate the queryset with the value. | ||
# Some discussion about it can be found here: | ||
# https://code.djangoproject.com/ticket/26511 | ||
if isinstance(expr["value"], str): | ||
qs = qs.annotate( | ||
field_val=KeyTextTransform(expr["key"], self.field_name) | ||
) | ||
lookup = {f"field_val__{lookup_expr}": expr["value"]} | ||
else: | ||
lookup = { | ||
f"{self.field_name}__{expr['key']}__{lookup_expr}": expr["value"] | ||
} | ||
qs = qs.filter(**lookup) | ||
return qs | ||
|
||
def _valid_lookups(self, qs): | ||
# We need some traversal magic in case field name is a related lookup | ||
traversals = self.field_name.split("__") | ||
actual_field = traversals.pop() | ||
|
||
model = qs.model | ||
for field in traversals: # pragma: no cover | ||
model = model._meta.get_field(field).related_model | ||
|
||
return model._meta.get_field(actual_field).get_lookups() | ||
|
||
|
||
class TemplateFilterSet(FilterSet): | ||
meta = JSONValueFilter(field_name="meta") | ||
|
||
class Meta: | ||
model = models.Template | ||
fields = { | ||
"slug": ["exact"], | ||
"description": ["icontains", "search"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import json | ||
|
||
import pytest | ||
from django.urls import reverse | ||
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"value,status_code", | ||
[ | ||
(json.dumps([{"key": "foo", "value": "bar"}]), HTTP_200_OK), | ||
(json.dumps([{"key": "int", "value": 5, "lookup": "gt"}]), HTTP_200_OK), | ||
( | ||
json.dumps( | ||
[{"key": "foo", "value": "bar"}, {"key": "baz", "value": "bla"}] | ||
), | ||
HTTP_200_OK, | ||
), | ||
( | ||
json.dumps([{"key": "foo", "value": "bar", "lookup": "asdfgh"}]), | ||
HTTP_400_BAD_REQUEST, | ||
), | ||
(json.dumps([{"key": "foo"}]), HTTP_400_BAD_REQUEST), | ||
(json.dumps({"key": "foo"}), HTTP_400_BAD_REQUEST), | ||
("foo", HTTP_400_BAD_REQUEST), | ||
("[{foo, no json)", HTTP_400_BAD_REQUEST), | ||
], | ||
) | ||
def test_json_value_filter(db, template_factory, admin_client, value, status_code): | ||
doc = template_factory(meta={"foo": "bar", "baz": "bla", "int": 23}) | ||
template_factory(meta={"foo": "baz"}) | ||
template_factory() | ||
url = reverse("template-list") | ||
resp = admin_client.get(url, {"meta": value}) | ||
assert resp.status_code == status_code | ||
if status_code == HTTP_200_OK: | ||
result = resp.json() | ||
assert len(result["results"]) == 1 | ||
assert result["results"][0]["slug"] == str(doc.pk) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters