v0.150.0
  1from __future__ import annotations
  2
  3from functools import cached_property
  4from typing import Any
  5
  6from plain.assets.views import AssetView
  7from plain.http import (
  8    NotFoundError404,
  9    RedirectResponse,
 10    Response,
 11)
 12from plain.runtime import settings
 13from plain.templates.views import TemplateView
 14from plain.utils.cache import patch_vary_headers
 15from plain.views import View
 16
 17from .exceptions import PageNotFoundError, RedirectPageError
 18from .pages import Page
 19from .registry import pages_registry
 20
 21__all__ = ["PageView"]
 22
 23
 24class PageViewMixin:
 25    @cached_property
 26    def page(self) -> Page:
 27        url_name = self.request.resolver_match.url_name  # ty: ignore[unresolved-attribute]
 28
 29        try:
 30            return pages_registry.get_page_from_name(url_name)
 31        except PageNotFoundError:
 32            raise NotFoundError404()
 33
 34    def get_template_context(self) -> dict[str, Any]:
 35        context = super().get_template_context()  # ty: ignore[unresolved-attribute]
 36        context["page"] = self.page
 37        self.page.set_template_context(context)  # Pass the standard context through
 38        return context
 39
 40
 41class PageView(PageViewMixin, TemplateView):
 42    template_name = "page.html"
 43
 44    def _markdown_response(self, page: Page) -> Response:
 45        """Build a plain-text markdown response with Vary header."""
 46        context = {**self.get_template_context(), "page": page}
 47        page.set_template_context(context)
 48        markdown_content = page.rendered_source(context)
 49        response = Response(markdown_content, content_type="text/plain; charset=utf-8")
 50        patch_vary_headers(response, ["Accept"])
 51        return response
 52
 53    def _prefers_markdown(self, *types: str) -> bool:
 54        """Check if the request prefers markdown over the given types."""
 55        preferred = self.request.get_preferred_type(*types)
 56        return preferred in ("text/markdown", "text/plain")
 57
 58    def get(self) -> Response:
 59        """Check Accept header and serve markdown if requested."""
 60        if not settings.PAGES_SERVE_MARKDOWN:
 61            return super().get()
 62
 63        # Standalone markdown page -- markdown is preferred by default.
 64        # Type order matters: first type listed wins ties, so markdown
 65        # types come first here to default to raw markdown.
 66        if self.page.is_markdown():
 67            if self._prefers_markdown("text/markdown", "text/plain", "text/html"):
 68                return self._markdown_response(self.page)
 69            response = super().get()
 70            patch_vary_headers(response, ["Accept"])
 71            return response
 72
 73        # HTML page -- only serve markdown if a companion exists and is explicitly preferred.
 74        # Type order matters: text/html first so HTML wins ties.
 75        url_name = self.page.get_url_name()
 76        if not url_name:
 77            return super().get()
 78
 79        companion = pages_registry.get_markdown_companion(url_name)
 80        if not companion:
 81            return super().get()
 82
 83        if self._prefers_markdown("text/html", "text/markdown", "text/plain"):
 84            return self._markdown_response(companion)
 85
 86        # HTML response varies by Accept when a companion exists
 87        response = super().get()
 88        patch_vary_headers(response, ["Accept"])
 89        return response
 90
 91    def get_template_names(self) -> list[str]:
 92        """
 93        Allow for more specific user templates like
 94        markdown.html or html.html
 95        """
 96        if template_name := self.page.get_template_name():
 97            return [template_name]
 98
 99        return super().get_template_names()
100
101
102class PageRedirectView(PageViewMixin, View):
103    def get(self) -> RedirectResponse:
104        url = self.page.vars.get("url")
105
106        if not url:
107            raise RedirectPageError("Redirect page is missing a url")
108
109        status_code = self.page.vars.get("status_code", 302)
110        return RedirectResponse(url, status_code=status_code, allow_external=True)
111
112
113class PageAssetView(PageViewMixin, AssetView):
114    def get_url_path(self) -> str | None:
115        return self.page.get_url_path()
116
117    def get_asset_path(self, path: str) -> str:
118        return self.page.absolute_path
119
120    def get_debug_asset_path(self, path: str) -> str:
121        return self.page.absolute_path
122
123
124class PageMarkdownView(PageViewMixin, TemplateView):
125    def get(self) -> Response:
126        """Serve the markdown content without frontmatter."""
127        context = self.get_template_context()
128        markdown_content = self.page.rendered_source(context)
129        return Response(markdown_content, content_type="text/plain; charset=utf-8")