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