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