Bulk loss of redirects after overriding `Page.get_url_parts`
Author: alxbridgeCreated Dec 13, 2023Updated Sep 17, 2026
Labelstype:Bugcomponent:Redirectscomponent:Page
Issue Summary
For models inheriting from Page, overriding the get_url_parts method can, in certain circumstances, result in bulk deletion of Redirects.
Steps to Reproduce
- Implement a model inheriting from
Page, overridingget_url_partsin such a way that the returnedpathvalue doesn't make use of the item'sslug. - Create a draft item using this model
- Edit the draft item to update its
slug - Publish the item
Result: All Redirects which are flagged as automatically_created are deleted
This example model is sufficient to trigger the issue:
class DemonstrationPage(Page):
def get_url_parts(self, request: "HttpRequest" = None) -> tuple[int, str, str]:
"""
Overrides `Page.get_url_parts()` so that the page's path uses its ID instead of its slug.
"""
site_id, root_url, path = self.get_parent().get_url_parts(request)
return site_id, root_url, f"{path}{self.id}/"- I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes
Technical details
This issue occurs due to a combination of the overridden get_url_parts method and handling inside wagtail.contrib.redirects.signal_handlers.
- Changes to the item's
slugresult in a call to theautocreate_redirects_on_slug_changefunction (which handles thepage_slug_changedsignal). - When the item's
slugisn't used in determining its path, this function doesn't add any redirects to the batch for processing (as the item's path hasn't changed). - The
BatchRedirectCreator.pre_processmethod attempts to delete any existing redirects which clash with items in the batch:
def pre_process(self):
# delete any existing automatically-created redirects that might clash
# with the items in `self.items`
clashes_q = Q()
for item in self.items:
clashes_q |= Q(old_path=item.old_path, site_id=item.site_id)
Redirect.objects.filter(automatically_created=True).filter(clashes_q).delete()- Since there's nothing in
self.items, the list of Redirects isn't filtered down further, resulting in deletion of all those set asautomatically_created=True.
Possible fixes
- The most immediate fix would be to add a check to
BatchRedirectCreator.pre_process, to ensure thatself.itemsis populated before proceeding. A similar check already exists inBatchCreator._do_processingfor the actual processing of batch items. - Check
batch.itemsbefore the call tobatch.process()in thecreate_redirectsfunction. - We could move the check from
BatchCreator._do_processingto the mainBatchCreator.processmethod, so that no processing action occurs unless a batch contains items. However, I'm not certain whether this would be desirable, and I've not checked whether it might have any unexpected knock-on effects.
Source: wagtail/wagtail