1from __future__ import annotations
2
3from typing import TYPE_CHECKING, Any
4
5from plain.auth.views import AuthView
6from plain.preflight import get_check_counts
7from plain.runtime import settings
8from plain.urls import reverse
9from plain.utils import timezone
10from plain.views import (
11 TemplateView,
12)
13
14from ..models import PinnedNavItem
15from ..utils import get_gravatar_url
16from .registry import registry, track_recent_nav
17from .types import Img
18
19if TYPE_CHECKING:
20 from plain.http import ResponseBase
21
22 from ..cards import Card
23 from .viewsets import AdminViewset
24
25
26_URL_NAMESPACE = "admin"
27
28
29class AdminView(AuthView, TemplateView):
30 admin_required = True
31
32 title: str = ""
33 description: str = "" # Optional description shown below the title
34 path: str = ""
35 image: Img | None = None
36
37 # Leave empty to hide from nav
38 #
39 # An explicit disabling of showing this url/page in the nav
40 # which importantly effects the (future) recent pages list
41 # so you can also use this for pages that can never be bookmarked
42 nav_title = ""
43 nav_section = ""
44 nav_icon = "" # Bootstrap Icons name (e.g., "cart", "person", "flag")
45
46 links: dict[str, str] = {}
47
48 parent_view_class: AdminView | None = None
49
50 # Set dynamically by AdminViewset.get_views()
51 viewset: type[AdminViewset] | None = None
52
53 template_name = "admin/page.html"
54 cards: list[Card] = []
55
56 def get_response(self) -> ResponseBase:
57 # Track this page visit for recent nav tabs
58 if self.nav_section is not None:
59 track_recent_nav(self.request, self.get_slug())
60
61 response = super().get_response()
62 response.headers["Cache-Control"] = (
63 "no-cache, no-store, must-revalidate, max-age=0"
64 )
65
66 return response
67
68 def get_template_context(self) -> dict[str, Any]:
69 context = super().get_template_context()
70 context["title"] = self.get_title()
71 context["description"] = self.get_description()
72 context["image"] = self.get_image()
73 context["slug"] = self.get_slug()
74 context["links"] = self.get_links()
75 context["parent_view_classes"] = self.get_parent_view_classes()
76 context["admin_registry"] = registry
77 context["cards"] = self.get_cards()
78 context["render_card"] = lambda card: card().render(self, self.request)
79 context["time_zone"] = timezone.get_current_timezone_name()
80 context["view_class"] = self.__class__
81 context["app_name"] = settings.NAME
82 context["get_gravatar_url"] = get_gravatar_url
83 context["nav_tabs"] = registry.get_nav_tabs(self.request)
84 context["pinned_slugs"] = set(
85 PinnedNavItem.query.filter(user=self.user).values_list(
86 "view_slug", flat=True
87 )
88 )
89 context["preflight_counts"] = get_check_counts()
90
91 return context
92
93 @classmethod
94 def view_name(cls) -> str:
95 return f"view_{cls.get_slug()}"
96
97 @classmethod
98 def get_slug(cls) -> str:
99 return f"{cls.__module__}.{cls.__qualname__}".lower().replace(".", "_")
100
101 # Can actually use @classmethod, @staticmethod or regular method for these?
102 def get_title(self) -> str:
103 return self.title
104
105 def get_description(self) -> str:
106 return self.description
107
108 def get_image(self) -> Img | None:
109 return self.image
110
111 @classmethod
112 def get_path(cls) -> str:
113 return cls.path
114
115 @classmethod
116 def get_parent_view_classes(cls) -> list[AdminView]:
117 parents = []
118 parent = cls.parent_view_class
119 while parent:
120 parents.append(parent)
121 parent = parent.parent_view_class
122 return parents
123
124 @classmethod
125 def get_nav_title(cls) -> str:
126 if cls.nav_title:
127 return cls.nav_title
128
129 if cls.title:
130 return cls.title
131
132 raise NotImplementedError(
133 f"Please set a title or nav_title on the {cls} class or implement get_nav_title()."
134 )
135
136 @classmethod
137 def get_view_url(cls, obj: Any = None) -> str:
138 # Check if this view's path expects an id parameter
139 if obj and "<int:id>" in cls.get_path():
140 return reverse(f"{_URL_NAMESPACE}:" + cls.view_name(), id=obj.id)
141 else:
142 return reverse(f"{_URL_NAMESPACE}:" + cls.view_name())
143
144 def get_links(self) -> dict[str, str]:
145 return self.links.copy()
146
147 def get_cards(self) -> list[Card]:
148 return self.cards.copy()