-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(filters): implement new order filter
This introduces an ordering feature analog to the rewritten filtering. What this allows us to do is to order by a chain of arbitrary fields, choosing ascending/descending on the way. New fields can be added easily by extending an `OrderSet` class - similar to `FilterSet` classes we already have.
- Loading branch information
David Vogt
committed
Oct 17, 2019
1 parent
cbb8ac9
commit 012d1f1
Showing
3 changed files
with
275 additions
and
35 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,101 @@ | ||
from typing import Any, Tuple, Union | ||
|
||
from django import forms | ||
from django.db.models.expressions import CombinedExpression, F, Value | ||
from django.db.models.query import QuerySet | ||
from django_filters.fields import ChoiceField | ||
from django_filters.rest_framework import Filter | ||
from graphene import Enum | ||
from graphene_django.forms.converter import convert_form_field | ||
from graphene_django.registry import get_global_registry | ||
from rest_framework import exceptions | ||
|
||
OrderingFieldType = Union[F, str] | ||
|
||
|
||
class CalumaOrdering(Filter): | ||
"""Base class for new-style ordering filters. | ||
This is not really a filter in the rest framework sense, | ||
but we use it's infrastructure to get conversion to GraphQL | ||
types. | ||
This is an abstract base class. To use it, you need to | ||
implement the `get_ordering_value()` method to return a tuple | ||
of `(queryset, ordering_field)`. The queryset may be modified, | ||
for example if you need to annotate it to get to the desired | ||
ordering field. | ||
""" | ||
|
||
def get_ordering_value( | ||
self, qs: QuerySet, value: Any | ||
) -> Tuple[QuerySet, OrderingFieldType]: # pragma: no cover | ||
raise NotImplementedError("get_ordering_value() needs to be implemented") | ||
|
||
|
||
class MetaFieldOrdering(CalumaOrdering): | ||
"""Ordering filter for ordering by `meta` values.""" | ||
|
||
def __init__(self, field_name="meta"): | ||
super().__init__() | ||
self.field_name = field_name | ||
|
||
field_class = forms.CharField | ||
|
||
def get_ordering_value( | ||
self, qs: QuerySet, value: Any | ||
) -> Tuple[QuerySet, OrderingFieldType]: | ||
|
||
return qs, CombinedExpression(F(self.field_name), "->", Value(value)) | ||
|
||
|
||
class AttributeOrderingMixin(CalumaOrdering): | ||
def get_ordering_value(self, qs, value): | ||
if value not in self._fields: # pragma: no cover | ||
# this is normally covered by graphene enforcing its schema, | ||
# but we still need to handle it | ||
raise exceptions.ValidationError( | ||
f"Field '{value}' not available for sorting on {qs.model.__name}" | ||
) | ||
return qs, F(value) | ||
|
||
|
||
def AttributeOrderingFactory(model, fields=None, exclude_fields=None): | ||
"""Build ordering field for a given model. | ||
Used to define an ordering field that is presented as an enum | ||
""" | ||
if not exclude_fields: | ||
exclude_fields = [] | ||
|
||
if not fields: | ||
fields = [f.name for f in model._meta.fields if f.name not in exclude_fields] | ||
|
||
field_enum = type( | ||
f"Sortable{model.__name__}Attributes", | ||
(Enum,), | ||
{field.upper(): field for field in fields}, | ||
) | ||
|
||
field_class = type(f"{model.__name__}AttributeField", (ChoiceField,), {}) | ||
|
||
ordering_class = type( | ||
f"{model.__name__}AttributeOrdering", | ||
(AttributeOrderingMixin, CalumaOrdering), | ||
{"field_class": field_class, "_fields": fields}, | ||
) | ||
|
||
@convert_form_field.register(field_class) | ||
def to_enum(field): | ||
registry = get_global_registry() | ||
converted = registry.get_converted_field(field) | ||
|
||
if converted: # pragma: no cover | ||
return converted | ||
|
||
converted = field_enum() | ||
|
||
registry.register_converted_field(field, converted) | ||
return converted | ||
|
||
return ordering_class() |
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,65 @@ | ||
from typing import Any, Tuple | ||
|
||
from django import forms | ||
from django.db.models import OuterRef, Subquery | ||
from django.db.models.expressions import F | ||
from django.db.models.query import QuerySet | ||
from rest_framework import exceptions | ||
|
||
from caluma.core.ordering import CalumaOrdering, OrderingFieldType | ||
from caluma.form.models import Answer, Question | ||
|
||
|
||
class AnswerValueOrdering(CalumaOrdering): | ||
"""Ordering filter for ordering documents by value of an answer. | ||
This can also be used for ordering other things that contain documents, | ||
such as cases, work items etc. | ||
""" | ||
|
||
field_class = forms.CharField | ||
|
||
def __init__(self, document_via=None, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
|
||
self._document_locator_prefix = ( | ||
"" if document_via is None else f"{document_via}__" | ||
) | ||
|
||
def get_ordering_value( | ||
self, qs: QuerySet, value: Any | ||
) -> Tuple[QuerySet, OrderingFieldType]: | ||
# First, join the requested answer, then annotate the QS accordingly. | ||
# Last, return a field corresponding to the value | ||
# | ||
question = Question.objects.get(pk=value) | ||
QUESTION_TYPE_TO_FIELD = { | ||
Question.TYPE_INTEGER: "value", | ||
Question.TYPE_FLOAT: "value", | ||
Question.TYPE_DATE: "date", | ||
Question.TYPE_CHOICE: "value", | ||
Question.TYPE_TEXTAREA: "value", | ||
Question.TYPE_TEXT: "value", | ||
Question.TYPE_FILE: "file", | ||
Question.TYPE_DYNAMIC_CHOICE: "value", | ||
Question.TYPE_STATIC: "value", | ||
} | ||
|
||
try: | ||
value_field = QUESTION_TYPE_TO_FIELD[question.type] | ||
except KeyError: # pragma: no cover | ||
raise exceptions.ValidationError( | ||
f"Question '{question.slug}' has unsupported type {question.type} for ordering" | ||
) | ||
|
||
answers_subquery = Subquery( | ||
Answer.objects.filter(question=question, document=OuterRef("pk")).values( | ||
value_field | ||
) | ||
) | ||
ann_name = f"order_{value}" | ||
|
||
qs = qs.annotate(**{ann_name: answers_subquery}) | ||
|
||
# TODO: respect document_via | ||
return qs, F(ann_name) |