Repository: wagtail/wagtail

Issue:
Allow ordering by multiple fields on the Wagtail API
### Is your proposal related to a problem?

<!--
  Provide a clear and concise description of what the problem is.
  For example, "I'm always frustrated when..."
-->

The Wagtail API does not allow ordering by multiple fields at the same time. For example, let's say I want a list of blog pages sorted by type and in reverse chronological order. This is currently not possible in the API.

### Describe the solution you'd like

<!--
  Provide a clear and concise description of what you want to happen.
-->

`[GET] /api/v2/pages/?type=blog.BlogPage&order=-published_at,blog_type`

The result should be the same as if this query was run:

```python
BlogPage.objects.order_by('-published_at', 'blog_type')
```

### Describe alternatives you've considered

<!--
  Let us know about other solutions you've tried or researched.
-->

In the meantime, we're going to add this functionality ad-hoc.

### Additional context

<!--
  Is there anything else you can add about the proposal?
  You might want to link to related issues here, if you haven't already.
-->

This is for a headless website.

Allow ordering by multiple fields on the Wagtail API
### Is your proposal related to a problem?

<!--
  Provide a clear and concise description of what the problem is.
  For example, "I'm always frustrated when..."
-->

The Wagtail API does not allow ordering by multiple fields at the same time. For example, let's say I want a list of blog pages sorted by type and in reverse chronological order. This is currently not possible in the API.

### Describe the solution you'd like

<!--
  Provide a clear and concise description of what you want to happen.
-->

`[GET] /api/v2/pages/?type=blog.BlogPage&order=-published_at,blog_type`

The result should be the same as if this query was run:

```python
BlogPage.objects.order_by('-published_at', 'blog_type')
```

### Describe alternatives you've considered

<!--
  Let us know about other solutions you've tried or researched.
-->

In the meantime, we're going to add this functionality ad-hoc.

### Additional context

<!--
  Is there anything else you can add about the proposal?
  You might want to link to related issues here, if you haven't already.
-->

This is for a headless website.



Discussion:
Labelling as a good first issue for anyone interested in the API - there's an older PR that could be redone and polished up for review also. 

https://github.com/wagtail/wagtail/pull/7433
@lb- I will work on this issue. please assign it to me

@shaikhmudassir we don't assign issues, just get started and do a PR when you're ready. 

See https://docs.wagtail.org/en/stable/contributing/first_contribution_guide.html#how-can-i-be-assigned-an-issue-to-contribute-to (also, this guide has lots of helpful info for your first contribution). 
Labelling as a good first issue for anyone interested in the API - there's an older PR that could be redone and polished up for review also. 

https://github.com/wagtail/wagtail/pull/7433
@lb- I will work on this issue. please assign it to me

@shaikhmudassir we don't assign issues, just get started and do a PR when you're ready. 

See https://docs.wagtail.org/en/stable/contributing/first_contribution_guide.html#how-can-i-be-assigned-an-issue-to-contribute-to (also, this guide has lots of helpful info for your first contribution). 

Resolving patch:
diff --git a/wagtail/api/v2/filters.py b/wagtail/api/v2/filters.py
--- a/wagtail/api/v2/filters.py
+++ b/wagtail/api/v2/filters.py
@@ -63,8 +63,8 @@ def filter_queryset(self, request, queryset, view):
 class OrderingFilter(BaseFilterBackend):
     def filter_queryset(self, request, queryset, view):
         """
-        This applies ordering to the result set
-        Eg: ?order=title
+        This applies ordering to the result set with support for multiple fields.
+        Eg: ?order=title or ?order=title,created_at
 
         It also supports reverse ordering
         Eg: ?order=-title
@@ -73,10 +73,14 @@ def filter_queryset(self, request, queryset, view):
         Eg: ?order=random
         """
         if "order" in request.GET:
-            order_by = request.GET["order"]
+            order_by_list = request.GET["order"].split(",")
 
             # Random ordering
-            if order_by == "random":
+            if "random" in order_by_list:
+                if len(order_by_list) > 1:
+                    raise BadRequestError(
+                        "random ordering cannot be combined with other fields"
+                    )
                 # Prevent ordering by random with offset
                 if "offset" in request.GET:
                     raise BadRequestError(
@@ -85,21 +89,28 @@ def filter_queryset(self, request, queryset, view):
 
                 return queryset.order_by("?")
 
-            # Check if reverse ordering is set
-            if order_by.startswith("-"):
-                reverse_order = True
-                order_by = order_by[1:]
-            else:
-                reverse_order = False
-
-            # Add ordering
-            if order_by in view.get_available_fields(queryset.model):
-                queryset = queryset.order_by(order_by)
-            else:
-                # Unknown field
-                raise BadRequestError("cannot order by '%s' (unknown field)" % order_by)
-
-            # Reverse order
+            order_by_fields = []
+            for order_by in order_by_list:
+                # Check if reverse ordering is set
+                if order_by.startswith("-"):
+                    reverse_order = True
+                    order_by = order_by[1:]
+                else:
+                    reverse_order = False
+
+                # Add ordering
+                if order_by in view.get_available_fields(queryset.model):
+                    order_by_fields.append(order_by)
+                else:
+                    # Unknown field
+                    raise BadRequestError(
+                        "cannot order by '%s' (unknown field)" % order_by
+                    )
+
+            # Apply ordering to the queryset
+            queryset = queryset.order_by(*order_by_fields)
+
+            # Reverse order if needed
             if reverse_order:
                 queryset = queryset.reverse()
 


Test patch:
diff --git a/wagtail/api/v2/tests/test_pages.py b/wagtail/api/v2/tests/test_pages.py
--- a/wagtail/api/v2/tests/test_pages.py
+++ b/wagtail/api/v2/tests/test_pages.py
@@ -941,6 +941,71 @@ def test_ordering_by_unknown_field_gives_error(self):
             content, {"message": "cannot order by 'not_a_field' (unknown field)"}
         )
 
+    def test_random_ordering_with_unknown_field_gives_error(self):
+        response = self.get_response(order=["random,id"])
+        content = json.loads(response.content.decode("UTF-8"))
+
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(
+            content, {"message": "random ordering cannot be combined with other fields"}
+        )
+
+    def test_ordering_by_id_and_slug(self):
+        response = self.get_response(order=["id,slug"])
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+        expected_order = [
+            2,
+            4,
+            5,
+            6,
+            8,
+            9,
+            10,
+            12,
+            13,
+            14,
+            15,
+            16,
+            17,
+            18,
+            19,
+            20,
+            21,
+            22,
+            23,
+        ]
+        self.assertEqual(page_id_list[:15], expected_order[:15])
+
+    def test_ordering_by_title_and_id_backwards(self):
+        response = self.get_response(order=["title,-id"])
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+        expected_order = [
+            15,
+            10,
+            6,
+            17,
+            20,
+            13,
+            2,
+            4,
+            9,
+            8,
+            14,
+            12,
+            18,
+            16,
+            5,
+            23,
+            19,
+            22,
+            21,
+        ]
+        self.assertEqual(page_id_list[:5], expected_order[:5])
+
     # LIMIT
 
     def test_limit_only_two_items_returned(self):