Synchronise alias page content when the source is updated

pull/6450/head
Karl Hobley 2020-09-28 12:07:39 +01:00 zatwierdzone przez Karl Hobley
rodzic 7af655d0d1
commit b5eddca28b
3 zmienionych plików z 190 dodań i 1 usunięć

Wyświetl plik

@ -267,6 +267,8 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. automethod:: create_alias
.. automethod:: update_aliases
.. autoattribute:: has_workflow
.. automethod:: get_workflow

Wyświetl plik

@ -1218,6 +1218,13 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
:param clean: Set this to False to skip cleaning page content before saving this revision
:return: the newly created revision
"""
# Raise an error if this page is an alias.
if self.alias_of_id:
raise RuntimeError(
"save_revision() was called on an alias page. "
"Revisions are not required for alias pages as they are an exact copy of another page."
)
if clean:
self.full_clean()
@ -1296,6 +1303,108 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
else:
return self.specific
def update_aliases(self, *, revision=None, user=None, _content_json=None, _updated_ids=None):
"""
Publishes all aliases that follow this page with the latest content from this page.
This is called by Wagtail whenever a page with aliases is published.
:param revision: The revision of the original page that we are updating to (used for logging purposes)
:type revision: PageRevision, optional
:param user: The user who is publishing (used for logging purposes)
:type user: User, optional
"""
specific_self = self.specific
# Only compute this if necessary since it's quite a heavy operation
if _content_json is None:
_content_json = self.to_json()
# A list of IDs that have already been updated. This is just in case someone has
# created an alias loop (which is impossible to do with the UI Wagtail provides)
_updated_ids = _updated_ids or []
for alias in self.specific_class.objects.filter(alias_of=self).exclude(id__in=_updated_ids):
# FIXME: Switch to the same fields that are excluded from copy
# We can't do this right now because we can't exclude fields from with_content_json
exclude_fields = ['id', 'path', 'depth', 'numchild', 'url_path', 'path', 'index_entries']
# Copy field content
alias_updated = alias.with_content_json(_content_json)
# Copy child relations
child_object_map = specific_self.copy_all_child_relations(target=alias_updated, exclude=exclude_fields)
# Process child objects
# This has two jobs:
# - If the alias is in a different locale, this updates the
# locale of any translatable child objects to match
# - If the alias is not a translation of the original, this
# changes the translation_key field of all child objects
# so they do not clash
if child_object_map:
alias_is_translation = alias.translation_key == self.translation_key
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
# If the alias isn't a translation of the original page,
# change the child object's translation_keys so they are
# not either
if not alias_is_translation:
child_object.translation_key = uuid.uuid4()
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(specific_self, 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.slug
alias_updated.set_url_path(alias_updated.get_parent())
# Aliases don't have revisions, so update fields that would normally be updated by save_revision
alias_updated.draft_title = alias_updated.title
alias_updated.latest_revision_created_at = self.latest_revision_created_at
alias_updated.save(clean=False)
page_published.send(sender=alias_updated.specific_class, instance=alias_updated, revision=revision, alias=True)
# Log the publish of the alias
PageLogEntry.objects.log_action(
instance=alias_updated,
action='wagtail.publish',
user=user,
)
# Update any aliases of that alias
# Design note:
# It could be argued that this will be faster if we just changed these alias-of-alias
# pages to all point to the original page and avoid having to update them recursively.
#
# But, it's useful to have a record of how aliases have been chained.
# For example, In Wagtail Localize, we use aliases to create mirrored trees, but those
# trees themselves could have aliases within them. If an alias within a tree is
# converted to a regular page, we want the alias in the mirrored tree to follow that
# new page and stop receiving updates from the original page.
#
# Doing it this way requires an extra lookup query per alias but this is small in
# comparison to the work required to update the alias.
alias.update_aliases(revision=revision, _content_json=_content_json, _updated_ids=_updated_ids)
update_aliases.alters_data = True
def unpublish(self, set_expired=False, commit=True, user=None, log_action=True):
"""
Unpublish the page by setting ``live`` to ``False``. Does nothing if ``live`` is already ``False``
@ -1327,6 +1436,10 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
self.revisions.update(approved_go_live_at=None)
# Unpublish aliases
for alias in self.aliases.all():
alias.unpublish()
context_object_name = None
def get_context(self, request, *args, **kwargs):
@ -2695,6 +2808,9 @@ class PageRevision(models.Model):
if page.live:
page_published.send(sender=page.specific_class, instance=page.specific, revision=self)
# Update alias pages
page.update_aliases(revision=self, user=user, _content_json=self.content_json)
if log_action:
data = None
if previous_revision:

Wyświetl plik

@ -17,7 +17,7 @@ from django.utils import timezone, translation
from freezegun import freeze_time
from wagtail.core.models import (
Locale, Page, PageManager, ParentNotTranslatedError, Site, get_page_models, get_translatable_models)
Locale, Page, PageLogEntry, PageManager, ParentNotTranslatedError, Site, get_page_models, get_translatable_models)
from wagtail.core.signals import page_published
from wagtail.tests.testapp.models import (
AbstractPage, Advert, AlwaysShowInMenusPage, BlogCategory, BlogCategoryBlogPage, BusinessChild,
@ -1896,6 +1896,52 @@ class TestCreateAlias(TestCase):
EventPage.exclude_fields_in_copy = []
class TestUpdateAliases(TestCase):
fixtures = ['test.json']
def test_update_aliases(self):
event_page = EventPage.objects.get(url_path='/home/events/christmas/')
alias = event_page.create_alias(update_slug='new-event-page')
alias_alias = alias.create_alias(update_slug='new-event-page-2')
# Update the title and add a speaker
event_page.title = "Updated title"
event_page.draft_title = "A different draft title"
event_page.speakers.add(EventPageSpeaker(
first_name="Ted",
last_name="Crilly",
))
event_page.save()
# Nothing should've happened yet
alias.refresh_from_db()
alias_alias.refresh_from_db()
self.assertEqual(alias.title, "Christmas")
self.assertEqual(alias_alias.title, "Christmas")
self.assertEqual(alias.speakers.count(), 1)
self.assertEqual(alias_alias.speakers.count(), 1)
PageLogEntry.objects.all().delete()
event_page.update_aliases()
# Check that the aliases have been updated
alias.refresh_from_db()
alias_alias.refresh_from_db()
self.assertEqual(alias.title, "Updated title")
self.assertEqual(alias_alias.title, "Updated title")
self.assertEqual(alias.speakers.count(), 2)
self.assertEqual(alias_alias.speakers.count(), 2)
# Draft titles shouldn't update as alias pages do not have drafts
self.assertEqual(alias.draft_title, "Updated title")
self.assertEqual(alias_alias.draft_title, "Updated title")
# Check log entries were created
self.assertTrue(PageLogEntry.objects.filter(page=alias, action='wagtail.publish').exists())
self.assertTrue(PageLogEntry.objects.filter(page=alias_alias, action='wagtail.publish').exists())
class TestCopyForTranslation(TestCase):
fixtures = ['test.json']
@ -2553,6 +2599,7 @@ class TestPageWithContentJSON(TestCase):
class TestUnpublish(TestCase):
fixtures = ['test.json']
def test_unpublish_doesnt_call_full_clean_before_save(self):
root_page = Page.objects.get(id=1)
@ -2564,6 +2611,30 @@ class TestUnpublish(TestCase):
# This shouldn't fail with a ValidationError.
home_page.unpublish()
def test_unpublish_also_unpublishes_aliases(self):
event_page = EventPage.objects.get(url_path='/home/events/christmas/')
alias = event_page.create_alias(update_slug='new-event-page')
alias_alias = alias.create_alias(update_slug='new-event-page-2')
self.assertTrue(event_page.live)
self.assertTrue(alias.live)
self.assertTrue(alias_alias.live)
PageLogEntry.objects.all().delete()
# Unpublish the event page
event_page.unpublish()
alias.refresh_from_db()
alias_alias.refresh_from_db()
self.assertFalse(event_page.live)
self.assertFalse(alias.live)
self.assertFalse(alias_alias.live)
# Check log entries were created for the aliases
self.assertTrue(PageLogEntry.objects.filter(page=alias, action='wagtail.unpublish').exists())
self.assertTrue(PageLogEntry.objects.filter(page=alias_alias, action='wagtail.unpublish').exists())
class TestCachedContentType(TestCase):
"""Tests for Page.cached_content_type"""