Repository: wagtail/wagtail
File: wagtail/models/reference_index.py (Python)

Code before:
            return f" → {capfirst(field_name)}"
        return capfirst(field_name)

    def describe_source_field(self):
        """
        Returns a string describing the field that this reference was extracted from.

        For StreamField, this returns the label of the block that contains the reference.
        For other fields, this returns the verbose name of the field.
        """
        field = self.source_field
        model_path_components = self.model_path.split(".")

        # ManyToOneRel (reverse accessor for ParentalKey) does not have a verbose name. So get the name of the child field instead
        if isinstance(field, models.ManyToOneRel):
            label = ""
            idx = 2
            child_field = field.related_model._meta.get_field(
                model_path_components[idx]
            )
            while isinstance(child_field, models.ManyToOneRel):
                label += self.get_label(
                    child_field.related_model._meta.verbose_name, idx
                )
                idx += 2
                child_field = field.related_model._meta.get_field(
                    model_path_components[idx]
                )
            label += self.get_label(child_field.verbose_name, idx)
            return label
        elif isinstance(field, StreamField):
            label = f"{capfirst(field.verbose_name)}"
            block = field.stream_block
            block_idx = 1
            while isinstance(block, StreamBlock):
                block = block.child_blocks[model_path_components[block_idx]]
                block_label = capfirst(block.label)
                label += f" → {block_label}"
                block_idx += 1
            return label
        else:
            try:
                field_name = field.verbose_name
            except AttributeError:
                # generate verbose name from field name in the same way that Django does:
                # https://github.com/django/django/blob/7b94847e384b1a8c05a7d4c8778958c0290bdf9a/django/db/models/fields/__init__.py#L858
                field_name = field.name.replace("_", " ")
            return capfirst(field_name)

    def describe_on_delete(self):
        """

Review comment (bug):
We're traversing the child field recursively, so we should call `get_field()` from the `child_field.related_model._meta` rather than the original `field.related_model.meta`.

```suggestion
                child_field = child_field.related_model._meta.get_field(
```

Unfortunately the added test case didn't catch this bug, because the `ForeignKey` in `Book` and `BookRelationShip` are both named `page`, so the `get_field` happily returns a field where we could get the name from. When using a different name, this would throw a `FieldDoesNotExist` error.

Code after:

    @cached_property
    def related_field(self):
        """
        The final field in which the reference exists.

        The field stored on the reference index can be a related field or a
        reverse related field, depending on whether the reference was extracted
        directly from a ``ForeignKey`` or through a parent ``ClusterableModel``.
        This property normalizes to the related field.
        """
        # Drill through nested inline models to find where the reference exists.
        # Increment idx by 2 each time to skip over the `.item.` component.
        field = self.source_field
        idx = 2
        while isinstance(field, models.ForeignObjectRel):
            related_model_opts = field.related_model._meta
            field = related_model_opts.get_field(self.model_path_components[idx])
            idx += 2
        return field

    @cached_property
    def reverse_related_field(self):
        # This property normalises to the reverse related field, which is where
        # the on_delete attribute is stored.
        return self.related_field.remote_field

    def describe_source_field(self):
        """
        Returns a string describing the field that this reference was extracted from.

        For StreamField, this returns the label of the block that contains the reference.
        For other fields, this returns the verbose name of the field.
        """
        field = self.source_field
        model_path_components = self.model_path_components

        # ManyToOneRel (reverse accessor for ParentalKey) does not have a verbose name,
        # so get the name of the child field instead. While we don't yet support
        # ManyToManyRel, check for ForeignObjectRel (common base class of both
        # ManyToOneRel and ManyToManyRel) to future-proof this.
        if isinstance(field, models.ForeignObjectRel):
            labels = []
            idx = 0
            child_field = field
            while isinstance(child_field, models.ForeignObjectRel):
                related_model_opts = child_field.related_model._meta
                labels.append(capfirst(related_model_opts.verbose_name))
                idx += 2
                child_field = related_model_opts.get_field(model_path_components[idx])
            labels.append(capfirst(child_field.verbose_name))