Sync tree: cascade unpublish, move and delete (#7984)

* Add construct_synced_page_tree_list hook and use in page unpublish view

* Implement construct_synced_page_tree_list in simple_translation

but only when sync page tree is enabled

* Add hook documentation

* Add construct_synced_page_tree_list hook tests (#8058)

* Move translated and alias pages when WAGTAIL_I18N_ENABLED and WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE are enabled

Co-Authored-By: Kalob Taulien <4743971+KalobTaulien@users.noreply.github.com>

* Delete corresponding translations when WAGTAIL_I18N_ENABLED and WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE are true

Co-Authored-By: Kalob Taulien <4743971+KalobTaulien@users.noreply.github.com>

* Rename the hook to be more specific

* Update singular string version in confirm_move.html

* Update test test_translation_count_in_context

Co-authored-by: Kalob Taulien <4743971+KalobTaulien@users.noreply.github.com>
Co-authored-by: Karl Hobley <karl@kaed.uk>
pull/8375/head
Dan Braghis 2022-04-17 17:34:38 +01:00 zatwierdzone przez GitHub
rodzic 84c414e758
commit 4cc10322a1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
9 zmienionych plików z 697 dodań i 22 usunięć

Wyświetl plik

@ -792,6 +792,21 @@ Hooks for customising the way users are directed through the process of creating
The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
.. _construct_translated_pages_to_cascade_actions:
``construct_translated_pages_to_cascade_actions``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Return additional pages to process in a synced tree setup.
This hook is only triggered on unpublishing a page when ``WAGTAIL_I18N_ENABLED = True``.
The list of pages and the action are passed in as arguments to the hook.
The function should return a dictionary with the page from the pages list as key, and a list of additional pages to perform the action on.
We recommend they are non-aliased, direct translations of the pages from the function argument.
.. _register_page_action_menu_item:
``register_page_action_menu_item``

Wyświetl plik

@ -9,12 +9,60 @@
<div class="nice-padding">
<p>
{% trans 'Are you sure you want to delete this page?' %}
{% if descendant_count %}
{% blocktrans trimmed count counter=descendant_count %}
This will also delete one more subpage.
Deleting this page will also delete {{ descendant_count }} child page.
{% plural %}
This will also delete {{ descendant_count }} more subpages.
Deleting this page will also delete {{ descendant_count }} more child pages.
{% endblocktrans %}
{% if translation_count %} {# has translations #}
{% if translation_descendant_count %} {# has translations with descendants #}
{% if translation_count == 1 %}
{% blocktrans trimmed count counter=translation_descendant_count %}
It will also delete 1 translation and its combined {{ translation_descendant_count }} translated child page.
{% plural %}
It will also delete 1 translation and its combined {{ translation_descendant_count }} translated child pages.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed count counter=translation_descendant_count %}
It will also delete {{ translation_count }} translations and their combined {{ translation_descendant_count }} translated child page.
{% plural %}
It will also delete {{ translation_count }} translations and their combined {{ translation_descendant_count }} translated child pages.
{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans trimmed count counter=translation_count %}
It will also delete {{ translation_count }} translation.
{% plural %}
It will also delete {{ translation_count }} translations.
{% endblocktrans %}
{% endif %}
{% endif %}
{% elif translation_count %} {# no descendants #}
{% if translation_descendant_count %} {# has translations with descendants #}
{% if translation_count == 1 %}
{% blocktrans trimmed count counter=translation_descendant_count %}
Deleting this page will also delete 1 translation and its combined {{ translation_descendant_count }} translated child page.
{% plural %}
Deleting this page will also delete 1 translation and its combined {{ translation_descendant_count }} translated child pages.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed count counter=translation_descendant_count %}
Deleting this page will also delete {{ translation_count }} translations and their combined {{ translation_descendant_count }} translated child page.
{% plural %}
Deleting this page will also delete {{ translation_count }} translations and their combined {{ translation_descendant_count }} translated child pages.
{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans trimmed count counter=translation_count %}
Deleting this page will also delete {{ translation_count }} translation of this page.
{% plural %}
This will also delete {{ descendant_count }} more child pages.
Deleting this page will also delete {{ translation_count }} translations of this page.
{% endblocktrans %}
{% endif %}
{% endif %}
</p>

Wyświetl plik

@ -6,11 +6,20 @@
{% include "wagtailadmin/shared/header.html" with title=move_str subtitle=page_to_move.get_admin_display_title icon="doc-empty-inverse" %}
<div class="nice-padding">
{% if page_to_move.is_leaf %}
<p>{% blocktrans trimmed with title=destination.get_admin_display_title %}Are you sure you want to move this page into '{{ title }}'?{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans trimmed with title=destination.get_admin_display_title %}Are you sure you want to move this page and all of its children into '{{ title }}'?{% endblocktrans %}</p>
{% endif %}
<p>
{% if page_to_move.is_leaf %}
{% blocktrans trimmed with title=destination.get_admin_display_title %}Are you sure you want to move this page into '{{ title }}'?{% endblocktrans %}
{% else %}
{% blocktrans trimmed with title=destination.get_admin_display_title %}Are you sure you want to move this page and all of its children into '{{ title }}'?{% endblocktrans %}
{% endif %}
{% if translations_to_move_count %}
{% blocktrans trimmed count counter=translations_to_move_count %}
This will also move one translation of this page and its child pages
{% plural %}
This will also move {{ translations_to_move_count }} translations of this page and their child pages
{% endblocktrans %}
{% endif %}
</p>
<form action="{% url 'wagtailadmin_pages:move_confirm' page_to_move.id destination.id %}" method="POST">
{% csrf_token %}

Wyświetl plik

@ -6,22 +6,50 @@
{% include "wagtailadmin/shared/header.html" with title=unpublish_str subtitle=page.get_admin_display_title icon="doc-empty-inverse" %}
<div class="nice-padding">
<p>{% trans "Are you sure you want to unpublish this page?" %}</p>
<p>
{% trans "Are you sure you want to unpublish this page?" %}
{% if translation_count %}
{% blocktrans trimmed with translation_count=translation_count count counter=translation_count %}
This will also unpublish one translation of the page.
{% plural %}
This will also unpublish all {{ translation_count }} translations of the page.
{% endblocktrans %}
{% endif %}
</p>
<form action="{% url 'wagtailadmin_pages:unpublish' page.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<ul class="fields">
{% if live_descendant_count > 0 %}
{% if live_descendant_count > 0 or translation_descendant_count > 0 %}
<li>
<div class="field boolean_field checkbox_input">
<div class="field-content">
<div class="input">
<input id="id_include_descendants" name="include_descendants" type="checkbox">
<label for="id_include_descendants" class="plain-checkbox-label">{% blocktrans trimmed count counter=live_descendant_count %}
This page has one subpage. Unpublish this too
{% plural %}
This page has {{ live_descendant_count }} subpages. Unpublish these too
{% endblocktrans %}</label>
<label for="id_include_descendants" class="plain-checkbox-label">
{% if translation_descendant_count %}
{% if translation_descendant_count == 1 %}
{% blocktrans trimmed count counter=live_descendant_count %}
This page has one subpage and its translations have a combined one translated child page. Unpublish these too
{% plural %}
This page has {{ live_descendant_count }} child pages and its translations have a combined one translated child page. Unpublish these too
{% endblocktrans %}
{% else %}
{% blocktrans trimmed count counter=live_descendant_count %}
This page has one child page and its translations have a combined {{ translation_descendant_count }} translated child pages. Unpublish these too
{% plural %}
This page has {{ live_descendant_count }} child pages and its translations have a combined {{ translation_descendant_count }} translated child pages. Unpublish these too
{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans trimmed count counter=live_descendant_count %}
This page has one subpage. Unpublish this too
{% plural %}
This page has {{ live_descendant_count }} subpages. Unpublish these too
{% endblocktrans %}
{% endif %}
</label>
</div>
</div>
</div>

Wyświetl plik

@ -1,3 +1,4 @@
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
@ -24,12 +25,35 @@ def delete(request, page_id):
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":
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 '{0}' deleted.").format(page.get_admin_display_title())
)
@ -50,5 +74,21 @@ def delete(request, page_id):
"page": page,
"descendant_count": page.get_descendant_count(),
"next": next_url,
# 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
]
),
},
)

Wyświetl plik

@ -1,3 +1,4 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect
@ -74,14 +75,42 @@ def move_confirm(request, page_to_move_id, destination_id):
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
# 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 '{0}' moved.").format(page_to_move.get_admin_display_title()),
@ -106,5 +135,12 @@ def move_confirm(request, page_to_move_id, destination_id):
{
"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
]
),
},
)

Wyświetl plik

@ -1,3 +1,4 @@
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
@ -20,6 +21,17 @@ def unpublish(request, page_id):
next_url = get_valid_next_url_from_request(request)
pages_to_unpublish = {page}
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
fn_pages = fn([page], "unpublish")
if fn_pages and isinstance(fn_pages, dict):
for additional_pages in fn_pages.values():
pages_to_unpublish.update(additional_pages)
pages_to_unpublish = list(pages_to_unpublish)
if request.method == "POST":
include_descendants = request.POST.get("include_descendants", False)
@ -28,10 +40,11 @@ def unpublish(request, page_id):
if hasattr(result, "status_code"):
return result
action = UnpublishPageAction(
page, user=request.user, include_descendants=include_descendants
)
action.execute(skip_permission_checks=True)
for page in pages_to_unpublish:
action = UnpublishPageAction(
page, user=request.user, include_descendants=include_descendants
)
action.execute(skip_permission_checks=True)
for fn in hooks.get_hooks("after_unpublish_page"):
result = fn(request, page)
@ -59,5 +72,12 @@ def unpublish(request, page_id):
"page": page,
"next": next_url,
"live_descendant_count": page.get_descendants().live().count(),
"translation_count": len(pages_to_unpublish[1:]),
"translation_descendant_count": sum(
[
p.get_descendants().filter(alias_of__isnull=True).live().count()
for p in pages_to_unpublish[1:]
]
),
},
)

Wyświetl plik

@ -1,8 +1,11 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.test import TestCase
from django.test import TestCase, override_settings
from django.urls import reverse
from wagtail import hooks
from wagtail.actions.create_alias import CreatePageAliasAction
from wagtail.actions.move_page import MovePageAction
from wagtail.admin import widgets as wagtailadmin_widgets
from wagtail.contrib.simple_translation.wagtail_hooks import (
page_listing_more_buttons,
@ -102,3 +105,456 @@ class TestWagtailHooksButtons(Utils):
list(page_listing_more_buttons(blog_page, page_perms))[0],
wagtailadmin_widgets.Button,
)
class TestConstructSyncedPageTreeListHook(Utils):
def unpublish_hook(self, pages, action):
self.assertEqual(action, "unpublish")
self.assertIsInstance(pages, list)
def missing_hook_action(self, pages, action):
self.assertEqual(action, "")
self.assertIsInstance(pages, list)
def test_double_registered_hook(self):
# We should have two implementations of `construct_translated_pages_to_cascade_actions`
# One in simple_translation.wagtail_hooks and the other will be
# registered as a temporary hook.
with hooks.register_temporarily(
"construct_translated_pages_to_cascade_actions", self.unpublish_hook
):
defined_hooks = hooks.get_hooks(
"construct_translated_pages_to_cascade_actions"
)
self.assertEqual(len(defined_hooks), 2)
@override_settings(WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True)
def test_page_tree_sync_on(self):
with hooks.register_temporarily(
"construct_translated_pages_to_cascade_actions", self.unpublish_hook
):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
response = fn([self.en_homepage], "unpublish")
if response:
self.assertIsInstance(response, dict)
self.assertEqual(len(response.items()), 1)
@override_settings(WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=False)
def test_page_tree_sync_off(self):
with hooks.register_temporarily(
"construct_translated_pages_to_cascade_actions", self.unpublish_hook
):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
response = fn([self.en_homepage], "unpublish")
self.assertIsNone(response)
@override_settings(WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True)
def test_missing_hook_action(self):
with hooks.register_temporarily(
"construct_translated_pages_to_cascade_actions", self.missing_hook_action
):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
response = fn([self.en_homepage], "")
if response is not None:
self.assertIsInstance(response, dict)
@override_settings(
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True, WAGTAIL_I18N_ENABLED=True
)
def test_other_l10n_pages_were_unpublished(self):
# Login to access the admin
self.login()
# Make sur the French homepage is published/live
self.fr_homepage.live = True
self.fr_homepage.save()
self.assertTrue(self.en_homepage.live)
self.assertTrue(self.fr_homepage.live)
response = self.client.post(
reverse("wagtailadmin_pages:unpublish", args=(self.en_homepage.id,)),
{"include_descendants": False},
follow=True,
)
self.assertEqual(response.status_code, 200)
# Refresh objects from the database
self.en_homepage.refresh_from_db()
self.fr_homepage.refresh_from_db()
# Test that both the English and French homepages are unpublished
self.assertFalse(self.en_homepage.live)
self.assertFalse(self.fr_homepage.live)
class TestMovingTranslatedPages(Utils):
@override_settings(
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True, WAGTAIL_I18N_ENABLED=True
)
def test_move_translated_pages(self):
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
self.de_blog_index = self.en_blog_index.copy_for_translation(self.de_locale)
# Create blog_post copies for translation
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
self.de_blog_post = self.en_blog_post.copy_for_translation(self.de_locale)
# Confirm location of English blog post page before it is moved
# Should be living at /blog/blog-post/ right now. But will eventually
# exist at /blog-post/
self.assertEqual(self.en_blog_post.get_parent().id, self.en_blog_index.id)
# Check if fr and de blog post parent ids are in the translated list
# This is to make sure the fr blog_post is situated under /fr/blog/
# (same concept with /de/).
# We'll check these after the move to ensure they exist under /fr/ without
# the /blog/ parent page.
original_translated_parent_ids = [
p.id for p in self.en_blog_index.get_translations()
]
self.assertIn(self.fr_blog_post.get_parent().id, original_translated_parent_ids)
self.assertIn(self.de_blog_post.get_parent().id, original_translated_parent_ids)
response = self.client.post(
reverse(
"wagtailadmin_pages:move_confirm",
args=(
self.en_blog_post.id,
self.en_homepage.id,
),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.fr_blog_post.refresh_from_db()
self.de_blog_post.refresh_from_db()
# Check if the new pages exist under their respective translated homepages
home_page_translation_ids = [p.id for p in self.en_homepage.get_translations()]
self.assertIn(
self.fr_blog_post.get_parent(update=True).id, home_page_translation_ids
)
self.assertIn(
self.de_blog_post.get_parent(update=True).id, home_page_translation_ids
)
@override_settings(WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=False)
def test_unmovable_translation_pages(self):
"""
Test that moving a page with WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE
disabled doesn't apply to its translations.
"""
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
self.de_blog_index = self.en_blog_index.copy_for_translation(self.de_locale)
# Create blog_post copies for translation
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
self.de_blog_post = self.en_blog_post.copy_for_translation(self.de_locale)
# Confirm location of English blog post page before it is moved
# Should be living at /blog/blog-post/ right now. But will eventually
# exist at /blog-post/
self.assertEqual(self.en_blog_post.get_parent().id, self.en_blog_index.id)
# Confirm the fr and de blog post pages are under the blog index page
# We'll confirm these have not moved after ther POST request.
original_translated_parent_ids = [
p.id for p in self.en_blog_index.get_translations()
]
self.assertIn(self.fr_blog_post.get_parent().id, original_translated_parent_ids)
self.assertIn(self.de_blog_post.get_parent().id, original_translated_parent_ids)
response = self.client.post(
reverse(
"wagtailadmin_pages:move_confirm",
args=(
self.en_blog_post.id,
self.en_homepage.id,
),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.en_blog_post.refresh_from_db()
self.fr_blog_post.refresh_from_db()
self.de_blog_post.refresh_from_db()
# Check that the en_blog_post page has moved directly under the home page.
self.assertEqual(
self.en_blog_post.get_parent(update=True).id, self.en_homepage.id
)
# Check if the fr and de pages exist under their original parent page (/blog/)
self.assertIn(
self.fr_blog_post.get_parent(update=True).id, original_translated_parent_ids
)
self.assertIn(
self.de_blog_post.get_parent(update=True).id, original_translated_parent_ids
)
@override_settings(
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True, WAGTAIL_I18N_ENABLED=True
)
def test_translation_count_in_context(self):
"""Test translation count is correct in the confirm_move.html template."""
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
self.de_blog_index = self.en_blog_index.copy_for_translation(self.de_locale)
# create translation in FR tree
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
# create alias in DE tree
self.de_blog_post = self.en_blog_post.copy_for_translation(
self.de_locale, alias=True
)
response = self.client.get(
reverse(
"wagtailadmin_pages:move_confirm",
args=(
self.en_blog_post.id,
self.en_homepage.id,
),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["translations_to_move_count"], 1)
self.assertIn(
"This will also move one translation of this page and its child pages",
response.content.decode("utf-8"),
)
@override_settings(
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE=True, WAGTAIL_I18N_ENABLED=True
)
class TestDeletingTranslatedPages(Utils):
def delete_hook(self, pages, action):
self.assertEqual(action, "delete")
self.assertIsInstance(pages, list)
def test_construct_translated_pages_to_cascade_actions_when_deleting(self):
with hooks.register_temporarily(
"construct_translated_pages_to_cascade_actions", self.delete_hook
):
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
response = fn([self.en_homepage], "delete")
if response is not None:
self.assertIsInstance(response, dict)
self.assertEqual(len(response.items()), 1)
def test_delete_translated_pages(self):
# Login to the Wagtail admin with a superuser account
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
# Create a copy of the en_blog_post object as a translated page
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
# 1. Delete the en_blog_post by making a POST request to /delete/
response = self.client.post(
reverse(
"wagtailadmin_pages:delete",
args=(self.en_blog_post.id,),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
# 2. Confirm fr_blog_post is deleted
self.assertIsNone(Page.objects.filter(pk=self.fr_blog_post.id).first())
def test_delete_confirmation_template(self):
"""Test the context info is correct in the confirm_delete.html template."""
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
# Create a copy of the en_blog_post object as a translated page
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
# Create an alias page to test the `translations_to_move_count`
# in the template context
new_page = CreatePageAliasAction(
self.en_blog_post,
recursive=False,
parent=self.en_blog_index,
update_slug="alias-page-slug",
user=None,
)
new_page.execute(skip_permission_checks=True)
response = self.client.get(
reverse(
"wagtailadmin_pages:delete",
args=(self.en_blog_post.id,),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["translation_count"], 1)
self.assertEqual(response.context["translation_descendant_count"], 0)
self.assertIn(
"Deleting this page will also delete 1 translation of this page.",
response.content.decode("utf-8"),
)
def test_deleting_page_with_divergent_translation_tree(self):
self.login()
# New parent to eventually hold the fr_blog_post object.
self.en_new_parent = TestPage(title="Test Parent", slug="test-parent")
self.en_homepage.add_child(instance=self.en_new_parent)
# Copy the /blog/ and French /blog-post/ pages.
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
self.fr_blog_post = self.en_blog_post.copy_for_translation(self.fr_locale)
# Copy the en new parent to be a french page
self.fr_new_parent = self.en_new_parent.copy_for_translation(self.fr_locale)
# Manually move the fr_blog_post to live under fr_new_parent
# Because this does not go through the POST request in pages/move.py
# this action will create a diverged tree scnenario where en_blog_post
# and fr_blog_post don't mirror their original positions in the tree.
action = MovePageAction(
self.fr_blog_post,
self.fr_new_parent,
pos="last-child",
user=None,
)
action.execute(skip_permission_checks=True)
self.fr_blog_post.refresh_from_db()
self.en_blog_post.refresh_from_db()
# Confirm fr_blog_post parent id is the fr_new_parent id.
# Confirm en_blog_post parent id is the en_blog_index id
self.assertEqual(
self.fr_blog_post.get_parent(update=True).id, self.fr_new_parent.id
)
self.assertEqual(
self.en_blog_post.get_parent(update=True).id, self.en_blog_index.id
)
# Make a post request to move the en_blog_post to live under en_homepage
response = self.client.post(
reverse(
"wagtailadmin_pages:delete",
args=(self.en_blog_post.id,),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
# Confirm that the en_blog_post object no longer exists.
self.assertFalse(Page.objects.filter(pk=self.en_blog_post.id).exists())
# Confirm that the fr_blog_post object stll exists, because it was moved
self.assertTrue(Page.objects.filter(pk=self.fr_blog_post.id).exists())
# Confirm the fr_blog_post parent id matches the new_parent_page id
# This confirms is hasn't moved and hasn't been deleted.
# to a different location in the tree.
self.fr_blog_post.refresh_from_db()
self.assertEqual(
self.fr_blog_post.get_parent(update=True).id, self.fr_new_parent.id
)
def test_alias_pages_when_deleting_source_page(self):
"""
When deleting a page that has an alias page in the same tree, the alias page
should continue to exist while the original page should be deleted
while using the `construct_translated_pages_to_cascade_actions` hook is active.
"""
self.login()
# Test the source page exists in the right tree location
self.assertEqual(self.en_blog_post.get_parent().id, self.en_blog_index.id)
# Create an alias page from en_blog_post
action = CreatePageAliasAction(
self.en_blog_post,
recursive=False,
parent=self.en_blog_index,
update_slug="sample-slug",
user=None,
)
new_page = action.execute(skip_permission_checks=True)
# Make sure the alias page is an alias of the en_blog_post
# and exists under the same parent page.
self.assertEqual(new_page.get_parent().id, self.en_blog_index.id)
# Test alias of source page
self.assertEqual(new_page.alias_of_id, self.en_blog_post.id)
# Delete the en_blog_post page and make sure the alias page is kept in tact.
response = self.client.post(
reverse(
"wagtailadmin_pages:delete",
args=(self.en_blog_post.id,),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertFalse(Page.objects.filter(pk=self.en_blog_post.id).exists())
self.assertTrue(Page.objects.filter(pk=new_page.id).exists())
def test_translation_alias_pages_when_deleting_source_page(self):
"""
When deleting a page that has an alias page, the alias page
should be deleted while using the `construct_translated_pages_to_cascade_actions`
hook is active.
"""
self.login()
# BlogIndex needs translated pages before child pages can be translated
self.fr_blog_index = self.en_blog_index.copy_for_translation(self.fr_locale)
# Create a copy of the en_blog_post object as a translated alias page
self.fr_blog_post = self.en_blog_post.copy_for_translation(
self.fr_locale, alias=True
)
self.assertEqual(self.fr_blog_post.alias_of_id, self.en_blog_post.id)
self.assertEqual(self.fr_blog_post.get_parent().id, self.fr_blog_index.id)
# Test that the fr_blog_post alias_id is in the list of translations, is a
# proper alias of en_blog_post, and is using the french locale (fr).
# Also check that the page is in the correct language tree
translation_ids = [p.id for p in self.fr_blog_post.get_translations()]
self.assertIn(self.fr_blog_post.alias_of_id, translation_ids)
self.assertEqual(self.fr_blog_post.alias_of_id, self.en_blog_post.id)
self.assertEqual(self.fr_blog_post.locale.language_code, "fr")
# Test the source is in the source tree root (source HomePage)
# Test that the translated alias is in the translated root (fr HomePage)
en_root = Page.objects.filter(depth__gt=1, locale=self.en_locale).first()
fr_root = Page.objects.filter(depth__gt=1, locale=self.fr_locale).first()
self.assertIn(self.en_blog_post, en_root.get_descendants().specific())
self.assertIn(self.fr_blog_post, fr_root.get_descendants().specific())
# Delete the en_blog_post page and make sure the alias page is kept in tact.
response = self.client.post(
reverse(
"wagtailadmin_pages:delete",
args=(self.en_blog_post.id,),
),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertFalse(Page.objects.filter(pk=self.en_blog_post.id).exists())
self.assertFalse(Page.objects.filter(pk=self.fr_blog_post.id).exists())
# The source page continues to exist in the source tree root (HomePage)
self.assertNotIn(self.en_blog_post, en_root.get_descendants().specific())
# The alias should no longer be in the translated tree root (fr HomePage)
self.assertNotIn(self.fr_blog_post, fr_root.get_descendants().specific())

Wyświetl plik

@ -1,3 +1,6 @@
from typing import List
from django.conf import settings
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
from django.urls import include, path, reverse
@ -5,7 +8,7 @@ from django.utils.translation import gettext as _
from wagtail import hooks
from wagtail.admin import widgets as wagtailadmin_widgets
from wagtail.models import Locale, TranslatableMixin
from wagtail.models import Locale, Page, TranslatableMixin
from wagtail.snippets.widgets import SnippetListingButton
from .views import SubmitPageTranslationView, SubmitSnippetTranslationView
@ -89,3 +92,23 @@ def register_snippet_listing_buttons(snippet, user, next_url=None):
},
priority=100,
)
@hooks.register("construct_translated_pages_to_cascade_actions")
def construct_translated_pages_to_cascade_actions(pages: List[Page], action: str):
if not getattr(settings, "WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE", False):
return
page_list = {}
if action == "unpublish":
# Only return non-alias translations of the page
for page in pages:
page_list[page] = Page.objects.translation_of(page, inclusive=False).filter(
alias_of__isnull=True
)
elif action == "move" or action == "delete":
# Return all translations or aliases in other trees
for page in pages:
page_list[page] = Page.objects.translation_of(page, inclusive=False)
return page_list