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