Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
from .delete import DeleteBulkAction
from .move import MoveBulkAction
from .publish import PublishBulkAction
from .unpublish import UnpublishBulkAction
__all__ = [
"DeleteBulkAction",
"MoveBulkAction",
"PublishBulkAction",
"UnpublishBulkAction",
]

View File

@@ -0,0 +1,57 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
class DeleteBulkAction(PageBulkAction):
display_name = _("Delete")
action_type = "delete"
aria_label = _("Delete selected pages")
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_delete.html"
action_priority = 30
classes = {"serious"}
def check_perm(self, page):
return page.permissions_for_user(self.request.user).can_delete()
def object_context(self, page):
return {
"item": page,
"descendant_count": page.get_descendant_count(),
}
@classmethod
def execute_action(cls, objects, user=None, **kwargs):
num_parent_objects, num_child_objects = 0, 0
for page in objects:
num_parent_objects += 1
num_child_objects += page.get_descendant_count()
page.delete(user=user)
return num_parent_objects, num_child_objects
def get_success_message(self, num_parent_objects, num_child_objects):
if num_parent_objects == 1:
if num_child_objects == 0:
success_message = _("1 page has been deleted")
else:
success_message = ngettext(
"1 page and %(num_child_objects)d child page have been deleted",
"1 page and %(num_child_objects)d child pages have been deleted",
num_child_objects,
) % {"num_child_objects": num_child_objects}
else:
if num_child_objects == 0:
success_message = _(
"%(num_parent_objects)d pages have been deleted"
) % {"num_parent_objects": num_parent_objects}
else:
success_message = ngettext(
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been deleted",
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been deleted",
num_child_objects,
) % {
"num_child_objects": num_child_objects,
"num_parent_objects": num_parent_objects,
}
return success_message

View File

@@ -0,0 +1,158 @@
from django import forms
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin import widgets
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
from wagtail.models import Page
class MoveForm(forms.Form):
def __init__(self, *args, **kwargs):
destination = kwargs.pop("destination")
target_parent_models = kwargs.pop("target_parent_models")
pages_to_move = kwargs.pop("pages_to_move")
super().__init__(*args, **kwargs)
self.fields["chooser"] = forms.ModelChoiceField(
initial=destination,
queryset=Page.objects.all(),
widget=widgets.AdminPageMoveChooser(
can_choose_root=True,
user_perms="bulk_move_to",
target_models=target_parent_models,
pages_to_move=pages_to_move,
),
label=_("Select a new parent page"),
)
class MoveBulkAction(PageBulkAction):
display_name = _("Move")
action_type = "move"
aria_label = _("Move selected pages")
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_move.html"
action_priority = 10
form_class = MoveForm
destination = None
def __init__(self, request, model):
super().__init__(request, model)
self.target_parent_models = set()
self.pages_to_move = []
def get_form_kwargs(self):
ctx = super().get_form_kwargs()
ctx["destination"] = self.destination or Page.get_first_root_node()
ctx["target_parent_models"] = self.target_parent_models
ctx["pages_to_move"] = self.pages_to_move
return ctx
def check_perm(self, page):
return page.permissions_for_user(self.request.user).can_move()
def get_success_message(self, num_parent_objects, num_child_objects):
success_message = ngettext(
"%(num_pages)d page has been moved",
"%(num_pages)d pages have been moved",
num_parent_objects,
) % {"num_pages": num_parent_objects}
return success_message
def object_context(self, obj):
context = super().object_context(obj)
context["child_pages"] = context["item"].get_descendants().count()
return context
def get_actionable_objects(self):
objects, objects_without_access = super().get_actionable_objects()
request = self.request
if objects:
self.target_parent_models = set(
objects[0].specific_class.allowed_parent_page_models()
)
for obj in objects:
self.target_parent_models.intersection_update(
set(obj.specific_class.allowed_parent_page_models())
)
self.pages_to_move = [page.id for page in objects]
if self.cleaned_form is None:
if len(self.target_parent_models) == 0:
return [], {
**objects_without_access,
"pages_without_common_parent_page": [
{
"item": page,
"can_edit": page.permissions_for_user(
self.request.user
).can_edit(),
}
for page in objects
],
}
return objects, objects_without_access
destination = self.cleaned_form.cleaned_data["chooser"]
pages = []
pages_without_destination_access = []
pages_with_duplicate_slugs = []
for page in objects:
if not page.permissions_for_user(request.user).can_move_to(destination):
pages_without_destination_access.append(page)
elif not Page._slug_is_available(page.slug, destination, page=page):
pages_with_duplicate_slugs.append(page)
else:
pages.append(page)
return pages, {
**objects_without_access,
"pages_without_destination_access": [
{
"item": page,
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
}
for page in pages_without_destination_access
],
"pages_with_duplicate_slugs": [
{
"item": page,
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
}
for page in pages_with_duplicate_slugs
],
}
def prepare_action(self, pages, pages_without_access):
request = self.request
destination = self.cleaned_form.cleaned_data["chooser"]
if (
pages_without_access["pages_without_destination_access"]
or pages_without_access["pages_with_duplicate_slugs"]
):
# this will be picked up by the form
self.destination = destination
return TemplateResponse(
request,
self.template_name,
{"destination": destination, **self.get_context_data()},
)
def get_execution_context(self):
return {
**super().get_execution_context(),
"destination": self.cleaned_form.cleaned_data["chooser"],
}
@classmethod
def execute_action(cls, objects, destination=None, user=None, **kwargs):
num_parent_objects = 0
if destination is None:
return
for page in objects:
page.move(destination, pos="last-child", user=user)
num_parent_objects += 1
return num_parent_objects, 0

View File

@@ -0,0 +1,57 @@
from django import forms
from wagtail.admin.views.bulk_action import BulkAction
from wagtail.admin.views.pages.search import page_filter_search
from wagtail.models import Page
class DefaultPageForm(forms.Form):
include_descendants = forms.BooleanField(required=False)
class PageBulkAction(BulkAction):
models = [Page]
form_class = DefaultPageForm
def get_all_objects_in_listing_query(self, parent_id):
listing_objects = self.model.objects.all()
q = None
if "q" in self.request.GET:
q = self.request.GET.get("q", "")
if parent_id is not None:
listing_objects = listing_objects.get(id=parent_id)
# If we're searching, include the descendants as well.
# Otherwise, just include the direct children.
if q:
listing_objects = listing_objects.get_descendants()
else:
listing_objects = listing_objects.get_children()
listing_objects = listing_objects.values_list("pk", flat=True)
if q:
listing_objects = page_filter_search(q, listing_objects)[0].results()
return listing_objects
def object_context(self, obj):
context = super().object_context(obj)
# Make 'item' into the specific instance, so that custom get_admin_display_title methods are respected
context["item"] = context["item"].specific_deferred
return context
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["items_with_no_access"] = [
{
"item": page,
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
}
for page in context["items_with_no_access"]
]
return context
def get_execution_context(self):
return {"user": self.request.user}

View File

@@ -0,0 +1,105 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
class PublishBulkAction(PageBulkAction):
display_name = _("Publish")
action_type = "publish"
aria_label = _("Publish selected pages")
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_publish.html"
action_priority = 40
def check_perm(self, page):
return page.permissions_for_user(self.request.user).can_publish()
def object_context(self, obj):
context = super().object_context(obj)
context["draft_descendant_count"] = (
context["item"].get_descendants().not_live().count()
)
return context
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["has_draft_descendants"] = any(
item["draft_descendant_count"] for item in context["items"]
)
return context
def get_execution_context(self):
return {
**super().get_execution_context(),
"include_descendants": self.cleaned_form.cleaned_data[
"include_descendants"
],
}
@classmethod
def execute_action(cls, objects, include_descendants=False, user=None, **kwargs):
num_parent_objects, num_child_objects = 0, 0
for page in objects:
revision = page.get_latest_revision() or page.specific.save_revision(
user=user
)
revision.publish(user=user)
num_parent_objects += 1
if include_descendants:
for draft_descendant_page in (
page.get_descendants()
.not_live()
.defer_streamfields()
.specific()
.iterator()
):
if (
user is None
or draft_descendant_page.permissions_for_user(
user
).can_publish()
):
draft_descendant_revision = (
draft_descendant_page.get_latest_revision()
or draft_descendant_page.save_revision(user=user)
)
draft_descendant_revision.publish(user=user)
num_child_objects += 1
return num_parent_objects, num_child_objects
def get_success_message(self, num_parent_objects, num_child_objects):
include_descendants = self.cleaned_form.cleaned_data["include_descendants"]
if num_parent_objects == 1:
if include_descendants:
if num_child_objects == 0:
success_message = _("1 page has been published")
else:
success_message = ngettext(
"1 page and %(num_child_objects)d child page have been published",
"1 page and %(num_child_objects)d child pages have been published",
num_child_objects,
) % {"num_child_objects": num_child_objects}
else:
success_message = _("1 page has been published")
else:
if include_descendants:
if num_child_objects == 0:
success_message = _(
"%(num_parent_objects)d pages have been published"
) % {"num_parent_objects": num_parent_objects}
else:
success_message = ngettext(
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been published",
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been published",
num_child_objects,
) % {
"num_child_objects": num_child_objects,
"num_parent_objects": num_parent_objects,
}
else:
success_message = _(
"%(num_parent_objects)d pages have been published"
) % {"num_parent_objects": num_parent_objects}
return success_message

View File

@@ -0,0 +1,99 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
class UnpublishBulkAction(PageBulkAction):
display_name = _("Unpublish")
action_type = "unpublish"
aria_label = _("Unpublish selected pages")
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_unpublish.html"
action_priority = 50
def check_perm(self, page):
return page.permissions_for_user(self.request.user).can_unpublish()
def object_context(self, page):
return {
**super().object_context(page),
"live_descendant_count": page.get_descendants().live().count(),
}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["has_live_descendants"] = any(
item["live_descendant_count"] > 0 for item in context["items"]
)
return context
def get_execution_context(self):
return {
**super().get_execution_context(),
"permission_checker": self.check_perm,
"include_descendants": self.cleaned_form.cleaned_data[
"include_descendants"
],
}
@classmethod
def execute_action(
cls,
objects,
include_descendants=False,
user=None,
permission_checker=None,
**kwargs,
):
num_parent_objects, num_child_objects = 0, 0
for page in objects:
page.unpublish(user=user)
num_parent_objects += 1
if include_descendants:
for live_descendant_page in (
page.get_descendants()
.live()
.defer_streamfields()
.specific()
.iterator()
):
if user is None or permission_checker(live_descendant_page):
live_descendant_page.unpublish()
num_child_objects += 1
return num_parent_objects, num_child_objects
def get_success_message(self, num_parent_objects, num_child_objects):
include_descendants = self.cleaned_form.cleaned_data["include_descendants"]
if num_parent_objects == 1:
if include_descendants:
if num_child_objects == 0:
success_message = _("1 page has been unpublished")
else:
success_message = ngettext(
"1 page and %(num_child_objects)d child page have been unpublished",
"1 page and %(num_child_objects)d child pages have been unpublished",
num_child_objects,
) % {"num_child_objects": num_child_objects}
else:
success_message = _("1 page has been unpublished")
else:
if include_descendants:
if num_child_objects == 0:
success_message = _(
"%(num_parent_objects)d pages have been unpublished"
) % {"num_parent_objects": num_parent_objects}
else:
success_message = ngettext(
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been unpublished",
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been unpublished",
num_child_objects,
) % {
"num_child_objects": num_child_objects,
"num_parent_objects": num_parent_objects,
}
else:
success_message = _(
"%(num_parent_objects)d pages have been unpublished"
) % {"num_parent_objects": num_parent_objects}
return success_message

View File

@@ -0,0 +1,135 @@
from django.contrib.admin.utils import quote
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import redirect
from django.urls import reverse
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 django.views.generic import FormView
from wagtail.admin.forms.pages import ParentChooserForm
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
from wagtail.models import Page
from wagtail.permissions import page_permission_policy
class ChooseParentView(WagtailAdminTemplateMixin, FormView):
template_name = "wagtailadmin/pages/choose_parent.html"
model = Page
index_url_name = None
page_title = gettext_lazy("Choose parent")
def get_valid_parent_pages(self, user):
"""
Identifies possible parent pages for the current user by first looking
at allowed_parent_page_models() on self.model to limit options to the
correct type of page, then checking permissions on those individual
pages to make sure we have permission to add a subpage to it.
"""
# Get queryset of pages where this page type can be added
allowed_parent_page_content_types = list(
ContentType.objects.get_for_models(
*self.model.allowed_parent_page_models()
).values()
)
allowed_parent_pages = Page.objects.filter(
content_type__in=allowed_parent_page_content_types
)
# Get queryset of pages where the user has permission to add subpages
if user.is_superuser:
pages_where_user_can_add = Page.objects.all()
else:
pages_where_user_can_add = Page.objects.none()
perms = {
perm
for perm in page_permission_policy.get_cached_permissions_for_user(user)
if perm.permission.codename == "add_page"
}
for perm in perms:
# user has add permission on any subpage of perm.page
# (including perm.page itself)
pages_where_user_can_add |= Page.objects.descendant_of(
perm.page, inclusive=True
)
# Combine them
return allowed_parent_pages & pages_where_user_can_add
def get(self, request, *args, **kwargs):
parents = self.get_valid_parent_pages(request.user)
# Only fetch the IDs for the first two parents to check if there's only
# one valid parent, and if so, redirect to the add page view with that
# parent pre-selected
parent_ids = parents.values_list("pk", flat=True)[:2]
if len(parent_ids) == 1:
parent_id = quote(parent_ids[0])
model_opts = self.model._meta
return redirect(
"wagtailadmin_pages:add",
model_opts.app_label,
model_opts.model_name,
parent_id,
)
# The page can be added in multiple places, so proceed with rendering
# the view's form so that the parent can be specified
return super().get(request, *args, **kwargs)
def get_form(self):
if self.request.method == "POST":
return ParentChooserForm(self.model, self.request.user, self.request.POST)
return ParentChooserForm(self.model, self.request.user)
def get_index_url(self):
if self.index_url_name:
return reverse(self.index_url_name)
def get_breadcrumbs_items(self):
items = []
index_url = self.get_index_url()
if index_url:
items.append(
{
"url": index_url,
"label": capfirst(self.model._meta.verbose_name_plural),
}
)
items.append(
{
"url": "",
"label": self.get_page_title(),
"sublabel": self.get_page_subtitle(),
}
)
return self.breadcrumbs_items + items
def get_page_subtitle(self):
return self.model.get_verbose_name()
@cached_property
def submit_button_label(self):
return _("Create a new %(model_name)s") % {
"model_name": self.model._meta.verbose_name,
}
def form_valid(self, form):
model_opts = self.model._meta
parent_id = quote(form.cleaned_data["parent_page"].pk)
return redirect(
"wagtailadmin_pages:add",
model_opts.app_label,
model_opts.model_name,
parent_id,
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["media"] = context["form"].media
context["submit_button_label"] = self.submit_button_label
return context

View File

@@ -0,0 +1,53 @@
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.translation import gettext as _
from wagtail import hooks
from wagtail.actions.convert_alias import ConvertAliasPageAction
from wagtail.admin import messages
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.models import Page
def convert_alias(request, page_id):
page = get_object_or_404(Page, id=page_id, alias_of_id__isnull=False).specific
if not page.permissions_for_user(request.user).can_edit():
raise PermissionDenied
with transaction.atomic():
for fn in hooks.get_hooks("before_convert_alias_page"):
result = fn(request, page)
if hasattr(result, "status_code"):
return result
next_url = get_valid_next_url_from_request(request)
if request.method == "POST":
action = ConvertAliasPageAction(page, user=request.user)
action.execute(skip_permission_checks=True)
messages.success(
request,
_("Page '%(page_title)s' has been converted into an ordinary page.")
% {"page_title": page.get_admin_display_title()},
)
for fn in hooks.get_hooks("after_convert_alias_page"):
result = fn(request, page)
if hasattr(result, "status_code"):
return result
if next_url:
return redirect(next_url)
return redirect("wagtailadmin_pages:edit", page.id)
return TemplateResponse(
request,
"wagtailadmin/pages/confirm_convert_alias.html",
{
"page": page,
"next": next_url,
},
)

View File

@@ -0,0 +1,115 @@
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.translation import gettext as _
from wagtail import hooks
from wagtail.actions.copy_page import CopyPageAction
from wagtail.actions.create_alias import CreatePageAliasAction
from wagtail.admin import messages
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
from wagtail.admin.forms.pages import CopyForm
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.models import Page
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
page = Page.objects.get(id=page_id)
# Parent page defaults to parent of source page
parent_page = page.get_parent()
# Check if the user has permission to publish subpages on the parent
can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
form_class = getattr(page.specific_class, "copy_form_class", CopyForm)
form = form_class(
request.POST or None, user=request.user, page=page, can_publish=can_publish
)
next_url = get_valid_next_url_from_request(request)
for fn in hooks.get_hooks("before_copy_page"):
result = fn(request, page)
if hasattr(result, "status_code"):
return result
# Check if user is submitting
if request.method == "POST":
# Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
# because ModelChoiceField seems to not fall back to the user given value)
parent_page = Page.objects.get(id=request.POST["new_parent_page"])
if form.is_valid():
# Receive the parent page (this should never be empty)
if form.cleaned_data["new_parent_page"]:
parent_page = form.cleaned_data["new_parent_page"]
# Re-check if the user has permission to publish subpages on the new parent
can_publish = parent_page.permissions_for_user(
request.user
).can_publish_subpage()
keep_live = can_publish and form.cleaned_data.get("publish_copies")
# Copy the page
# Note that only users who can publish in the new parent page can create an alias.
# This is because alias pages must always match their original page's state.
if can_publish and form.cleaned_data.get("alias"):
action = CreatePageAliasAction(
page.specific,
recursive=form.cleaned_data.get("copy_subpages"),
parent=parent_page,
update_slug=form.cleaned_data["new_slug"],
user=request.user,
)
new_page = action.execute(skip_permission_checks=True)
else:
action = CopyPageAction(
page=page,
recursive=form.cleaned_data.get("copy_subpages"),
to=parent_page,
update_attrs={
"title": form.cleaned_data["new_title"],
"slug": form.cleaned_data["new_slug"],
},
keep_live=keep_live,
user=request.user,
)
new_page = action.execute()
# Give a success message back to the user
if form.cleaned_data.get("copy_subpages"):
messages.success(
request,
_("Page '%(page_title)s' and %(subpages_count)s subpages copied.")
% {
"page_title": page.specific_deferred.get_admin_display_title(),
"subpages_count": new_page.get_descendants().count(),
},
)
else:
messages.success(
request,
_("Page '%(page_title)s' copied.")
% {"page_title": page.specific_deferred.get_admin_display_title()},
)
for fn in hooks.get_hooks("after_copy_page"):
result = fn(request, page, new_page)
if hasattr(result, "status_code"):
return result
# Redirect to explore of parent page
if next_url:
return redirect(next_url)
return redirect("wagtailadmin_explore", parent_page.id)
return TemplateResponse(
request,
"wagtailadmin/pages/copy.html",
{
"page": page,
"form": form,
"next": next_url,
},
)

View File

@@ -0,0 +1,454 @@
from urllib.parse import quote, urlencode
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django.views.generic.base import View
from wagtail.admin import messages, signals
from wagtail.admin.action_menu import PageActionMenu
from wagtail.admin.ui.components import MediaContainer
from wagtail.admin.ui.side_panels import (
ChecksSidePanel,
CommentsSidePanel,
PageStatusSidePanel,
PreviewSidePanel,
)
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.admin.views.generic import HookResponseMixin
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
from wagtail.models import Locale, Page, PageSubscription
def add_subpage(request, parent_page_id):
parent_page = get_object_or_404(Page, id=parent_page_id).specific
if not parent_page.permissions_for_user(request.user).can_add_subpage():
raise PermissionDenied
page_types = [
(
model.get_verbose_name(),
model._meta.app_label,
model._meta.model_name,
model.get_page_description(),
)
for model in type(parent_page).creatable_subpage_models()
if model.can_create_at(parent_page)
]
# sort by lower-cased version of verbose name
page_types.sort(key=lambda page_type: page_type[0].lower())
if len(page_types) == 1:
# Only one page type is available - redirect straight to the create form rather than
# making the user choose
verbose_name, app_label, model_name, description = page_types[0]
return redirect("wagtailadmin_pages:add", app_label, model_name, parent_page.id)
return TemplateResponse(
request,
"wagtailadmin/pages/add_subpage.html",
{
"parent_page": parent_page,
"page_types": page_types,
"next": get_valid_next_url_from_request(request),
},
)
class CreateView(WagtailAdminTemplateMixin, HookResponseMixin, View):
template_name = "wagtailadmin/pages/create.html"
page_title = gettext_lazy("New")
def dispatch(
self, request, content_type_app_name, content_type_model_name, parent_page_id
):
self.parent_page = get_object_or_404(Page, id=parent_page_id).specific
self.parent_page_perms = self.parent_page.permissions_for_user(
self.request.user
)
if not self.parent_page_perms.can_add_subpage():
raise PermissionDenied
try:
self.page_content_type = ContentType.objects.get_by_natural_key(
content_type_app_name, content_type_model_name
)
except ContentType.DoesNotExist:
raise Http404
# Get class
self.page_class = self.page_content_type.model_class()
# Make sure the class is a descendant of Page
if not issubclass(self.page_class, Page):
raise Http404
# page must be in the list of allowed subpage types for this parent ID
if self.page_class not in self.parent_page.creatable_subpage_models():
raise PermissionDenied
if not self.page_class.can_create_at(self.parent_page):
raise PermissionDenied
response = self.run_hook(
"before_create_page", self.request, self.parent_page, self.page_class
)
if response:
return response
self.locale = self.parent_page.locale
self.page = self.page_class(owner=self.request.user)
self.page.locale = self.locale
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
# If the parent page is the root page. The user may specify any locale they like
if self.parent_page.is_root():
selected_locale = request.GET.get("locale", None) or request.POST.get(
"locale", None
)
if selected_locale:
self.locale = get_object_or_404(
Locale, language_code=selected_locale
)
self.page.locale = self.locale
self.translations = self.get_translations()
else:
self.locale = None
self.translations = []
self.edit_handler = self.page_class.get_edit_handler()
self.form_class = self.edit_handler.get_form_class()
# Note: Comment notifications should be enabled by default for pages that a user creates
self.subscription = PageSubscription(
page=self.page, user=self.request.user, comment_notifications=True
)
self.next_url = get_valid_next_url_from_request(self.request)
return super().dispatch(request)
def post(self, request):
self.form = self.form_class(
self.request.POST,
self.request.FILES,
instance=self.page,
subscription=self.subscription,
parent_page=self.parent_page,
for_user=self.request.user,
)
if self.form.is_valid():
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
def form_valid(self, form):
if (
bool(self.request.POST.get("action-publish"))
and self.parent_page_perms.can_publish_subpage()
):
return self.publish_action()
elif (
bool(self.request.POST.get("action-submit"))
and self.parent_page.has_workflow
):
return self.submit_action()
else:
return self.save_action()
def get_page_subtitle(self):
return self.page_class.get_verbose_name()
def get_edit_message_button(self):
return messages.button(
reverse("wagtailadmin_pages:edit", args=(self.page.id,)), _("Edit")
)
def get_view_draft_message_button(self):
return messages.button(
reverse("wagtailadmin_pages:view_draft", args=(self.page.id,)),
_("View draft"),
new_window=False,
)
def get_view_live_message_button(self):
return messages.button(self.page.url, _("View live"), new_window=False)
def save_action(self):
self.page = self.form.save(commit=False)
self.page.live = False
# Save page
self.parent_page.add_child(instance=self.page)
# Save revision
self.page.save_revision(user=self.request.user, log_action=True)
# Save subscription settings
self.subscription.page = self.page
self.subscription.save()
# Notification
messages.success(
self.request,
_("Page '%(page_title)s' created.")
% {"page_title": self.page.get_admin_display_title()},
)
response = self.run_hook("after_create_page", self.request, self.page)
if response:
return response
# remain on edit page for further edits
return self.redirect_and_remain()
def publish_action(self):
self.page = self.form.save(commit=False)
# Save page
self.parent_page.add_child(instance=self.page)
# Save revision
revision = self.page.save_revision(user=self.request.user, log_action=True)
# Save subscription settings
self.subscription.page = self.page
self.subscription.save()
# Publish
response = self.run_hook("before_publish_page", self.request, self.page)
if response:
return response
revision.publish(user=self.request.user)
# get a fresh copy so that any changes coming from revision.publish() are passed on
self.page.refresh_from_db()
response = self.run_hook("after_publish_page", self.request, self.page)
if response:
return response
# Notification
if self.page.go_live_at and self.page.go_live_at > timezone.now():
messages.success(
self.request,
_("Page '%(page_title)s' created and scheduled for publishing.")
% {"page_title": self.page.get_admin_display_title()},
buttons=[self.get_edit_message_button()],
)
else:
buttons = []
if self.page.url is not None:
buttons.append(self.get_view_live_message_button())
buttons.append(self.get_edit_message_button())
messages.success(
self.request,
_("Page '%(page_title)s' created and published.")
% {"page_title": self.page.get_admin_display_title()},
buttons=buttons,
)
response = self.run_hook("after_create_page", self.request, self.page)
if response:
return response
return self.redirect_away()
def submit_action(self):
self.page = self.form.save(commit=False)
self.page.live = False
# Save page
self.parent_page.add_child(instance=self.page)
# Save revision
self.page.save_revision(user=self.request.user, log_action=True)
# Submit
workflow = self.page.get_workflow()
workflow.start(self.page, self.request.user)
# Save subscription settings
self.subscription.page = self.page
self.subscription.save()
# Notification
buttons = []
if self.page.is_previewable():
buttons.append(self.get_view_draft_message_button())
buttons.append(self.get_edit_message_button())
messages.success(
self.request,
_("Page '%(page_title)s' created and submitted for moderation.")
% {"page_title": self.page.get_admin_display_title()},
buttons=buttons,
)
response = self.run_hook("after_create_page", self.request, self.page)
if response:
return response
return self.redirect_away()
def redirect_away(self):
if self.next_url:
# redirect back to 'next' url if present
return redirect(self.next_url)
else:
# redirect back to the explorer
return redirect("wagtailadmin_explore", self.page.get_parent().id)
def redirect_and_remain(self):
target_url = reverse("wagtailadmin_pages:edit", args=[self.page.id])
if self.next_url:
# Ensure the 'next' url is passed through again if present
target_url += "?next=%s" % quote(self.next_url)
return redirect(target_url)
def form_invalid(self, form):
messages.validation_error(
self.request,
_("The page could not be created due to validation errors"),
self.form,
)
self.has_unsaved_changes = True
return self.render_to_response(self.get_context_data())
def get(self, request):
signals.init_new_page.send(
sender=CreateView, page=self.page, parent=self.parent_page
)
self.form = self.form_class(
instance=self.page,
subscription=self.subscription,
parent_page=self.parent_page,
for_user=self.request.user,
)
self.has_unsaved_changes = False
return self.render_to_response(self.get_context_data())
def get_preview_url(self):
return reverse(
"wagtailadmin_pages:preview_on_add",
args=[
self.page_content_type.app_label,
self.page_content_type.model,
self.parent_page.id,
],
)
def get_side_panels(self):
side_panels = [
PageStatusSidePanel(
self.page,
self.request,
show_schedule_publishing_toggle=self.form.show_schedule_publishing_toggle,
locale=self.locale,
translations=self.translations,
),
]
if self.page.is_previewable():
side_panels.append(
PreviewSidePanel(
self.page, self.request, preview_url=self.get_preview_url()
)
)
side_panels.append(
ChecksSidePanel(
self.page,
self.request,
)
)
if self.form.show_comments_toggle:
side_panels.append(CommentsSidePanel(self.page, self.request))
return MediaContainer(side_panels)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
bound_panel = self.edit_handler.get_bound_panel(
request=self.request, instance=self.page, form=self.form
)
action_menu = PageActionMenu(
self.request,
view="create",
parent_page=self.parent_page,
lock=None,
locked_for_user=False,
)
side_panels = self.get_side_panels()
media = MediaContainer([bound_panel, self.form, action_menu, side_panels]).media
context.update(
{
"content_type": self.page_content_type,
"page_class": self.page_class,
"parent_page": self.parent_page,
"edit_handler": bound_panel,
"action_menu": action_menu,
"side_panels": side_panels,
"form": self.form,
"next": self.next_url,
"has_unsaved_changes": self.has_unsaved_changes,
"locale": self.locale,
"media": media,
}
)
return context
def get_translations(self):
# Pages can be created in any language at the root level
if self.parent_page.is_root():
return [
{
"locale": locale,
"url": reverse(
"wagtailadmin_pages:add",
args=[
self.page_content_type.app_label,
self.page_content_type.model,
self.parent_page.id,
],
)
+ "?"
+ urlencode({"locale": locale.language_code}),
}
# Do not show the switcher for the current locale
for locale in Locale.objects.exclude(pk=self.locale.pk)
]
else:
return [
{
"locale": translation.locale,
"url": reverse(
"wagtailadmin_pages:add",
args=[
self.page_content_type.app_label,
self.page_content_type.model,
translation.id,
],
),
}
for translation in self.parent_page.get_translations()
.only("id", "locale")
.select_related("locale")
if translation.permissions_for_user(self.request.user).can_add_subpage()
and self.page_class
in translation.specific_class.creatable_subpage_models()
and self.page_class.can_create_at(translation)
]

View File

@@ -0,0 +1,119 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from wagtail import hooks
from wagtail.actions.delete_page import DeletePageAction
from wagtail.admin import messages
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.models import Page, ReferenceIndex
def delete(request, page_id):
page = get_object_or_404(Page, id=page_id).specific
if not page.permissions_for_user(request.user).can_delete():
raise PermissionDenied
wagtail_site_name = getattr(settings, "WAGTAIL_SITE_NAME", "wagtail")
with transaction.atomic():
for fn in hooks.get_hooks("before_delete_page"):
result = fn(request, page)
if hasattr(result, "status_code"):
return result
next_url = get_valid_next_url_from_request(request)
pages_to_delete = {page}
# The `construct_translated_pages_to_cascade_actions` hook returns translation and
# alias pages when the action is set to "delete"
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
fn_pages = fn([page], "delete")
if fn_pages and isinstance(fn_pages, dict):
for additional_pages in fn_pages.values():
pages_to_delete.update(additional_pages)
pages_to_delete = list(pages_to_delete)
if request.method == "POST":
continue_deleting = True
if (
request.POST.get("confirm_site_name")
and request.POST.get("confirm_site_name") != wagtail_site_name
):
messages.error(
request, f"Please type '{wagtail_site_name}' to confirm."
)
continue_deleting = False
if continue_deleting:
parent_id = page.get_parent().id
# Delete the source page.
action = DeletePageAction(page, user=request.user)
# Permission checks are done above, so skip them in execute.
action.execute(skip_permission_checks=True)
# Delete translation and alias pages if they have the same parent page.
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
parent_page_translations = page.get_parent().get_translations()
for page_or_alias in pages_to_delete:
if page_or_alias.get_parent() in parent_page_translations:
action = DeletePageAction(page_or_alias, user=request.user)
# Permission checks are done above, so skip them in execute.
action.execute(skip_permission_checks=True)
messages.success(
request,
_("Page '%(page_title)s' deleted.")
% {"page_title": page.get_admin_display_title()},
)
for fn in hooks.get_hooks("after_delete_page"):
result = fn(request, page)
if hasattr(result, "status_code"):
return result
if next_url:
return redirect(next_url)
return redirect("wagtailadmin_explore", parent_id)
usage = ReferenceIndex.get_references_to(page).group_by_source_object()
descendant_count = page.get_descendant_count()
return TemplateResponse(
request,
"wagtailadmin/pages/confirm_delete.html",
{
"page": page,
"descendant_count": descendant_count,
"next": next_url,
"model_opts": page._meta,
"usage_url": reverse("wagtailadmin_pages:usage", args=(page.id,))
+ "?describe_on_delete=1",
"usage_count": usage.count(),
"is_protected": usage.is_protected,
# if the number of pages ( child pages + current page) exceeds this limit, then confirm before delete.
"confirm_before_delete": (descendant_count + 1)
>= getattr(settings, "WAGTAILADMIN_UNSAFE_PAGE_DELETION_LIMIT", 10),
"wagtail_site_name": wagtail_site_name,
# note that while pages_to_delete may contain a mix of translated pages
# and aliases, we count the "translations" only, as aliases are similar
# to symlinks, so they should just follow the source
"translation_count": len(
[
translation.id
for translation in pages_to_delete
if not translation.alias_of_id and translation.id != page.id
]
),
"translation_descendant_count": sum(
[
translation.get_descendants().filter(alias_of__isnull=True).count()
for translation in pages_to_delete
]
),
},
)

View File

@@ -0,0 +1,970 @@
import json
from urllib.parse import quote
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.db.models import Prefetch, Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext as _
from django.views.generic.base import View
from wagtail.actions.publish_page_revision import PublishPageRevisionAction
from wagtail.admin import messages
from wagtail.admin.action_menu import PageActionMenu
from wagtail.admin.mail import send_notification
from wagtail.admin.models import EditingSession
from wagtail.admin.ui.components import MediaContainer
from wagtail.admin.ui.editing_sessions import EditingSessionsModule
from wagtail.admin.ui.side_panels import (
ChecksSidePanel,
CommentsSidePanel,
PageStatusSidePanel,
PreviewSidePanel,
)
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.admin.views.generic import HookResponseMixin
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
from wagtail.exceptions import PageClassNotFoundError
from wagtail.locks import BasicLock, ScheduledForPublishLock, WorkflowLock
from wagtail.models import (
COMMENTS_RELATION_NAME,
Comment,
CommentReply,
Page,
PageSubscription,
WorkflowState,
get_default_page_content_type,
)
from wagtail.utils.timestamps import render_timestamp
class EditView(WagtailAdminTemplateMixin, HookResponseMixin, View):
def get_page_title(self):
return _("Editing %(page_type)s") % {
"page_type": self.page_class.get_verbose_name()
}
def get_page_subtitle(self):
return self.page.get_admin_display_title()
def get_template_names(self):
if self.page.alias_of_id:
return ["wagtailadmin/pages/edit_alias.html"]
else:
return ["wagtailadmin/pages/edit.html"]
def add_save_confirmation_message(self):
if self.is_reverting:
message = _(
"Page '%(page_title)s' has been replaced "
"with version from %(previous_revision_datetime)s."
) % {
"page_title": self.page.get_admin_display_title(),
"previous_revision_datetime": render_timestamp(
self.previous_revision.created_at
),
}
else:
message = _("Page '%(page_title)s' has been updated.") % {
"page_title": self.page.get_admin_display_title()
}
messages.success(self.request, message)
def get_commenting_changes(self):
"""
Finds comments that have been changed during this request.
Returns a tuple of 5 lists:
- New comments
- Deleted comments
- Resolved comments
- Edited comments
- Replied comments (dict containing the instance and list of replies)
"""
# Get changes
comments_formset = self.form.formsets["comments"]
new_comments = comments_formset.new_objects
deleted_comments = comments_formset.deleted_objects
# Assume any changed comments that are resolved were only just resolved
resolved_comments = []
edited_comments = []
for changed_comment, changed_fields in comments_formset.changed_objects:
if changed_comment.resolved_at and "resolved" in changed_fields:
resolved_comments.append(changed_comment)
if "text" in changed_fields:
edited_comments.append(changed_comment)
new_replies = []
deleted_replies = []
edited_replies = []
for comment_form in comments_formset.forms:
# New
replies = getattr(comment_form.formsets["replies"], "new_objects", [])
if replies:
new_replies.append((comment_form.instance, replies))
# Deleted
replies = getattr(comment_form.formsets["replies"], "deleted_objects", [])
if replies:
deleted_replies.append((comment_form.instance, replies))
# Edited
replies = getattr(comment_form.formsets["replies"], "changed_objects", [])
replies = [
reply for reply, changed_fields in replies if "text" in changed_fields
]
if replies:
edited_replies.append((comment_form.instance, replies))
return {
"new_comments": new_comments,
"deleted_comments": deleted_comments,
"resolved_comments": resolved_comments,
"edited_comments": edited_comments,
"new_replies": new_replies,
"deleted_replies": deleted_replies,
"edited_replies": edited_replies,
}
def send_commenting_notifications(self, changes):
"""
Sends notifications about any changes to comments to anyone who is subscribed.
"""
relevant_comment_ids = []
relevant_comment_ids.extend(
comment.pk for comment in changes["resolved_comments"]
)
relevant_comment_ids.extend(
comment.pk for comment, replies in changes["new_replies"]
)
# Skip if no changes were made
# Note: We don't email about edited comments so ignore those here
if (
not changes["new_comments"]
and not changes["deleted_comments"]
and not changes["resolved_comments"]
and not changes["new_replies"]
):
return
# Get global page comment subscribers
subscribers = PageSubscription.objects.filter(
page=self.page, comment_notifications=True
).select_related("user")
global_recipient_users = [
subscriber.user
for subscriber in subscribers
if subscriber.user != self.request.user
]
# Get subscribers to individual threads
replies = CommentReply.objects.filter(comment_id__in=relevant_comment_ids)
comments = Comment.objects.filter(id__in=relevant_comment_ids)
thread_users = (
get_user_model()
.objects.exclude(pk=self.request.user.pk)
.exclude(pk__in=subscribers.values_list("user_id", flat=True))
.filter(
Q(comment_replies__comment_id__in=relevant_comment_ids)
| Q(**{("%s__pk__in" % COMMENTS_RELATION_NAME): relevant_comment_ids})
)
.prefetch_related(
Prefetch("comment_replies", queryset=replies),
Prefetch(COMMENTS_RELATION_NAME, queryset=comments),
)
)
# Skip if no recipients
if not (global_recipient_users or thread_users):
return
thread_users = [
(
user,
set(
list(user.comment_replies.values_list("comment_id", flat=True))
+ list(
getattr(user, COMMENTS_RELATION_NAME).values_list(
"pk", flat=True
)
)
),
)
for user in thread_users
]
mailed_users = set()
for current_user, current_threads in thread_users:
# We are trying to avoid calling send_notification for each user for performance reasons
# so group the users receiving the same thread notifications together here
if current_user in mailed_users:
continue
users = [current_user]
mailed_users.add(current_user)
for user, threads in thread_users:
if user not in mailed_users and threads == current_threads:
users.append(user)
mailed_users.add(user)
send_notification(
users,
"updated_comments",
{
"page": self.page,
"editor": self.request.user,
"new_comments": [
comment
for comment in changes["new_comments"]
if comment.pk in threads
],
"resolved_comments": [
comment
for comment in changes["resolved_comments"]
if comment.pk in threads
],
"deleted_comments": [],
"replied_comments": [
{
"comment": comment,
"replies": replies,
}
for comment, replies in changes["new_replies"]
if comment.pk in threads
],
},
)
return send_notification(
global_recipient_users,
"updated_comments",
{
"page": self.page,
"editor": self.request.user,
"new_comments": changes["new_comments"],
"resolved_comments": changes["resolved_comments"],
"deleted_comments": changes["deleted_comments"],
"replied_comments": [
{
"comment": comment,
"replies": replies,
}
for comment, replies in changes["new_replies"]
],
},
)
def log_commenting_changes(self, changes, revision):
"""
Generates log entries for any changes made to comments or replies.
"""
for comment in changes["new_comments"]:
comment.log_create(page_revision=revision, user=self.request.user)
for comment in changes["edited_comments"]:
comment.log_edit(page_revision=revision, user=self.request.user)
for comment in changes["resolved_comments"]:
comment.log_resolve(page_revision=revision, user=self.request.user)
for comment in changes["deleted_comments"]:
comment.log_delete(page_revision=revision, user=self.request.user)
for comment, replies in changes["new_replies"]:
for reply in replies:
reply.log_create(page_revision=revision, user=self.request.user)
for comment, replies in changes["edited_replies"]:
for reply in replies:
reply.log_edit(page_revision=revision, user=self.request.user)
for comment, replies in changes["deleted_replies"]:
for reply in replies:
reply.log_delete(page_revision=revision, user=self.request.user)
def get_edit_message_button(self):
return messages.button(
reverse("wagtailadmin_pages:edit", args=(self.page.id,)), _("Edit")
)
def get_view_draft_message_button(self):
return messages.button(
reverse("wagtailadmin_pages:view_draft", args=(self.page.id,)),
_("View draft"),
new_window=False,
)
def get_view_live_message_button(self):
return messages.button(self.page.url, _("View live"), new_window=False)
def get_compare_with_live_message_button(self):
return messages.button(
reverse(
"wagtailadmin_pages:revisions_compare",
args=(self.page.id, "live", self.latest_revision.id),
),
_("Compare with live version"),
)
def get_page_for_status(self):
if self.page.live and self.page.has_unpublished_changes:
# Page status needs to present the version of the page containing the correct live URL
return self.real_page_record.specific
else:
return self.page
def dispatch(self, request, page_id):
self.real_page_record = get_object_or_404(
Page.objects.prefetch_workflow_states(), id=page_id
)
self.latest_revision = self.real_page_record.get_latest_revision()
self.scheduled_revision = self.real_page_record.scheduled_revision
self.page_content_type = self.real_page_record.cached_content_type
self.page_class = self.real_page_record.specific_class
if self.page_class is None:
raise PageClassNotFoundError(
f"The page '{self.real_page_record}' cannot be edited because the "
f"model class used to create it ({self.page_content_type.app_label}."
f"{self.page_content_type.model}) can no longer be found in the codebase. "
"This usually happens as a result of switching between git "
"branches without running migrations to trigger the removal of "
"unused ContentTypes. To edit the page, you will need to switch "
"back to a branch where the model class is still present."
)
self.page = self.real_page_record.get_latest_revision_as_object()
self.parent = self.page.get_parent()
self.scheduled_page = self.real_page_record.get_scheduled_revision_as_object()
self.page_perms = self.page.permissions_for_user(self.request.user)
self.lock = self.page.get_lock()
self.locked_for_user = self.lock is not None and self.lock.for_user(
self.request.user
)
if not self.page_perms.can_edit():
raise PermissionDenied
self.next_url = get_valid_next_url_from_request(self.request)
response = self.run_hook("before_edit_page", self.request, self.page)
if response:
return response
self.subscription, created = PageSubscription.objects.get_or_create(
page=self.page,
user=self.request.user,
defaults={
"comment_notifications": False,
},
)
self.edit_handler = self.page_class.get_edit_handler()
self.form_class = self.edit_handler.get_form_class()
if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
# Retrieve current workflow state if set, default to last workflow state
self.workflow_state = (
self.page.current_workflow_state
or self.page.workflow_states.order_by("created_at").last()
)
else:
self.workflow_state = None
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
self.locale = self.page.locale
self.translations = self.get_translations()
else:
self.locale = None
self.translations = []
if self.workflow_state:
self.workflow_tasks = self.workflow_state.all_tasks_with_status()
else:
self.workflow_tasks = []
self.errors_debug = None
return super().dispatch(request)
def get(self, request):
if self.lock:
lock_message = self.lock.get_message(self.request.user)
if lock_message:
if isinstance(self.lock, BasicLock) and self.page_perms.can_unlock():
lock_message = format_html(
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
lock_message,
reverse("wagtailadmin_pages:unlock", args=(self.page.id,)),
_("Unlock"),
)
if (
isinstance(self.lock, ScheduledForPublishLock)
and self.page_perms.can_unschedule()
):
lock_message = format_html(
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
lock_message,
reverse(
"wagtailadmin_pages:revisions_unschedule",
args=[self.page.id, self.scheduled_revision.pk],
),
_("Cancel scheduled publish"),
)
if (
not isinstance(self.lock, ScheduledForPublishLock)
and self.locked_for_user
):
messages.warning(self.request, lock_message, extra_tags="lock")
else:
messages.info(self.request, lock_message, extra_tags="lock")
self.form = self.form_class(
instance=self.page,
subscription=self.subscription,
parent_page=self.parent,
for_user=self.request.user,
)
self.has_unsaved_changes = False
self.page_for_status = self.get_page_for_status()
return self.render_to_response(self.get_context_data())
def add_cancel_workflow_confirmation_message(self):
message = _("Workflow on page '%(page_title)s' has been cancelled.") % {
"page_title": self.page.get_admin_display_title()
}
messages.success(
self.request,
message,
buttons=[
self.get_view_draft_message_button(),
self.get_edit_message_button(),
],
)
def post(self, request):
# Don't allow POST requests if the page is an alias
if self.page.alias_of_id:
# Return 405 "Method Not Allowed" response
return HttpResponse(status=405)
self.form = self.form_class(
self.request.POST,
self.request.FILES,
instance=self.page,
subscription=self.subscription,
parent_page=self.parent,
for_user=self.request.user,
)
self.is_cancelling_workflow = (
bool(self.request.POST.get("action-cancel-workflow"))
and self.workflow_state
and self.workflow_state.user_can_cancel(self.request.user)
)
if self.form.is_valid() and not self.locked_for_user:
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
def workflow_action_is_valid(self):
self.workflow_action = self.request.POST["workflow-action-name"]
available_actions = self.page.current_workflow_task.get_actions(
self.page, self.request.user
)
available_action_names = [
name for name, verbose_name, modal in available_actions
]
return self.workflow_action in available_action_names
def form_valid(self, form):
self.is_reverting = bool(self.request.POST.get("revision"))
# If a revision ID was passed in the form, get that revision so its
# date can be referenced in notification messages
if self.is_reverting:
self.previous_revision = get_object_or_404(
self.page.revisions, id=self.request.POST.get("revision")
)
self.has_content_changes = self.form.has_changed()
if self.request.POST.get("action-publish") and self.page_perms.can_publish():
return self.publish_action()
elif (
self.request.POST.get("action-submit")
and self.page_perms.can_submit_for_moderation()
):
return self.submit_action()
elif (
self.request.POST.get("action-restart-workflow")
and self.page_perms.can_submit_for_moderation()
and self.workflow_state
and self.workflow_state.user_can_cancel(self.request.user)
):
return self.restart_workflow_action()
elif (
self.request.POST.get("action-workflow-action")
and self.workflow_action_is_valid()
):
return self.perform_workflow_action()
elif self.is_cancelling_workflow:
return self.cancel_workflow_action()
else:
return self.save_action()
def save_action(self):
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
# Save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(self.previous_revision if self.is_reverting else None),
)
self.add_save_confirmation_message()
if self.has_content_changes and "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# Just saving - remain on edit page for further edits
return self.redirect_and_remain()
def publish_action(self):
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
# Save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(self.previous_revision if self.is_reverting else None),
)
# store submitted go_live_at for messaging below
go_live_at = self.page.go_live_at
response = self.run_hook("before_publish_page", self.request, self.page)
if response:
return response
action = PublishPageRevisionAction(
revision,
user=self.request.user,
changed=self.has_content_changes,
previous_revision=(self.previous_revision if self.is_reverting else None),
)
action.execute(skip_permission_checks=True)
if self.has_content_changes and "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
# Need to reload the page because the URL may have changed, and we
# need the up-to-date URL for the "View Live" button.
self.page = self.page.specific_class.objects.get(pk=self.page.pk)
response = self.run_hook("after_publish_page", self.request, self.page)
if response:
return response
# Notifications
if go_live_at and go_live_at > timezone.now():
# Page has been scheduled for publishing in the future
if self.is_reverting:
message = _(
"Version from %(previous_revision_datetime)s "
"of page '%(page_title)s' has been scheduled for publishing."
) % {
"previous_revision_datetime": render_timestamp(
self.previous_revision.created_at
),
"page_title": self.page.get_admin_display_title(),
}
else:
if self.page.live:
message = _(
"Page '%(page_title)s' is live and this version has been scheduled for publishing."
) % {"page_title": self.page.get_admin_display_title()}
else:
message = _(
"Page '%(page_title)s' has been scheduled for publishing."
) % {"page_title": self.page.get_admin_display_title()}
messages.success(
self.request, message, buttons=[self.get_edit_message_button()]
)
else:
# Page is being published now
if self.is_reverting:
message = _(
"Version from %(datetime)s of page '%(page_title)s' has been published."
) % {
"datetime": render_timestamp(self.previous_revision.created_at),
"page_title": self.page.get_admin_display_title(),
}
else:
message = _("Page '%(page_title)s' has been published.") % {
"page_title": self.page.get_admin_display_title()
}
buttons = []
if self.page.url is not None:
buttons.append(self.get_view_live_message_button())
buttons.append(self.get_edit_message_button())
messages.success(self.request, message, buttons=buttons)
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# we're done here - redirect back to the explorer
return self.redirect_away()
def submit_action(self):
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
# Save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(self.previous_revision if self.is_reverting else None),
)
if self.has_content_changes and "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
if (
self.workflow_state
and self.workflow_state.status == WorkflowState.STATUS_NEEDS_CHANGES
):
# If the workflow was in the needs changes state, resume the existing workflow on submission
self.workflow_state.resume(self.request.user)
else:
# Otherwise start a new workflow
workflow = self.page.get_workflow()
workflow.start(self.page, self.request.user)
message = _("Page '%(page_title)s' has been submitted for moderation.") % {
"page_title": self.page.get_admin_display_title()
}
messages.success(
self.request,
message,
buttons=[
self.get_view_draft_message_button(),
self.get_edit_message_button(),
],
)
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# we're done here - redirect back to the explorer
return self.redirect_away()
def restart_workflow_action(self):
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
# save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(self.previous_revision if self.is_reverting else None),
)
if self.has_content_changes and "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
# cancel workflow
self.workflow_state.cancel(user=self.request.user)
# start new workflow
workflow = self.page.get_workflow()
workflow.start(self.page, self.request.user)
message = _("Workflow on page '%(page_title)s' has been restarted.") % {
"page_title": self.page.get_admin_display_title()
}
messages.success(
self.request,
message,
buttons=[
self.get_view_draft_message_button(),
self.get_edit_message_button(),
],
)
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# we're done here - redirect back to the explorer
return self.redirect_away()
def perform_workflow_action(self):
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
if self.has_content_changes:
# Save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(
self.previous_revision if self.is_reverting else None
),
)
if "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
extra_workflow_data_json = self.request.POST.get(
"workflow-action-extra-data", "{}"
)
extra_workflow_data = json.loads(extra_workflow_data_json)
self.page.current_workflow_task.on_action(
self.page.current_workflow_task_state,
self.request.user,
self.workflow_action,
**extra_workflow_data,
)
self.add_save_confirmation_message()
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# we're done here - redirect back to the explorer
return self.redirect_away()
def cancel_workflow_action(self):
self.workflow_state.cancel(user=self.request.user)
self.page = self.form.save(commit=not self.page.live)
self.subscription.save()
# Save revision
revision = self.page.save_revision(
user=self.request.user,
log_action=True, # Always log the new revision on edit
previous_revision=(self.previous_revision if self.is_reverting else None),
)
if self.has_content_changes and "comments" in self.form.formsets:
changes = self.get_commenting_changes()
self.log_commenting_changes(changes, revision)
self.send_commenting_notifications(changes)
# Notifications
self.add_cancel_workflow_confirmation_message()
response = self.run_hook("after_edit_page", self.request, self.page)
if response:
return response
# Just saving - remain on edit page for further edits
return self.redirect_and_remain()
def redirect_away(self):
if self.next_url:
# redirect back to 'next' url if present
return redirect(self.next_url)
else:
# redirect back to the explorer
return redirect("wagtailadmin_explore", self.page.get_parent().id)
def redirect_and_remain(self):
target_url = reverse("wagtailadmin_pages:edit", args=[self.page.id])
if self.next_url:
# Ensure the 'next' url is passed through again if present
target_url += "?next=%s" % quote(self.next_url)
return redirect(target_url)
def form_invalid(self, form):
# even if the page is locked due to not having permissions, the original submitter can still cancel the workflow
if self.is_cancelling_workflow:
self.workflow_state.cancel(user=self.request.user)
self.add_cancel_workflow_confirmation_message()
# Refresh the lock object as now WorkflowLock no longer applies
self.lock = self.page.get_lock()
self.locked_for_user = self.lock is not None and self.lock.for_user(
self.request.user
)
elif self.locked_for_user:
messages.error(
self.request, _("The page could not be saved as it is locked")
)
else:
messages.validation_error(
self.request,
_("The page could not be saved due to validation errors"),
self.form,
)
self.errors_debug = repr(self.form.errors) + repr(
[
(name, formset.errors)
for (name, formset) in self.form.formsets.items()
if formset.errors
]
)
self.has_unsaved_changes = True
self.page_for_status = self.get_page_for_status()
return self.render_to_response(self.get_context_data())
def get_preview_url(self):
return reverse("wagtailadmin_pages:preview_on_edit", args=[self.page.id])
def get_history_url(self):
permissions = self.page.permissions_for_user(self.request.user)
if permissions.can_view_revisions():
return reverse("wagtailadmin_pages:history", args=[self.page.id])
def get_side_panels(self):
side_panels = [
PageStatusSidePanel(
self.page,
self.request,
show_schedule_publishing_toggle=self.form.show_schedule_publishing_toggle,
live_object=self.real_page_record,
scheduled_object=self.scheduled_page,
locale=self.locale,
translations=self.translations,
),
]
if self.page.is_previewable():
side_panels.append(
PreviewSidePanel(
self.page, self.request, preview_url=self.get_preview_url()
)
)
side_panels.append(ChecksSidePanel(self.page, self.request))
if self.form.show_comments_toggle:
side_panels.append(CommentsSidePanel(self.page, self.request))
return MediaContainer(side_panels)
def get_editing_sessions(self):
EditingSession.cleanup()
content_type = get_default_page_content_type()
session = EditingSession.objects.create(
user=self.request.user,
content_type=content_type,
object_id=self.page.pk,
last_seen_at=timezone.now(),
)
return EditingSessionsModule(
session,
reverse(
"wagtailadmin_editing_sessions:ping",
args=("wagtailcore", "page", self.page.pk, session.id),
),
reverse(
"wagtailadmin_editing_sessions:release",
args=(session.id,),
),
[],
self.page.latest_revision_id,
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user_perms = self.page.permissions_for_user(self.request.user)
bound_panel = self.edit_handler.get_bound_panel(
instance=self.page, request=self.request, form=self.form
)
action_menu = PageActionMenu(
self.request,
view="edit",
page=self.page,
lock=self.lock,
locked_for_user=self.locked_for_user,
)
side_panels = self.get_side_panels()
media = MediaContainer([bound_panel, self.form, action_menu, side_panels]).media
context.update(
{
"page": self.page,
"page_for_status": self.page_for_status,
"content_type": self.page_content_type,
"edit_handler": bound_panel,
"errors_debug": self.errors_debug,
"action_menu": action_menu,
"side_panels": side_panels,
"form": self.form,
"next": self.next_url,
"history_url": self.get_history_url(),
"has_unsaved_changes": self.has_unsaved_changes,
"page_locked": self.locked_for_user,
"workflow_state": self.workflow_state
if self.workflow_state and self.workflow_state.is_active
else None,
"current_task_state": self.page.current_workflow_task_state,
"publishing_will_cancel_workflow": self.workflow_tasks
and getattr(settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True),
"confirm_workflow_cancellation_url": reverse(
"wagtailadmin_pages:confirm_workflow_cancellation",
args=(self.page.id,),
),
"user_can_lock": (not self.lock or isinstance(self.lock, WorkflowLock))
and user_perms.can_lock(),
"user_can_unlock": isinstance(self.lock, BasicLock)
and user_perms.can_unlock(),
"locale": self.locale,
"media": media,
"editing_sessions": self.get_editing_sessions(),
}
)
return context
def get_translations(self):
return [
{
"locale": translation.locale,
"url": reverse("wagtailadmin_pages:edit", args=[translation.id]),
}
for translation in self.page.get_translations()
.only("id", "locale", "depth")
.select_related("locale")
if translation.permissions_for_user(self.request.user).can_edit()
]

View File

@@ -0,0 +1,95 @@
import django_filters
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy
from wagtail.admin.views.generic import history
from wagtail.admin.views.pages.utils import (
GenericPageBreadcrumbsMixin,
)
from wagtail.admin.widgets import BooleanRadioSelect
from wagtail.models import Page, PageLogEntry
from wagtail.permissions import page_permission_policy
class PageHistoryFilterSet(history.HistoryFilterSet):
is_commenting_action = django_filters.BooleanFilter(
label=gettext_lazy("Is commenting action"),
method="filter_is_commenting_action",
widget=BooleanRadioSelect,
)
def filter_is_commenting_action(self, queryset, name, value):
if value is None:
return queryset
q = Q(action__startswith="wagtail.comments")
if value is False:
q = ~q
return queryset.filter(q)
class PageWorkflowHistoryViewMixin:
model = Page
pk_url_kwarg = "page_id"
def dispatch(self, request, *args, **kwargs):
if not self.object.permissions_for_user(request.user).can_edit():
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs, page=self.object)
class WorkflowHistoryView(PageWorkflowHistoryViewMixin, history.WorkflowHistoryView):
header_icon = "doc-empty-inverse"
workflow_history_url_name = "wagtailadmin_pages:workflow_history"
workflow_history_detail_url_name = "wagtailadmin_pages:workflow_history_detail"
class WorkflowHistoryDetailView(
PageWorkflowHistoryViewMixin, history.WorkflowHistoryDetailView
):
object_icon = "doc-empty-inverse"
workflow_history_url_name = "wagtailadmin_pages:workflow_history"
class PageHistoryView(GenericPageBreadcrumbsMixin, history.HistoryView):
template_name = "wagtailadmin/pages/history.html"
filterset_class = PageHistoryFilterSet
model = Page
pk_url_kwarg = "page_id"
permission_policy = page_permission_policy
any_permission_required = {
"add",
"change",
"publish",
"bulk_delete",
"lock",
"unlock",
}
history_url_name = "wagtailadmin_pages:history"
history_results_url_name = "wagtailadmin_pages:history_results"
edit_url_name = "wagtailadmin_pages:edit"
revisions_view_url_name = "wagtailadmin_pages:revisions_view"
revisions_revert_url_name = "wagtailadmin_pages:revisions_revert"
revisions_compare_url_name = "wagtailadmin_pages:revisions_compare"
revisions_unschedule_url_name = "wagtailadmin_pages:revisions_unschedule"
def get_object(self):
return get_object_or_404(Page, id=self.pk).specific
def get_page_subtitle(self):
return self.object.get_admin_display_title()
def user_can_unschedule(self):
return self.object.permissions_for_user(self.request.user).can_unschedule()
def get_base_queryset(self):
return self._annotate_queryset(PageLogEntry.objects.filter(page=self.object))
def _annotate_queryset(self, queryset):
return super()._annotate_queryset(queryset).select_related("page")

View File

@@ -0,0 +1,523 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import F
from django.forms import CheckboxSelectMultiple, RadioSelect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property, classproperty
from django.utils.translation import gettext_lazy as _
from django_filters.filters import (
ChoiceFilter,
DateFromToRangeFilter,
ModelMultipleChoiceFilter,
)
from wagtail import hooks
from wagtail.admin.filters import (
DateRangePickerWidget,
MultipleContentTypeFilter,
MultipleUserFilter,
WagtailFilterSet,
)
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.ui.components import MediaContainer
from wagtail.admin.ui.side_panels import (
PageStatusSidePanel,
)
from wagtail.admin.ui.tables import Column, DateColumn
from wagtail.admin.ui.tables.pages import (
BulkActionsColumn,
NavigateToChildrenColumn,
OrderingColumn,
PageStatusColumn,
PageTable,
PageTitleColumn,
)
from wagtail.admin.views import generic
from wagtail.models import Page, PageLogEntry, Site, get_page_content_types
from wagtail.permissions import page_permission_policy
class SiteFilter(ModelMultipleChoiceFilter):
def get_filter_predicate(self, v):
return {"path__startswith": v.root_page.path}
class HasChildPagesFilter(ChoiceFilter):
def filter(self, qs, value):
if value == "true":
return qs.filter(numchild__gt=0)
elif value == "false":
return qs.filter(numchild=0)
else: # None / empty string
return qs
class EditedByFilter(MultipleUserFilter):
def filter(self, qs, value):
if value:
qs = qs.filter(
pk__in=PageLogEntry.objects.filter(
action="wagtail.edit", user__in=value
)
.order_by()
.values_list("page_id", flat=True)
.distinct()
)
return qs
class PageFilterSet(WagtailFilterSet):
latest_revision_created_at = DateFromToRangeFilter(
label=_("Date updated"),
widget=DateRangePickerWidget,
)
owner = MultipleUserFilter(
label=_("Owner"),
queryset=(
lambda request: get_user_model().objects.filter(
pk__in=Page.objects.order_by()
.values_list("owner_id", flat=True)
.distinct()
)
),
widget=CheckboxSelectMultiple,
)
edited_by = EditedByFilter(
label=_("Edited by"),
queryset=(
lambda request: get_user_model().objects.filter(
pk__in=PageLogEntry.objects.filter(action="wagtail.edit")
.order_by()
.values_list("user_id", flat=True)
.distinct()
)
),
widget=CheckboxSelectMultiple,
)
site = SiteFilter(
label=_("Site"),
queryset=Site.objects.all(),
widget=CheckboxSelectMultiple,
)
has_child_pages = HasChildPagesFilter(
label=_("Has child pages"),
empty_label=_("Any"),
choices=[
("true", _("Yes")),
("false", _("No")),
],
widget=RadioSelect,
)
class Meta:
model = Page
fields = [] # only needed for filters being generated automatically
class ExplorablePageFilterSet(PageFilterSet):
content_type = MultipleContentTypeFilter(
label=_("Page type"),
queryset=lambda request: get_page_content_types(include_base_page_type=False),
widget=CheckboxSelectMultiple,
)
class IndexView(generic.IndexView):
template_name = "wagtailadmin/pages/index.html"
results_template_name = "wagtailadmin/pages/index_results.html"
permission_policy = page_permission_policy
any_permission_required = {
"add",
"change",
"publish",
"bulk_delete",
"lock",
"unlock",
}
context_object_name = "pages"
page_kwarg = "p"
paginate_by = 50
table_class = PageTable
table_classname = "listing full-width"
filterset_class = PageFilterSet
index_url_name = None
index_results_url_name = None
default_ordering = "-latest_revision_created_at"
model = Page
_show_breadcrumbs = True
columns = [
BulkActionsColumn("bulk_actions"),
PageTitleColumn(
"title",
label=_("Title"),
sort_key="title",
classname="title",
),
DateColumn(
"latest_revision_created_at",
label=_("Updated"),
sort_key="latest_revision_created_at",
width="12%",
),
PageStatusColumn(
"status",
label=_("Status"),
sort_key="live",
width="12%",
),
]
def get(self, request):
# Search
self.query_string = None
self.is_searching = False
if "q" in self.request.GET:
self.search_form = SearchForm(self.request.GET)
if self.search_form.is_valid():
self.query_string = self.search_form.cleaned_data["q"]
else:
self.search_form = SearchForm()
if self.query_string:
self.is_searching = True
return super().get(request)
def get_valid_orderings(self):
valid_orderings = super().get_valid_orderings()
if self.is_searching:
# ordering by content type not currently available when searching, due to
# https://github.com/wagtail/wagtail/issues/6616
try:
valid_orderings.remove("content_type")
valid_orderings.remove("-content_type")
except ValueError:
pass
return valid_orderings
def get_ordering(self):
if self.is_searching and not self.is_explicitly_ordered:
# default to ordering by relevance
default_ordering = None
else:
default_ordering = self.default_ordering
ordering = self.request.GET.get("ordering", default_ordering)
if ordering not in self.get_valid_orderings():
ordering = default_ordering
return ordering
def get_base_queryset(self):
pages = self.model.objects.filter(depth__gt=1)
pages = self._annotate_queryset(pages)
return pages
def _annotate_queryset(self, pages):
pages = pages.prefetch_related("content_type", "sites_rooted_here").filter(
pk__in=self.permission_policy.explorable_instances(
self.request.user
).values_list("pk", flat=True)
)
# We want specific page instances, but do not need streamfield values here
pages = pages.defer_streamfields().specific()
# Annotate queryset with various states to be used later for performance optimisations
if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
pages = pages.prefetch_workflow_states()
pages = pages.annotate_site_root_state().annotate_approved_schedule()
return pages
def order_queryset(self, queryset):
if self.is_searching and not self.is_explicitly_ordered:
# search backend will order by relevance in this case, so don't bother to
# apply an ordering on the queryset
return queryset
if self.ordering == "ord":
# preserve the native ordering from get_children()
pass
elif self.ordering == "latest_revision_created_at" and not self.is_searching:
# order by oldest revision first.
# Special case NULL entries - these should go at the top of the list.
# Skip this special case when searching (and fall through to plain field ordering
# instead) as search backends do not support F objects in order_by
queryset = queryset.order_by(
F("latest_revision_created_at").asc(nulls_first=True)
)
elif self.ordering == "-latest_revision_created_at" and not self.is_searching:
# order by oldest revision first.
# Special case NULL entries - these should go at the end of the list.
# Skip this special case when searching (and fall through to plain field ordering
# instead) as search backends do not support F objects in order_by
queryset = queryset.order_by(
F("latest_revision_created_at").desc(nulls_last=True)
)
else:
queryset = super().order_queryset(queryset)
return queryset
def search_queryset(self, queryset):
if self.is_searching:
queryset = queryset.autocomplete(
self.query_string, order_by_relevance=(not self.is_explicitly_ordered)
)
return queryset
def get_index_url(self):
return reverse(self.index_url_name)
def get_index_results_url(self):
return reverse(self.index_results_url_name)
def get_breadcrumbs_items(self):
return self.breadcrumbs_items + [{"url": "", "label": self.get_page_title()}]
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs["actions_next_url"] = self.index_url
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
"ordering": self.ordering,
"search_form": self.search_form,
"is_searching": self.is_searching,
}
)
return context
class ExplorableIndexView(IndexView):
"""
A version of the page listing where the user is presented with a view of a specified parent page;
normally this will show the children of that page, but it may show results from the whole tree while
searching or filtering.
"""
template_name = "wagtailadmin/pages/explorable_index.html"
index_url_name = "wagtailadmin_explore"
index_results_url_name = "wagtailadmin_explore_results"
page_title = _("Exploring")
filterset_class = ExplorablePageFilterSet
@classproperty
def columns(cls):
columns = super().columns.copy()
columns.insert(
3,
Column(
"type",
label=_("Type"),
accessor="page_type_display_name",
sort_key="content_type",
width="12%",
),
)
columns.append(NavigateToChildrenColumn("navigate", width="10%"))
return columns
def get(self, request, parent_page_id=None):
if parent_page_id:
self.parent_page = get_object_or_404(
Page.objects.all().prefetch_workflow_states(), id=parent_page_id
)
else:
self.parent_page = Page.get_first_root_node()
# This will always succeed because of the check performed by PermissionCheckedMixin.
root_page = self.permission_policy.explorable_root_instance(request.user)
# If this page isn't a descendant of the user's explorable root page,
# then redirect to that explorable root page instead.
if not (
self.parent_page.pk == root_page.pk
or self.parent_page.is_descendant_of(root_page)
):
return redirect(self.index_url_name, root_page.pk)
self.parent_page = self.parent_page.specific
self.scheduled_page = self.parent_page.get_scheduled_revision_as_object()
self.i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
if self.i18n_enabled and not self.parent_page.is_root():
self.locale = self.parent_page.locale
self.translations = self.get_translations()
else:
self.locale = None
self.translations = []
return super().get(request)
@cached_property
def is_searching_whole_tree(self):
return bool(self.request.GET.get("search_all")) and (
self.is_searching or self.is_filtering
)
def get_base_queryset(self):
if self.is_searching or self.is_filtering:
if self.is_searching_whole_tree:
pages = Page.objects.all()
else:
pages = self.parent_page.get_descendants()
else:
pages = self.parent_page.get_children()
pages = self._annotate_queryset(pages)
return pages
def search_queryset(self, queryset):
# allow hooks to modify queryset. This should happen as close as possible to the
# final queryset, but (for backward compatibility) needs to be passed an actual queryset
# rather than a search result object
for hook in hooks.get_hooks("construct_explorer_page_queryset"):
queryset = hook(self.parent_page, queryset, self.request)
return super().search_queryset(queryset)
def get_index_url(self):
return reverse(self.index_url_name, args=[self.parent_page.id])
def get_index_results_url(self):
return reverse(self.index_results_url_name, args=[self.parent_page.id])
def get_history_url(self):
permissions = self.parent_page.permissions_for_user(self.request.user)
if permissions.can_view_revisions():
return reverse("wagtailadmin_pages:history", args=[self.parent_page.id])
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs["use_row_ordering_attributes"] = self.show_ordering_column
kwargs["parent_page"] = self.parent_page
kwargs["show_locale_labels"] = self.i18n_enabled and self.parent_page.is_root()
if self.show_ordering_column:
kwargs["caption"] = _(
"Focus on the drag button and press up or down arrows to move the item, then press enter to submit the change."
)
kwargs["attrs"] = {
"data-controller": "w-orderable",
"data-w-orderable-active-class": "w-orderable--active",
"data-w-orderable-chosen-class": "w-orderable__item--active",
"data-w-orderable-container-value": "tbody",
"data-w-orderable-message-value": _(
"'%(page_title)s' has been moved successfully."
)
% {"page_title": "__LABEL__"},
"data-w-orderable-url-value": reverse(
"wagtailadmin_pages:set_page_position", args=[999999]
),
}
return kwargs
def get_valid_orderings(self):
valid_orderings = super().get_valid_orderings()
if not self.is_searching:
# ordering by page order is only available when not searching
valid_orderings.append("ord")
return valid_orderings
def get_ordering(self):
if self.is_searching and not self.is_explicitly_ordered:
# default to ordering by relevance
default_ordering = None
else:
default_ordering = self.parent_page.get_admin_default_ordering()
ordering = self.request.GET.get("ordering", default_ordering)
if ordering not in self.get_valid_orderings():
ordering = default_ordering
return ordering
def get_paginate_by(self, queryset):
if self.ordering == "ord":
# Don't paginate if sorting by page order - all pages must be shown to
# allow drag-and-drop reordering
return None
else:
return self.paginate_by
def get_page_subtitle(self):
return self.parent_page.get_admin_display_title()
def get_context_data(self, **kwargs):
self.show_ordering_column = self.ordering == "ord"
if self.show_ordering_column:
self.columns = self.columns.copy()
self.columns[0] = OrderingColumn("ordering", width="80px", sort_key="ord")
context = super().get_context_data(**kwargs)
if self.is_searching:
# postprocess this page of results to annotate each result with its parent page
parent_page_paths = {
page.path[: -page.steplen] for page in context["object_list"]
}
parent_pages_by_path = {
page.path: page
for page in Page.objects.filter(path__in=parent_page_paths).specific()
}
for page in context["object_list"]:
parent_page = parent_pages_by_path.get(page.path[: -page.steplen])
# add annotation if parent page is found and is not the currently viewed parent
if parent_page and parent_page != self.parent_page:
page.annotated_parent_page = parent_page
context.update(
{
"parent_page": self.parent_page,
"history_url": self.get_history_url(),
"is_searching_whole_tree": self.is_searching_whole_tree,
}
)
if not self.results_only:
side_panels = self.get_side_panels()
context["side_panels"] = side_panels
context["media"] += side_panels.media
return context
def get_side_panels(self):
# Don't show side panels on the root page
if self.parent_page.is_root():
return MediaContainer()
side_panels = [
PageStatusSidePanel(
self.parent_page.get_latest_revision_as_object(),
self.request,
show_schedule_publishing_toggle=False,
live_object=self.parent_page,
scheduled_object=self.scheduled_page,
locale=self.locale,
translations=self.translations,
),
]
return MediaContainer(side_panels)
def get_translations(self):
return [
{
"locale": translation.locale,
"url": reverse(self.index_url_name, args=[translation.id]),
}
for translation in self.parent_page.get_translations()
.only("id", "locale")
.select_related("locale")
]

View File

@@ -0,0 +1,38 @@
from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.utils.translation import gettext as _
from wagtail.admin.views.generic import lock
from wagtail.models import Page
class PageOperationViewMixin:
model = Page
pk_url_kwarg = "page_id"
def get_object(self):
return super().get_object().specific
def get_success_url(self):
if self.next_url:
return self.next_url
return reverse("wagtailadmin_explore", args=[self.object.get_parent().id])
class LockView(PageOperationViewMixin, lock.LockView):
def perform_operation(self):
if not self.object.permissions_for_user(self.request.user).can_lock():
raise PermissionDenied
return super().perform_operation()
class UnlockView(PageOperationViewMixin, lock.UnlockView):
def perform_operation(self):
if not self.object.permissions_for_user(self.request.user).can_unlock():
raise PermissionDenied
return super().perform_operation()
def get_success_message(self):
return _("Page '%(page_title)s' is now unlocked.") % {
"page_title": self.object.get_admin_display_title()
}

View File

@@ -0,0 +1,142 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from wagtail import hooks
from wagtail.actions.move_page import MovePageAction
from wagtail.admin import messages
from wagtail.admin.forms.pages import MoveForm
from wagtail.models import Page
def move_choose_destination(request, page_to_move_id):
page_to_move = get_object_or_404(Page, id=page_to_move_id)
page_perms = page_to_move.permissions_for_user(request.user)
if not page_perms.can_move():
raise PermissionDenied
target_parent_models = set(page_to_move.specific_class.allowed_parent_page_models())
move_form = MoveForm(
request.POST or None,
page_to_move=page_to_move,
target_parent_models=target_parent_models,
)
if request.method == "POST":
if move_form.is_valid():
# Receive the new parent page (this should never be empty)
if move_form.cleaned_data["new_parent_page"]:
new_parent_page = move_form.cleaned_data["new_parent_page"]
return redirect(
"wagtailadmin_pages:move_confirm",
page_to_move.id,
new_parent_page.id,
)
return TemplateResponse(
request,
"wagtailadmin/pages/move_choose_destination.html",
{
"page_to_move": page_to_move,
"move_form": move_form,
},
)
def move_confirm(request, page_to_move_id, destination_id):
page_to_move = get_object_or_404(Page, id=page_to_move_id).specific
# Needs .specific_deferred because the .get_admin_display_title method is called in template
destination = get_object_or_404(Page, id=destination_id).specific_deferred
if not Page._slug_is_available(page_to_move.slug, destination, page=page_to_move):
messages.error(
request,
_(
"The slug '%(page_slug)s' is already in use at the selected parent page. Make sure the slug is unique and try again"
)
% {"page_slug": page_to_move.slug},
)
return redirect(
"wagtailadmin_pages:move",
page_to_move.id,
)
for fn in hooks.get_hooks("before_move_page"):
result = fn(request, page_to_move, destination)
if hasattr(result, "status_code"):
return result
pages_to_move = {page_to_move}
# The `construct_translated_pages_to_cascade_actions` hook returns translation and
# alias pages when the action is set to "move"
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
fn_pages = fn([page_to_move], "move")
if fn_pages and isinstance(fn_pages, dict):
for additional_pages in fn_pages.values():
pages_to_move.update(additional_pages)
pages_to_move = list(pages_to_move)
if request.method == "POST":
# any invalid moves *should* be caught by the permission check in the action
# class, so don't bother to catch InvalidMoveToDescendant
action = MovePageAction(
page_to_move, destination, pos="last-child", user=request.user
)
action.execute()
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
# Move translation and alias pages if they have the same parent page.
parent_page_translations = page_to_move.get_parent().get_translations()
for translation in pages_to_move:
if translation.get_parent() in parent_page_translations:
# Move the translated or alias page to it's translated or
# alias "destination" page.
action = MovePageAction(
translation,
destination.get_translation(translation.locale),
pos="last-child",
user=request.user,
)
action.execute()
messages.success(
request,
_("Page '%(page_title)s' moved.")
% {"page_title": page_to_move.get_admin_display_title()},
buttons=[
messages.button(
reverse("wagtailadmin_pages:edit", args=(page_to_move.id,)),
_("Edit"),
)
],
)
for fn in hooks.get_hooks("after_move_page"):
result = fn(request, page_to_move)
if hasattr(result, "status_code"):
return result
return redirect("wagtailadmin_explore", destination.id)
return TemplateResponse(
request,
"wagtailadmin/pages/confirm_move.html",
{
"page_to_move": page_to_move,
"destination": destination,
"translations_to_move_count": len(
[
translation.id
for translation in pages_to_move
if not translation.alias_of_id and translation.id != page_to_move.id
]
),
},
)

View File

@@ -0,0 +1,44 @@
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from wagtail.models import Page
def set_page_position(request, page_to_move_id):
page_to_move = get_object_or_404(Page, id=page_to_move_id)
parent_page = page_to_move.get_parent()
if not parent_page.permissions_for_user(request.user).can_reorder_children():
raise PermissionDenied
if request.method == "POST":
# Get position parameter
position = request.GET.get("position", None)
# Find page that's already in this position
position_page = None
if position is not None:
try:
position_page = parent_page.get_children()[int(position)]
except IndexError:
pass # No page in this position
# Move page
# any invalid moves *should* be caught by the permission check above,
# so don't bother to catch InvalidMoveToDescendant
if position_page:
# If the page has been moved to the right, insert it to the
# right. If left, then left.
old_position = list(parent_page.get_children()).index(page_to_move)
if int(position) < old_position:
page_to_move.move(position_page, pos="left", user=request.user)
elif int(position) > old_position:
page_to_move.move(position_page, pos="right", user=request.user)
else:
# Move page to end
page_to_move.move(parent_page, pos="last-child", user=request.user)
return HttpResponse("")

View File

@@ -0,0 +1,103 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404
from wagtail.admin.views.generic.preview import PreviewOnEdit as GenericPreviewOnEdit
from wagtail.models import Page
def view_draft(request, page_id):
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_object()
perms = page.permissions_for_user(request.user)
if not (perms.can_publish() or perms.can_edit()):
raise PermissionDenied
try:
preview_mode = page.default_preview_mode
except IndexError:
raise PermissionDenied
return page.make_preview_request(request, preview_mode)
class PreviewOnEdit(GenericPreviewOnEdit):
@property
def session_key(self):
return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"])
def get_object(self):
return get_object_or_404(
Page, id=self.kwargs["page_id"]
).get_latest_revision_as_object()
def get_form(self, query_dict):
form_class = self.object.get_edit_handler().get_form_class()
parent_page = self.object.get_parent().specific
if not query_dict:
# Query dict is empty, return null form
return form_class(
instance=self.object,
parent_page=parent_page,
for_user=self.request.user,
)
return form_class(
query_dict,
instance=self.object,
parent_page=parent_page,
for_user=self.request.user,
)
class PreviewOnCreate(PreviewOnEdit):
@property
def session_key(self):
return "{}{}-{}-{}".format(
self.session_key_prefix,
self.kwargs["content_type_app_name"],
self.kwargs["content_type_model_name"],
self.kwargs["parent_page_id"],
)
def get_object(self):
content_type_app_name = self.kwargs["content_type_app_name"]
content_type_model_name = self.kwargs["content_type_model_name"]
parent_page_id = self.kwargs["parent_page_id"]
try:
content_type = ContentType.objects.get_by_natural_key(
content_type_app_name, content_type_model_name
)
except ContentType.DoesNotExist:
raise Http404
page = content_type.model_class()()
parent_page = get_object_or_404(Page, id=parent_page_id).specific
# We need to populate treebeard's path / depth fields in order to
# pass validation. We can't make these 100% consistent with the rest
# of the tree without making actual database changes (such as
# incrementing the parent's numchild field), but by calling treebeard's
# internal _get_path method, we can set a 'realistic' value that will
# hopefully enable tree traversal operations
# to at least partially work.
page.depth = parent_page.depth + 1
# Puts the page at the next available path
# for a child of `parent_page`.
if parent_page.is_leaf():
# set the path as the first child of parent_page
page.path = page._get_path(parent_page.path, page.depth, 1)
else:
# add the new page after the last child of parent_page
page.path = parent_page.get_last_child()._inc_path()
return page
def get_form(self, query_dict):
form = super().get_form(query_dict)
if form.is_valid():
# Ensures our unsaved page has a suitable url.
form.instance.set_url_path(form.parent_page)
form.instance.full_clean()
return form

View File

@@ -0,0 +1,206 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from wagtail.admin import messages
from wagtail.admin.action_menu import PageActionMenu
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
from wagtail.admin.ui.components import MediaContainer
from wagtail.admin.ui.side_panels import (
ChecksSidePanel,
CommentsSidePanel,
PageStatusSidePanel,
PreviewSidePanel,
)
from wagtail.admin.views.generic.models import (
RevisionsCompareView,
RevisionsUnscheduleView,
)
from wagtail.admin.views.generic.preview import PreviewRevision
from wagtail.models import Page
from wagtail.utils.timestamps import render_timestamp
def revisions_index(request, page_id):
return redirect("wagtailadmin_pages:history", page_id)
def revisions_revert(request, page_id, revision_id):
# TODO: refactor this into a class-based view that extends the EditView
page = get_object_or_404(Page, id=page_id).specific
page_perms = page.permissions_for_user(request.user)
if not page_perms.can_edit():
raise PermissionDenied
revision = get_object_or_404(page.revisions, id=revision_id)
revision_page = revision.as_object()
scheduled_page = page.get_scheduled_revision_as_object()
content_type = ContentType.objects.get_for_model(page)
page_class = content_type.model_class()
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
locale = page.locale
translations = [
{
"locale": translation.locale,
"url": reverse("wagtailadmin_pages:edit", args=[translation.id]),
}
for translation in page.get_translations()
.only("id", "locale", "depth")
.select_related("locale")
if translation.permissions_for_user(request.user).can_edit()
]
else:
locale = None
translations = []
edit_handler = page_class.get_edit_handler()
form_class = edit_handler.get_form_class()
form = form_class(instance=revision_page, for_user=request.user)
edit_handler = edit_handler.get_bound_panel(
instance=revision_page, request=request, form=form
)
preview_url = reverse("wagtailadmin_pages:preview_on_edit", args=[page.id])
lock = page.get_lock()
action_menu = PageActionMenu(
request,
view="revisions_revert",
is_revision=True,
page=page,
lock=lock,
locked_for_user=lock is not None and lock.for_user(request.user),
)
side_panels = [
PageStatusSidePanel(
revision_page,
request,
show_schedule_publishing_toggle=form.show_schedule_publishing_toggle,
live_object=page,
scheduled_object=scheduled_page,
locale=locale,
translations=translations,
),
]
if page.is_previewable():
side_panels.append(PreviewSidePanel(page, request, preview_url=preview_url))
side_panels.append(ChecksSidePanel(page, request))
if form.show_comments_toggle:
side_panels.append(CommentsSidePanel(page, request))
side_panels = MediaContainer(side_panels)
media = MediaContainer([edit_handler, form, action_menu, side_panels]).media
user_avatar = render_to_string(
"wagtailadmin/shared/user_avatar.html", {"user": revision.user}
)
messages.warning(
request,
mark_safe(
_(
"You are viewing a previous version of this page from <b>%(created_at)s</b> by %(user)s"
)
% {
"created_at": render_timestamp(revision.created_at),
"user": user_avatar,
}
),
)
page_title = _("Editing %(page_type)s") % {
"page_type": page_class.get_verbose_name()
}
page_subtitle = page.get_admin_display_title()
header_title = f"{page_title}: {page_subtitle}"
return TemplateResponse(
request,
"wagtailadmin/pages/edit.html",
{
"page": page,
"revision": revision,
"is_revision": True,
"content_type": content_type,
"edit_handler": edit_handler,
"errors_debug": None,
"action_menu": action_menu,
"side_panels": side_panels,
"header_title": header_title,
"form": form, # Used in unit tests
"media": media,
},
)
@method_decorator(user_passes_test(user_has_any_page_permission), name="dispatch")
class RevisionsView(PreviewRevision):
model = Page
def setup(self, request, page_id, revision_id, *args, **kwargs):
# Rename path kwargs from pk to page_id
return super().setup(request, page_id, revision_id, *args, **kwargs)
def get_object(self):
page = get_object_or_404(Page, id=self.pk).specific
perms = page.permissions_for_user(self.request.user)
if not (perms.can_publish() or perms.can_edit()):
raise PermissionDenied
return page
class RevisionsCompare(RevisionsCompareView):
history_label = gettext_lazy("Page history")
edit_label = gettext_lazy("Edit this page")
history_url_name = "wagtailadmin_pages:history"
edit_url_name = "wagtailadmin_pages:edit"
header_icon = "doc-empty-inverse"
@method_decorator(user_passes_test(user_has_any_page_permission))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
return get_object_or_404(Page, id=self.pk).specific
def get_edit_handler(self):
return self.object.get_edit_handler()
def get_page_subtitle(self):
return self.object.get_admin_display_title()
class RevisionsUnschedule(RevisionsUnscheduleView):
model = Page
edit_url_name = "wagtailadmin_pages:edit"
history_url_name = "wagtailadmin_pages:history"
revisions_unschedule_url_name = "wagtailadmin_pages:revisions_unschedule"
header_icon = "doc-empty-inverse"
def setup(self, request, page_id, revision_id, *args, **kwargs):
# Rename path kwargs from pk to page_id
return super().setup(request, page_id, revision_id, *args, **kwargs)
def get_object(self, queryset=None):
page = get_object_or_404(Page, id=self.pk).specific
if not page.permissions_for_user(self.request.user).can_unschedule():
raise PermissionDenied
return page
def get_object_display_title(self):
return self.object.get_admin_display_title()

View File

@@ -0,0 +1,192 @@
from typing import Any, Dict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.ui.tables import Column, DateColumn
from wagtail.admin.ui.tables.pages import (
BulkActionsColumn,
NavigateToChildrenColumn,
PageStatusColumn,
PageTable,
PageTitleColumn,
ParentPageColumn,
)
from wagtail.admin.views.generic.base import BaseListingView
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
from wagtail.models import Page
from wagtail.permissions import page_permission_policy
from wagtail.search.query import MATCH_ALL
from wagtail.search.utils import parse_query_string
def page_filter_search(q, pages, all_pages=None, ordering=None):
# Parse query
filters, query = parse_query_string(q, operator="and", zero_terms=MATCH_ALL)
# Live filter
live_filter = filters.get("live") or filters.get("published")
live_filter = live_filter and live_filter.lower()
if live_filter in ["yes", "true"]:
if all_pages is not None:
all_pages = all_pages.filter(live=True)
pages = pages.filter(live=True)
elif live_filter in ["no", "false"]:
if all_pages is not None:
all_pages = all_pages.filter(live=False)
pages = pages.filter(live=False)
# Search
if all_pages is not None:
all_pages = all_pages.autocomplete(query, order_by_relevance=not ordering)
pages = pages.autocomplete(query, order_by_relevance=not ordering)
return pages, all_pages
class BaseSearchView(PermissionCheckedMixin, BaseListingView):
permission_policy = page_permission_policy
any_permission_required = {
"add",
"change",
"publish",
"bulk_delete",
"lock",
"unlock",
}
paginate_by = 20
page_kwarg = "p"
context_object_name = "pages"
table_class = PageTable
index_url_name = "wagtailadmin_pages:search"
columns = [
BulkActionsColumn("bulk_actions"),
PageTitleColumn(
"title",
classname="title",
label=_("Title"),
sort_key="title",
),
ParentPageColumn("parent", label=_("Parent")),
DateColumn(
"latest_revision_created_at",
label=_("Updated"),
sort_key="latest_revision_created_at",
width="12%",
),
Column(
"type",
label=_("Type"),
accessor="page_type_display_name",
width="12%",
),
PageStatusColumn(
"status",
label=_("Status"),
sort_key="live",
width="12%",
),
NavigateToChildrenColumn("navigate", width="10%"),
]
def get(self, request):
self.show_locale_labels = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
self.content_types = []
self.ordering = None
if "ordering" in request.GET and request.GET["ordering"] in [
"title",
"-title",
"latest_revision_created_at",
"-latest_revision_created_at",
"live",
"-live",
]:
self.ordering = request.GET["ordering"]
if "content_type" in request.GET:
try:
app_label, model_name = request.GET["content_type"].split(".")
except ValueError:
raise Http404
try:
self.selected_content_type = ContentType.objects.get_by_natural_key(
app_label, model_name
)
except ContentType.DoesNotExist:
raise Http404
else:
self.selected_content_type = None
self.q = self.request.GET.get("q", "")
return super().get(request)
def get_queryset(self) -> QuerySet[Any]:
pages = self.all_pages = (
Page.objects.all().prefetch_related("content_type").specific()
)
if self.show_locale_labels:
pages = pages.select_related("locale")
if self.ordering:
pages = pages.order_by(self.ordering)
if self.selected_content_type:
pages = pages.filter(content_type=self.selected_content_type)
# Parse query and filter
pages, self.all_pages = page_filter_search(
self.q, pages, self.all_pages, self.ordering
)
# Facets
if pages.supports_facet:
self.content_types = [
(ContentType.objects.get(id=content_type_id), count)
for content_type_id, count in self.all_pages.facet(
"content_type_id"
).items()
]
return pages
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs["show_locale_labels"] = self.show_locale_labels
kwargs["actions_next_url"] = self.get_index_url()
return kwargs
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context.update(
{
"all_pages": self.all_pages,
"query_string": self.q,
"content_types": self.content_types,
"selected_content_type": self.selected_content_type,
"ordering": self.ordering,
}
)
return context
class SearchView(BaseSearchView):
template_name = "wagtailadmin/pages/search.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["search_form"] = SearchForm(self.request.GET)
return context
class SearchResultsView(BaseSearchView):
template_name = "wagtailadmin/pages/search_results.html"

View File

@@ -0,0 +1,96 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail import hooks
from wagtail.actions.unpublish_page import UnpublishPageAction
from wagtail.admin.utils import get_valid_next_url_from_request
from wagtail.admin.views.generic.models import UnpublishView
from wagtail.models import Page
class Unpublish(UnpublishView):
model = Page
index_url_name = "wagtailadmin_explore"
edit_url_name = "wagtailadmin_pages:edit"
unpublish_url_name = "wagtailadmin_pages:unpublish"
usage_url_name = "wagtailadmin_pages:usage"
success_message = _("Page '%(page_title)s' unpublished.")
template_name = "wagtailadmin/pages/confirm_unpublish.html"
header_icon = "doc-empty-inverse"
def setup(self, request, page_id, *args, **kwargs):
# Rename path kwargs from pk to page_id
return super().setup(request, page_id, *args, **kwargs)
def get_object(self, queryset=None):
return get_object_or_404(Page, id=self.pk).specific
def get_object_display_title(self):
return self.object.get_admin_display_title()
def dispatch(self, request, *args, **kwargs):
if not self.object.permissions_for_user(request.user).can_unpublish():
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def get_success_message(self):
return self.success_message % {
"page_title": self.object.get_admin_display_title()
}
def get_next_url(self):
next_url = get_valid_next_url_from_request(self.request)
if next_url:
return next_url
return reverse(self.index_url_name, args=(self.object.get_parent().id,))
def get_objects_to_unpublish(self):
objects_to_unpublish = {self.object}
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
fn_pages = fn([self.object], "unpublish")
if fn_pages and isinstance(fn_pages, dict):
for additional_pages in fn_pages.values():
objects_to_unpublish.update(additional_pages)
return list(objects_to_unpublish)
def unpublish(self):
hook_response = self.run_hook(
"before_unpublish_page", self.request, self.object
)
if hook_response is not None:
return hook_response
include_descendants = self.request.POST.get("include_descendants", False)
for object in self.objects_to_unpublish:
action = UnpublishPageAction(
object, user=self.request.user, include_descendants=include_descendants
)
action.execute(skip_permission_checks=True)
hook_response = self.run_hook("after_unpublish_page", self.request, self.object)
if hook_response is not None:
return hook_response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
"page": self.object,
"live_descendant_count": self.object.get_descendants().live().count(),
"translation_count": len(self.objects_to_unpublish[1:]),
"translation_descendant_count": sum(
[
p.get_descendants().filter(alias_of__isnull=True).live().count()
for p in self.objects_to_unpublish[1:]
]
),
}
)
return context

View File

@@ -0,0 +1,87 @@
from typing import Any, Dict
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail.admin.ui.tables import Column, DateColumn
from wagtail.admin.ui.tables.pages import (
PageStatusColumn,
PageTable,
PageTitleColumn,
ParentPageColumn,
)
from wagtail.admin.views import generic
from wagtail.admin.views.generic.base import BaseListingView
from wagtail.admin.views.pages.utils import (
GenericPageBreadcrumbsMixin,
)
from wagtail.models import Page
class ContentTypeUseView(BaseListingView):
results_template_name = "wagtailadmin/pages/usage_results.html"
page_title = _("Pages using")
header_icon = "doc-empty-inverse"
page_kwarg = "p"
paginate_by = 50
columns = [
PageTitleColumn("title", classname="title", label=_("Title")),
ParentPageColumn("parent", label=_("Parent")),
DateColumn("latest_revision_created_at", label=_("Updated"), width="12%"),
Column("type", label=_("Type"), accessor="page_type_display_name", width="12%"),
PageStatusColumn("status", label=_("Status"), width="12%"),
]
table_class = PageTable
table_classname = "listing"
def get(self, request, *, content_type_app_name, content_type_model_name):
try:
content_type = ContentType.objects.get_by_natural_key(
content_type_app_name, content_type_model_name
)
except ContentType.DoesNotExist:
raise Http404
self.page_class = content_type.model_class()
# page_class must be a Page type and not some other random model
if not issubclass(self.page_class, Page):
raise Http404
return super().get(request)
def get_page_subtitle(self):
return self.page_class.get_verbose_name()
def get_queryset(self):
return self.page_class.objects.all().specific(defer=True)
def get_index_url(self):
return reverse(
"wagtailadmin_pages:type_use",
args=[
self.kwargs["content_type_app_name"],
self.kwargs["content_type_model_name"],
],
)
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["page_class"] = self.page_class
return context
class UsageView(GenericPageBreadcrumbsMixin, generic.UsageView):
model = Page
pk_url_kwarg = "page_id"
header_icon = "doc-empty-inverse"
usage_url_name = "wagtailadmin_pages:usage"
edit_url_name = "wagtailadmin_pages:edit"
def dispatch(self, request, *args, **kwargs):
if not self.object.permissions_for_user(request.user).can_edit():
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,71 @@
from django.urls import reverse
from django.utils.functional import cached_property
# Retain backwards compatibility for imports
from wagtail.admin.utils import ( # noqa: F401
get_latest_str,
get_valid_next_url_from_request,
)
from wagtail.permissions import page_permission_policy
def get_breadcrumbs_items_for_page(
page,
user,
url_name="wagtailadmin_explore",
root_url_name="wagtailadmin_explore_root",
include_self=True,
querystring_value="",
):
# find the closest common ancestor of the pages that this user has direct explore permission
# (i.e. add/edit/publish/lock) over; this will be the root of the breadcrumb
cca = page_permission_policy.explorable_root_instance(user)
if not cca:
return []
pages = (
page.get_ancestors(inclusive=include_self)
.descendant_of(cca, inclusive=True)
.specific(defer=True)
)
items = []
for page in pages:
if page.is_root() and root_url_name:
url = reverse(root_url_name)
else:
url = reverse(url_name, args=(page.id,))
items.append({"url": url + querystring_value, "label": get_latest_str(page)})
return items
class GenericPageBreadcrumbsMixin:
"""
A mixin that allows a view for pages that extends a generic view to combine
the page explorer breadcrumbs with the generic view's breadcrumbs.
This is done by generating the explorer breadcrumbs items for the page as a
normalised breadcrumbs items list, and then concatenating that with the last
item of the generic view's generated breadcrumbs items.
"""
_show_breadcrumbs = True
breadcrumbs_items_to_take = 1
@cached_property
def breadcrumbs_items(self):
return get_breadcrumbs_items_for_page(self.object, self.request.user)
def get_breadcrumbs_items(self):
# The generic view tends to generate breadcrumbs with items such as
# IndexView > EditView > CurrentView,
# but we don't want that because we want the preceding items to be links
# to the explore view of the page's ancestors for consistency with how
# page breadcrumbs have always worked. So we only take the last N items,
# which in most cases is the final item that links to the current view.
# However, this can be customised in the case of generic views that are
# nested inside another generic view.
return self.breadcrumbs_items + [
super().get_breadcrumbs_items()[-self.breadcrumbs_items_to_take]
]

View File

@@ -0,0 +1,54 @@
from django.utils.translation import gettext as _
from wagtail.admin import messages
from wagtail.admin.utils import get_latest_str
from wagtail.admin.views.generic import workflow
from wagtail.models import Page
class WorkflowPageViewMixin:
model = Page
pk_url_kwarg = "page_id"
redirect_url_name = "wagtailadmin_pages:edit"
def add_not_in_moderation_error(self):
messages.error(
self.request,
_("The page '%(page_title)s' is not currently awaiting moderation.")
% {
"page_title": get_latest_str(self.object),
},
)
def get_context_data(self, **kwargs):
return super().get_context_data(page=self.object, **kwargs)
class WorkflowAction(WorkflowPageViewMixin, workflow.WorkflowAction):
submit_url_name = "wagtailadmin_pages:workflow_action"
class CollectWorkflowActionData(
WorkflowPageViewMixin, workflow.CollectWorkflowActionData
):
submit_url_name = "wagtailadmin_pages:collect_workflow_action_data"
class ConfirmWorkflowCancellation(
WorkflowPageViewMixin, workflow.ConfirmWorkflowCancellation
):
template_name = "wagtailadmin/pages/confirm_workflow_cancellation.html"
class PreviewRevisionForTask(WorkflowPageViewMixin, workflow.PreviewRevisionForTask):
def add_error_message(self):
messages.error(
self.request,
_(
"The page '%(page_title)s' is not currently awaiting moderation in task '%(task_name)s'."
)
% {
"page_title": get_latest_str(self.object),
"task_name": self.task.name,
},
)