from django.urls import reverse from django.utils.text import capfirst from django.utils.translation import gettext_lazy, ngettext from wagtail import hooks from wagtail.admin.ui.components import Component from wagtail.admin.userbar import AccessibilityItem from wagtail.models import DraftStateMixin, LockableMixin, Page, ReferenceIndex class BaseSidePanel(Component): class SidePanelToggle(Component): template_name = "wagtailadmin/shared/side_panel_toggle.html" aria_label = "" icon_name = "" has_counter = True counter_classname = "" keyboard_shortcut = None def __init__(self, panel): self.panel = panel def get_context_data(self, parent_context): # Inherit classes from fragments defined in slim_header.html inherit = { "nav_icon_button_classes", "nav_icon_classes", "nav_icon_counter_classes", } context = {key: parent_context.get(key) for key in inherit} context["toggle"] = self context["panel"] = self.panel context["count"] = 0 return context def __init__(self, object, request): self.object = object self.request = request self.model = type(self.object) self.toggle = self.SidePanelToggle(panel=self) def get_context_data(self, parent_context): context = {"panel": self, "object": self.object, "request": self.request} if issubclass(self.model, Page): context["page"] = self.object return context class StatusSidePanel(BaseSidePanel): class SidePanelToggle(BaseSidePanel.SidePanelToggle): aria_label = gettext_lazy("Toggle status") icon_name = "info-circle" counter_classname = "w-bg-critical-200" def get_context_data(self, parent_context): context = super().get_context_data(parent_context) form = parent_context.get("form") context["count"] = form and len( form.errors.keys() & {"go_live_at", "expire_at"} ) return context name = "status" title = gettext_lazy("Status") template_name = "wagtailadmin/shared/side_panels/status.html" order = 100 def __init__( self, *args, show_schedule_publishing_toggle=None, live_object=None, scheduled_object=None, locale=None, translations=None, usage_url=None, history_url=None, last_updated_info=None, **kwargs, ): super().__init__(*args, **kwargs) self.show_schedule_publishing_toggle = show_schedule_publishing_toggle self.live_object = live_object self.scheduled_object = scheduled_object self.locale = locale self.translations = translations self.usage_url = usage_url self.history_url = history_url self.last_updated_info = last_updated_info self.locking_enabled = isinstance(self.object, LockableMixin) def get_status_templates(self, context): templates = ["wagtailadmin/shared/side_panels/includes/status/workflow.html"] if self.locale: templates.append( "wagtailadmin/shared/side_panels/includes/status/locale.html" ) if self.object.pk: if self.locking_enabled: templates.append( "wagtailadmin/shared/side_panels/includes/status/locked.html" ) if self.usage_url: templates.append( "wagtailadmin/shared/side_panels/includes/status/usage.html" ) return templates def get_scheduled_publishing_context(self, parent_context): if not isinstance(self.object, DraftStateMixin): return {"draftstate_enabled": False} context = { # Used for hiding the info completely if the model doesn't extend DraftStateMixin "draftstate_enabled": True, # Show error message if any of the scheduled publishing fields has errors "schedule_has_errors": False, # The dialog toggle can be hidden (e.g. if PublishingPanel is not present) # but the scheduled publishing info should still be shown "show_schedule_publishing_toggle": self.show_schedule_publishing_toggle, # These are the dates that show up with the unticked calendar icon, # aka "draft schedule" "draft_go_live_at": None, "draft_expire_at": None, # These are the dates that show up with the ticked calendar icon, # aka "active schedule" "scheduled_go_live_at": None, "scheduled_expire_at": None, # This is for an edge case where the live object already has an # expire_at, which can still take effect if the active schedule's # go_live_at is later than that "live_expire_at": None, } # Reuse logic from the toggle to get the count of errors if self.toggle.get_context_data(parent_context)["count"]: context["schedule_has_errors"] = True # Only consider draft schedule if the object hasn't been created # or if there are unpublished changes if not self.object.pk or self.object.has_unpublished_changes: context["draft_go_live_at"] = self.object.go_live_at context["draft_expire_at"] = self.object.expire_at # Get active schedule from the scheduled revision's object (if any) if self.scheduled_object: context["scheduled_go_live_at"] = self.scheduled_object.go_live_at context["scheduled_expire_at"] = self.scheduled_object.expire_at # Ignore draft schedule if it's the same as the active schedule if context["draft_go_live_at"] == context["scheduled_go_live_at"]: context["draft_go_live_at"] = None if context["draft_expire_at"] == context["scheduled_expire_at"]: context["draft_expire_at"] = None # The live object can still have its own active expiry date # that's separate from the active schedule if ( self.live_object and self.live_object.expire_at and not self.live_object.expired ): context["live_expire_at"] = self.live_object.expire_at # Ignore the live object's expiry date if the active schedule has # an earlier go_live_at, as the active schedule's expiry date will # override the live object's expiry date when the draft is published if ( context["scheduled_go_live_at"] and context["scheduled_go_live_at"] < context["live_expire_at"] ): context["live_expire_at"] = None # Only show the box for the live object expire_at edge case # if it passes the checks above context["has_live_publishing_schedule"] = bool(context["live_expire_at"]) # Only show the main scheduled publishing box if it has at least one of # the draft/active schedule dates after passing the checks above context["has_draft_publishing_schedule"] = any( ( context["scheduled_go_live_at"], context["scheduled_expire_at"], context["draft_go_live_at"], context["draft_expire_at"], ) ) return context def get_lock_context(self, parent_context): self.lock = None lock_context = {} if self.locking_enabled: self.lock = self.object.get_lock() if self.lock: lock_context = self.lock.get_context_for_user( self.request.user, parent_context ) return { "lock": self.lock, "user_can_lock": parent_context.get("user_can_lock"), "user_can_unlock": parent_context.get("user_can_unlock"), "lock_context": lock_context, } def get_usage_context(self): return { "usage_count": ReferenceIndex.get_grouped_references_to( self.object ).count(), "usage_url": self.usage_url, } def get_context_data(self, parent_context): context = super().get_context_data(parent_context) context["locale"] = self.locale context["translations"] = self.translations if self.translations: context["translations_total"] = len(self.translations) + 1 context["model_name"] = capfirst(self.model._meta.verbose_name) context["base_model_name"] = context["model_name"] context["history_url"] = self.history_url context["status_templates"] = self.get_status_templates(context) context["last_updated_info"] = self.last_updated_info context.update(self.get_scheduled_publishing_context(parent_context)) context.update(self.get_lock_context(parent_context)) if self.object.pk and self.usage_url: context.update(self.get_usage_context()) return context class PageStatusSidePanel(StatusSidePanel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.object.pk: self.usage_url = reverse("wagtailadmin_pages:usage", args=(self.object.pk,)) permissions = self.object.permissions_for_user(self.request.user) if permissions.can_view_revisions(): self.history_url = reverse( "wagtailadmin_pages:history", args=(self.object.pk,), ) def get_status_templates(self, context): templates = super().get_status_templates(context) templates.insert( -1, "wagtailadmin/shared/side_panels/includes/status/privacy.html" ) return templates def get_usage_context(self): context = super().get_usage_context() context["usage_url_text"] = ngettext( "Referenced %(count)s time", "Referenced %(count)s times", context["usage_count"], ) % {"count": context["usage_count"]} return context def get_context_data(self, parent_context): context = super().get_context_data(parent_context) page = self.object if page.id: context.update( { "workflow_history_url": reverse( "wagtailadmin_pages:workflow_history", args=(page.id,) ), "revisions_compare_url_name": "wagtailadmin_pages:revisions_compare", "lock_url": reverse("wagtailadmin_pages:lock", args=(page.id,)), "unlock_url": reverse("wagtailadmin_pages:unlock", args=(page.id,)), } ) context.update( { "model_name": self.model.get_verbose_name(), "base_model_name": Page._meta.verbose_name, "model_description": self.model.get_page_description(), "status_templates": self.get_status_templates(context), } ) return context class CommentsSidePanel(BaseSidePanel): class SidePanelToggle(BaseSidePanel.SidePanelToggle): aria_label = gettext_lazy("Toggle comments") icon_name = "comment" name = "comments" title = gettext_lazy("Comments") template_name = "wagtailadmin/shared/side_panels/comments.html" order = 300 def get_context_data(self, parent_context): context = super().get_context_data(parent_context) context["form"] = parent_context.get("form") return context class ChecksSidePanel(BaseSidePanel): class SidePanelToggle(BaseSidePanel.SidePanelToggle): aria_label = gettext_lazy("Toggle checks") icon_name = "glasses" name = "checks" title = gettext_lazy("Checks") template_name = "wagtailadmin/shared/side_panels/checks.html" order = 350 def get_axe_configuration(self): # Retrieve the Axe configuration from the userbar. userbar_items = [AccessibilityItem()] for fn in hooks.get_hooks("construct_wagtail_userbar"): fn(self.request, userbar_items) for item in userbar_items: if isinstance(item, AccessibilityItem): return item.get_axe_configuration(self.request) def get_context_data(self, parent_context): context = super().get_context_data(parent_context) context["axe_configuration"] = self.get_axe_configuration() return context class PreviewSidePanel(BaseSidePanel): class SidePanelToggle(BaseSidePanel.SidePanelToggle): aria_label = gettext_lazy("Toggle preview") icon_name = "mobile-alt" has_counter = False keyboard_shortcut = "mod+p" name = "preview" title = gettext_lazy("Preview") template_name = "wagtailadmin/shared/side_panels/preview.html" order = 400 def __init__(self, object, request, *, preview_url): super().__init__(object, request) self.preview_url = preview_url def get_context_data(self, parent_context): context = super().get_context_data(parent_context) context["preview_url"] = self.preview_url context["has_multiple_modes"] = len(self.object.preview_modes) > 1 return context