angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/models/preview.py

282 lines
11 KiB
Python
Raw Normal View History

2025-07-25 21:32:16 +10:00
from io import StringIO
from urllib.parse import urlsplit
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.http.request import validate_host
from django.template.response import TemplateResponse
from django.utils.cache import patch_cache_control
from django.utils.translation import gettext_lazy as _
class PreviewableMixin:
"""A mixin that allows a model to have previews."""
def make_preview_request(
self, original_request=None, preview_mode=None, extra_request_attrs=None
):
"""
Simulate a request to this object, by constructing a fake HttpRequest object that is (as far
as possible) representative of a real request to this object's front-end URL, and invoking
serve_preview with that request (and the given preview_mode).
Used for previewing / moderation and any other place where we
want to display a view of this object in the admin interface without going through the regular
page routing logic.
If you pass in a real request object as original_request, additional information (e.g. client IP, cookies)
will be included in the dummy request.
"""
dummy_meta = self._get_dummy_headers(original_request)
request = WSGIRequest(dummy_meta)
# Add a flag to let middleware know that this is a dummy request.
request.is_dummy = True
if extra_request_attrs:
for k, v in extra_request_attrs.items():
setattr(request, k, v)
obj = self
# Build a custom django.core.handlers.BaseHandler subclass that invokes serve_preview as
# the eventual view function called at the end of the middleware chain, rather than going
# through the URL resolver
class Handler(BaseHandler):
def _get_response(self, request):
request.is_preview = True
request.preview_mode = preview_mode
response = obj.serve_preview(request, preview_mode)
if hasattr(response, "render") and callable(response.render):
response = response.render()
patch_cache_control(response, private=True)
return response
# Invoke this custom handler.
handler = Handler()
handler.load_middleware()
return handler.get_response(request)
def _get_fallback_hostname(self):
"""
Return a hostname that can be used on preview requests when the object has no
routable URL, or the real hostname is not valid according to ALLOWED_HOSTS.
"""
try:
hostname = settings.ALLOWED_HOSTS[0]
except IndexError:
# Django disallows empty ALLOWED_HOSTS outright when DEBUG=False, so we must
# have DEBUG=True. In this mode Django allows localhost amongst others.
return "localhost"
if hostname == "*":
# Any hostname is allowed
return "localhost"
# Hostnames beginning with a dot are domain wildcards such as ".example.com" -
# these allow example.com itself, so just strip the dot
return hostname.lstrip(".")
def _get_dummy_headers(self, original_request=None):
"""
Return a dict of META information to be included in a faked HttpRequest object to pass to
serve_preview.
"""
url = self._get_dummy_header_url(original_request)
if url:
url_info = urlsplit(url)
hostname = url_info.hostname
if not validate_host(
hostname,
settings.ALLOWED_HOSTS or [".localhost", "127.0.0.1", "[::1]"],
):
# The hostname is not valid according to ALLOWED_HOSTS - use a fallback
hostname = self._get_fallback_hostname()
path = url_info.path
port = url_info.port or (443 if url_info.scheme == "https" else 80)
scheme = url_info.scheme
else:
# Cannot determine a URL to this object - cobble together an arbitrary valid one
hostname = self._get_fallback_hostname()
path = "/"
port = 80
scheme = "http"
http_host = hostname
if port != (443 if scheme == "https" else 80):
http_host = f"{http_host}:{port}"
dummy_values = {
"REQUEST_METHOD": "GET",
"PATH_INFO": path,
"SERVER_NAME": hostname,
"SERVER_PORT": port,
"SERVER_PROTOCOL": "HTTP/1.1",
"HTTP_HOST": http_host,
"wsgi.version": (1, 0),
"wsgi.input": StringIO(),
"wsgi.errors": StringIO(),
"wsgi.url_scheme": scheme,
"wsgi.multithread": True,
"wsgi.multiprocess": True,
"wsgi.run_once": False,
}
# Add important values from the original request object, if it was provided.
HEADERS_FROM_ORIGINAL_REQUEST = [
"REMOTE_ADDR",
"HTTP_X_FORWARDED_FOR",
"HTTP_COOKIE",
"HTTP_USER_AGENT",
"HTTP_AUTHORIZATION",
"wsgi.version",
"wsgi.multithread",
"wsgi.multiprocess",
"wsgi.run_once",
]
if settings.SECURE_PROXY_SSL_HEADER:
HEADERS_FROM_ORIGINAL_REQUEST.append(settings.SECURE_PROXY_SSL_HEADER[0])
if original_request:
for header in HEADERS_FROM_ORIGINAL_REQUEST:
if header in original_request.META:
dummy_values[header] = original_request.META[header]
return dummy_values
def _get_dummy_header_url(self, original_request=None):
"""
Return the URL that _get_dummy_headers() should use to set META headers
for the faked HttpRequest.
"""
return self.full_url
def get_full_url(self):
return None
full_url = property(get_full_url)
DEFAULT_PREVIEW_MODES = [("", _("Default"))]
DEFAULT_PREVIEW_SIZES = [
{
"name": "mobile",
"icon": "mobile-alt",
"device_width": 375,
"label": _("Preview in mobile size"),
},
{
"name": "tablet",
"icon": "tablet-alt",
"device_width": 768,
"label": _("Preview in tablet size"),
},
{
"name": "desktop",
"icon": "desktop",
"device_width": 1280,
"label": _("Preview in desktop size"),
},
]
@property
def preview_modes(self):
"""
A list of ``(internal_name, display_name)`` tuples for the modes in which
this object can be displayed for preview/moderation purposes. Ordinarily an object
will only have one display mode, but subclasses can override this -
for example, a page containing a form might have a default view of the form,
and a post-submission 'thank you' page.
Set to ``[]`` to completely disable previewing for this model.
"""
return PreviewableMixin.DEFAULT_PREVIEW_MODES
@property
def default_preview_mode(self):
"""
The default preview mode to use in live preview.
This default is also used in areas that do not give the user the option of selecting a
mode explicitly, e.g. in the moderator approval workflow.
If ``preview_modes`` is empty, an ``IndexError`` will be raised.
"""
return self.preview_modes[0][0]
@property
def preview_sizes(self):
"""
A list of dictionaries, each representing a preview size option for this object.
Override this property to customize the preview sizes.
Each dictionary in the list should include the following keys:
- ``name``: A string representing the internal name of the preview size.
- ``icon``: A string specifying the icon's name for the preview size button.
- ``device_width``: An integer indicating the device's width in pixels.
- ``label``: A string for the aria label on the preview size button.
.. code-block:: python
@property
def preview_sizes(self):
return [
{
"name": "mobile",
"icon": "mobile-icon",
"device_width": 320,
"label": "Preview in mobile size"
},
# Add more preview size dictionaries as needed.
]
"""
return PreviewableMixin.DEFAULT_PREVIEW_SIZES
@property
def default_preview_size(self):
"""
The default preview size name to use in live preview.
Defaults to ``"mobile"``, which is the first one defined in ``preview_sizes``.
If ``preview_sizes`` is empty, an ``IndexError`` will be raised.
"""
return self.preview_sizes[0]["name"]
def is_previewable(self):
"""Returns ``True`` if at least one preview mode is specified in ``preview_modes``."""
return bool(self.preview_modes)
def serve_preview(self, request, mode_name):
"""
Returns an HTTP response for use in object previews.
This method can be overridden to implement custom rendering and/or
routing logic.
Any templates rendered during this process should use the ``request``
object passed here - this ensures that ``request.user`` and other
properties are set appropriately for the wagtail user bar to be
displayed/hidden. This request will always be a GET.
"""
return TemplateResponse(
request,
self.get_preview_template(request, mode_name),
self.get_preview_context(request, mode_name),
)
def get_preview_context(self, request, mode_name):
"""
Returns a context dictionary for use in templates for previewing this object.
"""
return {"object": self, "request": request}
def get_preview_template(self, request, mode_name):
"""
Returns a template to be used when previewing this object.
Subclasses of ``PreviewableMixin`` must override this method to return the
template name to be used in the preview. Alternatively, subclasses can also
override the ``serve_preview`` method to completely customise the preview
rendering logic.
"""
raise ImproperlyConfigured(
"%s (subclass of PreviewableMixin) must override get_preview_template or serve_preview"
% type(self).__name__
)