Plain is headed towards 1.0! Subscribe for development updates →

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