Skip to content

Commit

Permalink
feat(filters): add template meta filter
Browse files Browse the repository at this point in the history
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
Yelinz committed Jan 27, 2023
1 parent ca2d9de commit 2daf8ec
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ ignore_missing_imports = True

[mypy-generic_permissions.*]
ignore_missing_imports = True

[mypy-django_filters.*]
ignore_missing_imports = True
80 changes: 80 additions & 0 deletions document_merge_service/api/filters.py
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"],
}
40 changes: 40 additions & 0 deletions document_merge_service/api/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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")
print(url)
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)
4 changes: 2 additions & 2 deletions document_merge_service/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
from rest_framework.decorators import action
from rest_framework.generics import RetrieveAPIView

from . import engines, models, serializers
from . import engines, filters, models, serializers
from .unoconv import Unoconv


class TemplateView(VisibilityViewMixin, PermissionViewMixin, viewsets.ModelViewSet):
queryset = models.Template.objects
serializer_class = serializers.TemplateSerializer
filterset_fields = {"slug": ["exact"], "description": ["icontains", "search"]}
filterset_class = filters.TemplateFilterSet
ordering_fields = ("slug", "description")
ordering = ("slug",)

Expand Down

0 comments on commit 2daf8ec

Please sign in to comment.