kopia lustrzana https://github.com/wagtail/wagtail
1253 wiersze
45 KiB
Python
1253 wiersze
45 KiB
Python
from warnings import warn
|
|
|
|
from django.apps import apps
|
|
from django.contrib.admin.utils import quote
|
|
from django.core import checks
|
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
|
from django.http import Http404
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.urls import path, re_path, reverse, reverse_lazy
|
|
from django.utils.functional import cached_property
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import gettext as _
|
|
from django.utils.translation import gettext_lazy
|
|
|
|
from wagtail import hooks
|
|
from wagtail.admin.checks import check_panels_in_model
|
|
from wagtail.admin.panels import ObjectList, extract_panel_definitions_from_model_class
|
|
from wagtail.admin.ui.components import MediaContainer
|
|
from wagtail.admin.ui.side_panels import ChecksSidePanel, PreviewSidePanel
|
|
from wagtail.admin.ui.tables import (
|
|
BulkActionsCheckboxColumn,
|
|
Column,
|
|
LiveStatusTagColumn,
|
|
TitleColumn,
|
|
)
|
|
from wagtail.admin.views import generic
|
|
from wagtail.admin.views.generic import history, lock, workflow
|
|
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
|
|
from wagtail.admin.views.generic.preview import (
|
|
PreviewOnCreate,
|
|
PreviewOnEdit,
|
|
PreviewRevision,
|
|
)
|
|
from wagtail.admin.viewsets import viewsets
|
|
from wagtail.admin.viewsets.model import ModelViewSet, ModelViewSetGroup
|
|
from wagtail.admin.widgets.button import (
|
|
BaseDropdownMenuButton,
|
|
ButtonWithDropdown,
|
|
)
|
|
from wagtail.models import (
|
|
DraftStateMixin,
|
|
LockableMixin,
|
|
PreviewableMixin,
|
|
RevisionMixin,
|
|
WorkflowMixin,
|
|
)
|
|
from wagtail.permissions import ModelPermissionPolicy
|
|
from wagtail.snippets.action_menu import SnippetActionMenu
|
|
from wagtail.snippets.models import SnippetAdminURLFinder, get_snippet_models
|
|
from wagtail.snippets.permissions import user_can_edit_snippet_type
|
|
from wagtail.snippets.side_panels import SnippetStatusSidePanel
|
|
from wagtail.snippets.views.chooser import SnippetChooserViewSet
|
|
from wagtail.utils.deprecation import RemovedInWagtail70Warning
|
|
|
|
|
|
# == Helper functions ==
|
|
def get_snippet_model_from_url_params(app_name, model_name):
|
|
"""
|
|
Retrieve a model from an app_label / model_name combo.
|
|
Raise Http404 if the model is not a valid snippet type.
|
|
"""
|
|
try:
|
|
model = apps.get_model(app_name, model_name)
|
|
except LookupError:
|
|
raise Http404
|
|
if model not in get_snippet_models():
|
|
# don't allow people to hack the URL to edit content types that aren't registered as snippets
|
|
raise Http404
|
|
|
|
return model
|
|
|
|
|
|
# == Views ==
|
|
|
|
|
|
class ModelIndexView(generic.BaseListingView):
|
|
page_title = gettext_lazy("Snippets")
|
|
header_icon = "snippet"
|
|
index_url_name = "wagtailsnippets:index"
|
|
default_ordering = "name"
|
|
_show_breadcrumbs = True
|
|
|
|
def setup(self, request, *args, **kwargs):
|
|
super().setup(request, *args, **kwargs)
|
|
self.snippet_types = self._get_snippet_types()
|
|
|
|
def _get_snippet_types(self):
|
|
return [
|
|
{
|
|
"name": capfirst(model._meta.verbose_name_plural),
|
|
"count": model._default_manager.all().count(),
|
|
"model": model,
|
|
}
|
|
for model in get_snippet_models()
|
|
if user_can_edit_snippet_type(self.request.user, model)
|
|
]
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not self.snippet_types:
|
|
raise PermissionDenied
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_breadcrumbs_items(self):
|
|
return self.breadcrumbs_items + [{"url": "", "label": _("Snippets")}]
|
|
|
|
def get_list_url(self, type):
|
|
return reverse(type["model"].snippet_viewset.get_url_name("list"))
|
|
|
|
def get_queryset(self):
|
|
return None
|
|
|
|
@cached_property
|
|
def columns(self):
|
|
return [
|
|
TitleColumn(
|
|
"name",
|
|
label=_("Name"),
|
|
get_url=self.get_list_url,
|
|
sort_key="name",
|
|
),
|
|
Column(
|
|
"count",
|
|
label=_("Instances"),
|
|
sort_key="count",
|
|
),
|
|
]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
reverse = self.ordering[0] == "-"
|
|
|
|
if self.ordering in ["count", "-count"]:
|
|
snippet_types = sorted(
|
|
self.snippet_types,
|
|
key=lambda type: type["count"],
|
|
reverse=reverse,
|
|
)
|
|
else:
|
|
snippet_types = sorted(
|
|
self.snippet_types,
|
|
key=lambda type: type["name"].lower(),
|
|
reverse=reverse,
|
|
)
|
|
|
|
return super().get_context_data(object_list=snippet_types)
|
|
|
|
def get_template_names(self):
|
|
# We use the generic index template instead of model_index.html,
|
|
# but we look for it anyway so users can customise this view's template
|
|
# without having to override the entire view or the generic template.
|
|
return [
|
|
"wagtailsnippets/snippets/model_index.html",
|
|
self.template_name,
|
|
]
|
|
|
|
|
|
class IndexView(generic.IndexViewOptionalFeaturesMixin, generic.IndexView):
|
|
view_name = "list"
|
|
|
|
def get_base_queryset(self):
|
|
# Allow the queryset to be a callable that takes a request
|
|
# so that it can be evaluated in the context of the request
|
|
if callable(self.queryset):
|
|
self.queryset = self.queryset(self.request)
|
|
return super().get_base_queryset()
|
|
|
|
@cached_property
|
|
def columns(self):
|
|
return [
|
|
BulkActionsCheckboxColumn("bulk_actions", obj_type="snippet"),
|
|
*super().columns,
|
|
]
|
|
|
|
def _get_title_column(self, *args, **kwargs):
|
|
return super()._get_title_column(
|
|
*args,
|
|
**kwargs,
|
|
get_title_id=lambda instance: f"snippet_{quote(instance.pk)}_title",
|
|
)
|
|
|
|
def get_list_buttons(self, instance):
|
|
more_buttons = self.get_list_more_buttons(instance)
|
|
next_url = self.request.path
|
|
list_buttons = []
|
|
|
|
for hook in hooks.get_hooks("register_snippet_listing_buttons"):
|
|
hook_buttons = hook(instance, self.request.user, next_url)
|
|
for button in hook_buttons:
|
|
if isinstance(button, BaseDropdownMenuButton):
|
|
# If the button is a dropdown menu, add it to the top-level
|
|
# because we do not support nested dropdowns
|
|
list_buttons.append(button)
|
|
else:
|
|
# Otherwise, add it to the default "More" dropdown
|
|
more_buttons.append(button)
|
|
|
|
# Pass the more_buttons to the construct hooks, as that's what contains
|
|
# the default buttons and most buttons added via register_snippet_listing_buttons
|
|
for hook in hooks.get_hooks("construct_snippet_listing_buttons"):
|
|
try:
|
|
hook(more_buttons, instance, self.request.user)
|
|
except TypeError:
|
|
warn(
|
|
"construct_snippet_listing_buttons hook no longer accepts a context argument",
|
|
RemovedInWagtail70Warning,
|
|
stacklevel=2,
|
|
)
|
|
hook(more_buttons, instance, self.request.user, {})
|
|
|
|
list_buttons.append(
|
|
ButtonWithDropdown(
|
|
buttons=more_buttons,
|
|
icon_name="dots-horizontal",
|
|
attrs={
|
|
"aria-label": _("More options for '%(title)s'")
|
|
% {"title": str(instance)},
|
|
},
|
|
)
|
|
)
|
|
|
|
return list_buttons
|
|
|
|
|
|
class CreateView(generic.CreateEditViewOptionalFeaturesMixin, generic.CreateView):
|
|
view_name = "create"
|
|
template_name = "wagtailsnippets/snippets/create.html"
|
|
|
|
def run_before_hook(self):
|
|
return self.run_hook("before_create_snippet", self.request, self.model)
|
|
|
|
def run_after_hook(self):
|
|
return self.run_hook("after_create_snippet", self.request, self.object)
|
|
|
|
def _get_action_menu(self):
|
|
return SnippetActionMenu(self.request, view=self.view_name, model=self.model)
|
|
|
|
def get_side_panels(self):
|
|
side_panels = [
|
|
SnippetStatusSidePanel(
|
|
self.form.instance,
|
|
self.request,
|
|
show_schedule_publishing_toggle=getattr(
|
|
self.form, "show_schedule_publishing_toggle", False
|
|
),
|
|
locale=self.locale,
|
|
translations=self.translations,
|
|
)
|
|
]
|
|
if self.preview_enabled and self.form.instance.is_previewable():
|
|
side_panels.append(
|
|
PreviewSidePanel(
|
|
self.form.instance, self.request, preview_url=self.get_preview_url()
|
|
)
|
|
)
|
|
side_panels.append(ChecksSidePanel(self.form.instance, self.request))
|
|
return MediaContainer(side_panels)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
action_menu = self._get_action_menu()
|
|
context["media"] += action_menu.media
|
|
context["action_menu"] = action_menu
|
|
return context
|
|
|
|
|
|
class CopyView(CreateView):
|
|
def get_object(self):
|
|
return get_object_or_404(self.model, pk=self.kwargs["pk"])
|
|
|
|
def _get_initial_form_instance(self):
|
|
instance = self.get_object()
|
|
# Set locale of the new instance
|
|
if self.locale:
|
|
instance.locale = self.locale
|
|
return instance
|
|
|
|
|
|
class EditView(generic.CreateEditViewOptionalFeaturesMixin, generic.EditView):
|
|
view_name = "edit"
|
|
template_name = "wagtailsnippets/snippets/edit.html"
|
|
|
|
def run_before_hook(self):
|
|
return self.run_hook("before_edit_snippet", self.request, self.object)
|
|
|
|
def run_after_hook(self):
|
|
return self.run_hook("after_edit_snippet", self.request, self.object)
|
|
|
|
def _get_action_menu(self):
|
|
return SnippetActionMenu(
|
|
self.request,
|
|
view=self.view_name,
|
|
instance=self.object,
|
|
locked_for_user=self.locked_for_user,
|
|
)
|
|
|
|
def get_side_panels(self):
|
|
side_panels = [
|
|
SnippetStatusSidePanel(
|
|
self.object,
|
|
self.request,
|
|
show_schedule_publishing_toggle=getattr(
|
|
self.form, "show_schedule_publishing_toggle", False
|
|
),
|
|
live_object=self.live_object,
|
|
scheduled_object=self.live_object.get_scheduled_revision_as_object()
|
|
if self.draftstate_enabled
|
|
else None,
|
|
locale=self.locale,
|
|
translations=self.translations,
|
|
usage_url=self.get_usage_url(),
|
|
history_url=self.get_history_url(),
|
|
last_updated_info=self.get_last_updated_info(),
|
|
)
|
|
]
|
|
if self.preview_enabled and self.object.is_previewable():
|
|
side_panels.append(
|
|
PreviewSidePanel(
|
|
self.object, self.request, preview_url=self.get_preview_url()
|
|
)
|
|
)
|
|
side_panels.append(ChecksSidePanel(self.object, self.request))
|
|
return MediaContainer(side_panels)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
action_menu = self._get_action_menu()
|
|
context["media"] += action_menu.media
|
|
context["action_menu"] = action_menu
|
|
return context
|
|
|
|
|
|
class DeleteView(generic.DeleteView):
|
|
view_name = "delete"
|
|
|
|
def run_before_hook(self):
|
|
return self.run_hook("before_delete_snippet", self.request, [self.object])
|
|
|
|
def run_after_hook(self):
|
|
return self.run_hook("after_delete_snippet", self.request, [self.object])
|
|
|
|
|
|
class UsageView(generic.UsageView):
|
|
view_name = "usage"
|
|
|
|
|
|
class HistoryView(history.HistoryView):
|
|
view_name = "history"
|
|
|
|
|
|
class InspectView(generic.InspectView):
|
|
view_name = "inspect"
|
|
|
|
|
|
class PreviewOnCreateView(PreviewOnCreate):
|
|
pass
|
|
|
|
|
|
class PreviewOnEditView(PreviewOnEdit):
|
|
pass
|
|
|
|
|
|
class PreviewRevisionView(PermissionCheckedMixin, PreviewRevision):
|
|
permission_required = "change"
|
|
|
|
|
|
class RevisionsCompareView(PermissionCheckedMixin, generic.RevisionsCompareView):
|
|
permission_required = "change"
|
|
|
|
@property
|
|
def edit_label(self):
|
|
return _("Edit this %(model_name)s") % {
|
|
"model_name": self.model._meta.verbose_name
|
|
}
|
|
|
|
@property
|
|
def history_label(self):
|
|
return _("%(model_name)s history") % {
|
|
"model_name": self.model._meta.verbose_name
|
|
}
|
|
|
|
|
|
class UnpublishView(PermissionCheckedMixin, generic.UnpublishView):
|
|
permission_required = "publish"
|
|
|
|
|
|
class RevisionsUnscheduleView(PermissionCheckedMixin, generic.RevisionsUnscheduleView):
|
|
permission_required = "publish"
|
|
|
|
|
|
class LockView(PermissionCheckedMixin, lock.LockView):
|
|
permission_required = "lock"
|
|
|
|
def user_has_permission(self, permission):
|
|
if self.request.user.is_superuser:
|
|
return True
|
|
|
|
if permission == self.permission_required and isinstance(
|
|
self.object, WorkflowMixin
|
|
):
|
|
current_workflow_task = self.object.current_workflow_task
|
|
if current_workflow_task:
|
|
return current_workflow_task.user_can_lock(
|
|
self.object, self.request.user
|
|
)
|
|
|
|
return super().user_has_permission(permission)
|
|
|
|
|
|
class UnlockView(PermissionCheckedMixin, lock.UnlockView):
|
|
permission_required = "unlock"
|
|
|
|
def user_has_permission(self, permission):
|
|
if self.request.user.is_superuser:
|
|
return True
|
|
|
|
if permission == self.permission_required:
|
|
# Allow unlocking even if the user does not have the 'unlock' permission
|
|
# if they are the user who locked the object
|
|
if self.object.locked_by_id == self.request.user.pk:
|
|
return True
|
|
|
|
if isinstance(self.object, WorkflowMixin):
|
|
current_workflow_task = self.object.current_workflow_task
|
|
if current_workflow_task:
|
|
return current_workflow_task.user_can_unlock(
|
|
self.object, self.request.user
|
|
)
|
|
|
|
return super().user_has_permission(permission)
|
|
|
|
|
|
class WorkflowActionView(workflow.WorkflowAction):
|
|
pass
|
|
|
|
|
|
class CollectWorkflowActionDataView(workflow.CollectWorkflowActionData):
|
|
pass
|
|
|
|
|
|
class ConfirmWorkflowCancellationView(workflow.ConfirmWorkflowCancellation):
|
|
pass
|
|
|
|
|
|
class WorkflowPreviewView(workflow.PreviewRevisionForTask):
|
|
pass
|
|
|
|
|
|
class WorkflowHistoryView(PermissionCheckedMixin, history.WorkflowHistoryView):
|
|
permission_required = "change"
|
|
|
|
|
|
class WorkflowHistoryDetailView(
|
|
PermissionCheckedMixin, history.WorkflowHistoryDetailView
|
|
):
|
|
permission_required = "change"
|
|
|
|
|
|
class SnippetViewSet(ModelViewSet):
|
|
"""
|
|
A viewset that instantiates the admin views for snippets.
|
|
|
|
All attributes and methods from
|
|
:class:`~wagtail.admin.viewsets.model.ModelViewSet` are available.
|
|
|
|
For more information on how to use this class,
|
|
see :ref:`wagtailsnippets_custom_admin_views`.
|
|
"""
|
|
|
|
#: The model class to be registered as a snippet with this viewset.
|
|
model = None
|
|
|
|
#: The number of items to display in the chooser view. Defaults to 10.
|
|
chooser_per_page = 10
|
|
|
|
#: The URL namespace to use for the admin views.
|
|
#: If left unset, ``wagtailsnippets_{app_label}_{model_name}`` is used instead.
|
|
#:
|
|
#: **Deprecated** - the preferred attribute to customise is :attr:`~.ViewSet.url_namespace`.
|
|
admin_url_namespace = None
|
|
|
|
#: The base URL path to use for the admin views.
|
|
#: If left unset, ``snippets/{app_label}/{model_name}`` is used instead.
|
|
#:
|
|
#: **Deprecated** - the preferred attribute to customise is :attr:`~.ViewSet.url_prefix`.
|
|
base_url_path = None
|
|
|
|
#: The URL namespace to use for the chooser admin views.
|
|
#: If left unset, ``wagtailsnippetchoosers_{app_label}_{model_name}`` is used instead.
|
|
chooser_admin_url_namespace = None
|
|
|
|
#: The base URL path to use for the chooser admin views.
|
|
#: If left unset, ``snippets/choose/{app_label}/{model_name}`` is used instead.
|
|
chooser_base_url_path = None
|
|
|
|
#: The view class to use for the index view; must be a subclass of ``wagtail.snippets.views.snippets.IndexView``.
|
|
index_view_class = IndexView
|
|
|
|
#: The view class to use for the create view; must be a subclass of ``wagtail.snippets.views.snippets.CreateView``.
|
|
add_view_class = CreateView
|
|
|
|
#: The view class to use for the copy view; must be a subclass of ``wagtail.snippet.views.snippets.CopyView``.
|
|
copy_view_class = CopyView
|
|
|
|
#: The view class to use for the edit view; must be a subclass of ``wagtail.snippets.views.snippets.EditView``.
|
|
edit_view_class = EditView
|
|
|
|
#: The view class to use for the delete view; must be a subclass of ``wagtail.snippets.views.snippets.DeleteView``.
|
|
delete_view_class = DeleteView
|
|
|
|
#: The view class to use for the usage view; must be a subclass of ``wagtail.snippets.views.snippets.UsageView``.
|
|
usage_view_class = UsageView
|
|
|
|
#: The view class to use for the history view; must be a subclass of ``wagtail.snippets.views.snippets.HistoryView``.
|
|
history_view_class = HistoryView
|
|
|
|
#: The view class to use for the inspect view; must be a subclass of ``wagtail.snippets.views.snippets.InspectView``.
|
|
inspect_view_class = InspectView
|
|
|
|
#: The view class to use for previewing revisions; must be a subclass of ``wagtail.snippets.views.snippets.PreviewRevisionView``.
|
|
revisions_view_class = PreviewRevisionView
|
|
|
|
#: The view class to use for comparing revisions; must be a subclass of ``wagtail.snippets.views.snippets.RevisionsCompareView``.
|
|
revisions_compare_view_class = RevisionsCompareView
|
|
|
|
#: The view class to use for unscheduling revisions; must be a subclass of ``wagtail.snippets.views.snippets.RevisionsUnscheduleView``.
|
|
revisions_unschedule_view_class = RevisionsUnscheduleView
|
|
|
|
#: The view class to use for unpublishing a snippet; must be a subclass of ``wagtail.snippets.views.snippets.UnpublishView``.
|
|
unpublish_view_class = UnpublishView
|
|
|
|
#: The view class to use for previewing on the create view; must be a subclass of ``wagtail.snippets.views.snippets.PreviewOnCreateView``.
|
|
preview_on_add_view_class = PreviewOnCreateView
|
|
|
|
#: The view class to use for previewing on the edit view; must be a subclass of ``wagtail.snippets.views.snippets.PreviewOnEditView``.
|
|
preview_on_edit_view_class = PreviewOnEditView
|
|
|
|
#: The view class to use for locking a snippet; must be a subclass of ``wagtail.snippets.views.snippets.LockView``.
|
|
lock_view_class = LockView
|
|
|
|
#: The view class to use for unlocking a snippet; must be a subclass of ``wagtail.snippets.views.snippets.UnlockView``.
|
|
unlock_view_class = UnlockView
|
|
|
|
#: The view class to use for performing a workflow action; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowActionView``.
|
|
workflow_action_view_class = WorkflowActionView
|
|
|
|
#: The view class to use for performing a workflow action that returns the validated data in the response; must be a subclass of ``wagtail.snippets.views.snippets.CollectWorkflowActionDataView``.
|
|
collect_workflow_action_data_view_class = CollectWorkflowActionDataView
|
|
|
|
#: The view class to use for confirming the cancellation of a workflow; must be a subclass of ``wagtail.snippets.views.snippets.ConfirmWorkflowCancellationView``.
|
|
confirm_workflow_cancellation_view_class = ConfirmWorkflowCancellationView
|
|
|
|
#: The view class to use for previewing a revision for a specific task; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowPreviewView``.
|
|
workflow_preview_view_class = WorkflowPreviewView
|
|
|
|
#: The view class to use for the workflow history view; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowHistoryView``.
|
|
workflow_history_view_class = WorkflowHistoryView
|
|
|
|
#: The view class to use for the workflow history detail view; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowHistoryDetailView``.
|
|
workflow_history_detail_view_class = WorkflowHistoryDetailView
|
|
|
|
#: The ViewSet class to use for the chooser views; must be a subclass of ``wagtail.snippets.views.chooser.SnippetChooserViewSet``.
|
|
chooser_viewset_class = SnippetChooserViewSet
|
|
|
|
#: The prefix of template names to look for when rendering the admin views.
|
|
template_prefix = "wagtailsnippets/snippets/"
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
if self.model is None:
|
|
raise ImproperlyConfigured(
|
|
"SnippetViewSet must be passed a model or define a model attribute."
|
|
)
|
|
|
|
self.model_opts = self.model._meta
|
|
self.app_label = self.model_opts.app_label
|
|
self.model_name = self.model_opts.model_name
|
|
|
|
self.preview_enabled = issubclass(self.model, PreviewableMixin)
|
|
self.revision_enabled = issubclass(self.model, RevisionMixin)
|
|
self.draftstate_enabled = issubclass(self.model, DraftStateMixin)
|
|
self.workflow_enabled = issubclass(self.model, WorkflowMixin)
|
|
self.locking_enabled = issubclass(self.model, LockableMixin)
|
|
|
|
self.menu_item_is_registered = getattr(
|
|
self, "menu_item_is_registered", bool(self.menu_hook)
|
|
)
|
|
|
|
@cached_property
|
|
def url_prefix(self):
|
|
# SnippetViewSet historically allows overriding the URL prefix via the
|
|
# get_admin_base_path method or the admin_base_path attribute, so preserve that here
|
|
return self.get_admin_base_path()
|
|
|
|
@cached_property
|
|
def url_namespace(self):
|
|
# SnippetViewSet historically allows overriding the URL namespace via the
|
|
# get_admin_url_namespace method or the admin_url_namespace attribute,
|
|
# so preserve that here
|
|
return self.get_admin_url_namespace()
|
|
|
|
@property
|
|
def revisions_revert_view_class(self):
|
|
"""
|
|
The view class to use for reverting to a previous revision.
|
|
|
|
By default, this class is generated by combining the edit view with
|
|
``wagtail.admin.views.generic.mixins.RevisionsRevertMixin``. As a result,
|
|
this class must be a subclass of ``wagtail.snippets.views.snippets.EditView``
|
|
and must handle the reversion correctly.
|
|
"""
|
|
revisions_revert_view_class = type(
|
|
"_RevisionsRevertView",
|
|
(generic.RevisionsRevertMixin, self.edit_view_class),
|
|
{"view_name": "revisions_revert"},
|
|
)
|
|
return revisions_revert_view_class
|
|
|
|
@property
|
|
def permission_policy(self):
|
|
return ModelPermissionPolicy(self.model)
|
|
|
|
def get_common_view_kwargs(self, **kwargs):
|
|
return super().get_common_view_kwargs(
|
|
**{
|
|
"index_url_name": self.get_url_name("list"),
|
|
"index_results_url_name": self.get_url_name("list_results"),
|
|
"lock_url_name": self.get_url_name("lock"),
|
|
"unlock_url_name": self.get_url_name("unlock"),
|
|
"revisions_view_url_name": self.get_url_name("revisions_view"),
|
|
"revisions_revert_url_name": self.get_url_name("revisions_revert"),
|
|
"revisions_compare_url_name": self.get_url_name("revisions_compare"),
|
|
"revisions_unschedule_url_name": self.get_url_name(
|
|
"revisions_unschedule"
|
|
),
|
|
"unpublish_url_name": self.get_url_name("unpublish"),
|
|
"breadcrumbs_items": self.breadcrumbs_items,
|
|
**kwargs,
|
|
}
|
|
)
|
|
|
|
def get_index_view_kwargs(self, **kwargs):
|
|
return super().get_index_view_kwargs(
|
|
queryset=self.get_queryset,
|
|
**kwargs,
|
|
)
|
|
|
|
def get_add_view_kwargs(self, **kwargs):
|
|
return super().get_add_view_kwargs(
|
|
preview_url_name=self.get_url_name("preview_on_add"),
|
|
**kwargs,
|
|
)
|
|
|
|
def get_copy_view_kwargs(self, **kwargs):
|
|
return self.get_add_view_kwargs(**kwargs)
|
|
|
|
def get_edit_view_kwargs(self, **kwargs):
|
|
return super().get_edit_view_kwargs(
|
|
preview_url_name=self.get_url_name("preview_on_edit"),
|
|
workflow_history_url_name=self.get_url_name("workflow_history"),
|
|
confirm_workflow_cancellation_url_name=self.get_url_name(
|
|
"confirm_workflow_cancellation"
|
|
),
|
|
**kwargs,
|
|
)
|
|
|
|
@property
|
|
def revisions_view(self):
|
|
return self.construct_view(self.revisions_view_class)
|
|
|
|
@property
|
|
def revisions_revert_view(self):
|
|
return self.construct_view(
|
|
self.revisions_revert_view_class,
|
|
**self.get_edit_view_kwargs(),
|
|
)
|
|
|
|
@property
|
|
def revisions_compare_view(self):
|
|
return self.construct_view(
|
|
self.revisions_compare_view_class,
|
|
edit_handler=self._edit_handler,
|
|
template_name=self.get_templates(
|
|
"revisions_compare",
|
|
fallback=self.revisions_compare_view_class.template_name,
|
|
),
|
|
)
|
|
|
|
@property
|
|
def revisions_unschedule_view(self):
|
|
return self.construct_view(
|
|
self.revisions_unschedule_view_class,
|
|
template_name=self.get_templates(
|
|
"revisions_unschedule",
|
|
fallback=self.revisions_unschedule_view_class.template_name,
|
|
),
|
|
)
|
|
|
|
@property
|
|
def unpublish_view(self):
|
|
return self.construct_view(
|
|
self.unpublish_view_class,
|
|
template_name=self.get_templates(
|
|
"unpublish", fallback=self.unpublish_view_class.template_name
|
|
),
|
|
)
|
|
|
|
@property
|
|
def preview_on_add_view(self):
|
|
return self.construct_view(
|
|
self.preview_on_add_view_class,
|
|
form_class=self.get_form_class(),
|
|
)
|
|
|
|
@property
|
|
def preview_on_edit_view(self):
|
|
return self.construct_view(
|
|
self.preview_on_edit_view_class,
|
|
form_class=self.get_form_class(for_update=True),
|
|
)
|
|
|
|
@property
|
|
def lock_view(self):
|
|
return self.construct_view(
|
|
self.lock_view_class,
|
|
success_url_name=self.get_url_name("edit"),
|
|
)
|
|
|
|
@property
|
|
def copy_view(self):
|
|
return self.construct_view(self.copy_view_class, **self.get_copy_view_kwargs())
|
|
|
|
@property
|
|
def unlock_view(self):
|
|
return self.construct_view(
|
|
self.unlock_view_class,
|
|
success_url_name=self.get_url_name("edit"),
|
|
)
|
|
|
|
@property
|
|
def workflow_action_view(self):
|
|
return self.construct_view(
|
|
self.workflow_action_view_class,
|
|
redirect_url_name=self.get_url_name("edit"),
|
|
submit_url_name=self.get_url_name("workflow_action"),
|
|
)
|
|
|
|
@property
|
|
def collect_workflow_action_data_view(self):
|
|
return self.construct_view(
|
|
self.collect_workflow_action_data_view_class,
|
|
redirect_url_name=self.get_url_name("edit"),
|
|
submit_url_name=self.get_url_name("collect_workflow_action_data"),
|
|
)
|
|
|
|
@property
|
|
def confirm_workflow_cancellation_view(self):
|
|
return self.construct_view(self.confirm_workflow_cancellation_view_class)
|
|
|
|
@property
|
|
def workflow_preview_view(self):
|
|
return self.construct_view(self.workflow_preview_view_class)
|
|
|
|
@property
|
|
def workflow_history_view(self):
|
|
return self.construct_view(
|
|
self.workflow_history_view_class,
|
|
template_name=self.get_templates(
|
|
"workflow_history/index",
|
|
fallback=self.workflow_history_view_class.template_name,
|
|
),
|
|
workflow_history_url_name=self.get_url_name("workflow_history"),
|
|
workflow_history_detail_url_name=self.get_url_name(
|
|
"workflow_history_detail"
|
|
),
|
|
_show_breadcrumbs=False,
|
|
)
|
|
|
|
@property
|
|
def workflow_history_detail_view(self):
|
|
return self.construct_view(
|
|
self.workflow_history_detail_view_class,
|
|
template_name=self.get_templates(
|
|
"workflow_history/detail",
|
|
fallback=self.workflow_history_detail_view_class.template_name,
|
|
),
|
|
object_icon=self.icon,
|
|
header_icon="list-ul",
|
|
workflow_history_url_name=self.get_url_name("workflow_history"),
|
|
_show_breadcrumbs=False,
|
|
)
|
|
|
|
@property
|
|
def redirect_to_usage_view(self):
|
|
def redirect_to_usage(request, pk):
|
|
warn(
|
|
(
|
|
"%s's `/<pk>/usage/` usage view URL pattern has been "
|
|
"deprecated in favour of /usage/<pk>/."
|
|
)
|
|
% (self.__class__.__name__),
|
|
category=RemovedInWagtail70Warning,
|
|
)
|
|
return redirect(self.get_url_name("usage"), pk, permanent=True)
|
|
|
|
return redirect_to_usage
|
|
|
|
@property
|
|
def chooser_viewset(self):
|
|
return self.chooser_viewset_class(
|
|
self.get_chooser_admin_url_namespace(),
|
|
model=self.model,
|
|
url_prefix=self.get_chooser_admin_base_path(),
|
|
icon=self.icon,
|
|
per_page=self.chooser_per_page,
|
|
)
|
|
|
|
@cached_property
|
|
def list_display(self):
|
|
list_display = super().list_display.copy()
|
|
if self.draftstate_enabled:
|
|
list_display.append(LiveStatusTagColumn())
|
|
return list_display
|
|
|
|
@cached_property
|
|
def icon(self):
|
|
return self.get_icon()
|
|
|
|
def get_icon(self):
|
|
"""
|
|
Returns the icon to be used for the admin views.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define an ``icon`` property.
|
|
"""
|
|
return "snippet"
|
|
|
|
@cached_property
|
|
def menu_label(self):
|
|
return self.get_menu_label()
|
|
|
|
def get_menu_label(self):
|
|
"""
|
|
Returns the label text to be used for the menu item.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``menu_label`` property.
|
|
"""
|
|
return self.model_opts.verbose_name_plural.title()
|
|
|
|
@cached_property
|
|
def menu_name(self):
|
|
return self.get_menu_name()
|
|
|
|
def get_menu_name(self):
|
|
"""
|
|
Returns the name to be used for the menu item.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``menu_name`` property.
|
|
"""
|
|
return ""
|
|
|
|
@cached_property
|
|
def menu_icon(self):
|
|
return self.get_menu_icon()
|
|
|
|
def get_menu_icon(self):
|
|
"""
|
|
Returns the icon to be used for the menu item.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``menu_icon`` property.
|
|
"""
|
|
return self.icon
|
|
|
|
@cached_property
|
|
def menu_order(self):
|
|
return self.get_menu_order()
|
|
|
|
def get_menu_order(self):
|
|
"""
|
|
Returns the ordering number to be applied to the menu item.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``menu_order`` property.
|
|
"""
|
|
# By default, put it at the last item before Reports, whose order is 9000.
|
|
return 8999
|
|
|
|
def get_menu_item_is_registered(self):
|
|
return self.menu_item_is_registered
|
|
|
|
@cached_property
|
|
def breadcrumbs_items(self):
|
|
# Use reverse_lazy instead of reverse
|
|
# because this will be passed to the view classes at startup
|
|
return [
|
|
{"url": reverse_lazy("wagtailadmin_home"), "label": _("Home")},
|
|
{"url": reverse_lazy("wagtailsnippets:index"), "label": _("Snippets")},
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
"""
|
|
Returns a QuerySet of all model instances to be shown on the index view.
|
|
If ``None`` is returned (the default), the logic in
|
|
``index_view.get_base_queryset()`` will be used instead.
|
|
"""
|
|
return None
|
|
|
|
@cached_property
|
|
def index_template_name(self):
|
|
return self.get_index_template()
|
|
|
|
def get_index_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``index_view``. If a
|
|
template is specified by the ``index_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define an ``index_template_name`` property.
|
|
"""
|
|
return self.get_templates("index")
|
|
|
|
@cached_property
|
|
def index_results_template_name(self):
|
|
return self.get_index_results_template()
|
|
|
|
def get_index_results_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``index_results_view``. If a
|
|
template is specified by the ``index_results_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define an ``index_results_template_name`` property.
|
|
"""
|
|
return self.get_templates("index_results")
|
|
|
|
@cached_property
|
|
def create_template_name(self):
|
|
return self.get_create_template()
|
|
|
|
def get_create_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``add_view``. If a
|
|
template is specified by the ``create_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``create_template_name`` property.
|
|
"""
|
|
return self.get_templates("create")
|
|
|
|
@cached_property
|
|
def edit_template_name(self):
|
|
return self.get_edit_template()
|
|
|
|
def get_edit_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``edit_view``. If a
|
|
template is specified by the ``edit_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define an ``edit_template_name`` property.
|
|
"""
|
|
return self.get_templates("edit")
|
|
|
|
@cached_property
|
|
def delete_template_name(self):
|
|
return self.get_delete_template()
|
|
|
|
def get_delete_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``delete_view``. If a
|
|
template is specified by the ``delete_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``delete_template_name`` property.
|
|
"""
|
|
return self.get_templates("delete")
|
|
|
|
@cached_property
|
|
def history_template_name(self):
|
|
"""
|
|
A template to be used when rendering ``history_view``.
|
|
|
|
Default: if :attr:`template_prefix` is specified, a ``history.html``
|
|
template in the prefix directory and its ``{app_label}/{model_name}/``
|
|
or ``{app_label}/`` subdirectories will be used. Otherwise, the
|
|
``history_view_class.template_name`` will be used.
|
|
"""
|
|
return self.get_history_template()
|
|
|
|
def get_history_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``history_view``. If a
|
|
template is specified by the ``history_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``history_template_name`` property.
|
|
"""
|
|
return self.get_templates("history")
|
|
|
|
@cached_property
|
|
def inspect_template_name(self):
|
|
"""
|
|
A template to be used when rendering ``inspect_view``.
|
|
|
|
Default: if :attr:`template_prefix` is specified, an ``inspect.html``
|
|
template in the prefix directory and its ``{app_label}/{model_name}/``
|
|
or ``{app_label}/`` subdirectories will be used. Otherwise, the
|
|
``inspect_view_class.template_name`` will be used.
|
|
"""
|
|
return self.get_inspect_template()
|
|
|
|
def get_inspect_template(self):
|
|
"""
|
|
Returns a template to be used when rendering ``inspect_view``. If a
|
|
template is specified by the ``inspect_template_name`` attribute, that will
|
|
be used. Otherwise, a list of preferred template names are returned.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define an ``inspect_template_name`` property.
|
|
"""
|
|
return self.get_templates(
|
|
"inspect", fallback=self.inspect_view_class.template_name
|
|
)
|
|
|
|
def get_admin_url_namespace(self):
|
|
"""
|
|
Returns the URL namespace for the admin URLs for this model.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``url_namespace`` property.
|
|
"""
|
|
if self.admin_url_namespace:
|
|
return self.admin_url_namespace
|
|
return f"wagtailsnippets_{self.app_label}_{self.model_name}"
|
|
|
|
def get_admin_base_path(self):
|
|
"""
|
|
Returns the base path for the admin URLs for this model.
|
|
The returned string must not begin or end with a slash.
|
|
|
|
**Deprecated** - the preferred way to customise this is to define a ``url_prefix`` property.
|
|
"""
|
|
if self.base_url_path:
|
|
return self.base_url_path.strip().strip("/")
|
|
return f"snippets/{self.app_label}/{self.model_name}"
|
|
|
|
def get_chooser_admin_url_namespace(self):
|
|
"""Returns the URL namespace for the chooser admin URLs for this model."""
|
|
if self.chooser_admin_url_namespace:
|
|
return self.chooser_admin_url_namespace
|
|
return f"wagtailsnippetchoosers_{self.app_label}_{self.model_name}"
|
|
|
|
def get_chooser_admin_base_path(self):
|
|
"""
|
|
Returns the base path for the chooser admin URLs for this model.
|
|
The returned string must not begin or end with a slash.
|
|
"""
|
|
if self.chooser_base_url_path:
|
|
return self.chooser_base_url_path.strip().strip("/")
|
|
return f"snippets/choose/{self.app_label}/{self.model_name}"
|
|
|
|
@property
|
|
def url_finder_class(self):
|
|
return type(
|
|
"_SnippetAdminURLFinder", (SnippetAdminURLFinder,), {"model": self.model}
|
|
)
|
|
|
|
def get_urlpatterns(self):
|
|
urlpatterns = [
|
|
path("", self.index_view, name="list"),
|
|
path("results/", self.index_results_view, name="list_results"),
|
|
path("add/", self.add_view, name="add"),
|
|
path("edit/<str:pk>/", self.edit_view, name="edit"),
|
|
path("delete/<str:pk>/", self.delete_view, name="delete"),
|
|
path("usage/<str:pk>/", self.usage_view, name="usage"),
|
|
path("history/<str:pk>/", self.history_view, name="history"),
|
|
path(
|
|
"history-results/<str:pk>/",
|
|
self.history_results_view,
|
|
name="history_results",
|
|
),
|
|
]
|
|
|
|
if self.copy_view_enabled:
|
|
urlpatterns += [path("copy/<str:pk>/", self.copy_view, name="copy")]
|
|
|
|
if self.inspect_view_enabled:
|
|
urlpatterns += [
|
|
path("inspect/<str:pk>/", self.inspect_view, name="inspect")
|
|
]
|
|
|
|
if self.preview_enabled:
|
|
urlpatterns += [
|
|
path("preview/", self.preview_on_add_view, name="preview_on_add"),
|
|
path(
|
|
"preview/<str:pk>/",
|
|
self.preview_on_edit_view,
|
|
name="preview_on_edit",
|
|
),
|
|
]
|
|
|
|
if self.revision_enabled:
|
|
if self.preview_enabled:
|
|
urlpatterns += [
|
|
path(
|
|
"history/<str:pk>/revisions/<int:revision_id>/view/",
|
|
self.revisions_view,
|
|
name="revisions_view",
|
|
)
|
|
]
|
|
|
|
urlpatterns += [
|
|
path(
|
|
"history/<str:pk>/revisions/<int:revision_id>/revert/",
|
|
self.revisions_revert_view,
|
|
name="revisions_revert",
|
|
),
|
|
re_path(
|
|
r"history/(?P<pk>.+)/revisions/compare/(?P<revision_id_a>live|earliest|\d+)\.\.\.(?P<revision_id_b>live|latest|\d+)/$",
|
|
self.revisions_compare_view,
|
|
name="revisions_compare",
|
|
),
|
|
]
|
|
|
|
if self.draftstate_enabled:
|
|
urlpatterns += [
|
|
path(
|
|
"history/<str:pk>/revisions/<int:revision_id>/unschedule/",
|
|
self.revisions_unschedule_view,
|
|
name="revisions_unschedule",
|
|
),
|
|
path("unpublish/<str:pk>/", self.unpublish_view, name="unpublish"),
|
|
]
|
|
|
|
if self.locking_enabled:
|
|
urlpatterns += [
|
|
path("lock/<str:pk>/", self.lock_view, name="lock"),
|
|
path("unlock/<str:pk>/", self.unlock_view, name="unlock"),
|
|
]
|
|
|
|
if self.workflow_enabled:
|
|
urlpatterns += [
|
|
path(
|
|
"workflow/action/<str:pk>/<slug:action_name>/<int:task_state_id>/",
|
|
self.workflow_action_view,
|
|
name="workflow_action",
|
|
),
|
|
path(
|
|
"workflow/collect_action_data/<str:pk>/<slug:action_name>/<int:task_state_id>/",
|
|
self.collect_workflow_action_data_view,
|
|
name="collect_workflow_action_data",
|
|
),
|
|
path(
|
|
"workflow/confirm_cancellation/<str:pk>/",
|
|
self.confirm_workflow_cancellation_view,
|
|
name="confirm_workflow_cancellation",
|
|
),
|
|
path(
|
|
"workflow_history/<str:pk>/",
|
|
self.workflow_history_view,
|
|
name="workflow_history",
|
|
),
|
|
path(
|
|
"workflow_history/<str:pk>/detail/<int:workflow_state_id>/",
|
|
self.workflow_history_detail_view,
|
|
name="workflow_history_detail",
|
|
),
|
|
]
|
|
|
|
if self.preview_enabled:
|
|
urlpatterns += [
|
|
path(
|
|
"workflow/preview/<str:pk>/<int:task_id>/",
|
|
self.workflow_preview_view,
|
|
name="workflow_preview",
|
|
),
|
|
]
|
|
|
|
# RemovedInWagtail70Warning: Remove legacy URL patterns
|
|
return urlpatterns + self._legacy_urlpatterns
|
|
|
|
@cached_property
|
|
def _legacy_urlpatterns(self):
|
|
return [
|
|
# RemovedInWagtail70Warning: Remove legacy URL patterns
|
|
# legacy URLs that could potentially collide if the pk matches one of the reserved names above
|
|
# ('add', 'edit' etc) - redirect to the unambiguous version
|
|
path("<str:pk>/", self.redirect_to_edit_view),
|
|
path("<str:pk>/delete/", self.redirect_to_delete_view),
|
|
path("<str:pk>/usage/", self.redirect_to_usage_view),
|
|
]
|
|
|
|
def get_edit_handler(self):
|
|
"""
|
|
Like :meth:`ModelViewSet.get_edit_handler()
|
|
<wagtail.admin.viewsets.model.ModelViewSet.get_edit_handler>`,
|
|
but falls back to extracting panel definitions from the model class
|
|
if no edit handler is defined.
|
|
"""
|
|
edit_handler = super().get_edit_handler()
|
|
if edit_handler:
|
|
return edit_handler
|
|
|
|
exclude = self.get_exclude_form_fields()
|
|
panels = extract_panel_definitions_from_model_class(self.model, exclude=exclude)
|
|
edit_handler = ObjectList(panels)
|
|
return edit_handler.bind_to_model(self.model)
|
|
|
|
def register_chooser_viewset(self):
|
|
viewsets.register(self.chooser_viewset)
|
|
|
|
def register_model_check(self):
|
|
def snippets_model_check(app_configs, **kwargs):
|
|
return check_panels_in_model(self.model, "snippets")
|
|
|
|
checks.register(snippets_model_check, "panels")
|
|
|
|
def register_snippet_model(self):
|
|
snippet_models = get_snippet_models()
|
|
if self.model in snippet_models:
|
|
raise ImproperlyConfigured(
|
|
f"The {self.model.__name__} model is already registered as a snippet"
|
|
)
|
|
snippet_models.append(self.model)
|
|
snippet_models.sort(key=lambda x: x._meta.verbose_name)
|
|
|
|
def on_register(self):
|
|
super().on_register()
|
|
# For convenience, attach viewset to the model class to allow accessing
|
|
# the configuration of a given model.
|
|
self.model.snippet_viewset = self
|
|
self.register_chooser_viewset()
|
|
self.register_model_check()
|
|
self.register_snippet_model()
|
|
|
|
|
|
class SnippetViewSetGroup(ModelViewSetGroup):
|
|
"""
|
|
A container for grouping together multiple
|
|
:class:`~wagtail.snippets.views.snippets.SnippetViewSet` instances.
|
|
|
|
All attributes and methods from
|
|
:class:`~wagtail.admin.viewsets.model.ModelViewSetGroup` are available.
|
|
"""
|
|
|
|
def __init__(self):
|
|
menu_item_is_registered = getattr(
|
|
self, "menu_item_is_registered", bool(self.menu_hook)
|
|
)
|
|
# If the menu item is registered, mark all viewsets as such so that we can
|
|
# hide the "Snippets" menu item if all snippets have their own menu items.
|
|
for item in self.items:
|
|
item.menu_item_is_registered = menu_item_is_registered
|
|
|
|
# Call super() after setting menu_item_is_registered so that nested groups
|
|
# can inherit the value.
|
|
super().__init__()
|