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