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