From 7d97ecd902f0882425f0d306be47ac3c28f945ce Mon Sep 17 00:00:00 2001 From: Dan Braghis Date: Fri, 11 Feb 2022 10:31:33 +0000 Subject: [PATCH] Sync with source page when converting back to alias --- .../tests/test_convert_to_alias.py | 10 ++ wagtail_localize/views/convert.py | 132 +++++++++++++++--- 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/wagtail_localize/tests/test_convert_to_alias.py b/wagtail_localize/tests/test_convert_to_alias.py index 48e339479..693b3f1b8 100644 --- a/wagtail_localize/tests/test_convert_to_alias.py +++ b/wagtail_localize/tests/test_convert_to_alias.py @@ -161,6 +161,16 @@ def test_convert(self): self.fr_page.refresh_from_db() self.assertEqual(self.fr_page.alias_of_id, self.page.id) + def test_convert_syncs_alias_with_source(self): + # update the source page + self.page.title = "Updated title" + self.page.save() + self.page.save_revision().publish() + + self.client.post(self.convert_url) + self.fr_page.refresh_from_db() + self.assertEqual(self.fr_page.title, "Updated title") + def test_convert_adds_entry_to_audit_log(self): self.assertFalse( PageLogEntry.objects.filter( diff --git a/wagtail_localize/views/convert.py b/wagtail_localize/views/convert.py index 094a729c2..db742a49b 100644 --- a/wagtail_localize/views/convert.py +++ b/wagtail_localize/views/convert.py @@ -1,13 +1,18 @@ from django.core.exceptions import PermissionDenied from django.db import transaction -from django.db.models import Q from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.translation import gettext as _ from wagtail.admin import messages from wagtail.admin.views.pages.utils import get_valid_next_url_from_request -from wagtail.core.models import Page, PageLogEntry +from wagtail.core.models import ( + Page, + PageLogEntry, + TranslatableMixin, + _copy_m2m_relations, +) +from wagtail.core.signals import page_published from wagtail_localize.models import TranslationSource @@ -18,39 +23,49 @@ def convert_to_alias(request, page_id): raise PermissionDenied try: - # Attempt to get the source page id, if it exists - source_page = Page.objects.get( - ~Q(pk=page.pk), - translation_key=page.translation_key, - locale_id=TranslationSource.objects.get( - object_id=page.translation_key, - specific_content_type=page.content_type_id, - ).locale_id, + translation_source = TranslationSource.objects.get( + object_id=page.translation_key, + specific_content_type=page.content_type_id, ) + + source_page = translation_source.get_source_instance() except (Page.DoesNotExist, TranslationSource.DoesNotExist): raise Http404 + # prevent self-aliasing + if source_page.id == page_id: + raise Http404 + + page_to_alias = page.specific + with transaction.atomic(): next_url = get_valid_next_url_from_request(request) if request.method == "POST": - page.alias_of_id = source_page.id - page.save(update_fields=["alias_of_id"], clean=False) + # Sync with source page + sync_alias( + source_page, page_to_alias, revision=source_page.get_latest_revision() + ) + + # mark the page as an alias + page_to_alias.alias_of_id = source_page.id + page_to_alias.save(update_fields=["alias_of_id"], clean=False) - page_title = page.specific_deferred.get_admin_display_title() + # note, this logs the page title from before the sync + page_title = page_to_alias.get_admin_display_title() PageLogEntry.objects.log_action( - instance=page, - revision=page.get_latest_revision(), + instance=page_to_alias, + revision=page_to_alias.get_latest_revision(), action="wagtail_localize.convert_to_alias", user=request.user, data={ "page": { - "id": page.id, + "id": page_to_alias.id, "title": page_title, }, "source": { "id": source_page.id, - "title": source_page.specific_deferred.get_admin_display_title(), + "title": source_page.get_admin_display_title(), }, }, ) @@ -62,14 +77,93 @@ def convert_to_alias(request, page_id): if next_url: return redirect(next_url) - return redirect("wagtailadmin_pages:edit", page.id) + return redirect("wagtailadmin_pages:edit", page_to_alias.id) return TemplateResponse( request, "wagtail_localize/admin/confirm_convert_to_alias.html", { - "page": page, + "page": page_to_alias, "source_page": source_page, "next": next_url, }, ) + + +def sync_alias(source_page, alias_page, revision=None, _content_json=None): + """ + Updates the page converted to an alias to be up-to-date with the source page. + It will also update all aliases that follow this page with the latest content from this page. + Note: this is + """ + source_page = source_page.specific + + # Only compute this if necessary since it's quite a heavy operation + if _content_json is None: + _content_json = source_page.to_json() + + # FIXME: update when core adds better mechanism for the exclusions + exclude_fields = [ + "id", + "path", + "depth", + "numchild", + "url_path", + "path", + "index_entries", + "postgres_index_entries", + ] + + # Copy field content + alias_updated = alias_page.with_content_json(_content_json) + + # Mirror the publishing status of the status page + alias_updated.live = source_page.live + alias_updated.has_unpublished_changes = False + + # Copy child relations + child_object_map = source_page.copy_all_child_relations( + target=alias_updated, exclude=exclude_fields + ) + + # Process child objects + if child_object_map: + + def process_child_object(child_object): + if isinstance(child_object, TranslatableMixin): + # Child object's locale must always match the page + child_object.locale = alias_updated.locale + + for (_rel, previous_id), child_objects in child_object_map.items(): + if previous_id is None: + for child_object in child_objects: + process_child_object(child_object) + else: + process_child_object(child_objects) + + # Copy M2M relations + _copy_m2m_relations(source_page, alias_updated, exclude_fields=exclude_fields) + + # Don't change the aliases slug + # Aliases can have their own slugs so they can be siblings of the original + alias_updated.slug = alias_page.slug + alias_updated.set_url_path(alias_updated.get_parent()) + + # Technically, aliases don't have revisions, and in Page.update_aliases() fields that + # would normally be updated by save_revision get updated. Let's mirror that. + alias_updated.draft_title = alias_updated.title + alias_updated.latest_revision_created_at = source_page.latest_revision_created_at + + alias_updated.save(clean=False) + + # question: should we sent the published alias? + if alias_updated.live and not alias_page.live: + page_published.send( + sender=alias_updated.specific_class, + instance=alias_updated, + revision=revision, + alias=True, + ) + + # Update any aliases of that alias + alias_page.update_aliases(revision=revision, _content_json=_content_json)