Skip to content

Commit

Permalink
Merge pull request #1060 from gounux/QF-4681-sort-projects-api
Browse files Browse the repository at this point in the history
Add projects API ordering
  • Loading branch information
suricactus authored Nov 22, 2024
2 parents b3872ac + 24bbb86 commit 6033c37
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 0 deletions.
125 changes: 125 additions & 0 deletions docker-app/qfieldcloud/core/drf_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from django.db.models import QuerySet
from django.http import HttpRequest
from rest_framework import filters, viewsets


class QfcOrderingFilter(filters.OrderingFilter):
"""Custom QFC OrderingFilter class that allows usage of custom attributes expression.
Use it in a ModelViewSet by setting the `filter_backends` and `ordering_fields` fields.
It is possible to use an `ordering_fields` expression value with attributes.
Custom attributes expression has form : "my_field::alias=my_field_alias,key=value"
"""

SEPARATOR = "::"
TOKENS_LIST_SEPARATOR = ","
TOKENS_VALUE_SEPARATOR = "="

def _get_query_field(self, fields: list[str], term: str) -> str | None:
"""Searches a term in a query field list.
The field list elements may start with "-".
This method should be used to search a term from a query's ordering fields.
Args:
fields (list[str]): list of fields to search
term (str): term to search in the list
Returns:
str | None: the matching field, if present in the list
"""
for field in fields:
compare_value = field

# the "-" is used when the fields are sorted descending
if field.startswith("-"):
compare_value = field[1:]

if compare_value != term:
continue

return field
return None

def _parse_tokenized_attributes(self, raw: str) -> dict[str, str]:
"""Parses an ordering field attributes expression.
Args:
raw (str): raw expression to parse, e.g.: "alias=my_field_alias,key=value"
Returns:
dict[str, str]: dict containing the expression's attributes
"""
definition_attrs = raw.split(self.TOKENS_LIST_SEPARATOR)

attr_dict = {}
for attr in definition_attrs:
token, value = attr.split(self.TOKENS_VALUE_SEPARATOR, 1)
attr_dict[token] = value

return attr_dict

def _parse_definition(self, definition: str) -> tuple[str, dict[str, str]]:
"""Parses a custom ordering field with attributes expression.
Args:
definition (str): raw definition of the ordering field to parse,
e.g.: "my_field::alias=my_field_alias,key=value"
Returns :
tuple[str, dict[str, str]]: tuple containing the field name (1st) and its attributes dict (2nd)
"""
name, attr_str = definition.split(self.SEPARATOR, 1)
attrs = self._parse_tokenized_attributes(attr_str)

return name, attrs

def remove_invalid_fields(
self,
queryset: QuerySet,
fields: list[str],
view: viewsets.ModelViewSet,
request: HttpRequest,
) -> list[str]:
"""Process ordering fields by parsing custom field expression.
Custom attributes expression has form : "my_field::alias=my_field_alias,key=value".
In the above example, `alias` is the URL GET param value,
but `my_field` is the real model field.
Args:
queryset (QuerySet): Django's ORM queryset of the same model as the one used in view of the `ModelViewSet`
fields (list[str]): ordering fields passed to the HTTP querystring
view (ModelViewSet): DRF view instance
request (HttpRequest): DRF request instance
Returns :
list[str]: parsed ordering fields where aliases have been replaced
"""
base_fields = super().remove_invalid_fields(queryset, fields, view, request)
valid_fields = []

for field_name, _verbose_name in self.get_valid_fields(
queryset, view, context={"request": request}
):
# standard handling of fields from the base class
query_field_name = self._get_query_field(base_fields, field_name)

if query_field_name:
valid_fields.append(query_field_name)
continue

# skip fields without custom attributes expression
if self.SEPARATOR not in field_name:
continue

definition_name, attrs = self._parse_definition(field_name)
alias = attrs.get("alias", definition_name)
query_field_name = self._get_query_field(fields, alias)

# field is not in the HTTP GET request querystring
if not query_field_name:
continue

if query_field_name.startswith("-"):
definition_name = f"-{definition_name}"

valid_fields.append(definition_name)

return valid_fields
5 changes: 5 additions & 0 deletions docker-app/qfieldcloud/core/views/projects_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
extend_schema_view,
)
from qfieldcloud.core import pagination, permissions_utils
from qfieldcloud.core.drf_utils import QfcOrderingFilter
from qfieldcloud.core.models import Project, ProjectQueryset
from qfieldcloud.core.serializers import ProjectSerializer
from qfieldcloud.core.utils2 import storage
Expand Down Expand Up @@ -77,6 +78,8 @@ class ProjectViewSet(viewsets.ModelViewSet):
lookup_url_kwarg = "projectid"
permission_classes = [permissions.IsAuthenticated, ProjectViewSetPermissions]
pagination_class = pagination.QfcLimitOffsetPagination()
filter_backends = [QfcOrderingFilter]
ordering_fields = ["owner__username::alias=owner", "name", "created_at"]

def get_queryset(self):
projects = Project.objects.for_user(self.request.user)
Expand Down Expand Up @@ -133,6 +136,8 @@ class PublicProjectsListView(generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = ProjectSerializer
pagination_class = pagination.QfcLimitOffsetPagination()
filter_backends = [QfcOrderingFilter]
ordering_fields = ["owner__username::alias=owner", "name", "created_at"]

def get_queryset(self):
return Project.objects.for_user(self.request.user).filter(is_public=True)

0 comments on commit 6033c37

Please sign in to comment.