kopia lustrzana https://github.com/wagtail/wagtail
Refactor page fetching logic to cache per request (#11683)
Adds two new helper static methods: - `Page.route_for_request()` - to find the page route, given a request object and a URL path - `Page.find_for_request()` - to find the page given, a request object and a URL pathpull/11831/head
rodzic
6843fbe643
commit
06ed3ae6b8
|
@ -186,6 +186,10 @@ See also [django-treebeard](https://django-treebeard.readthedocs.io/en/latest/in
|
||||||
|
|
||||||
.. automethod:: serve
|
.. automethod:: serve
|
||||||
|
|
||||||
|
.. automethod:: route_for_request
|
||||||
|
|
||||||
|
.. automethod:: find_for_request
|
||||||
|
|
||||||
.. autoattribute:: context_object_name
|
.. autoattribute:: context_object_name
|
||||||
|
|
||||||
Custom name for page instance in page's ``Context``.
|
Custom name for page instance in page's ``Context``.
|
||||||
|
|
|
@ -9,11 +9,14 @@ should implement low-level generic functionality which is then imported by highe
|
||||||
as Page.
|
as Page.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import posixpath
|
import posixpath
|
||||||
import uuid
|
import uuid
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -126,6 +129,9 @@ from .sites import Site, SiteManager, SiteRootPath # noqa: F401
|
||||||
from .specific import SpecificMixin
|
from .specific import SpecificMixin
|
||||||
from .view_restrictions import BaseViewRestriction
|
from .view_restrictions import BaseViewRestriction
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
logger = logging.getLogger("wagtail")
|
logger = logging.getLogger("wagtail")
|
||||||
|
|
||||||
PAGE_TEMPLATE_VAR = "page"
|
PAGE_TEMPLATE_VAR = "page"
|
||||||
|
@ -1283,6 +1289,43 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
||||||
promote_panels = []
|
promote_panels = []
|
||||||
settings_panels = []
|
settings_panels = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def route_for_request(request: "HttpRequest", path: str) -> RouteResult | None:
|
||||||
|
"""
|
||||||
|
Find the page route for the given HTTP request object, and URL path. The route
|
||||||
|
result (`page`, `args`, and `kwargs`) will be cached via
|
||||||
|
`request._wagtail_route_for_request`.
|
||||||
|
"""
|
||||||
|
if not hasattr(request, "_wagtail_route_for_request"):
|
||||||
|
try:
|
||||||
|
# we need a valid Site object for this request in order to proceed
|
||||||
|
if site := Site.find_for_request(request):
|
||||||
|
path_components = [
|
||||||
|
component for component in path.split("/") if component
|
||||||
|
]
|
||||||
|
request._wagtail_route_for_request = (
|
||||||
|
site.root_page.localized.specific.route(
|
||||||
|
request, path_components
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request._wagtail_route_for_request = None
|
||||||
|
except Http404:
|
||||||
|
# .route() can raise Http404
|
||||||
|
request._wagtail_route_for_request = None
|
||||||
|
|
||||||
|
return request._wagtail_route_for_request
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_for_request(request: "HttpRequest", path: str) -> "Page" | None:
|
||||||
|
"""
|
||||||
|
Find the page for the given HTTP request object, and URL path. The full
|
||||||
|
page route will be cached via `request._wagtail_route_for_request`
|
||||||
|
"""
|
||||||
|
result = Page.route_for_request(request, path)
|
||||||
|
if result is not None:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not self.id:
|
if not self.id:
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
from wagtail.models import Page
|
||||||
|
from wagtail.views import serve
|
||||||
|
|
||||||
|
|
||||||
class BlockDodgyUserAgentMiddleware(MiddlewareMixin):
|
class BlockDodgyUserAgentMiddleware(MiddlewareMixin):
|
||||||
# Used to test that we're correctly handling responses returned from middleware during page
|
# Used to test that we're correctly handling responses returned from middleware during page
|
||||||
|
@ -16,3 +19,13 @@ class BlockDodgyUserAgentMiddleware(MiddlewareMixin):
|
||||||
and request.headers.get("user-agent") == "EvilHacker"
|
and request.headers.get("user-agent") == "EvilHacker"
|
||||||
):
|
):
|
||||||
return HttpResponseForbidden("Forbidden")
|
return HttpResponseForbidden("Forbidden")
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePageViewInterceptorMiddleware(MiddlewareMixin):
|
||||||
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
if serve == view_func:
|
||||||
|
page = Page.find_for_request(request, *view_args, **view_kwargs)
|
||||||
|
if page is None:
|
||||||
|
raise Http404
|
||||||
|
elif page.content == "Intercept me":
|
||||||
|
return HttpResponse("Intercepted")
|
||||||
|
|
|
@ -68,6 +68,7 @@ from wagtail.test.testapp.models import (
|
||||||
TaggedPage,
|
TaggedPage,
|
||||||
)
|
)
|
||||||
from wagtail.test.utils import WagtailTestUtils
|
from wagtail.test.utils import WagtailTestUtils
|
||||||
|
from wagtail.url_routing import RouteResult
|
||||||
|
|
||||||
|
|
||||||
def get_ct(model):
|
def get_ct(model):
|
||||||
|
@ -211,6 +212,50 @@ class TestSiteRouting(TestCase):
|
||||||
self.unrecognised_port = "8000"
|
self.unrecognised_port = "8000"
|
||||||
self.unrecognised_hostname = "unknown.site.com"
|
self.unrecognised_hostname = "unknown.site.com"
|
||||||
|
|
||||||
|
def test_route_for_request_query_count(self):
|
||||||
|
request = get_dummy_request(site=self.events_site)
|
||||||
|
with self.assertNumQueries(2):
|
||||||
|
# expect queries for site & page
|
||||||
|
Page.route_for_request(request, request.path)
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
# subsequent lookups should be cached on the request
|
||||||
|
Page.route_for_request(request, request.path)
|
||||||
|
|
||||||
|
def test_route_for_request_value(self):
|
||||||
|
request = get_dummy_request(site=self.events_site)
|
||||||
|
self.assertFalse(hasattr(request, "_wagtail_route_for_request"))
|
||||||
|
result = Page.route_for_request(request, request.path)
|
||||||
|
self.assertTrue(isinstance(result, RouteResult))
|
||||||
|
self.assertEqual(
|
||||||
|
(result[0], result[1], result[2]),
|
||||||
|
(self.events_site.root_page.specific, [], {}),
|
||||||
|
)
|
||||||
|
self.assertTrue(hasattr(request, "_wagtail_route_for_request"))
|
||||||
|
self.assertIs(request._wagtail_route_for_request, result)
|
||||||
|
|
||||||
|
def test_route_for_request_cached(self):
|
||||||
|
request = get_dummy_request(site=self.events_site)
|
||||||
|
m = Mock()
|
||||||
|
request._wagtail_route_for_request = m
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
self.assertEqual(Page.route_for_request(request, request.path), m)
|
||||||
|
|
||||||
|
def test_route_for_request_suppresses_404(self):
|
||||||
|
request = get_dummy_request(path="does-not-exist", site=self.events_site)
|
||||||
|
self.assertIsNone(Page.route_for_request(request, request.path))
|
||||||
|
|
||||||
|
def test_find_for_request(self):
|
||||||
|
request_200 = get_dummy_request(site=self.events_site)
|
||||||
|
self.assertEqual(
|
||||||
|
Page.find_for_request(request_200, request_200.path),
|
||||||
|
self.events_site.root_page.specific,
|
||||||
|
)
|
||||||
|
request_404 = get_dummy_request(path="does-not-exist", site=self.events_site)
|
||||||
|
self.assertEqual(
|
||||||
|
Page.find_for_request(request_404, request_404.path),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def test_valid_headers_route_to_specific_site(self):
|
def test_valid_headers_route_to_specific_site(self):
|
||||||
# requests with a known Host: header should be directed to the specific site
|
# requests with a known Host: header should be directed to the specific site
|
||||||
request = get_dummy_request(site=self.events_site)
|
request = get_dummy_request(site=self.events_site)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from wagtail.models import Page
|
from wagtail.coreutils import get_dummy_request
|
||||||
|
from wagtail.models import Page, Site
|
||||||
|
from wagtail.test.testapp.models import SimplePage
|
||||||
from wagtail.test.utils import WagtailTestUtils
|
from wagtail.test.utils import WagtailTestUtils
|
||||||
|
from wagtail.views import serve
|
||||||
|
|
||||||
|
|
||||||
class TestLoginView(WagtailTestUtils, TestCase):
|
class TestLoginView(WagtailTestUtils, TestCase):
|
||||||
|
@ -47,3 +52,49 @@ class TestLoginView(WagtailTestUtils, TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, self.events_index.url)
|
self.assertRedirects(response, self.events_index.url)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("wagtail.hooks.get_hooks", mock.Mock(return_value=[]))
|
||||||
|
class TestServeView(TestCase):
|
||||||
|
fixtures = ["test.json"]
|
||||||
|
|
||||||
|
def test_serve_query_count(self):
|
||||||
|
request = get_dummy_request()
|
||||||
|
Site.find_for_request(request)
|
||||||
|
page, args, kwargs = Page.route_for_request(request, request.path)
|
||||||
|
with mock.patch.object(page, "serve", wraps=page.serve) as m:
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
serve(request, "/")
|
||||||
|
m.assert_called_once_with(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def test_process_view_by_page_query_count(self):
|
||||||
|
expected_query_count = 3
|
||||||
|
site = Site.objects.get()
|
||||||
|
page = site.root_page.add_child(
|
||||||
|
instance=SimplePage(title="Simple page", slug="simple", content="Simple")
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
Page, "route_for_request", wraps=Page.route_for_request
|
||||||
|
) as m:
|
||||||
|
with self.modify_settings(
|
||||||
|
MIDDLEWARE={
|
||||||
|
"prepend": "wagtail.test.middleware.SimplePageViewInterceptorMiddleware"
|
||||||
|
}
|
||||||
|
):
|
||||||
|
with self.assertNumQueries(expected_query_count):
|
||||||
|
response_a = self.client.get("/simple/")
|
||||||
|
self.assertEqual(
|
||||||
|
response_a.content,
|
||||||
|
b'\n\n\n\n<!DOCTYPE HTML>\n<html lang="en" dir="ltr">\n <head>\n <title>Simple page</title>\n </head>\n <body>\n \n <h1>Simple page</h1>\n \n <h2>Simple page</h2>\n\n </body>\n</html>\n',
|
||||||
|
)
|
||||||
|
self.assertEqual(m.call_count, 2)
|
||||||
|
page.content = "Intercept me"
|
||||||
|
page.save_revision().publish()
|
||||||
|
m.reset_mock()
|
||||||
|
with self.assertNumQueries(expected_query_count):
|
||||||
|
# verify the same number of queries are used when the
|
||||||
|
# middleware activates to demonstrate Page.route_for_request()
|
||||||
|
# prevents extra database queries for serving pages
|
||||||
|
response_b = self.client.get("/simple/")
|
||||||
|
self.assertEqual(response_b.content, b"Intercepted")
|
||||||
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
|
@ -6,19 +6,15 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
|
|
||||||
from wagtail import hooks
|
from wagtail import hooks
|
||||||
from wagtail.forms import PasswordViewRestrictionForm
|
from wagtail.forms import PasswordViewRestrictionForm
|
||||||
from wagtail.models import Page, PageViewRestriction, Site
|
from wagtail.models import Page, PageViewRestriction
|
||||||
|
|
||||||
|
|
||||||
def serve(request, path):
|
def serve(request, path):
|
||||||
# we need a valid Site object corresponding to this request in order to proceed
|
route_result = Page.route_for_request(request, path)
|
||||||
site = Site.find_for_request(request)
|
if route_result is None:
|
||||||
if not site:
|
|
||||||
raise Http404
|
raise Http404
|
||||||
|
else:
|
||||||
path_components = [component for component in path.split("/") if component]
|
page, args, kwargs = route_result
|
||||||
page, args, kwargs = site.root_page.localized.specific.route(
|
|
||||||
request, path_components
|
|
||||||
)
|
|
||||||
|
|
||||||
for fn in hooks.get_hooks("before_serve_page"):
|
for fn in hooks.get_hooks("before_serve_page"):
|
||||||
result = fn(page, request, args, kwargs)
|
result = fn(page, request, args, kwargs)
|
||||||
|
|
Ładowanie…
Reference in New Issue