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")