Fix IntegrityError for Orderable translation key on copy (#8321)

tl;dr - EditView builds the page instance from its latest revision and that is what is passed to the form and ultimately submitted.

In order to test the `translation_key`s for orderables, we need to be as close to it as possible, a simple `page.copy()` then `page.save_revision().publish()` doesn't capture the subtleties

Co-authored-by: Kalob Taulien <4743971+KalobTaulien@users.noreply.github.com>
stable/2.15.x
Dan Braghis 2022-04-10 09:28:25 +01:00 zatwierdzone przez GitHub
rodzic 1fdf8d299f
commit 2559d57c13
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 76 dodań i 3 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ Changelog
* Fix: Allow bulk publishing of pages without revisions (Andy Chosak)
* Fix: Ensure that all descendant pages are logged when deleting a page, not just immediate children (Jake Howard)
* Fix: Translation key `IntegrityError` when publishing pages with translatable `Orderable`s that were copied without being published (Kalob Taulien, Dan Braghis)
2.15.4 (11.02.2022)

Wyświetl plik

@ -15,6 +15,7 @@ Bug fixes
* Allow bulk publishing of pages without revisions (Andy Chosak)
* Ensure that all descendant pages are logged when deleting a page, not just immediate children (Jake Howard)
* Generate new translation keys for translatable ``Orderable`` when page is copied without being published (Kalob Taulien, Dan Braghis)
Upgrade considerations

Wyświetl plik

@ -4,7 +4,7 @@ from django.test import TestCase
from django.urls import reverse
from wagtail.core.models import GroupPagePermission, Page
from wagtail.tests.testapp.models import SimplePage
from wagtail.tests.testapp.models import EventPage, EventPageSpeaker, SimplePage
from wagtail.tests.utils import WagtailTestUtils
@ -587,3 +587,57 @@ class TestPageCopy(TestCase, WagtailTestUtils):
# We only need to check that it didn't crash
self.assertEqual(response.status_code, 302)
def test_copy_page_with_unique_uuids_in_orderables(self):
"""
Test that a page with orderables can be copied and the translation
keys are updated.
"""
event_page = EventPage(
title="Moon Landing",
location="the moon",
audience="public",
cost="free on TV",
date_from="1969-07-20",
)
self.root_page.add_child(instance=event_page)
event_page.speakers.add(
EventPageSpeaker(
first_name="Neil",
last_name="Armstrong",
)
)
# ensure there's a revision (which should capture the new speaker orderables)
# before copying the page
event_page.save_revision().publish()
post_data = {
"new_title": "New Moon landing",
"new_slug": "moon-landing-redux",
"new_parent_page": str(self.root_page.id),
"copy_subpages": False,
"publish_copies": False,
"alias": False,
}
self.client.post(
reverse("wagtailadmin_pages:copy", args=[event_page.id]), post_data
)
new_page = EventPage.objects.last()
# Hack: get the page instance from the edit form which assembles it from the
# latest revision. While we could do new_page.get_latest_revision().as_page_object()
# this follow the edit view closer and should it change the test is less
# prone to continue working because we're skipping some step
response = self.client.get(
reverse("wagtailadmin_pages:edit", args=[new_page.id])
)
new_page_on_edit_form = response.context["form"].instance
# publish the page, similar to EditView.publish_action()
new_page_on_edit_form.save_revision().publish()
self.assertNotEqual(
event_page.speakers.first().translation_key,
new_page.speakers.first().translation_key,
)

Wyświetl plik

@ -26,6 +26,7 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
from django.db.models import DEFERRED, Q, Value
from django.db.models.expressions import OuterRef, Subquery
@ -1584,6 +1585,17 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
page_copy, child_object_map = _copy(specific_self, exclude_fields=exclude_fields, update_attrs=base_update_attrs)
uuid_mapping = {}
def generate_translation_key(uuid_mapping, old_uuid):
"""
Generates a new UUID based on an old one, then re-uses it
"""
if old_uuid not in uuid_mapping:
uuid_mapping[old_uuid] = uuid.uuid4()
return uuid_mapping[old_uuid]
# Save copied child objects and run process_child_object on them if we need to
for (child_relation, old_pk), child_object in child_object_map.items():
if process_child_object:
@ -1591,7 +1603,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
# When we're not copying for translation, we should give the translation_key a new value for each child object as well
if reset_translation_key and isinstance(child_object, TranslatableMixin):
child_object.translation_key = uuid.uuid4()
child_object.translation_key = generate_translation_key(uuid_mapping, child_object.translation_key)
# Save the new page
if _mpnode_attrs:
@ -1642,7 +1654,12 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
copied_child_object = child_object_map.get((child_relation, child_object['pk']))
child_object['pk'] = copied_child_object.pk if copied_child_object else None
revision.content_json = json.dumps(revision_content)
if reset_translation_key and "translation_key" in child_object:
child_object["translation_key"] = generate_translation_key(
uuid_mapping, child_object["translation_key"]
)
revision.content_json = json.dumps(revision_content, cls=DjangoJSONEncoder)
# Save
revision.save()