From a87a5d8d5a99c69b21256bb2a496223e34f63d73 Mon Sep 17 00:00:00 2001
From: Matt Westcott <matt@west.co.tt>
Date: Fri, 4 Nov 2022 14:17:38 +0000
Subject: [PATCH] Gracefully skip reference indexing for objects with a null
 ParentalKey

Fixes #9583. The use case for null ParentalKeys is a bit questionable, but if they exist, we don't want reference indexing to break on them. Since references are always stored against the parent object, there's no valid way to record these references, so gracefully skip over them instead.
---
 wagtail/signal_handlers.py                    |  3 ++
 .../0011_modelwithnullableparentalkey.py      | 41 +++++++++++++++++++
 wagtail/test/testapp/models.py                | 11 +++++
 wagtail/tests/test_reference_index.py         | 18 +++++++-
 4 files changed, 72 insertions(+), 1 deletion(-)
 create mode 100644 wagtail/test/testapp/migrations/0011_modelwithnullableparentalkey.py

diff --git a/wagtail/signal_handlers.py b/wagtail/signal_handlers.py
index 8431843f5f..1695c67f10 100644
--- a/wagtail/signal_handlers.py
+++ b/wagtail/signal_handlers.py
@@ -83,6 +83,9 @@ def update_reference_index_on_save(instance, **kwargs):
             break
 
         instance = getattr(instance, parental_keys[0].name)
+        if instance is None:
+            # parent is null, so there is no valid object to record references against
+            return
 
     if ReferenceIndex.model_is_indexable(type(instance)):
         with transaction.atomic():
diff --git a/wagtail/test/testapp/migrations/0011_modelwithnullableparentalkey.py b/wagtail/test/testapp/migrations/0011_modelwithnullableparentalkey.py
new file mode 100644
index 0000000000..1ad92ddd94
--- /dev/null
+++ b/wagtail/test/testapp/migrations/0011_modelwithnullableparentalkey.py
@@ -0,0 +1,41 @@
+# Generated by Django 4.1.2 on 2022-11-04 14:00
+
+from django.db import migrations, models
+import django.db.models.deletion
+import modelcluster.fields
+import wagtail.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailcore", "0078_referenceindex"),
+        ("tests", "0010_alter_customimage_file_and_more"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ModelWithNullableParentalKey",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("content", wagtail.fields.RichTextField()),
+                (
+                    "page",
+                    modelcluster.fields.ParentalKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="wagtailcore.page",
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py
index 33ee9b2017..1f243b3355 100644
--- a/wagtail/test/testapp/models.py
+++ b/wagtail/test/testapp/models.py
@@ -1975,3 +1975,14 @@ class ModelWithStringTypePrimaryKey(models.Model):
 
     custom_id = models.CharField(max_length=255, primary_key=True)
     content = models.CharField(max_length=255)
+
+
+class ModelWithNullableParentalKey(models.Model):
+    """
+    There's not really a valid use case for null parental keys, but their presence should not
+    break things outright (e.g. when determining the object ID to store things under in the
+    references index).
+    """
+
+    page = ParentalKey(Page, blank=True, null=True)
+    content = RichTextField()
diff --git a/wagtail/tests/test_reference_index.py b/wagtail/tests/test_reference_index.py
index 10c78e87c7..30089d0830 100644
--- a/wagtail/tests/test_reference_index.py
+++ b/wagtail/tests/test_reference_index.py
@@ -7,7 +7,11 @@ from django.test import TestCase
 from wagtail.images import get_image_model
 from wagtail.images.tests.utils import get_test_image_file
 from wagtail.models import Page, ReferenceIndex
-from wagtail.test.testapp.models import EventPage, EventPageCarouselItem
+from wagtail.test.testapp.models import (
+    EventPage,
+    EventPageCarouselItem,
+    ModelWithNullableParentalKey,
+)
 
 
 class TestCreateOrUpdateForObject(TestCase):
@@ -163,6 +167,18 @@ class TestCreateOrUpdateForObject(TestCase):
             self.expected_references,
         )
 
+    def test_null_parental_key(self):
+        obj = ModelWithNullableParentalKey(
+            content="""<p><a linktype="page" id="%d">event page</a></p>"""
+            % self.event_page.id
+        )
+        obj.save()
+
+        # Models with a ParentalKey are not considered indexable - references are recorded against the parent model
+        # instead. Since the ParentalKey is null here, no reference will be recorded.
+        refs = ReferenceIndex.get_references_to(self.event_page)
+        self.assertEqual(refs.count(), 0)
+
     def test_rebuild_references_index_no_verbosity(self):
         stdout = StringIO()
         management.call_command(