Plain is headed towards 1.0! Subscribe for development updates →

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