Plain is headed towards 1.0! Subscribe for development updates →

 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    Http404,
 9    Response,
10    ResponseRedirect,
11)
12from plain.runtime import settings
13from plain.views import TemplateView, View
14
15from .exceptions import PageNotFoundError, RedirectPageError
16from .pages import Page
17from .registry import pages_registry
18
19
20class PageViewMixin:
21    @cached_property
22    def page(self) -> Page:
23        url_name = self.request.resolver_match.url_name
24
25        try:
26            return pages_registry.get_page_from_name(url_name)
27        except PageNotFoundError:
28            raise Http404()
29
30
31class PageView(PageViewMixin, TemplateView):
32    template_name = "page.html"
33
34    def get(self) -> Response:
35        """Check Accept header and serve markdown if requested."""
36        if self.page.is_markdown() and settings.PAGES_SERVE_MARKDOWN:
37            preferred = self.request.get_preferred_type(
38                "text/markdown", "text/plain", "text/html"
39            )
40            if preferred in ("text/markdown", "text/plain"):
41                markdown_content = self.page._frontmatter.content
42                response = Response(
43                    markdown_content, content_type="text/plain; charset=utf-8"
44                )
45                response.headers["Vary"] = "Accept"
46                return response
47
48        return super().get()
49
50    def get_template_names(self) -> list[str]:
51        """
52        Allow for more specific user templates like
53        markdown.html or html.html
54        """
55        if template_name := self.page.get_template_name():
56            return [template_name]
57
58        return super().get_template_names()
59
60    def get_template_context(self) -> dict[str, Any]:
61        context = super().get_template_context()
62        context["page"] = self.page
63        self.page.set_template_context(context)  # Pass the standard context through
64        return context
65
66
67class PageRedirectView(PageViewMixin, View):
68    def get(self) -> ResponseRedirect:
69        url = self.page.vars.get("url")
70
71        if not url:
72            raise RedirectPageError("Redirect page is missing a url")
73
74        status_code = self.page.vars.get("status_code", 302)
75        return ResponseRedirect(url, status_code=status_code)
76
77
78class PageAssetView(PageViewMixin, AssetView):
79    def get_url_path(self) -> str | None:
80        return self.page.get_url_path()
81
82    def get_asset_path(self, path: str) -> str:
83        return self.page.absolute_path
84
85    def get_debug_asset_path(self, path: str) -> str:
86        return self.page.absolute_path
87
88
89class PageMarkdownView(PageViewMixin, View):
90    def get(self) -> Response:
91        """Serve the markdown content without frontmatter."""
92        markdown_content = self.page._frontmatter.content
93        response = Response(markdown_content, content_type="text/plain; charset=utf-8")
94        response.headers["Vary"] = (
95            "Accept-Encoding"  # Set Vary header for proper caching
96        )
97        return response