We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/api/v2/filters.py`
Content:
```
1 from django.conf import settings
2 from django.db import models
3 from django.shortcuts import get_object_or_404
4 from rest_framework.filters import BaseFilterBackend
5 from taggit.managers import TaggableManager
6 
7 from wagtail.models import Locale, Page
8 from wagtail.search.backends import get_search_backend
9 from wagtail.search.backends.base import FilterFieldError, OrderByFieldError
10 
11 from .utils import BadRequestError, parse_boolean
12 
13 
14 class FieldsFilter(BaseFilterBackend):
15     def filter_queryset(self, request, queryset, view):
16         """
17         This performs field level filtering on the result set
18         Eg: ?title=James Joyce
19         """
20         fields = set(view.get_available_fields(queryset.model, db_fields_only=True))
21 
22         # Locale is a database field, but we provide a separate filter for it
23         if "locale" in fields:
24             fields.remove("locale")
25 
26         for field_name, value in request.GET.items():
27             if field_name in fields:
28                 try:
29                     field = queryset.model._meta.get_field(field_name)
30                 except LookupError:
31                     field = None
32 
33                 # Convert value into python
34                 try:
35                     if isinstance(
36                         field, (models.BooleanField, models.NullBooleanField)
37                     ):
38                         value = parse_boolean(value)
39                     elif isinstance(field, (models.IntegerField, models.AutoField)):
40                         value = int(value)
41                     elif isinstance(field, models.ForeignKey):
42                         value = field.target_field.get_prep_value(value)
43                 except ValueError as e:
44                     raise BadRequestError(
45                         "field filter error. '%s' is not a valid value for %s (%s)"
46                         % (value, field_name, str(e))
47                     )
48 
49                 if isinstance(field, TaggableManager):
50                     for tag in value.split(","):
51                         queryset = queryset.filter(**{field_name + "__name": tag})
52 
53                     # Stick a message on the queryset to indicate that tag filtering has been performed
54                     # This will let the do_search method know that it must raise an error as searching
55                     # and tag filtering at the same time is not supported
56                     queryset._filtered_by_tag = True
57                 else:
58                     queryset = queryset.filter(**{field_name: value})
59 
60         return queryset
61 
62 
63 class OrderingFilter(BaseFilterBackend):
64     def filter_queryset(self, request, queryset, view):
65         """
66         This applies ordering to the result set
67         Eg: ?order=title
68 
69         It also supports reverse ordering
70         Eg: ?order=-title
71 
72         And random ordering
73         Eg: ?order=random
74         """
75         if "order" in request.GET:
76             order_by = request.GET["order"]
77 
78             # Random ordering
79             if order_by == "random":
80                 # Prevent ordering by random with offset
81                 if "offset" in request.GET:
82                     raise BadRequestError(
83                         "random ordering with offset is not supported"
84                     )
85 
86                 return queryset.order_by("?")
87 
88             # Check if reverse ordering is set
89             if order_by.startswith("-"):
90                 reverse_order = True
91                 order_by = order_by[1:]
92             else:
93                 reverse_order = False
94 
95             # Add ordering
96             if order_by in view.get_available_fields(queryset.model):
97                 queryset = queryset.order_by(order_by)
98             else:
99                 # Unknown field
100                 raise BadRequestError("cannot order by '%s' (unknown field)" % order_by)
101 
102             # Reverse order
103             if reverse_order:
104                 queryset = queryset.reverse()
105 
106         return queryset
107 
108 
109 class SearchFilter(BaseFilterBackend):
110     def filter_queryset(self, request, queryset, view):
111         """
112         This performs a full-text search on the result set
113         Eg: ?search=James Joyce
114         """
115         search_enabled = getattr(settings, "WAGTAILAPI_SEARCH_ENABLED", True)
116 
117         if "search" in request.GET:
118             if not search_enabled:
119                 raise BadRequestError("search is disabled")
120 
121             # Searching and filtering by tag at the same time is not supported
122             if getattr(queryset, "_filtered_by_tag", False):
123                 raise BadRequestError(
124                     "filtering by tag with a search query is not supported"
125                 )
126 
127             search_query = request.GET["search"]
128             search_operator = request.GET.get("search_operator", None)
129             order_by_relevance = "order" not in request.GET
130 
131             sb = get_search_backend()
132             try:
133                 queryset = sb.search(
134                     search_query,
135                     queryset,
136                     operator=search_operator,
137                     order_by_relevance=order_by_relevance,
138                 )
139             except FilterFieldError as e:
140                 raise BadRequestError(
141                     "cannot filter by '{}' while searching (field is not indexed)".format(
142                         e.field_name
143                     )
144                 )
145             except OrderByFieldError as e:
146                 raise BadRequestError(
147                     "cannot order by '{}' while searching (field is not indexed)".format(
148                         e.field_name
149                     )
150                 )
151 
152         return queryset
153 
154 
155 class ChildOfFilter(BaseFilterBackend):
156     """
157     Implements the ?child_of filter used to filter the results to only contain
158     pages that are direct children of the specified page.
159     """
160 
161     def filter_queryset(self, request, queryset, view):
162         if "child_of" in request.GET:
163             try:
164                 parent_page_id = int(request.GET["child_of"])
165                 if parent_page_id < 0:
166                     raise ValueError()
167 
168                 parent_page = view.get_base_queryset().get(id=parent_page_id)
169             except ValueError:
170                 if request.GET["child_of"] == "root":
171                     parent_page = view.get_root_page()
172                 else:
173                     raise BadRequestError("child_of must be a positive integer")
174             except Page.DoesNotExist:
175                 raise BadRequestError("parent page doesn't exist")
176 
177             queryset = queryset.child_of(parent_page)
178 
179             # Save the parent page on the queryset. This is required for the page
180             # explorer, which needs to pass the parent page into
181             # `construct_explorer_page_queryset` hook functions
182             queryset._filtered_by_child_of = parent_page
183 
184         return queryset
185 
186 
187 class AncestorOfFilter(BaseFilterBackend):
188     """
189     Implements the ?ancestor filter which limits the set of pages to a
190     particular branch of the page tree.
191     """
192 
193     def filter_queryset(self, request, queryset, view):
194         if "ancestor_of" in request.GET:
195             try:
196                 descendant_page_id = int(request.GET["ancestor_of"])
197                 if descendant_page_id < 0:
198                     raise ValueError()
199 
200                 descendant_page = view.get_base_queryset().get(id=descendant_page_id)
201             except ValueError:
202                 raise BadRequestError("ancestor_of must be a positive integer")
203             except Page.DoesNotExist:
204                 raise BadRequestError("descendant page doesn't exist")
205 
206             queryset = queryset.ancestor_of(descendant_page)
207 
208         return queryset
209 
210 
211 class DescendantOfFilter(BaseFilterBackend):
212     """
213     Implements the ?decendant_of filter which limits the set of pages to a
214     particular branch of the page tree.
215     """
216 
217     def filter_queryset(self, request, queryset, view):
218         if "descendant_of" in request.GET:
219             if hasattr(queryset, "_filtered_by_child_of"):
220                 raise BadRequestError(
221                     "filtering by descendant_of with child_of is not supported"
222                 )
223             try:
224                 parent_page_id = int(request.GET["descendant_of"])
225                 if parent_page_id < 0:
226                     raise ValueError()
227 
228                 parent_page = view.get_base_queryset().get(id=parent_page_id)
229             except ValueError:
230                 if request.GET["descendant_of"] == "root":
231                     parent_page = view.get_root_page()
232                 else:
233                     raise BadRequestError("descendant_of must be a positive integer")
234             except Page.DoesNotExist:
235                 raise BadRequestError("ancestor page doesn't exist")
236 
237             queryset = queryset.descendant_of(parent_page)
238 
239         return queryset
240 
241 
242 class TranslationOfFilter(BaseFilterBackend):
243     """
244     Implements the ?translation_of filter which limits the set of pages to translations
245     of a page.
246     """
247 
248     def filter_queryset(self, request, queryset, view):
249         if "translation_of" in request.GET:
250             try:
251                 page_id = int(request.GET["translation_of"])
252                 if page_id < 0:
253                     raise ValueError()
254 
255                 page = view.get_base_queryset().get(id=page_id)
256             except ValueError:
257                 if request.GET["translation_of"] == "root":
258                     page = view.get_root_page()
259                 else:
260                     raise BadRequestError("translation_of must be a positive integer")
261             except Page.DoesNotExist:
262                 raise BadRequestError("translation_of page doesn't exist")
263 
264             _filtered_by_child_of = getattr(queryset, "_filtered_by_child_of", None)
265 
266             queryset = queryset.translation_of(page)
267 
268             if _filtered_by_child_of:
269                 queryset._filtered_by_child_of = _filtered_by_child_of
270 
271         return queryset
272 
273 
274 class LocaleFilter(BaseFilterBackend):
275     """
276     Implements the ?locale filter which limits the set of pages to a
277     particular locale.
278     """
279 
280     def filter_queryset(self, request, queryset, view):
281         if "locale" in request.GET:
282             _filtered_by_child_of = getattr(queryset, "_filtered_by_child_of", None)
283 
284             locale = get_object_or_404(Locale, language_code=request.GET["locale"])
285             queryset = queryset.filter(locale=locale)
286 
287             if _filtered_by_child_of:
288                 queryset._filtered_by_child_of = _filtered_by_child_of
289 
290         return queryset
291
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
 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 @@
         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 @@
 
                 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()

{"golden_diff": "diff --git a/wagtail/api/v2/filters.py b/wagtail/api/v2/filters.py\n--- a/wagtail/api/v2/filters.py\n+++ b/wagtail/api/v2/filters.py\n@@ -63,8 +63,8 @@\n class OrderingFilter(BaseFilterBackend):\n     def filter_queryset(self, request, queryset, view):\n         \"\"\"\n-        This applies ordering to the result set\n-        Eg: ?order=title\n+        This applies ordering to the result set with support for multiple fields.\n+        Eg: ?order=title or ?order=title,created_at\n \n         It also supports reverse ordering\n         Eg: ?order=-title\n@@ -73,10 +73,14 @@\n         Eg: ?order=random\n         \"\"\"\n         if \"order\" in request.GET:\n-            order_by = request.GET[\"order\"]\n+            order_by_list = request.GET[\"order\"].split(\",\")\n \n             # Random ordering\n-            if order_by == \"random\":\n+            if \"random\" in order_by_list:\n+                if len(order_by_list) > 1:\n+                    raise BadRequestError(\n+                        \"random ordering cannot be combined with other fields\"\n+                    )\n                 # Prevent ordering by random with offset\n                 if \"offset\" in request.GET:\n                     raise BadRequestError(\n@@ -85,21 +89,28 @@\n \n                 return queryset.order_by(\"?\")\n \n-            # Check if reverse ordering is set\n-            if order_by.startswith(\"-\"):\n-                reverse_order = True\n-                order_by = order_by[1:]\n-            else:\n-                reverse_order = False\n-\n-            # Add ordering\n-            if order_by in view.get_available_fields(queryset.model):\n-                queryset = queryset.order_by(order_by)\n-            else:\n-                # Unknown field\n-                raise BadRequestError(\"cannot order by '%s' (unknown field)\" % order_by)\n-\n-            # Reverse order\n+            order_by_fields = []\n+            for order_by in order_by_list:\n+                # Check if reverse ordering is set\n+                if order_by.startswith(\"-\"):\n+                    reverse_order = True\n+                    order_by = order_by[1:]\n+                else:\n+                    reverse_order = False\n+\n+                # Add ordering\n+                if order_by in view.get_available_fields(queryset.model):\n+                    order_by_fields.append(order_by)\n+                else:\n+                    # Unknown field\n+                    raise BadRequestError(\n+                        \"cannot order by '%s' (unknown field)\" % order_by\n+                    )\n+\n+            # Apply ordering to the queryset\n+            queryset = queryset.order_by(*order_by_fields)\n+\n+            # Reverse order if needed\n             if reverse_order:\n                 queryset = queryset.reverse()\n", "issue": "Allow ordering by multiple fields on the Wagtail API\n### Is your proposal related to a problem?\r\n\r\n<!--\r\n  Provide a clear and concise description of what the problem is.\r\n  For example, \"I'm always frustrated when...\"\r\n-->\r\n\r\nThe 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.\r\n\r\n### Describe the solution you'd like\r\n\r\n<!--\r\n  Provide a clear and concise description of what you want to happen.\r\n-->\r\n\r\n`[GET] /api/v2/pages/?type=blog.BlogPage&order=-published_at,blog_type`\r\n\r\nThe result should be the same as if this query was run:\r\n\r\n```python\r\nBlogPage.objects.order_by('-published_at', 'blog_type')\r\n```\r\n\r\n### Describe alternatives you've considered\r\n\r\n<!--\r\n  Let us know about other solutions you've tried or researched.\r\n-->\r\n\r\nIn the meantime, we're going to add this functionality ad-hoc.\r\n\r\n### Additional context\r\n\r\n<!--\r\n  Is there anything else you can add about the proposal?\r\n  You might want to link to related issues here, if you haven't already.\r\n-->\r\n\r\nThis is for a headless website.\r\n\nAllow ordering by multiple fields on the Wagtail API\n### Is your proposal related to a problem?\r\n\r\n<!--\r\n  Provide a clear and concise description of what the problem is.\r\n  For example, \"I'm always frustrated when...\"\r\n-->\r\n\r\nThe 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.\r\n\r\n### Describe the solution you'd like\r\n\r\n<!--\r\n  Provide a clear and concise description of what you want to happen.\r\n-->\r\n\r\n`[GET] /api/v2/pages/?type=blog.BlogPage&order=-published_at,blog_type`\r\n\r\nThe result should be the same as if this query was run:\r\n\r\n```python\r\nBlogPage.objects.order_by('-published_at', 'blog_type')\r\n```\r\n\r\n### Describe alternatives you've considered\r\n\r\n<!--\r\n  Let us know about other solutions you've tried or researched.\r\n-->\r\n\r\nIn the meantime, we're going to add this functionality ad-hoc.\r\n\r\n### Additional context\r\n\r\n<!--\r\n  Is there anything else you can add about the proposal?\r\n  You might want to link to related issues here, if you haven't already.\r\n-->\r\n\r\nThis is for a headless website.\r\n\n", "before_files": [{"content": "from django.conf import settings\nfrom django.db import models\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework.filters import BaseFilterBackend\nfrom taggit.managers import TaggableManager\n\nfrom wagtail.models import Locale, Page\nfrom wagtail.search.backends import get_search_backend\nfrom wagtail.search.backends.base import FilterFieldError, OrderByFieldError\n\nfrom .utils import BadRequestError, parse_boolean\n\n\nclass FieldsFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This performs field level filtering on the result set\n        Eg: ?title=James Joyce\n        \"\"\"\n        fields = set(view.get_available_fields(queryset.model, db_fields_only=True))\n\n        # Locale is a database field, but we provide a separate filter for it\n        if \"locale\" in fields:\n            fields.remove(\"locale\")\n\n        for field_name, value in request.GET.items():\n            if field_name in fields:\n                try:\n                    field = queryset.model._meta.get_field(field_name)\n                except LookupError:\n                    field = None\n\n                # Convert value into python\n                try:\n                    if isinstance(\n                        field, (models.BooleanField, models.NullBooleanField)\n                    ):\n                        value = parse_boolean(value)\n                    elif isinstance(field, (models.IntegerField, models.AutoField)):\n                        value = int(value)\n                    elif isinstance(field, models.ForeignKey):\n                        value = field.target_field.get_prep_value(value)\n                except ValueError as e:\n                    raise BadRequestError(\n                        \"field filter error. '%s' is not a valid value for %s (%s)\"\n                        % (value, field_name, str(e))\n                    )\n\n                if isinstance(field, TaggableManager):\n                    for tag in value.split(\",\"):\n                        queryset = queryset.filter(**{field_name + \"__name\": tag})\n\n                    # Stick a message on the queryset to indicate that tag filtering has been performed\n                    # This will let the do_search method know that it must raise an error as searching\n                    # and tag filtering at the same time is not supported\n                    queryset._filtered_by_tag = True\n                else:\n                    queryset = queryset.filter(**{field_name: value})\n\n        return queryset\n\n\nclass OrderingFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This applies ordering to the result set\n        Eg: ?order=title\n\n        It also supports reverse ordering\n        Eg: ?order=-title\n\n        And random ordering\n        Eg: ?order=random\n        \"\"\"\n        if \"order\" in request.GET:\n            order_by = request.GET[\"order\"]\n\n            # Random ordering\n            if order_by == \"random\":\n                # Prevent ordering by random with offset\n                if \"offset\" in request.GET:\n                    raise BadRequestError(\n                        \"random ordering with offset is not supported\"\n                    )\n\n                return queryset.order_by(\"?\")\n\n            # Check if reverse ordering is set\n            if order_by.startswith(\"-\"):\n                reverse_order = True\n                order_by = order_by[1:]\n            else:\n                reverse_order = False\n\n            # Add ordering\n            if order_by in view.get_available_fields(queryset.model):\n                queryset = queryset.order_by(order_by)\n            else:\n                # Unknown field\n                raise BadRequestError(\"cannot order by '%s' (unknown field)\" % order_by)\n\n            # Reverse order\n            if reverse_order:\n                queryset = queryset.reverse()\n\n        return queryset\n\n\nclass SearchFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This performs a full-text search on the result set\n        Eg: ?search=James Joyce\n        \"\"\"\n        search_enabled = getattr(settings, \"WAGTAILAPI_SEARCH_ENABLED\", True)\n\n        if \"search\" in request.GET:\n            if not search_enabled:\n                raise BadRequestError(\"search is disabled\")\n\n            # Searching and filtering by tag at the same time is not supported\n            if getattr(queryset, \"_filtered_by_tag\", False):\n                raise BadRequestError(\n                    \"filtering by tag with a search query is not supported\"\n                )\n\n            search_query = request.GET[\"search\"]\n            search_operator = request.GET.get(\"search_operator\", None)\n            order_by_relevance = \"order\" not in request.GET\n\n            sb = get_search_backend()\n            try:\n                queryset = sb.search(\n                    search_query,\n                    queryset,\n                    operator=search_operator,\n                    order_by_relevance=order_by_relevance,\n                )\n            except FilterFieldError as e:\n                raise BadRequestError(\n                    \"cannot filter by '{}' while searching (field is not indexed)\".format(\n                        e.field_name\n                    )\n                )\n            except OrderByFieldError as e:\n                raise BadRequestError(\n                    \"cannot order by '{}' while searching (field is not indexed)\".format(\n                        e.field_name\n                    )\n                )\n\n        return queryset\n\n\nclass ChildOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?child_of filter used to filter the results to only contain\n    pages that are direct children of the specified page.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"child_of\" in request.GET:\n            try:\n                parent_page_id = int(request.GET[\"child_of\"])\n                if parent_page_id < 0:\n                    raise ValueError()\n\n                parent_page = view.get_base_queryset().get(id=parent_page_id)\n            except ValueError:\n                if request.GET[\"child_of\"] == \"root\":\n                    parent_page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"child_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"parent page doesn't exist\")\n\n            queryset = queryset.child_of(parent_page)\n\n            # Save the parent page on the queryset. This is required for the page\n            # explorer, which needs to pass the parent page into\n            # `construct_explorer_page_queryset` hook functions\n            queryset._filtered_by_child_of = parent_page\n\n        return queryset\n\n\nclass AncestorOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?ancestor filter which limits the set of pages to a\n    particular branch of the page tree.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"ancestor_of\" in request.GET:\n            try:\n                descendant_page_id = int(request.GET[\"ancestor_of\"])\n                if descendant_page_id < 0:\n                    raise ValueError()\n\n                descendant_page = view.get_base_queryset().get(id=descendant_page_id)\n            except ValueError:\n                raise BadRequestError(\"ancestor_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"descendant page doesn't exist\")\n\n            queryset = queryset.ancestor_of(descendant_page)\n\n        return queryset\n\n\nclass DescendantOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?decendant_of filter which limits the set of pages to a\n    particular branch of the page tree.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"descendant_of\" in request.GET:\n            if hasattr(queryset, \"_filtered_by_child_of\"):\n                raise BadRequestError(\n                    \"filtering by descendant_of with child_of is not supported\"\n                )\n            try:\n                parent_page_id = int(request.GET[\"descendant_of\"])\n                if parent_page_id < 0:\n                    raise ValueError()\n\n                parent_page = view.get_base_queryset().get(id=parent_page_id)\n            except ValueError:\n                if request.GET[\"descendant_of\"] == \"root\":\n                    parent_page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"descendant_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"ancestor page doesn't exist\")\n\n            queryset = queryset.descendant_of(parent_page)\n\n        return queryset\n\n\nclass TranslationOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?translation_of filter which limits the set of pages to translations\n    of a page.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"translation_of\" in request.GET:\n            try:\n                page_id = int(request.GET[\"translation_of\"])\n                if page_id < 0:\n                    raise ValueError()\n\n                page = view.get_base_queryset().get(id=page_id)\n            except ValueError:\n                if request.GET[\"translation_of\"] == \"root\":\n                    page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"translation_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"translation_of page doesn't exist\")\n\n            _filtered_by_child_of = getattr(queryset, \"_filtered_by_child_of\", None)\n\n            queryset = queryset.translation_of(page)\n\n            if _filtered_by_child_of:\n                queryset._filtered_by_child_of = _filtered_by_child_of\n\n        return queryset\n\n\nclass LocaleFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?locale filter which limits the set of pages to a\n    particular locale.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"locale\" in request.GET:\n            _filtered_by_child_of = getattr(queryset, \"_filtered_by_child_of\", None)\n\n            locale = get_object_or_404(Locale, language_code=request.GET[\"locale\"])\n            queryset = queryset.filter(locale=locale)\n\n            if _filtered_by_child_of:\n                queryset._filtered_by_child_of = _filtered_by_child_of\n\n        return queryset\n", "path": "wagtail/api/v2/filters.py"}], "after_files": [{"content": "from django.conf import settings\nfrom django.db import models\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework.filters import BaseFilterBackend\nfrom taggit.managers import TaggableManager\n\nfrom wagtail.models import Locale, Page\nfrom wagtail.search.backends import get_search_backend\nfrom wagtail.search.backends.base import FilterFieldError, OrderByFieldError\n\nfrom .utils import BadRequestError, parse_boolean\n\n\nclass FieldsFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This performs field level filtering on the result set\n        Eg: ?title=James Joyce\n        \"\"\"\n        fields = set(view.get_available_fields(queryset.model, db_fields_only=True))\n\n        # Locale is a database field, but we provide a separate filter for it\n        if \"locale\" in fields:\n            fields.remove(\"locale\")\n\n        for field_name, value in request.GET.items():\n            if field_name in fields:\n                try:\n                    field = queryset.model._meta.get_field(field_name)\n                except LookupError:\n                    field = None\n\n                # Convert value into python\n                try:\n                    if isinstance(\n                        field, (models.BooleanField, models.NullBooleanField)\n                    ):\n                        value = parse_boolean(value)\n                    elif isinstance(field, (models.IntegerField, models.AutoField)):\n                        value = int(value)\n                    elif isinstance(field, models.ForeignKey):\n                        value = field.target_field.get_prep_value(value)\n                except ValueError as e:\n                    raise BadRequestError(\n                        \"field filter error. '%s' is not a valid value for %s (%s)\"\n                        % (value, field_name, str(e))\n                    )\n\n                if isinstance(field, TaggableManager):\n                    for tag in value.split(\",\"):\n                        queryset = queryset.filter(**{field_name + \"__name\": tag})\n\n                    # Stick a message on the queryset to indicate that tag filtering has been performed\n                    # This will let the do_search method know that it must raise an error as searching\n                    # and tag filtering at the same time is not supported\n                    queryset._filtered_by_tag = True\n                else:\n                    queryset = queryset.filter(**{field_name: value})\n\n        return queryset\n\n\nclass OrderingFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This applies ordering to the result set with support for multiple fields.\n        Eg: ?order=title or ?order=title,created_at\n\n        It also supports reverse ordering\n        Eg: ?order=-title\n\n        And random ordering\n        Eg: ?order=random\n        \"\"\"\n        if \"order\" in request.GET:\n            order_by_list = request.GET[\"order\"].split(\",\")\n\n            # Random ordering\n            if \"random\" in order_by_list:\n                if len(order_by_list) > 1:\n                    raise BadRequestError(\n                        \"random ordering cannot be combined with other fields\"\n                    )\n                # Prevent ordering by random with offset\n                if \"offset\" in request.GET:\n                    raise BadRequestError(\n                        \"random ordering with offset is not supported\"\n                    )\n\n                return queryset.order_by(\"?\")\n\n            order_by_fields = []\n            for order_by in order_by_list:\n                # Check if reverse ordering is set\n                if order_by.startswith(\"-\"):\n                    reverse_order = True\n                    order_by = order_by[1:]\n                else:\n                    reverse_order = False\n\n                # Add ordering\n                if order_by in view.get_available_fields(queryset.model):\n                    order_by_fields.append(order_by)\n                else:\n                    # Unknown field\n                    raise BadRequestError(\n                        \"cannot order by '%s' (unknown field)\" % order_by\n                    )\n\n            # Apply ordering to the queryset\n            queryset = queryset.order_by(*order_by_fields)\n\n            # Reverse order if needed\n            if reverse_order:\n                queryset = queryset.reverse()\n\n        return queryset\n\n\nclass SearchFilter(BaseFilterBackend):\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        This performs a full-text search on the result set\n        Eg: ?search=James Joyce\n        \"\"\"\n        search_enabled = getattr(settings, \"WAGTAILAPI_SEARCH_ENABLED\", True)\n\n        if \"search\" in request.GET:\n            if not search_enabled:\n                raise BadRequestError(\"search is disabled\")\n\n            # Searching and filtering by tag at the same time is not supported\n            if getattr(queryset, \"_filtered_by_tag\", False):\n                raise BadRequestError(\n                    \"filtering by tag with a search query is not supported\"\n                )\n\n            search_query = request.GET[\"search\"]\n            search_operator = request.GET.get(\"search_operator\", None)\n            order_by_relevance = \"order\" not in request.GET\n\n            sb = get_search_backend()\n            try:\n                queryset = sb.search(\n                    search_query,\n                    queryset,\n                    operator=search_operator,\n                    order_by_relevance=order_by_relevance,\n                )\n            except FilterFieldError as e:\n                raise BadRequestError(\n                    \"cannot filter by '{}' while searching (field is not indexed)\".format(\n                        e.field_name\n                    )\n                )\n            except OrderByFieldError as e:\n                raise BadRequestError(\n                    \"cannot order by '{}' while searching (field is not indexed)\".format(\n                        e.field_name\n                    )\n                )\n\n        return queryset\n\n\nclass ChildOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?child_of filter used to filter the results to only contain\n    pages that are direct children of the specified page.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"child_of\" in request.GET:\n            try:\n                parent_page_id = int(request.GET[\"child_of\"])\n                if parent_page_id < 0:\n                    raise ValueError()\n\n                parent_page = view.get_base_queryset().get(id=parent_page_id)\n            except ValueError:\n                if request.GET[\"child_of\"] == \"root\":\n                    parent_page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"child_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"parent page doesn't exist\")\n\n            queryset = queryset.child_of(parent_page)\n\n            # Save the parent page on the queryset. This is required for the page\n            # explorer, which needs to pass the parent page into\n            # `construct_explorer_page_queryset` hook functions\n            queryset._filtered_by_child_of = parent_page\n\n        return queryset\n\n\nclass AncestorOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?ancestor filter which limits the set of pages to a\n    particular branch of the page tree.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"ancestor_of\" in request.GET:\n            try:\n                descendant_page_id = int(request.GET[\"ancestor_of\"])\n                if descendant_page_id < 0:\n                    raise ValueError()\n\n                descendant_page = view.get_base_queryset().get(id=descendant_page_id)\n            except ValueError:\n                raise BadRequestError(\"ancestor_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"descendant page doesn't exist\")\n\n            queryset = queryset.ancestor_of(descendant_page)\n\n        return queryset\n\n\nclass DescendantOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?decendant_of filter which limits the set of pages to a\n    particular branch of the page tree.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"descendant_of\" in request.GET:\n            if hasattr(queryset, \"_filtered_by_child_of\"):\n                raise BadRequestError(\n                    \"filtering by descendant_of with child_of is not supported\"\n                )\n            try:\n                parent_page_id = int(request.GET[\"descendant_of\"])\n                if parent_page_id < 0:\n                    raise ValueError()\n\n                parent_page = view.get_base_queryset().get(id=parent_page_id)\n            except ValueError:\n                if request.GET[\"descendant_of\"] == \"root\":\n                    parent_page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"descendant_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"ancestor page doesn't exist\")\n\n            queryset = queryset.descendant_of(parent_page)\n\n        return queryset\n\n\nclass TranslationOfFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?translation_of filter which limits the set of pages to translations\n    of a page.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"translation_of\" in request.GET:\n            try:\n                page_id = int(request.GET[\"translation_of\"])\n                if page_id < 0:\n                    raise ValueError()\n\n                page = view.get_base_queryset().get(id=page_id)\n            except ValueError:\n                if request.GET[\"translation_of\"] == \"root\":\n                    page = view.get_root_page()\n                else:\n                    raise BadRequestError(\"translation_of must be a positive integer\")\n            except Page.DoesNotExist:\n                raise BadRequestError(\"translation_of page doesn't exist\")\n\n            _filtered_by_child_of = getattr(queryset, \"_filtered_by_child_of\", None)\n\n            queryset = queryset.translation_of(page)\n\n            if _filtered_by_child_of:\n                queryset._filtered_by_child_of = _filtered_by_child_of\n\n        return queryset\n\n\nclass LocaleFilter(BaseFilterBackend):\n    \"\"\"\n    Implements the ?locale filter which limits the set of pages to a\n    particular locale.\n    \"\"\"\n\n    def filter_queryset(self, request, queryset, view):\n        if \"locale\" in request.GET:\n            _filtered_by_child_of = getattr(queryset, \"_filtered_by_child_of\", None)\n\n            locale = get_object_or_404(Locale, language_code=request.GET[\"locale\"])\n            queryset = queryset.filter(locale=locale)\n\n            if _filtered_by_child_of:\n                queryset._filtered_by_child_of = _filtered_by_child_of\n\n        return queryset\n", "path": "wagtail/api/v2/filters.py"}]}