Explorar el Código

Adding multi site support to the pages API. (#8686)

Sævar Öfjörð Magnússon hace 2 años
padre
commit
e660326075

+ 16 - 0
docs/advanced_topics/api/v2/usage.md

@@ -332,6 +332,22 @@ the current page back to the site's root page.
 The `?descendant_of` filter takes the id of a page and filter the list
 to only include descendants of that page (children, grandchildren etc.).
 
+
+### Filtering pages by site
+
+By default, the API will look for the site based on the hostname of the request.
+In some cases, you might want to query pages belonging to a different site.
+The `?site=` filter is used to filter the listing to only include pages that 
+belong to a specific site. The filter requires the configured hostname of the 
+site. If you have multiple sites using the same hostname but a different port 
+number, it's possible to filter by port number using the format `hostname:port`.
+For example:
+
+```
+GET /api/v2/pages/?site=demo-site.local
+GET /api/v2/pages/?site=demo-site.local:8080
+```
+
 ### Search
 
 Passing a query to the `?search` parameter will perform a full-text search on

+ 82 - 7
wagtail/admin/tests/api/test_pages.py

@@ -4,6 +4,7 @@ import json
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group, Permission
+from django.test.utils import override_settings
 from django.urls import reverse
 from django.utils import timezone
 
@@ -37,6 +38,9 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
     def get_page_id_list(self, content):
         return [page["id"] for page in content["items"]]
 
+    def get_homepage(self):
+        return Page.objects.get(slug="home-page")
+
     # BASIC TESTS
 
     def test_basic(self):
@@ -439,7 +443,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
     def test_fields_translations(self):
         # Add a translation of the homepage
         french = Locale.objects.create(language_code="fr")
-        homepage = Page.objects.get(depth=2)
+        homepage = self.get_homepage()
         french_homepage = homepage.copy_for_translation(french)
 
         response = self.get_response(fields="translations")
@@ -474,7 +478,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
         content = json.loads(response.content.decode("UTF-8"))
 
         page_id_list = self.get_page_id_list(content)
-        self.assertEqual(page_id_list, [2])
+        self.assertEqual(page_id_list, [2, 24])
 
     def test_child_of_page_1(self):
         # Public API doesn't allow this, as it's the root page
@@ -495,7 +499,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
         page_id_list = self.get_page_id_list(content)
         self.assertEqual(
             page_id_list,
-            [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12],
+            [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12, 24],
         )
 
     def test_descendant_of_root_doesnt_give_error(self):
@@ -569,7 +573,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
         content = json.loads(response.content.decode("UTF-8"))
 
         page_id_list = self.get_page_id_list(content)
-        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20])
+        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20, 24])
 
     def test_has_children_filter_off(self):
         response = self.get_response(has_children="false")
@@ -577,7 +581,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
 
         page_id_list = self.get_page_id_list(content)
         self.assertEqual(
-            page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12]
+            page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12, 25]
         )
 
     def test_has_children_filter_int(self):
@@ -585,7 +589,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
         content = json.loads(response.content.decode("UTF-8"))
 
         page_id_list = self.get_page_id_list(content)
-        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20])
+        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20, 24])
 
     def test_has_children_filter_int_off(self):
         response = self.get_response(has_children=0)
@@ -593,7 +597,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
 
         page_id_list = self.get_page_id_list(content)
         self.assertEqual(
-            page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12]
+            page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12, 25]
         )
 
     def test_has_children_filter_invalid_integer(self):
@@ -649,6 +653,77 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
         self.assertTrue(blog_page_seen, "No blog pages were found in the items")
         self.assertTrue(event_page_seen, "No event pages were found in the items")
 
+    # Not applicable to the admin API
+    test_site_filter_same_hostname_returns_error = None
+    test_site_filter = None
+
+    def test_ordering_default(self):
+        # overridden because the admin API lists all pages, regardless of sites
+
+        response = self.get_response()
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+        self.assertEqual(
+            page_id_list,
+            [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12, 24],
+        )
+
+    def test_ordering_by_title(self):
+        # overridden because the admin API lists all pages, regardless of sites
+
+        response = self.get_response(order="title")
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+        self.assertEqual(
+            page_id_list,
+            [21, 22, 19, 23, 5, 16, 18, 12, 14, 8, 9, 4, 25, 2, 24, 13, 20, 17, 6, 10],
+        )
+
+    def test_ordering_by_title_backwards(self):
+        # overridden because the admin API lists all pages, regardless of sites
+
+        response = self.get_response(order="-title")
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+        self.assertEqual(
+            page_id_list,
+            [15, 10, 6, 17, 20, 13, 24, 2, 25, 4, 9, 8, 14, 12, 18, 16, 5, 23, 19, 22],
+        )
+
+    def test_limit_total_count(self):
+        # overridden because the admin API lists all pages, regardless of sites
+        # the function is actually unchanged, but uses a different total page count helper
+
+        response = self.get_response(limit=2)
+        content = json.loads(response.content.decode("UTF-8"))
+
+        # The total count must not be affected by "limit"
+        self.assertEqual(content["meta"]["total_count"], get_total_page_count())
+
+    def test_offset_total_count(self):
+        # overridden because the admin API lists all pages, regardless of sites
+        # the function is actually unchanged, but uses a different total page count helper
+
+        response = self.get_response(offset=10)
+        content = json.loads(response.content.decode("UTF-8"))
+
+        # The total count must not be affected by "offset"
+        self.assertEqual(content["meta"]["total_count"], get_total_page_count())
+
+    @override_settings(WAGTAILAPI_LIMIT_MAX=None)
+    def test_limit_max_none_gives_no_errors(self):
+        # overridden because the admin API lists all pages, regardless of sites
+        # the function is actually unchanged, but uses a different total page count helper
+
+        response = self.get_response(limit=1000000)
+        content = json.loads(response.content.decode("UTF-8"))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(content["items"]), get_total_page_count())
+
 
 class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
     fixtures = ["demosite.json"]

+ 36 - 7
wagtail/api/v2/tests/test_pages.py

@@ -18,8 +18,14 @@ from wagtail.test.utils import WagtailTestUtils
 
 
 def get_total_page_count():
-    # Need to take away 1 as the root page is invisible over the API
-    return Page.objects.live().public().count() - 1
+    return (
+        Page.objects.descendant_of(
+            Site.objects.get(is_default_site=True).root_page, inclusive=True
+        )
+        .live()
+        .public()
+        .count()
+    )
 
 
 class TestPageListing(TestCase, WagtailTestUtils):
@@ -31,6 +37,9 @@ class TestPageListing(TestCase, WagtailTestUtils):
     def get_page_id_list(self, content):
         return [page["id"] for page in content["items"]]
 
+    def get_homepage(self):
+        return Page.objects.get(slug="home-page")
+
     # BASIC TESTS
 
     def test_basic(self):
@@ -200,7 +209,7 @@ class TestPageListing(TestCase, WagtailTestUtils):
     @override_settings(WAGTAIL_I18N_ENABLED=True)
     def test_locale_filter(self):
         french = Locale.objects.create(language_code="fr")
-        homepage = Page.objects.get(depth=2)
+        homepage = self.get_homepage()
         french_homepage = homepage.copy_for_translation(french)
         french_homepage.get_latest_revision().publish()
 
@@ -213,7 +222,7 @@ class TestPageListing(TestCase, WagtailTestUtils):
     @override_settings(WAGTAIL_I18N_ENABLED=True)
     def test_locale_filter_with_search(self):
         french = Locale.objects.create(language_code="fr")
-        homepage = Page.objects.get(depth=2)
+        homepage = self.get_homepage()
         french_homepage = homepage.copy_for_translation(french)
         french_homepage.get_latest_revision().publish()
         events_index = Page.objects.get(url_path="/home-page/events-index/")
@@ -231,7 +240,7 @@ class TestPageListing(TestCase, WagtailTestUtils):
     @override_settings(WAGTAIL_I18N_ENABLED=True)
     def test_translation_of_filter(self):
         french = Locale.objects.create(language_code="fr")
-        homepage = Page.objects.get(depth=2)
+        homepage = self.get_homepage()
         french_homepage = homepage.copy_for_translation(french)
         french_homepage.get_latest_revision().publish()
 
@@ -244,7 +253,7 @@ class TestPageListing(TestCase, WagtailTestUtils):
     @override_settings(WAGTAIL_I18N_ENABLED=True)
     def test_translation_of_filter_with_search(self):
         french = Locale.objects.create(language_code="fr")
-        homepage = Page.objects.get(depth=2)
+        homepage = self.get_homepage()
         french_homepage = homepage.copy_for_translation(french)
         french_homepage.get_latest_revision().publish()
 
@@ -844,6 +853,27 @@ class TestPageListing(TestCase, WagtailTestUtils):
             {"message": "filtering by descendant_of with child_of is not supported"},
         )
 
+    # SITE FILTER
+
+    def test_site_filter_same_hostname_returns_error(self):
+        response = self.get_response(site="localhost")
+        content = json.loads(response.content.decode("UTF-8"))
+
+        self.assertEqual(
+            content,
+            {
+                "message": "Your query returned multiple sites. Try adding a port number to your site filter."
+            },
+        )
+
+    def test_site_filter(self):
+        response = self.get_response(site="localhost:8001")
+        content = json.loads(response.content.decode("UTF-8"))
+
+        page_id_list = self.get_page_id_list(content)
+
+        self.assertEqual(page_id_list, [24, 25])
+
     # ORDERING
 
     def test_ordering_default(self):
@@ -1031,7 +1061,6 @@ class TestPageListing(TestCase, WagtailTestUtils):
     def test_search_with_type(self):
         response = self.get_response(type="demosite.BlogEntryPage", search="blog")
         content = json.loads(response.content.decode("UTF-8"))
-
         page_id_list = self.get_page_id_list(content)
 
         self.assertEqual(set(page_id_list), {16, 18, 19})

+ 27 - 2
wagtail/api/v2/views.py

@@ -432,6 +432,7 @@ class PagesAPIViewSet(BaseAPIViewSet):
             "descendant_of",
             "translation_of",
             "locale",
+            "site",
         ]
     )
     body_fields = BaseAPIViewSet.body_fields + [
@@ -494,6 +495,9 @@ class PagesAPIViewSet(BaseAPIViewSet):
         This is used as the base for get_queryset and is also used to find the
         parent pages when using the child_of and descendant_of filters as well.
         """
+
+        request = self.request
+
         # Get all live pages
         queryset = Page.objects.all().live()
 
@@ -508,8 +512,29 @@ class PagesAPIViewSet(BaseAPIViewSet):
         for restricted_page in restricted_pages:
             queryset = queryset.not_descendant_of(restricted_page, inclusive=True)
 
-        # Filter by site
-        site = Site.find_for_request(self.request)
+        # Check if we have a specific site to look for
+        if "site" in request.GET:
+            # Optionally allow querying by port
+            if ":" in request.GET["site"]:
+                (hostname, port) = request.GET["site"].split(":", 1)
+                query = {
+                    "hostname": hostname,
+                    "port": port,
+                }
+            else:
+                query = {
+                    "hostname": request.GET["site"],
+                }
+            try:
+                site = Site.objects.get(**query)
+            except Site.MultipleObjectsReturned:
+                raise BadRequestError(
+                    "Your query returned multiple sites. Try adding a port number to your site filter."
+                )
+        else:
+            # Otherwise, find the site from the request
+            site = Site.find_for_request(self.request)
+
         if site:
             base_queryset = queryset
             queryset = base_queryset.descendant_of(site.root_page, inclusive=True)

+ 61 - 1
wagtail/test/demosite/fixtures/demosite.json

@@ -5,7 +5,7 @@
     "fields": {
       "title": "Root",
       "draft_title": "Root",
-      "numchild": 1,
+      "numchild": 2,
       "show_in_menus": false,
       "live": true,
       "seo_title": "",
@@ -399,6 +399,66 @@
       "slug": "another-grandchild-page"
     }
   },
+  {
+    "pk": 24,
+    "model": "wagtailcore.page",
+    "fields": {
+      "title": "Home page multi site",
+      "draft_title": "Home page multi site",
+      "numchild": 1,
+      "show_in_menus": true,
+      "live": true,
+      "seo_title": "",
+      "depth": 2,
+      "search_description": "",
+      "content_type": ["demosite", "homepage"],
+      "has_unpublished_changes": false,
+      "owner": null,
+      "path": "00010003",
+      "url_path": "/home-page-multi-site/",
+      "slug": "home-page-page-multi-site"
+    }
+  },
+  {
+    "pk": 25,
+    "model": "wagtailcore.page",
+    "fields": {
+      "title": "Events index multi site",
+      "draft_title": "Events index multi site",
+      "numchild": 0,
+      "show_in_menus": true,
+      "live": true,
+      "seo_title": "",
+      "depth": 3,
+      "search_description": "",
+      "content_type": ["demosite", "eventindexpage"],
+      "has_unpublished_changes": false,
+      "owner": null,
+      "path": "000100030001",
+      "url_path": "/home-page-multi-site/events-index-multi-site/",
+      "slug": "events-index-multi-site"
+    }
+  },
+  {
+    "pk": 1,
+    "model": "wagtailcore.site",
+    "fields": {
+      "root_page": 2,
+      "hostname": "localhost",
+      "port": 80,
+      "is_default_site": true
+    }
+  },
+  {
+    "pk": 2,
+    "model": "wagtailcore.site",
+    "fields": {
+      "root_page": 24,
+      "hostname": "localhost",
+      "port": 8001,
+      "is_default_site": false
+    }
+  },
   {
     "pk": 4,
     "model": "wagtailimages.image",